From f1dc1fb9c47006f6a42c6a8ad8240162ab94eed8 Mon Sep 17 00:00:00 2001 From: Martin Taillefer Date: Mon, 17 Jun 2024 19:34:00 -0700 Subject: [PATCH] Frozen collections --- .github/dependabot.yml | 11 + .github/workflows/main.yml | 57 + .gitignore | 4 + BENCHMARKS.md | 114 ++ CHANGELOG.md | 12 + Cargo.toml | 45 + LICENSE | 25 + README.md | 265 ++++ TODO.md | 46 + bench.ps1 | 12 + codegen/Cargo.toml | 34 + codegen/README.md | 5 + codegen/hashed_keys.rs | 87 ++ codegen/ordered_keys.rs | 78 + codegen/scalar_keys.rs | 111 ++ codegen/string_keys_length.rs | 59 + codegen/string_keys_subslice.rs | 56 + frozen-collections-core/Cargo.toml | 31 + frozen-collections-core/README.md | 6 + .../src/analyzers/hash_code_analyzer.rs | 187 +++ frozen-collections-core/src/analyzers/mod.rs | 9 + .../src/analyzers/scalar_key_analyzer.rs | 84 ++ .../src/analyzers/slice_key_analyzer.rs | 292 ++++ .../src/doc_snippets/about.md | 8 + .../src/doc_snippets/hash_warning.md | 21 + .../src/doc_snippets/order_warning.md | 12 + .../src/doc_snippets/type_compat_warning.md | 6 + .../src/facade_maps/facade_hash_map.rs | 286 ++++ .../src/facade_maps/facade_ordered_map.rs | 293 ++++ .../src/facade_maps/facade_scalar_map.rs | 293 ++++ .../src/facade_maps/facade_string_map.rs | 322 +++++ .../src/facade_maps/mod.rs | 11 + .../src/facade_sets/facade_hash_set.rs | 152 ++ .../src/facade_sets/facade_ordered_set.rs | 127 ++ .../src/facade_sets/facade_scalar_set.rs | 122 ++ .../src/facade_sets/facade_string_set.rs | 160 +++ .../src/facade_sets/mod.rs | 11 + .../src/hash_tables/hash_table.rs | 149 ++ .../src/hash_tables/hash_table_slot.rs | 21 + .../src/hash_tables/inline_hash_table.rs | 65 + .../src/hash_tables/mod.rs | 11 + .../src/hashers/bridge_hasher.rs | 37 + .../src/hashers/inline_left_range_hasher.rs | 124 ++ .../src/hashers/inline_right_range_hasher.rs | 144 ++ .../src/hashers/left_range_hasher.rs | 134 ++ .../src/hashers/mixing_hasher.rs | 67 + frozen-collections-core/src/hashers/mod.rs | 16 + .../src/hashers/passthrough_hasher.rs | 94 ++ .../src/hashers/right_range_hasher.rs | 145 ++ .../inline_dense_scalar_lookup_map.rs | 141 ++ .../src/inline_maps/inline_hash_map.rs | 186 +++ .../inline_maps/inline_ordered_scan_map.rs | 139 ++ .../src/inline_maps/inline_scan_map.rs | 136 ++ .../inline_sparse_scalar_lookup_map.rs | 186 +++ .../src/inline_maps/mod.rs | 13 + .../inline_dense_scalar_lookup_set.rs | 117 ++ .../src/inline_sets/inline_hash_set.rs | 178 +++ .../inline_sets/inline_ordered_scan_set.rs | 118 ++ .../src/inline_sets/inline_scan_set.rs | 114 ++ .../inline_sparse_scalar_lookup_set.rs | 153 ++ .../src/inline_sets/mod.rs | 13 + frozen-collections-core/src/lib.rs | 26 + .../src/macros/derive_scalar_macro.rs | 143 ++ .../src/macros/generator.rs | 902 ++++++++++++ .../src/macros/hash_table.rs | 14 + .../src/macros/map_macros.rs | 130 ++ frozen-collections-core/src/macros/mod.rs | 12 + .../src/macros/parsing/entry.rs | 31 + .../src/macros/parsing/long_form_map.rs | 49 + .../src/macros/parsing/long_form_set.rs | 57 + .../src/macros/parsing/map.rs | 43 + .../src/macros/parsing/mod.rs | 8 + .../src/macros/parsing/payload.rs | 56 + .../src/macros/parsing/set.rs | 43 + .../src/macros/parsing/short_form_map.rs | 14 + .../src/macros/parsing/short_form_set.rs | 14 + .../src/macros/set_macros.rs | 397 +++++ .../src/maps/binary_search_map.rs | 149 ++ .../src/maps/decl_macros.rs | 448 ++++++ .../src/maps/dense_scalar_lookup_map.rs | 189 +++ .../src/maps/eytzinger_search_map.rs | 151 ++ frozen-collections-core/src/maps/hash_map.rs | 234 +++ frozen-collections-core/src/maps/iterators.rs | 830 +++++++++++ frozen-collections-core/src/maps/mod.rs | 20 + .../src/maps/ordered_scan_map.rs | 150 ++ frozen-collections-core/src/maps/scan_map.rs | 140 ++ .../src/maps/sparse_scalar_lookup_map.rs | 174 +++ .../src/sets/binary_search_set.rs | 122 ++ .../src/sets/decl_macros.rs | 116 ++ .../src/sets/dense_scalar_lookup_set.rs | 128 ++ .../src/sets/eytzinger_search_set.rs | 122 ++ frozen-collections-core/src/sets/hash_set.rs | 163 +++ frozen-collections-core/src/sets/iterators.rs | 848 +++++++++++ frozen-collections-core/src/sets/mod.rs | 20 + .../src/sets/ordered_scan_set.rs | 122 ++ frozen-collections-core/src/sets/scan_set.rs | 121 ++ .../src/sets/sparse_scalar_lookup_set.rs | 131 ++ .../src/traits/collection_magnitude.rs | 35 + frozen-collections-core/src/traits/hasher.rs | 13 + frozen-collections-core/src/traits/len.rs | 400 ++++++ frozen-collections-core/src/traits/map.rs | 85 ++ .../src/traits/map_iteration.rs | 266 ++++ .../src/traits/map_query.rs | 118 ++ frozen-collections-core/src/traits/mod.rs | 27 + frozen-collections-core/src/traits/scalar.rs | 193 +++ frozen-collections-core/src/traits/set.rs | 31 + .../src/traits/set_iteration.rs | 59 + frozen-collections-core/src/traits/set_ops.rs | 103 ++ .../src/traits/set_query.rs | 65 + frozen-collections-core/src/utils/bitvec.rs | 92 ++ frozen-collections-core/src/utils/dedup.rs | 295 ++++ .../src/utils/eytzinger.rs | 69 + frozen-collections-core/src/utils/mod.rs | 11 + frozen-collections-core/src/utils/random.rs | 15 + frozen-collections-macros/Cargo.toml | 27 + frozen-collections-macros/README.md | 6 + frozen-collections-macros/src/lib.rs | 84 ++ frozen-collections/Cargo.toml | 56 + frozen-collections/benches/hashed_keys.rs | 14 + frozen-collections/benches/ordered_keys.rs | 8 + frozen-collections/benches/scalar_keys.rs | 14 + frozen-collections/benches/string_keys.rs | 14 + frozen-collections/build.rs | 422 ++++++ frozen-collections/src/lib.rs | 1279 +++++++++++++++++ .../tests/common/map_testing.rs | 199 +++ frozen-collections/tests/common/mod.rs | 5 + .../tests/common/set_testing.rs | 157 ++ .../tests/derive_scalar_macro_tests.rs | 26 + frozen-collections/tests/hash_macro_tests.rs | 484 +++++++ frozen-collections/tests/omni_tests.rs | 520 +++++++ .../tests/ordered_macro_tests.rs | 565 ++++++++ .../tests/scalar_macro_tests.rs | 345 +++++ .../tests/string_macro_tests.rs | 216 +++ mutate.ps1 | 1 + tables.toml | 48 + 135 files changed, 18639 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 BENCHMARKS.md create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TODO.md create mode 100644 bench.ps1 create mode 100644 codegen/Cargo.toml create mode 100644 codegen/README.md create mode 100644 codegen/hashed_keys.rs create mode 100644 codegen/ordered_keys.rs create mode 100644 codegen/scalar_keys.rs create mode 100644 codegen/string_keys_length.rs create mode 100644 codegen/string_keys_subslice.rs create mode 100644 frozen-collections-core/Cargo.toml create mode 100644 frozen-collections-core/README.md create mode 100644 frozen-collections-core/src/analyzers/hash_code_analyzer.rs create mode 100644 frozen-collections-core/src/analyzers/mod.rs create mode 100644 frozen-collections-core/src/analyzers/scalar_key_analyzer.rs create mode 100644 frozen-collections-core/src/analyzers/slice_key_analyzer.rs create mode 100644 frozen-collections-core/src/doc_snippets/about.md create mode 100644 frozen-collections-core/src/doc_snippets/hash_warning.md create mode 100644 frozen-collections-core/src/doc_snippets/order_warning.md create mode 100644 frozen-collections-core/src/doc_snippets/type_compat_warning.md create mode 100644 frozen-collections-core/src/facade_maps/facade_hash_map.rs create mode 100644 frozen-collections-core/src/facade_maps/facade_ordered_map.rs create mode 100644 frozen-collections-core/src/facade_maps/facade_scalar_map.rs create mode 100644 frozen-collections-core/src/facade_maps/facade_string_map.rs create mode 100644 frozen-collections-core/src/facade_maps/mod.rs create mode 100644 frozen-collections-core/src/facade_sets/facade_hash_set.rs create mode 100644 frozen-collections-core/src/facade_sets/facade_ordered_set.rs create mode 100644 frozen-collections-core/src/facade_sets/facade_scalar_set.rs create mode 100644 frozen-collections-core/src/facade_sets/facade_string_set.rs create mode 100644 frozen-collections-core/src/facade_sets/mod.rs create mode 100644 frozen-collections-core/src/hash_tables/hash_table.rs create mode 100644 frozen-collections-core/src/hash_tables/hash_table_slot.rs create mode 100644 frozen-collections-core/src/hash_tables/inline_hash_table.rs create mode 100644 frozen-collections-core/src/hash_tables/mod.rs create mode 100644 frozen-collections-core/src/hashers/bridge_hasher.rs create mode 100644 frozen-collections-core/src/hashers/inline_left_range_hasher.rs create mode 100644 frozen-collections-core/src/hashers/inline_right_range_hasher.rs create mode 100644 frozen-collections-core/src/hashers/left_range_hasher.rs create mode 100644 frozen-collections-core/src/hashers/mixing_hasher.rs create mode 100644 frozen-collections-core/src/hashers/mod.rs create mode 100644 frozen-collections-core/src/hashers/passthrough_hasher.rs create mode 100644 frozen-collections-core/src/hashers/right_range_hasher.rs create mode 100644 frozen-collections-core/src/inline_maps/inline_dense_scalar_lookup_map.rs create mode 100644 frozen-collections-core/src/inline_maps/inline_hash_map.rs create mode 100644 frozen-collections-core/src/inline_maps/inline_ordered_scan_map.rs create mode 100644 frozen-collections-core/src/inline_maps/inline_scan_map.rs create mode 100644 frozen-collections-core/src/inline_maps/inline_sparse_scalar_lookup_map.rs create mode 100644 frozen-collections-core/src/inline_maps/mod.rs create mode 100644 frozen-collections-core/src/inline_sets/inline_dense_scalar_lookup_set.rs create mode 100644 frozen-collections-core/src/inline_sets/inline_hash_set.rs create mode 100644 frozen-collections-core/src/inline_sets/inline_ordered_scan_set.rs create mode 100644 frozen-collections-core/src/inline_sets/inline_scan_set.rs create mode 100644 frozen-collections-core/src/inline_sets/inline_sparse_scalar_lookup_set.rs create mode 100644 frozen-collections-core/src/inline_sets/mod.rs create mode 100644 frozen-collections-core/src/lib.rs create mode 100644 frozen-collections-core/src/macros/derive_scalar_macro.rs create mode 100644 frozen-collections-core/src/macros/generator.rs create mode 100644 frozen-collections-core/src/macros/hash_table.rs create mode 100644 frozen-collections-core/src/macros/map_macros.rs create mode 100644 frozen-collections-core/src/macros/mod.rs create mode 100644 frozen-collections-core/src/macros/parsing/entry.rs create mode 100644 frozen-collections-core/src/macros/parsing/long_form_map.rs create mode 100644 frozen-collections-core/src/macros/parsing/long_form_set.rs create mode 100644 frozen-collections-core/src/macros/parsing/map.rs create mode 100644 frozen-collections-core/src/macros/parsing/mod.rs create mode 100644 frozen-collections-core/src/macros/parsing/payload.rs create mode 100644 frozen-collections-core/src/macros/parsing/set.rs create mode 100644 frozen-collections-core/src/macros/parsing/short_form_map.rs create mode 100644 frozen-collections-core/src/macros/parsing/short_form_set.rs create mode 100644 frozen-collections-core/src/macros/set_macros.rs create mode 100644 frozen-collections-core/src/maps/binary_search_map.rs create mode 100644 frozen-collections-core/src/maps/decl_macros.rs create mode 100644 frozen-collections-core/src/maps/dense_scalar_lookup_map.rs create mode 100644 frozen-collections-core/src/maps/eytzinger_search_map.rs create mode 100644 frozen-collections-core/src/maps/hash_map.rs create mode 100644 frozen-collections-core/src/maps/iterators.rs create mode 100644 frozen-collections-core/src/maps/mod.rs create mode 100644 frozen-collections-core/src/maps/ordered_scan_map.rs create mode 100644 frozen-collections-core/src/maps/scan_map.rs create mode 100644 frozen-collections-core/src/maps/sparse_scalar_lookup_map.rs create mode 100644 frozen-collections-core/src/sets/binary_search_set.rs create mode 100644 frozen-collections-core/src/sets/decl_macros.rs create mode 100644 frozen-collections-core/src/sets/dense_scalar_lookup_set.rs create mode 100644 frozen-collections-core/src/sets/eytzinger_search_set.rs create mode 100644 frozen-collections-core/src/sets/hash_set.rs create mode 100644 frozen-collections-core/src/sets/iterators.rs create mode 100644 frozen-collections-core/src/sets/mod.rs create mode 100644 frozen-collections-core/src/sets/ordered_scan_set.rs create mode 100644 frozen-collections-core/src/sets/scan_set.rs create mode 100644 frozen-collections-core/src/sets/sparse_scalar_lookup_set.rs create mode 100644 frozen-collections-core/src/traits/collection_magnitude.rs create mode 100644 frozen-collections-core/src/traits/hasher.rs create mode 100644 frozen-collections-core/src/traits/len.rs create mode 100644 frozen-collections-core/src/traits/map.rs create mode 100644 frozen-collections-core/src/traits/map_iteration.rs create mode 100644 frozen-collections-core/src/traits/map_query.rs create mode 100644 frozen-collections-core/src/traits/mod.rs create mode 100644 frozen-collections-core/src/traits/scalar.rs create mode 100644 frozen-collections-core/src/traits/set.rs create mode 100644 frozen-collections-core/src/traits/set_iteration.rs create mode 100644 frozen-collections-core/src/traits/set_ops.rs create mode 100644 frozen-collections-core/src/traits/set_query.rs create mode 100644 frozen-collections-core/src/utils/bitvec.rs create mode 100644 frozen-collections-core/src/utils/dedup.rs create mode 100644 frozen-collections-core/src/utils/eytzinger.rs create mode 100644 frozen-collections-core/src/utils/mod.rs create mode 100644 frozen-collections-core/src/utils/random.rs create mode 100644 frozen-collections-macros/Cargo.toml create mode 100644 frozen-collections-macros/README.md create mode 100644 frozen-collections-macros/src/lib.rs create mode 100644 frozen-collections/Cargo.toml create mode 100644 frozen-collections/benches/hashed_keys.rs create mode 100644 frozen-collections/benches/ordered_keys.rs create mode 100644 frozen-collections/benches/scalar_keys.rs create mode 100644 frozen-collections/benches/string_keys.rs create mode 100644 frozen-collections/build.rs create mode 100644 frozen-collections/src/lib.rs create mode 100644 frozen-collections/tests/common/map_testing.rs create mode 100644 frozen-collections/tests/common/mod.rs create mode 100644 frozen-collections/tests/common/set_testing.rs create mode 100644 frozen-collections/tests/derive_scalar_macro_tests.rs create mode 100644 frozen-collections/tests/hash_macro_tests.rs create mode 100644 frozen-collections/tests/omni_tests.rs create mode 100644 frozen-collections/tests/ordered_macro_tests.rs create mode 100644 frozen-collections/tests/scalar_macro_tests.rs create mode 100644 frozen-collections/tests/string_macro_tests.rs create mode 100644 mutate.ps1 create mode 100644 tables.toml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..22b1e8d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..4bad0ad --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,57 @@ +name: main + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - 1.83.0 + - beta + - nightly + + steps: + - uses: actions/checkout@v4 + - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} + - run: rustup component add clippy + - run: rustup component add rustfmt + - name: Build + run: cargo build --verbose --all-targets + - name: Check + run: cargo check --verbose --all-targets + - name: Clippy + run: cargo clippy --verbose --all-targets + - name: Format + run: cargo fmt -- --check + - name: Tests + run: cargo test --verbose + - name: Doc Tests + run: cargo test --doc --verbose + + coverage: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + - name: Install Rust + run: rustup update stable + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Generate code coverage + run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + files: lcov.info + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f2c2be --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/Cargo.lock +/.idea +/mutants* diff --git a/BENCHMARKS.md b/BENCHMARKS.md new file mode 100644 index 0000000..40fcb0d --- /dev/null +++ b/BENCHMARKS.md @@ -0,0 +1,114 @@ +# Benchmarks + +## Table of Contents + +- [Overview](#overview) +- [Benchmark Results](#benchmark-results) + - [dense_scalar](#dense_scalar) + - [sparse_scalar](#sparse_scalar) + - [random_scalar](#random_scalar) + - [random_string](#random_string) + - [prefixed_string](#prefixed_string) + - [hashed](#hashed) + - [ordered](#ordered) + +## Overview + +These benchmarks compare the performance of the frozen collecitons relative +to the classic Rust collections. + +The frozen collections have different optimizations depending on the type of data they +storeta and how it is declared. The benchmarks probe those different features to show +the effect of the different optimizations on effective performance. + +When you see `HashSet(classic)` vs. `HashSet(ahash)` this reflects the performance difference between the +normal hasher used by the standard collections as opposed to the performnace that the +`ahash` hasher provides. + +The benchmarks assume a 50% hit rate when probing for lookup, meaning that +half the queries are for non-existing data. Some algorithms perform differently between +present vs. non-existing cases, so real world performance of these algorithms depends on the +real world hit rate you experience. + +## Benchmark Results + +### dense_scalar + +Scalar sets where the values are in a contiguous range. + +| | `HashSet(classic)` | `HashSet(ahash)` | `fz_scalar_set(vector)` | `fz_scalar_set(literals)` | +|:-----------|:----------------------------|:--------------------------------|:---------------------------------|:----------------------------------- | +| **`3`** | `45.63 ns` (✅ **1.00x**) | `17.59 ns` (🚀 **2.59x faster**) | `4.74 ns` (🚀 **9.63x faster**) | `4.31 ns` (🚀 **10.59x faster**) | +| **`16`** | `242.30 ns` (✅ **1.00x**) | `95.88 ns` (🚀 **2.53x faster**) | `28.13 ns` (🚀 **8.61x faster**) | `24.36 ns` (🚀 **9.95x faster**) | +| **`256`** | `4.05 us` (✅ **1.00x**) | `1.51 us` (🚀 **2.69x faster**) | `470.94 ns` (🚀 **8.61x faster**) | `412.29 ns` (🚀 **9.83x faster**) | +| **`1000`** | `15.72 us` (✅ **1.00x**) | `6.12 us` (🚀 **2.57x faster**) | `1.82 us` (🚀 **8.65x faster**) | `1.59 us` (🚀 **9.86x faster**) | + +### sparse_scalar + +Scalar sets where the values are in a non-contiguous range. + +| | `HashSet(classic)` | `HashSet(ahash)` | `fz_scalar_set(vector)` | `fz_scalar_set(literals)` | +|:-----------|:----------------------------|:--------------------------------|:----------------------------------|:----------------------------------- | +| **`3`** | `49.63 ns` (✅ **1.00x**) | `17.57 ns` (🚀 **2.82x faster**) | `4.60 ns` (🚀 **10.80x faster**) | `5.89 ns` (🚀 **8.42x faster**) | +| **`16`** | `251.71 ns` (✅ **1.00x**) | `91.15 ns` (🚀 **2.76x faster**) | `20.71 ns` (🚀 **12.15x faster**) | `23.82 ns` (🚀 **10.57x faster**) | +| **`256`** | `4.03 us` (✅ **1.00x**) | `1.57 us` (🚀 **2.57x faster**) | `330.83 ns` (🚀 **12.18x faster**) | `379.66 ns` (🚀 **10.61x faster**) | +| **`1000`** | `15.72 us` (✅ **1.00x**) | `6.09 us` (🚀 **2.58x faster**) | `1.27 us` (🚀 **12.39x faster**) | `1.46 us` (🚀 **10.76x faster**) | + +### random_scalar + +Scalar sets where the values are randomly distributed. + +| | `HashSet(classic)` | `HashSet(ahash)` | `fz_scalar_set(vector)` | `fz_scalar_set(literals)` | +|:-----------|:----------------------------|:--------------------------------|:---------------------------------|:----------------------------------- | +| **`3`** | `48.23 ns` (✅ **1.00x**) | `17.15 ns` (🚀 **2.81x faster**) | `9.19 ns` (🚀 **5.25x faster**) | `11.41 ns` (🚀 **4.23x faster**) | +| **`16`** | `251.72 ns` (✅ **1.00x**) | `96.97 ns` (🚀 **2.60x faster**) | `53.23 ns` (🚀 **4.73x faster**) | `53.32 ns` (🚀 **4.72x faster**) | +| **`256`** | `4.03 us` (✅ **1.00x**) | `1.55 us` (🚀 **2.60x faster**) | `814.80 ns` (🚀 **4.94x faster**) | `814.97 ns` (🚀 **4.94x faster**) | +| **`1000`** | `15.68 us` (✅ **1.00x**) | `6.15 us` (🚀 **2.55x faster**) | `3.27 us` (🚀 **4.79x faster**) | `3.22 us` (🚀 **4.86x faster**) | + +### random_string + +String sets where the values are random. + +| | `HashSet(classic)` | `HashSet(ahash)` | `fz_string_set(vector)` | `fz_string_set(literals)` | +|:-----------|:----------------------------|:---------------------------------|:---------------------------------|:----------------------------------- | +| **`3`** | `82.65 ns` (✅ **1.00x**) | `42.94 ns` (🚀 **1.92x faster**) | `39.36 ns` (🚀 **2.10x faster**) | `26.20 ns` (🚀 **3.15x faster**) | +| **`16`** | `434.62 ns` (✅ **1.00x**) | `225.46 ns` (🚀 **1.93x faster**) | `221.74 ns` (🚀 **1.96x faster**) | `141.19 ns` (🚀 **3.08x faster**) | +| **`256`** | `6.79 us` (✅ **1.00x**) | `3.58 us` (🚀 **1.90x faster**) | `3.49 us` (🚀 **1.95x faster**) | `2.56 us` (🚀 **2.66x faster**) | +| **`1000`** | `27.72 us` (✅ **1.00x**) | `14.98 us` (🚀 **1.85x faster**) | `13.96 us` (🚀 **1.99x faster**) | `10.23 us` (🚀 **2.71x faster**) | + +### prefixed_string + +String sets where the values are random, but share a common prefix. + +| | `HashSet(classic)` | `HashSet(ahash)` | `fz_string_set(vector)` | `fz_string_set(literals)` | +|:-----------|:----------------------------|:---------------------------------|:---------------------------------|:----------------------------------- | +| **`3`** | `81.22 ns` (✅ **1.00x**) | `43.76 ns` (🚀 **1.86x faster**) | `35.47 ns` (🚀 **2.29x faster**) | `24.98 ns` (🚀 **3.25x faster**) | +| **`16`** | `459.80 ns` (✅ **1.00x**) | `243.87 ns` (🚀 **1.89x faster**) | `214.12 ns` (🚀 **2.15x faster**) | `135.28 ns` (🚀 **3.40x faster**) | +| **`256`** | `7.55 us` (✅ **1.00x**) | `4.00 us` (🚀 **1.89x faster**) | `3.68 us` (🚀 **2.05x faster**) | `2.84 us` (🚀 **2.66x faster**) | +| **`1000`** | `30.38 us` (✅ **1.00x**) | `16.47 us` (🚀 **1.84x faster**) | `14.06 us` (🚀 **2.16x faster**) | `10.64 us` (🚀 **2.85x faster**) | + +### hashed + +Sets with a complex key type that is hashable. + +| | `HashSet(classic)` | `HashSet(ahash)` | `fz_hash_set(vector)` | `fz_hash_set(literals)` | +|:-----------|:----------------------------|:---------------------------------|:---------------------------------|:--------------------------------- | +| **`3`** | `92.20 ns` (✅ **1.00x**) | `50.63 ns` (🚀 **1.82x faster**) | `40.94 ns` (🚀 **2.25x faster**) | `57.09 ns` (✅ **1.61x faster**) | +| **`16`** | `515.48 ns` (✅ **1.00x**) | `265.23 ns` (🚀 **1.94x faster**) | `207.83 ns` (🚀 **2.48x faster**) | `231.67 ns` (🚀 **2.23x faster**) | +| **`256`** | `8.34 us` (✅ **1.00x**) | `4.33 us` (🚀 **1.93x faster**) | `3.80 us` (🚀 **2.20x faster**) | `3.75 us` (🚀 **2.22x faster**) | +| **`1000`** | `33.77 us` (✅ **1.00x**) | `17.59 us` (🚀 **1.92x faster**) | `16.08 us` (🚀 **2.10x faster**) | `15.52 us` (🚀 **2.18x faster**) | + +### ordered + +Sets with a complex key type that is ordered. + +| | `BTreeSet` | `fz_hash_set(vector)` | `fz_ordered_set(literals)` | +|:-----------|:--------------------------|:---------------------------------|:------------------------------------ | +| **`3`** | `70.62 ns` (✅ **1.00x**) | `62.25 ns` (✅ **1.13x faster**) | `59.52 ns` (✅ **1.19x faster**) | +| **`16`** | `954.52 ns` (✅ **1.00x**) | `984.68 ns` (✅ **1.03x slower**) | `990.07 ns` (✅ **1.04x slower**) | +| **`256`** | `30.65 us` (✅ **1.00x**) | `27.37 us` (✅ **1.12x faster**) | `27.01 us` (✅ **1.13x faster**) | +| **`1000`** | `219.26 us` (✅ **1.00x**) | `199.92 us` (✅ **1.10x faster**) | `199.09 us` (✅ **1.10x faster**) | + +--- +Made with [criterion-table](https://github.com/nu11ptr/criterion-table) + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c979c68 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be 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). + +## 0.1.0 - 2024-12-07 + +### Added + +- Initial release diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4ca2d25 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,45 @@ +[workspace] +resolver = "2" +members = [ + "frozen-collections", + "frozen-collections-core", + "frozen-collections-macros", + "codegen", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +categories = ["data-structures", "no-std", "collections"] +keywords = ["map", "set", "collection"] +repository = "https://github.com/geeknoid/frozen-collections" +license = "MIT" +authors = ["Martin Taillefer "] +readme = "README.md" +rust-version = "1.83.0" + +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +wildcard_imports = "allow" +too_many_lines = "allow" +multiple_crate_versions = "allow" +from-iter-instead-of-collect = "allow" +into_iter_without_iter = "allow" +inline_always = "allow" +unnecessary_wraps = "allow" +cognitive_complexity = "allow" + +[profile.bench] +codegen-units = 1 +lto = "fat" + +[profile.release] # Modify profile settings via config. +codegen-units = 1 +lto = "fat" +debug = true # Include debug info. +strip = "none" # Removes symbols or debuginfo. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b5df8da --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2024 Martin Taillefer + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..80d1fdc --- /dev/null +++ b/README.md @@ -0,0 +1,265 @@ +# Frozen Collections - Fast _Partially_ Immutable Collections + +[![crate.io](https://img.shields.io/crates/v/frozen-collections.svg)](https://crates.io/crates/frozen-collections) +[![docs.rs](https://docs.rs/frozen-collections/badge.svg)](https://docs.rs/frozen-collections) +[![CI](https://github.com/geeknoid/frozen-collections/workflows/main/badge.svg)](https://github.com/geeknoid/frozen-collections/actions) +[![Coverage](https://codecov.io/gh/geeknoid/frozen-collections/graph/badge.svg?token=FCUG0EL5TI)](https://codecov.io/gh/geeknoid/frozen-collections) +[![Minimum Supported Rust Version 1.83](https://img.shields.io/badge/MSRV-1.83-blue.svg)]() +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) + +* [Summary](#summary) +* [Creation](#creation) + * [Short Form](#short-form) + * [Long Form](#long-form) +* [Traits](#traits) +* [Performance Considerations](#performance-considerations) +* [Analysis and Optimizations](#analysis-and-optimizations) +* [Cargo Features](#cargo-features) + +## Summary + +Frozen collections are designed to deliver improved +read performance relative to the standard [`HashMap`](https://doc.rust-lang.org/std/collections/hash/map/struct.HashMap.html) and +[`HashSet`](https://doc.rust-lang.org/std/collections/hash/set/struct.HashSet.html) types. They are ideal for use with long-lasting collections +which get initialized when an application starts and remain unchanged +permanently, or at least extended periods of time. This is a common +pattern in service applications. + +As part of creating a frozen collection, analysis is performed over the data that the collection +will hold to determine the best layout and algorithm to use to deliver optimal performance. +Depending on the situation, sometimes the analysis is done at compile-time whereas in +other cases it is done at runtime when the collection is initialized. +This analysis can take some time, but the value in spending this time up front +is that the collections provide faster read-time performance. + +Frozen maps are only partially immutable. The keys associated with a frozen map are determined +at creation time and cannot change, but the values can be updated at will if you have a +mutable reference to the map. Frozen sets however are completely immutable and so never +change after creation. + +See [BENCHMARKS.md](./BENCHMARKS.md) for current benchmark numbers. + +## Creation + +Frozen collections are created with one of eight macros: +[`fz_hash_map!`](https://docs.rs/frozen-collections/macro.fz_hash_map.html), +[`fz_ordered_map!`](https://docs.rs/frozen-collections/macro.fz_ordered_map.html), +[`fz_scalar_map!`](https://docs.rs/frozen-collections/macro.fz_scalar_map.html), +[`fz_string_map!`](https://docs.rs/frozen-collections/macro.fz_string_map.html), +[`fz_hash_set!`](https://docs.rs/frozen-collections/macro.fz_hash_set.html), +[`fz_ordered_set!`](https://docs.rs/frozen-collections/macro.fz_ordered_set.html), +[`fz_scalar_set!`](https://docs.rs/frozen-collections/macro.fz_scalar_set.html), or +[`fz_string_set!`](https://docs.rs/frozen-collections/macro.fz_string_set.html). +These macros analyze the data you provide +and return a custom implementation type that's optimized for the data. All the +possible implementations types implement the +[`Map`](https://docs.rs/frozen-collections/trait.Map.html) or +[`Set`](https://docs.rs/frozen-collections/trait.Set.html) traits. + +The macros exist in a short form and a long form, described below. + +### Short Form + +With the short form, you supply the data that +goes into the collection and get in return an initialized collection of an unnamed +type. For example: + +```rust +use frozen_collections::*; + +let m = fz_string_map!({ + "Alice": 1, + "Bob": 2, + "Sandy": 3, + "Tom": 4, +}); +``` + +At build time, the macro analyzes the data supplied and determines the best map +implementation type to use. As such, the type of `m` is not known to this code. `m` will +always implement the [`Map`] trait however, so you can leverage type inference even though +you don't know the actual type of `m`: + +```rust +use frozen_collections::*; + +fn main() { + let m = fz_string_map!({ + "Alice": 1, + "Bob": 2, + "Sandy": 3, + "Tom": 4, + }); + + more(m); +} + +fn more(m: M) +where + M: Map<&'static str, i32> +{ + assert!(m.contains_key(&"Alice")); +} +``` + +Rather than specifying all the data inline, you can also create a frozen collection by passing +a vector as input: + +```rust +use frozen_collections::*; + +let v = vec![ + ("Alice", 1), + ("Bob", 2), + ("Sandy", 3), + ("Tom", 4), +]; + +let m = fz_string_map!(v); +``` + +The inline form is preferred however since it results in faster code. However, whereas the inline form +requires all the data to be provided at compile time, the vector form enables the content of the +frozen collection to be determined at runtime. + +### Long Form + +The long form lets you provide a type alias name which will be created to +correspond to the collection implementation type chosen by the macro invocation. +Note that you must use the long form if you want to declare a static frozen collection. + +```rust +use frozen_collections::*; + +fz_string_map!(static MAP: MyMapType<&str, i32>, { + "Alice": 1, + "Bob": 2, + "Sandy": 3, + "Tom": 4, +}); +``` + +The above creates a static variable called `MAP` with keys that are strings and values which are +integers. As before, you don't know the specific implementation type selected by the macro, but +this time you have a type alias (i.e. `MyMapType`) representing that type. You can then use this alias +anywhere you'd like to in your code where you'd like to mention the type explicitly. + +To use the long form for non-static uses, replace `static` with `let`: + +```rust +use frozen_collections::*; + +fz_string_map!(let m: MyMapType<&str, i32>, { + "Alice": 1, + "Bob": 2, + "Sandy": 3, + "Tom": 4, +}); + +more(m); + +struct S { + m: MyMapType, +} + +fn more(m: MyMapType) { + assert!(m.contains_key("Alice")); +} +``` + +And like in the short form, you can also supply the collection's data via a vector: + +```rust +use frozen_collections::*; + +let v = vec![ + ("Alice", 1), + ("Bob", 2), + ("Sandy", 3), + ("Tom", 4), +]; + +fz_string_map!(let m: MyMapType<&str, i32>, v); +``` + +## Traits + +The maps created by the frozen collections macros implement the following traits: + +- [`Map`](https://docs.rs/frozen-collections/trait.Map.html). The primary representation of a map. This trait has [`MapQuery`](https://docs.rs/frozen-collections/trait.MapQuery.html) and + [`MapIteration`](https://docs.rs/frozen-collections/trait.MapIteration.html) as super-traits. +- [`MapQuery`](https://docs.rs/frozen-collections/trait.MapQuery.html). A trait for querying maps. This is an object-safe trait. +- [`MapIteration`](https://docs.rs/frozen-collections/trait.MapIteration.html). A trait for iterating over maps. + +The sets created by the frozen collection macros implement the following traits: + +- [`Set`](https://docs.rs/frozen-collections/trait.Set.html). The primary representation of a set. This trait has [`SetQuery`](https://docs.rs/frozen-collections/trait.Map.html), + [`SetIteration`](https://docs.rs/frozen-collections/trait.Map.html) and [`SetOps`](https://docs.rs/frozen-collections/trait.Map.html) as super-traits. +- [`SetQuery`](https://docs.rs/frozen-collections/trait.Map.html). A trait for querying sets. This is an object-safe trait. +- [`SetIteration`](https://docs.rs/frozen-collections/trait.Map.html). A trait for iterating over sets. +- [`SetOps`](https://docs.rs/frozen-collections/trait.Map.html). A trait for set operations like union and intersections. + +## Performance Considerations + +The analysis performed when creating maps tries to find the best concrete implementation type +given the data at hand. If all the data is visible to the macro at compile time, then you get +the best possible performance. If you supply a vector instead, then the analysis can only be +done at runtime and the resulting collection types are slightly slower. + +When creating static collections, the collections produced can often be embedded directly as constant data +into the binary of the application, thus requiring no initialization time and no heap space. at +This also happens to be the fastest form for these collections. If possible, this happens +automatically, you don't need to do anything special to enable this behavior. + +## Analysis and Optimizations + +Unlike normal collections, the frozen collections require you to provide all the data for +the collection when you create the collection. The data you supply is analyzed which determines +which specific underlying implementation strategy to use and how to lay out data internally. + +The available implementation strategies are: + +- **Scalar as Hash**. When the keys are of an integer or enum type, this uses the keys themselves + as hash codes, avoiding the overhead of hashing. + +- **Length as Hash**. When the keys are of a string type, the length of the keys + are used as hash code, avoiding the overhead of hashing. + +- **Dense Scalar Lookup**. When the keys represent a contiguous range of integer or enum values, + lookups use a simple array access instead of hashing. + +- **Sparse Scalar Lookup**. When the keys represent a sparse range of integer or enum values, + lookups use a sparse array access instead of hashing. + +- **Left Hand Substring Hashing**. When the keys are of a string type, this uses sub-slices of + the keys for hashing, reducing the overhead of hashing. + +- **Right Hand Substring Hashing**. Similar to the Left Hand Substring Hashing from above, but + using right-aligned sub-slices instead. + +- **Linear Scan**. For very small collections, this avoids hashing completely by scanning through + the entries in linear order. + +- **Ordered Scan**. For very small collections where the keys implement the [`Ord`] trait, + this avoids hashing completely by scanning through the entries in linear order. Unlike the + Linear Scan strategy, this one can early exit when keys are not found during the scan. + +- **Classic Hashing**. This is the fallback when none of the previous strategies apply. The + frozen implementations are generally faster than + [`HashMap`](https://doc.rust-lang.org/std/collections/hash/map/struct.HashMap.html) and + [`HashSet`](https://doc.rust-lang.org/std/collections/hash/set/struct.HashSet.html). + +- **Binary Search**. For relatively small collections where the keys implement the [`Ord`] trait, + classic binary searching is used. + +- **Eytzinger Search**. For larger collections where the keys implement the [`Ord`] trait, +a cache-friendly Eytzinger search is used. + +## Cargo Features + +You can specify the following features when you include the `frozen_collections` crate in your +`Cargo.toml` file: + +- **`std`**. Enables small features only available when building with the standard library. + +The `std` feature is enabled by default. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5fd66c9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,46 @@ +# TODO + +## Docs + +- Update docs to discuss traits and object safety. + +- Update docs to mention no_std support. + +## Performance + +- Look at https://lib.rs/crates/small-map for SIMD accelerated small maps. + +- with strings, use unique substrings as the actual hash code when possible. + +- Fine-tune the thresholds when we switch between different implementation types in + the facades and in the generator. + +## Features + +- Consider adding support for case-insensitive strings. + +- Extend the Scalar derive macro to support more varieties of enum types. + +- Simplify the sets/inline_sets to only have hash/ordered/scalar/string variants with the maps as a + generic argument. + +- Support serde serialization/deserialization. All the collections should be serializable. On the input side, + we should be smart and do the runtime analysis needed to pick the right collection implementation type. + +- Provide a mechanism that can be used in a build.rs script to generate static frozen collections from + arbitrary input. Read from file into data structure, give the data structure to the API, and you get + back a token stream representing the initialization of a frozen collection optimized for the data. + +## Trait Nightmares + +I'd like to add some super-traits to some existing traits, but unfortunately doing +so seemingly requires adding lifetime annotations to the existing traits, which I think +would be pretty painful. Any better idea? + +``` +SetIterator : IntoIterator +MatIterator: IntroIterator +Map: Eq + PartialEq +Set: Eq + PartialEq + BitOr + BitAnd + BitXor + Sub + SetOps +MapQuery: Index +``` \ No newline at end of file diff --git a/bench.ps1 b/bench.ps1 new file mode 100644 index 0000000..259fe27 --- /dev/null +++ b/bench.ps1 @@ -0,0 +1,12 @@ +# Run all benchmarks and convert into the markdown +cargo criterion --bench scalar_keys --message-format=json >scalar_keys.json +cargo criterion --bench string_keys --message-format=json >string_keys.json +cargo criterion --bench hashed_keys --message-format=json >hashed_keys.json +cargo criterion --bench ordered_keys --message-format=json >ordered_keys.json + +Get-Content .\scalar_keys.json,.\string_keys.json,.\hashed_keys.json,.\ordered_keys.json | criterion-table > BENCHMARKS.md + +rm scalar_keys.json +rm string_keys.json +rm hashed_keys.json +rm ordered_keys.json diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml new file mode 100644 index 0000000..055dde2 --- /dev/null +++ b/codegen/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "codegen" +version = "0.0.0" +publish = false +edition.workspace = true +rust-version.workspace = true + +[dev-dependencies] +frozen-collections = { path = "../frozen-collections", features = [], default-features = false } +ahash = "0.8.11" +hashbrown = "0.15.2" + +[[example]] +name = "hashed_keys" +path = "hashed_keys.rs" + +[[example]] +name = "string_keys_subslice" +path = "string_keys_subslice.rs" + +[[example]] +name = "string_keys_length" +path = "string_keys_length.rs" + +[[example]] +name = "scalar_keys" +path = "scalar_keys.rs" + +[[example]] +name = "ordered_keys" +path = "ordered_keys.rs" + +[lints] +workspace = true diff --git a/codegen/README.md b/codegen/README.md new file mode 100644 index 0000000..023dc87 --- /dev/null +++ b/codegen/README.md @@ -0,0 +1,5 @@ +# Codegen + +This contains a few simple examples that get compiled to release +mode in order to compare the generated code for various code fragments +to determine the best implementation choices. diff --git a/codegen/hashed_keys.rs b/codegen/hashed_keys.rs new file mode 100644 index 0000000..603b602 --- /dev/null +++ b/codegen/hashed_keys.rs @@ -0,0 +1,87 @@ +#![no_std] +extern crate alloc; + +use alloc::string::{String, ToString}; +use alloc::vec; +use core::hint::black_box; +use frozen_collections::facade_maps::FacadeHashMap; +use frozen_collections::hashers::BridgeHasher; +use frozen_collections::*; +use hashbrown::HashMap as HashbrownMap; + +#[derive(Hash, PartialEq, Eq, Clone)] +struct MyKey { + name: String, + city: String, +} + +fn main() { + let v = vec![ + ( + MyKey { + name: "Helter1".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter2".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter3".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter4".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter5".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter6".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ]; + + let fm = FacadeHashMap::new(v.clone(), BridgeHasher::default()); + let cm = maps::HashMap::new(v.clone(), BridgeHasher::new(ahash::RandomState::new())).unwrap(); + let mut hm = HashbrownMap::with_capacity_and_hasher(v.len(), ahash::RandomState::new()); + hm.extend(v.clone()); + + _ = black_box(call_facade_hash_map_with_bridge_hasher(&fm, &v[0].0)); + _ = black_box(call_hash_map_with_bridge_hasher(&cm, &v[0].0)); + _ = black_box(call_hashbrown_map(&hm, &v[0].0)); +} + +#[inline(never)] +fn call_facade_hash_map_with_bridge_hasher(map: &FacadeHashMap, key: &MyKey) -> bool { + map.contains_key(key) +} + +#[inline(never)] +fn call_hash_map_with_bridge_hasher(map: &maps::HashMap, key: &MyKey) -> bool { + map.contains_key(key) +} + +#[inline(never)] +fn call_hashbrown_map(map: &HashbrownMap, key: &MyKey) -> bool { + map.contains_key(key) +} diff --git a/codegen/ordered_keys.rs b/codegen/ordered_keys.rs new file mode 100644 index 0000000..50ef19f --- /dev/null +++ b/codegen/ordered_keys.rs @@ -0,0 +1,78 @@ +#![no_std] +extern crate alloc; + +use alloc::string::{String, ToString}; +use alloc::vec; +use core::hint::black_box; +use frozen_collections::facade_maps::FacadeOrderedMap; +use frozen_collections::maps::EytzingerSearchMap; +use frozen_collections::*; + +#[derive(Ord, PartialOrd, PartialEq, Eq, Clone)] +struct MyKey { + name: String, + city: String, +} + +fn main() { + let v = vec![ + ( + MyKey { + name: "Helter1".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter2".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter3".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter4".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter5".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ( + MyKey { + name: "Helter6".to_string(), + city: "Skelter".to_string(), + }, + 42, + ), + ]; + + let fm = FacadeOrderedMap::new(v.clone()); + let esm = EytzingerSearchMap::new(v.clone()); + + _ = black_box(call_facade_ordered_map(&fm, &v[0].0)); + _ = black_box(call_eytzinger_search_map(&esm, &v[0].0)); +} + +#[inline(never)] +fn call_facade_ordered_map(map: &FacadeOrderedMap, key: &MyKey) -> bool { + map.contains_key(key) +} + +#[inline(never)] +fn call_eytzinger_search_map(map: &maps::EytzingerSearchMap, key: &MyKey) -> bool { + map.contains_key(key) +} diff --git a/codegen/scalar_keys.rs b/codegen/scalar_keys.rs new file mode 100644 index 0000000..392fd8c --- /dev/null +++ b/codegen/scalar_keys.rs @@ -0,0 +1,111 @@ +#![no_std] +extern crate alloc; + +use alloc::vec; +use core::hint::black_box; +use frozen_collections::facade_maps::FacadeScalarMap; +use frozen_collections::hashers::PassthroughHasher; +use frozen_collections::maps::{ + BinarySearchMap, DenseScalarLookupMap, EytzingerSearchMap, HashMap, OrderedScanMap, ScanMap, + SparseScalarLookupMap, +}; +use frozen_collections::*; +use hashbrown::HashMap as HashbrownMap; + +fn main() { + let input = vec![(0, 0), (1, 0), (2, 0), (3, 0)]; + let probe = vec![0, 1, 2]; + + let map: HashbrownMap<_, _, ahash::RandomState> = input.clone().into_iter().collect(); + for key in probe.clone() { + _ = black_box(call_hashbrown_map(&map, key)); + } + + let map = HashMap::new(input.clone(), PassthroughHasher::default()).unwrap(); + for key in probe.clone() { + _ = black_box(call_hash_map_with_passthrough_hasher(&map, key)); + } + + let map = DenseScalarLookupMap::new(input.clone()).unwrap(); + for key in probe.clone() { + _ = black_box(call_dense_scalar_lookup_map(&map, key)); + } + + let map = SparseScalarLookupMap::new(input.clone()); + for key in probe.clone() { + _ = black_box(call_sparse_scalar_lookup_map(&map, key)); + } + + let map = ScanMap::new(input.clone()); + for key in probe.clone() { + _ = black_box(call_scan_map(&map, key)); + } + + let map = OrderedScanMap::new(input.clone()); + for key in probe.clone() { + _ = black_box(call_ordered_scan_map(&map, key)); + } + + let map = BinarySearchMap::new(input.clone()); + for key in probe.clone() { + _ = black_box(call_binary_search_map(&map, key)); + } + + let map = EytzingerSearchMap::new(input.clone()); + for key in probe.clone() { + _ = black_box(call_eytzinger_search_map(&map, key)); + } + + let map = FacadeScalarMap::new(input); + for key in probe { + _ = black_box(call_facade_scalar_map(&map, key)); + } +} + +#[inline(never)] +fn call_hashbrown_map(map: &HashbrownMap, key: i32) -> bool { + map.contains_key(&key) +} + +#[inline(never)] +fn call_hash_map_with_passthrough_hasher( + map: &HashMap, + key: i32, +) -> bool { + map.contains_key(&key) +} + +#[inline(never)] +fn call_dense_scalar_lookup_map(map: &DenseScalarLookupMap, key: i32) -> bool { + map.contains_key(&key) +} + +#[inline(never)] +fn call_sparse_scalar_lookup_map(map: &SparseScalarLookupMap, key: i32) -> bool { + map.contains_key(&key) +} + +#[inline(never)] +fn call_facade_scalar_map(map: &FacadeScalarMap, key: i32) -> bool { + map.contains_key(&key) +} + +#[inline(never)] +fn call_binary_search_map(map: &BinarySearchMap, key: i32) -> bool { + map.contains_key(&key) +} + +#[inline(never)] +fn call_eytzinger_search_map(map: &EytzingerSearchMap, key: i32) -> bool { + map.contains_key(&key) +} + +#[inline(never)] +fn call_scan_map(map: &ScanMap, key: i32) -> bool { + map.contains_key(&key) +} + +#[inline(never)] +fn call_ordered_scan_map(map: &OrderedScanMap, key: i32) -> bool { + map.contains_key(&key) +} diff --git a/codegen/string_keys_length.rs b/codegen/string_keys_length.rs new file mode 100644 index 0000000..8e8a00f --- /dev/null +++ b/codegen/string_keys_length.rs @@ -0,0 +1,59 @@ +#![no_std] +extern crate alloc; + +use core::hint::black_box; +use frozen_collections::ahash::RandomState; +use frozen_collections::facade_maps::FacadeStringMap; +use frozen_collections::*; +use hashbrown::HashMap as HashbrownMap; + +fz_string_map!(static MAP: MyMapType<&str, i32>, { "1": 1, "22":2, "333":3, "4444":4, "55555":5, "666666":6, "7777777":7, "88888888":8, "999999999":9 }); + +fn main() { + let input = MAP.clone(); + let probe = [ + "0", + "1", + "22", + "333", + "4444", + "A", + "B", + "C", + "D", + "ABCDEFGHIJKL", + ]; + + let map: HashbrownMap<_, _, RandomState> = input.iter().map(|x| (*x.0, *x.1)).collect(); + for key in probe { + _ = black_box(call_hashbrown_map(&map, key)); + } + + let map = FacadeStringMap::new( + input.iter().map(|x| (*x.0, *x.1)).collect(), + RandomState::new(), + ); + for key in probe { + _ = black_box(call_facade_string_map(&map, key)); + } + + let map = input; + for key in probe { + _ = black_box(call_inline_hash_map_with_passthrough_hasher(&map, key)); + } +} + +#[inline(never)] +fn call_hashbrown_map(map: &HashbrownMap<&str, i32, RandomState>, key: &str) -> bool { + map.contains_key(key) +} + +#[inline(never)] +fn call_inline_hash_map_with_passthrough_hasher(map: &MyMapType, key: &str) -> bool { + map.contains_key(key) +} + +#[inline(never)] +fn call_facade_string_map(map: &FacadeStringMap<&str, i32>, key: &str) -> bool { + map.contains_key(key) +} diff --git a/codegen/string_keys_subslice.rs b/codegen/string_keys_subslice.rs new file mode 100644 index 0000000..cda4acb --- /dev/null +++ b/codegen/string_keys_subslice.rs @@ -0,0 +1,56 @@ +#![no_std] +extern crate alloc; + +use core::hint::black_box; +use frozen_collections::ahash::RandomState; +use frozen_collections::facade_maps::FacadeStringMap; +use frozen_collections::*; +use hashbrown::HashMap as HashbrownMap; + +fz_string_map!(static MAP: MyMapType<&str, i32>, { "ALongPrefixRedd": 1, "ALongPrefixGree":2, "ALongPrefixBlue":3, "ALongPrefixCyan":4, "ALongPrefixPurple":5, "ALongPrefix:Yellow":6, "ALongPrefixMagenta":7 }); + +fn main() { + let input = MAP.clone(); + let probe = [ + "ALongPrefixRedd", + "ALongPrefixCyan", + "Tomato", + "Potato", + "Carrot", + ]; + + let map: HashbrownMap<_, _, RandomState> = input.iter().map(|x| (*x.0, *x.1)).collect(); + for key in probe { + _ = black_box(call_hashbrown_map(&map, key)); + } + + let map = FacadeStringMap::new( + input.iter().map(|x| (*x.0, *x.1)).collect(), + RandomState::default(), + ); + for key in probe { + _ = black_box(call_facade_string_map(&map, key)); + } + + let map = input; + for key in probe { + _ = black_box(call_inline_hash_map_with_inline_left_range_hasher( + &map, key, + )); + } +} + +#[inline(never)] +fn call_hashbrown_map(map: &HashbrownMap<&str, i32, RandomState>, key: &str) -> bool { + map.contains_key(key) +} + +#[inline(never)] +fn call_facade_string_map(map: &FacadeStringMap<&str, i32>, key: &str) -> bool { + map.contains_key(key) +} + +#[inline(never)] +fn call_inline_hash_map_with_inline_left_range_hasher(map: &MyMapType, key: &str) -> bool { + map.contains_key(key) +} diff --git a/frozen-collections-core/Cargo.toml b/frozen-collections-core/Cargo.toml new file mode 100644 index 0000000..bfe2145 --- /dev/null +++ b/frozen-collections-core/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "frozen-collections-core" +description = "Implementation logic for frozen collections." +version.workspace = true +edition.workspace = true +categories.workspace = true +keywords.workspace = true +repository.workspace = true +license.workspace = true +authors.workspace = true +readme = "README.md" + +[dependencies] +ahash = "0.8.11" +hashbrown = "0.15.1" +const-random = "0.1.18" +syn = { version = "2.0.90", features = ["extra-traits", "full"] } +quote = { version = "1.0.37" } +proc-macro2 = { version = "1.0.92" } +mutants = "0.0.3" +equivalent = "1.0.1" + +[dev-dependencies] +rand = "0.9.0-beta.1" + +[features] +default = ["std"] +std = [] + +[lints] +workspace = true diff --git a/frozen-collections-core/README.md b/frozen-collections-core/README.md new file mode 100644 index 0000000..f1f28c8 --- /dev/null +++ b/frozen-collections-core/README.md @@ -0,0 +1,6 @@ +# frozen-collections-core + +This crate contains some of the implementation logic for the +frozen-collections crate. Users of frozen collections +should take a dependency on the [`frozen-collections`](https://docs.rs/frozen-collections) crate +instead of this one. diff --git a/frozen-collections-core/src/analyzers/hash_code_analyzer.rs b/frozen-collections-core/src/analyzers/hash_code_analyzer.rs new file mode 100644 index 0000000..a0999ec --- /dev/null +++ b/frozen-collections-core/src/analyzers/hash_code_analyzer.rs @@ -0,0 +1,187 @@ +use crate::utils::BitVec; +use alloc::vec::Vec; + +/// How to treat a collection of hash codes for best performance. +//#[derive(Clone)] +pub struct HashCodeAnalysisResult { + /// The recommended hash table size. This is not necessarily optimal, but it's good enough. + pub num_hash_slots: usize, + + /// The number of collisions when using the recommended table size. + #[allow(dead_code)] + pub num_hash_collisions: usize, +} + +/// Look for an "optimal" hash table size for a given set of hash codes. +#[allow(clippy::cast_possible_truncation)] +#[mutants::skip] +pub fn analyze_hash_codes(hash_codes: I) -> HashCodeAnalysisResult +where + I: Iterator, +{ + // What is a satisfactory rate of hash collisions? + const ACCEPTABLE_COLLISION_PERCENTAGE: usize = 5; + + // By how much do we shrink the acceptable # collisions per iteration? + const ACCEPTABLE_COLLISION_PERCENTAGE_OF_REDUCTION: usize = 20; + + // thresholds to categorize input sizes + const MEDIUM_INPUT_SIZE_THRESHOLD: usize = 128; + const LARGE_INPUT_SIZE_THRESHOLD: usize = 1000; + + // amount by which the table can be larger than the input + const MAX_SMALL_INPUT_MULTIPLIER: usize = 10; + const MAX_MEDIUM_INPUT_MULTIPLIER: usize = 7; + const MAX_LARGE_INPUT_MULTIPLIER: usize = 3; + + let hash_codes: Vec = hash_codes.collect(); + let mut acceptable_collisions = if hash_codes.len() < MEDIUM_INPUT_SIZE_THRESHOLD { + // for small enough inputs, we try for perfection + 0 + } else { + (hash_codes.len() / 100) * ACCEPTABLE_COLLISION_PERCENTAGE + }; + + // the minimum table size we can tolerate, given the acceptable collision rate + let mut min_size = hash_codes.len() - acceptable_collisions; + if !min_size.is_power_of_two() { + min_size = min_size.next_power_of_two(); + } + + // the maximum table size we consider, given a scaled growth factor for different input sizes + let mut max_size = if hash_codes.len() < MEDIUM_INPUT_SIZE_THRESHOLD { + hash_codes.len() * MAX_SMALL_INPUT_MULTIPLIER + } else if hash_codes.len() < LARGE_INPUT_SIZE_THRESHOLD { + hash_codes.len() * MAX_MEDIUM_INPUT_MULTIPLIER + } else { + hash_codes.len() * MAX_LARGE_INPUT_MULTIPLIER + }; + + if !max_size.is_power_of_two() { + max_size = max_size.next_power_of_two(); + } + + let mut use_table = BitVec::with_capacity(max_size); + + let mut best_num_slots = 0; + let mut best_num_collisions = hash_codes.len(); + + let mut num_slots = min_size; + while num_slots <= max_size { + use_table.fill(false); + let mut num_collisions = 0; + + for code in &hash_codes { + let slot = (code % (num_slots as u64)) as usize; + if use_table.get(slot) { + num_collisions += 1; + if num_collisions >= best_num_collisions { + break; + } + } else { + use_table.set(slot, true); + } + } + + if num_collisions < best_num_collisions { + if best_num_slots == 0 || num_collisions <= acceptable_collisions { + best_num_collisions = num_collisions; + best_num_slots = num_slots; + } + + if num_collisions <= acceptable_collisions { + // we have a winner! + break; + } + } + + if acceptable_collisions > 0 { + // The larger the table, the fewer collisions we tolerate. The idea + // here is to reduce the risk of a table getting very big and still + // having a relatively high count of collisions. + acceptable_collisions = + (acceptable_collisions / 100) * ACCEPTABLE_COLLISION_PERCENTAGE_OF_REDUCTION; + } + + num_slots = (num_slots + 1).next_power_of_two(); + } + + HashCodeAnalysisResult { + num_hash_slots: best_num_slots, + num_hash_collisions: best_num_collisions, + } +} + +#[cfg(test)] +mod tests { + use rand::rngs::StdRng; + use rand::{Rng, SeedableRng}; + + use super::*; + + struct AnalysisTestCase { + num_hash_codes: usize, + randomize_hash_codes: bool, + expected_num_hash_slots: usize, + expected_num_hash_collisions: usize, + } + + #[test] + fn analyze_hash_codes_test() { + const ANALYSIS_TEST_CASES: [AnalysisTestCase; 5] = [ + AnalysisTestCase { + num_hash_codes: 0, + randomize_hash_codes: true, + expected_num_hash_slots: 0, + expected_num_hash_collisions: 0, + }, + AnalysisTestCase { + num_hash_codes: 2, + randomize_hash_codes: true, + expected_num_hash_slots: 2, + expected_num_hash_collisions: 0, + }, + AnalysisTestCase { + num_hash_codes: 1000, + randomize_hash_codes: true, + expected_num_hash_slots: 1024, + expected_num_hash_collisions: 349, + }, + AnalysisTestCase { + num_hash_codes: 8_000_000, + randomize_hash_codes: false, + expected_num_hash_slots: 8_388_608, + expected_num_hash_collisions: 0, + }, + AnalysisTestCase { + num_hash_codes: 8_000_000, + randomize_hash_codes: true, + expected_num_hash_slots: 8_388_608, + expected_num_hash_collisions: 2_843_788, + }, + ]; + + for case in &ANALYSIS_TEST_CASES { + let mut rng = StdRng::seed_from_u64(42); + let mut hash_codes = Vec::with_capacity(case.num_hash_codes); + + if case.randomize_hash_codes { + for _ in 0..case.num_hash_codes { + hash_codes.push(rng.random()); + } + } else { + for count in 0..case.num_hash_codes { + hash_codes.push(count as u64); + } + } + + let result = analyze_hash_codes(hash_codes.iter().copied()); + + assert_eq!(case.expected_num_hash_slots, result.num_hash_slots); + assert_eq!( + case.expected_num_hash_collisions, + result.num_hash_collisions + ); + } + } +} diff --git a/frozen-collections-core/src/analyzers/mod.rs b/frozen-collections-core/src/analyzers/mod.rs new file mode 100644 index 0000000..6ebed9e --- /dev/null +++ b/frozen-collections-core/src/analyzers/mod.rs @@ -0,0 +1,9 @@ +//! Logic to analyze collection input data to assess the best implementation choices. + +pub use hash_code_analyzer::*; +pub use scalar_key_analyzer::*; +pub use slice_key_analyzer::*; + +mod hash_code_analyzer; +mod scalar_key_analyzer; +mod slice_key_analyzer; diff --git a/frozen-collections-core/src/analyzers/scalar_key_analyzer.rs b/frozen-collections-core/src/analyzers/scalar_key_analyzer.rs new file mode 100644 index 0000000..090f369 --- /dev/null +++ b/frozen-collections-core/src/analyzers/scalar_key_analyzer.rs @@ -0,0 +1,84 @@ +use crate::traits::Scalar; + +/// How to treat integer keys for best performance. +#[derive(PartialEq, Eq, Debug)] +pub enum ScalarKeyAnalysisResult { + /// No special optimization possible. + General, + + /// All keys are in a continuous range. + DenseRange, + + /// All keys are in a sparse range. + SparseRange, +} + +/// Look for well-known patterns we can optimize for with integer keys. +#[mutants::skip] +pub fn analyze_scalar_keys(keys: I) -> ScalarKeyAnalysisResult +where + I: Iterator, +{ + const MAX_SPARSE_MULTIPLIER: usize = 10; + const ALWAYS_SPARSE_THRESHOLD: usize = 128; + + let mut min = usize::MAX; + let mut max = usize::MIN; + let mut count = 0; + for key in keys { + min = min.min(key.index()); + max = max.max(key.index()); + count += 1; + } + + if count == 0 { + return ScalarKeyAnalysisResult::General; + } + + let needed_count = max - min + 1; + if needed_count == count { + ScalarKeyAnalysisResult::DenseRange + } else if needed_count <= ALWAYS_SPARSE_THRESHOLD + || needed_count < count.saturating_mul(MAX_SPARSE_MULTIPLIER) + { + ScalarKeyAnalysisResult::SparseRange + } else { + ScalarKeyAnalysisResult::General + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_analyze_scalar_keys_empty() { + let keys = Vec::::new().into_iter(); + assert_eq!(analyze_scalar_keys(keys), ScalarKeyAnalysisResult::General); + } + + #[test] + fn test_analyze_scalar_keys_dense_range() { + let keys = 1..=5; + assert_eq!( + analyze_scalar_keys(keys), + ScalarKeyAnalysisResult::DenseRange + ); + } + + #[test] + fn test_analyze_scalar_keys_sparse_range() { + let keys = vec![1, 3, 5, 7, 128].into_iter(); + assert_eq!( + analyze_scalar_keys(keys), + ScalarKeyAnalysisResult::SparseRange + ); + } + + #[test] + fn test_analyze_scalar_keys_general() { + let keys = vec![1, 2, 4, 8, 129].into_iter(); + assert_eq!(analyze_scalar_keys(keys), ScalarKeyAnalysisResult::General); + } +} diff --git a/frozen-collections-core/src/analyzers/slice_key_analyzer.rs b/frozen-collections-core/src/analyzers/slice_key_analyzer.rs new file mode 100644 index 0000000..550c573 --- /dev/null +++ b/frozen-collections-core/src/analyzers/slice_key_analyzer.rs @@ -0,0 +1,292 @@ +use alloc::vec::Vec; +use core::cmp::{max, min}; +use core::hash::{BuildHasher, Hash}; +use core::ops::Range; +use hashbrown::HashMap as HashbrownMap; +use hashbrown::HashSet as HashbrownSet; + +/// How to treat keys which are slices for best performance. +#[derive(PartialEq, Eq, Debug)] +pub enum SliceKeyAnalysisResult { + /// No special optimization possible. + General, + + /// Hash left-justified subslices + LeftHandSubslice(Range), + + /// Hash right-justified subslices + RightHandSubslice(Range), + + /// Use the length of the slices as hash codes, instead of hashing the slices + Length, +} + +/// Look for well-known patterns we can optimize for when keys are slices. +/// +/// The idea here is to find the shortest subslice across all the input slices which are maximally unique. A corresponding +/// subslice range is then applied to incoming slices being hashed to perform lookups. Keeping the subslices as +/// short as possible minimizes the number of bytes involved in hashing, speeding up the whole process. +/// +/// What we do here is pretty simple. We loop over the input slices, looking for the shortest subslice with a good +/// enough uniqueness factor. We look at all the slices both left-justified and right-justified as this maximizes +/// the opportunities to find unique subslices, especially in the case of many slices with the same prefix or suffix. +/// +/// We also analyze the length of the input slices. If the length of the slices are sufficiently unique, +/// we can totally skip hashing and just use their lengths as hash codes. +pub fn analyze_slice_keys<'a, K, I, BH>(keys: I, bh: &BH) -> SliceKeyAnalysisResult +where + K: Hash + 'a, + I: Iterator, + BH: BuildHasher, +{ + let keys = keys.collect(); + + // first, see if we can just use slice lengths as hash codes + let result = analyze_lengths(&keys); + + if result == SliceKeyAnalysisResult::General { + // if we can't use slice lengths, look for suitable subslices + analyze_subslices(&keys, bh) + } else { + result + } +} + +/// See if we can use slice lengths instead of hashing +fn analyze_lengths(keys: &Vec<&[T]>) -> SliceKeyAnalysisResult { + const ACCEPTABLE_DUPLICATE_RATIO: usize = 20; // 5% duplicates are acceptable + + let max_identical = keys.len() / ACCEPTABLE_DUPLICATE_RATIO; + let mut lengths = HashbrownMap::::new(); + for s in keys { + let v = lengths.get(&s.len()); + if let Some(count) = v { + if *count >= max_identical { + return SliceKeyAnalysisResult::General; + } + + lengths.insert(s.len(), count + 1); + } else { + lengths.insert(s.len(), 1); + } + } + + SliceKeyAnalysisResult::Length +} + +/// See if we can use subslices to reduce the time spent hashing +fn analyze_subslices(keys: &Vec<&[T]>, bh: &BH) -> SliceKeyAnalysisResult +where + T: Hash, + BH: BuildHasher, +{ + // constrain the amount of work we do in this code + const MAX_SUBSLICE_LENGTH_LIMIT: usize = 16; + const ACCEPTABLE_DUPLICATE_RATIO: usize = 20; // 5% duplicates are acceptable + + let mut min_len = usize::MAX; + let mut max_len = 0; + for s in keys { + min_len = min(min_len, s.len()); + max_len = max(max_len, s.len()); + } + + // tolerate a certain amount of duplicate subslices + let acceptable_duplicates = keys.len() / ACCEPTABLE_DUPLICATE_RATIO; + + // this set is reused for each call to is_sufficiently_unique + let mut set = HashbrownSet::with_capacity(keys.len()); + + // for each subslice length, prefer the shortest length that provides enough uniqueness + let max_subslice_len = min(min_len, MAX_SUBSLICE_LENGTH_LIMIT); + + let mut subslice_len = 1; + while subslice_len <= max_subslice_len { + // For each index, get a uniqueness factor for the left-justified subslices. + // If any is above our threshold, we're done. + let mut subslice_index = 0; + while subslice_index <= min_len - subslice_len { + if is_sufficiently_unique( + keys, + subslice_index, + subslice_len, + true, + &mut set, + acceptable_duplicates, + bh, + ) { + return if subslice_len == max_len { + SliceKeyAnalysisResult::General + } else { + SliceKeyAnalysisResult::LeftHandSubslice( + subslice_index..subslice_index + subslice_len, + ) + }; + } + + subslice_index += 1; + } + + // There were no left-justified slices of this length available. + // If all the slices are of the same length, then just checking left-justification is sufficient. + // But if any slices are of different lengths, then we'll get different alignments for left- vs + // right-justified subslices, and so we also check right-justification. + if min_len != max_len { + // For each index, get a uniqueness factor for the right-justified subslices. + // If any is above our threshold, we're done. + subslice_index = 0; + while subslice_index <= min_len - subslice_len { + if is_sufficiently_unique( + keys, + subslice_index, + subslice_len, + false, + &mut set, + acceptable_duplicates, + bh, + ) { + return SliceKeyAnalysisResult::RightHandSubslice( + subslice_index..subslice_index + subslice_len, + ); + } + + subslice_index += 1; + } + } + + subslice_len += 1; + } + + // could not find a subslice that was good enough. + SliceKeyAnalysisResult::General +} + +fn is_sufficiently_unique( + keys: &Vec<&[T]>, + subslice_index: usize, + subslice_len: usize, + left_justified: bool, + set: &mut HashbrownSet, + mut acceptable_duplicates: usize, + bh: &BH, +) -> bool +where + T: Hash, + BH: BuildHasher, +{ + set.clear(); + + for s in keys { + let sub = if left_justified { + &s[subslice_index..subslice_index + subslice_len] + } else { + let start = s.len() - subslice_index - subslice_len; + &s[start..start + subslice_len] + }; + + if !set.insert(bh.hash_one(sub)) { + if acceptable_duplicates == 0 { + return false; + } + + acceptable_duplicates -= 1; + } + } + + true +} + +#[cfg(test)] +mod tests { + use alloc::string::{String, ToString}; + + use ahash::RandomState; + + use super::*; + + struct AnalysisTestCase<'a> { + slices: &'a [&'a str], + expected: SliceKeyAnalysisResult, + } + + #[test] + fn analyze_string_keys_test() { + const ANALYSIS_TEST_CASES: [AnalysisTestCase; 9] = [ + AnalysisTestCase { + slices: &[ + "AAA", "ABB", "ACC", "ADD", "AEE", "AFF", "AGG", "AHH", "AII", "AJJ", "AKK", + "ALL", "AMM", "ANN", "AOO", "APP", "AQQ", "ARR", "ASS", "ATT", "AUU", + ], + expected: SliceKeyAnalysisResult::LeftHandSubslice(1..2), + }, + AnalysisTestCase { + slices: &["A00", "B00", "C00", "D00"], + expected: SliceKeyAnalysisResult::LeftHandSubslice(0..1), + }, + AnalysisTestCase { + slices: &["A", "B", "C", "D", "E2"], + expected: SliceKeyAnalysisResult::LeftHandSubslice(0..1), + }, + AnalysisTestCase { + slices: &["A", "B", "C", "D", "E2", ""], + expected: SliceKeyAnalysisResult::General, + }, + AnalysisTestCase { + slices: &["XA", "XB", "XC", "XD", "XE2"], + expected: SliceKeyAnalysisResult::LeftHandSubslice(1..2), + }, + AnalysisTestCase { + slices: &["XXA", "XXB", "XXC", "XXD", "XXX", "XXXE"], + expected: SliceKeyAnalysisResult::RightHandSubslice(0..1), + }, + AnalysisTestCase { + slices: &["ABC", "DEFG", "HIJKL", "MNOPQR", "STUVWXY", "YZ"], + expected: SliceKeyAnalysisResult::Length, + }, + AnalysisTestCase { + slices: &[ + "ABC", "DEFG", "HIJKL", "MNOPQR", "STUVWX", "YZ", "D2", "D3", "D4", + ], + expected: SliceKeyAnalysisResult::LeftHandSubslice(1..2), + }, + AnalysisTestCase { + slices: &["AAA", "1AA", "A1A", "AA1", "BBB", "1BB", "B1B", "BB1"], + expected: SliceKeyAnalysisResult::General, + }, + ]; + + for case in &ANALYSIS_TEST_CASES { + let keys = case.slices.iter().map(|x| x.as_bytes()); + assert_eq!(case.expected, analyze_slice_keys(keys, &RandomState::new())); + } + } + + #[test] + fn out_of_range_bug() { + let mut v = Vec::new(); + for i in 0..30 { + let mut s = i.to_string(); + s.push('1'); + v.push(s); + } + + let x = v.iter().map(String::as_bytes); + let y = &RandomState::new(); + analyze_slice_keys(x, y); + } + + #[test] + fn too_many_slices() { + let mut v = Vec::new(); + for i in 0..300 { + let mut s = i.to_string(); + s.push('1'); + v.push(s); + } + + let x = v.iter().map(String::as_bytes); + let y = &RandomState::new(); + + assert_eq!(analyze_slice_keys(x, y), SliceKeyAnalysisResult::General); + } +} diff --git a/frozen-collections-core/src/doc_snippets/about.md b/frozen-collections-core/src/doc_snippets/about.md new file mode 100644 index 0000000..9e7284a --- /dev/null +++ b/frozen-collections-core/src/doc_snippets/about.md @@ -0,0 +1,8 @@ +A frozen collection differs from the traditional Rust collections, such as +[`HashMap`](std::collections::HashMap) and [`HashSet`](std::collections::HashSet) types in three +key ways. First, creating a frozen collection performs an analysis over the input data to +determine the best implementation strategy. Depending on the situation, this analysis is +performed at build time or runtime, and it can take a relatively long time when a collection is +very large. Second, once created, the keys in frozen collections are immutable. And third, +querying a frozen collection is typically considerably faster, which is the whole point. + diff --git a/frozen-collections-core/src/doc_snippets/hash_warning.md b/frozen-collections-core/src/doc_snippets/hash_warning.md new file mode 100644 index 0000000..903fabc --- /dev/null +++ b/frozen-collections-core/src/doc_snippets/hash_warning.md @@ -0,0 +1,21 @@ +This type requires that the keys +implement the [`Eq`] and [`Hash`] traits. This can frequently be achieved by +using `#[derive(PartialEq, Eq, Hash)]`. If you implement these yourself, +it is important that the following property holds: + +```text +k1 == k2 -> hash(k1) == hash(k2) +``` + +In other words, if two keys are equal, their hashes must be equal. +Violating this property is a logic error. + +It is also a logic error for a key to be modified in such a way that the key's +hash, as determined by the [`Hash`] trait, or its equality, as determined by +the [`Eq`] trait, changes while it is in the collection. This is normally only +possible through [`core::cell::Cell`], [`core::cell::RefCell`], global state, I/O, +or unsafe code. + +The behavior resulting from either logic error can include panics, incorrect results, +memory leaks, and non-termination. + diff --git a/frozen-collections-core/src/doc_snippets/order_warning.md b/frozen-collections-core/src/doc_snippets/order_warning.md new file mode 100644 index 0000000..2a7f01a --- /dev/null +++ b/frozen-collections-core/src/doc_snippets/order_warning.md @@ -0,0 +1,12 @@ +This type requires that the keys +implement the [`Ord`] trait. This can frequently be achieved by +using `#[derive(PartialOrd, Ord)]`. + +It is a logic error for a key to be modified in such a way that the key's +order, as determined by the [`Ord`] trait, or its equality, as determined by +the [`Eq`] trait, changes while it is in the map. This is normally only +possible through [`core::cell::Cell`], [`core::cell::RefCell`], global state, I/O, or unsafe code. + +The behavior resulting from the above logic error can include panics, incorrect results, +memory leaks, and non-termination. + diff --git a/frozen-collections-core/src/doc_snippets/type_compat_warning.md b/frozen-collections-core/src/doc_snippets/type_compat_warning.md new file mode 100644 index 0000000..889bb45 --- /dev/null +++ b/frozen-collections-core/src/doc_snippets/type_compat_warning.md @@ -0,0 +1,6 @@ +
+This type is an implementation detail of the `frozen_collections` crate. +This API is therefore not stable and may change at any time. Please do not +use this type directly, and instead use the public API provided by the +`frozen_collections` crate. +
diff --git a/frozen-collections-core/src/facade_maps/facade_hash_map.rs b/frozen-collections-core/src/facade_maps/facade_hash_map.rs new file mode 100644 index 0000000..34de960 --- /dev/null +++ b/frozen-collections-core/src/facade_maps/facade_hash_map.rs @@ -0,0 +1,286 @@ +use crate::hashers::BridgeHasher; +use crate::maps::{ + HashMap, IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, ScanMap, Values, ValuesMut, +}; +use crate::traits::{Hasher, LargeCollection, Len, Map, MapIteration, MapQuery}; +use crate::utils::dedup_by_hash_keep_last; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Equivalent; + +#[derive(Clone)] +enum MapTypes { + Hash(HashMap), + Scanning(ScanMap), +} + +/// A map optimized for fast read access with hashable keys. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/hash_warning.md")] +/// +/// # Alternate Choices +/// +/// If your keys are integers or enum variants, you should use the [`FacadeScalarMap`](crate::facade_maps::FacadeScalarMap) type instead. +/// If your keys are strings, you should use the [`FacadeStringMap`](crate::facade_maps::FacadeStringMap) type instead. Both of these will +/// deliver better performance since they are specifically optimized for those key types. +#[derive(Clone)] +pub struct FacadeHashMap { + map_impl: MapTypes, +} + +impl FacadeHashMap +where + K: Eq, + H: Hasher, +{ + /// Creates a frozen map which uses the given hash builder to hash keys. + #[must_use] + #[allow(clippy::missing_panics_doc)] + pub fn new(mut entries: Vec<(K, V)>, hasher: H) -> Self { + dedup_by_hash_keep_last(&mut entries, &hasher); + + Self { + map_impl: if entries.len() < 3 { + MapTypes::Scanning(ScanMap::new_raw(entries)) + } else { + MapTypes::Hash(HashMap::new_half_baked(entries, hasher).unwrap()) + }, + } + } +} + +impl Default for FacadeHashMap +where + H: Default, +{ + fn default() -> Self { + Self { + map_impl: MapTypes::Scanning(ScanMap::::default()), + } + } +} + +impl Map for FacadeHashMap +where + Q: ?Sized + Eq + Equivalent, + H: Hasher, +{ + #[must_use] + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]> { + match &mut self.map_impl { + MapTypes::Hash(m) => m.get_many_mut(keys), + MapTypes::Scanning(m) => m.get_many_mut(keys), + } + } +} + +impl MapQuery for FacadeHashMap +where + Q: ?Sized + Eq + Equivalent, + H: Hasher, +{ + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + match &self.map_impl { + MapTypes::Hash(m) => m.get(key), + MapTypes::Scanning(m) => m.get(key), + } + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + match &self.map_impl { + MapTypes::Hash(m) => m.get_key_value(key), + MapTypes::Scanning(m) => m.get_key_value(key), + } + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + match &mut self.map_impl { + MapTypes::Hash(m) => m.get_mut(key), + MapTypes::Scanning(m) => m.get_mut(key), + } + } +} + +impl MapIteration for FacadeHashMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a, + H: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a, + H: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a, + H: 'a; + + type IntoKeyIterator = IntoKeys; + type IntoValueIterator = IntoValues; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a, + H: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a, + H: 'a; + + fn iter(&self) -> Self::Iterator<'_> { + match &self.map_impl { + MapTypes::Hash(m) => m.iter(), + MapTypes::Scanning(m) => m.iter(), + } + } + + fn keys(&self) -> Self::KeyIterator<'_> { + match &self.map_impl { + MapTypes::Hash(m) => m.keys(), + MapTypes::Scanning(m) => m.keys(), + } + } + + fn values(&self) -> Self::ValueIterator<'_> { + match &self.map_impl { + MapTypes::Hash(m) => m.values(), + MapTypes::Scanning(m) => m.values(), + } + } + + fn into_keys(self) -> Self::IntoKeyIterator { + match self.map_impl { + MapTypes::Hash(m) => m.into_keys(), + MapTypes::Scanning(m) => m.into_keys(), + } + } + + fn into_values(self) -> Self::IntoValueIterator { + match self.map_impl { + MapTypes::Hash(m) => m.into_values(), + MapTypes::Scanning(m) => m.into_values(), + } + } + + fn iter_mut(&mut self) -> Self::MutIterator<'_> { + match &mut self.map_impl { + MapTypes::Hash(m) => m.iter_mut(), + MapTypes::Scanning(m) => m.iter_mut(), + } + } + + fn values_mut(&mut self) -> Self::ValueMutIterator<'_> { + match &mut self.map_impl { + MapTypes::Hash(m) => m.values_mut(), + MapTypes::Scanning(m) => m.values_mut(), + } + } +} + +impl Len for FacadeHashMap { + fn len(&self) -> usize { + match &self.map_impl { + MapTypes::Hash(m) => m.len(), + MapTypes::Scanning(m) => m.len(), + } + } +} + +impl Index<&Q> for FacadeHashMap +where + Q: ?Sized + Eq + Equivalent, + H: Hasher, +{ + type Output = V; + + fn index(&self, index: &Q) -> &Self::Output { + self.get(index).expect("index should be valid") + } +} + +impl<'a, K, V, H> IntoIterator for &'a FacadeHashMap { + type Item = (&'a K, &'a V); + type IntoIter = Iter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a, K, V, H> IntoIterator for &'a mut FacadeHashMap { + type Item = (&'a K, &'a mut V); + type IntoIter = IterMut<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl IntoIterator for FacadeHashMap { + type Item = (K, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self.map_impl { + MapTypes::Hash(m) => m.into_iter(), + MapTypes::Scanning(m) => m.into_iter(), + } + } +} + +impl PartialEq for FacadeHashMap +where + K: Eq, + V: PartialEq, + MT: Map, + H: Hasher, +{ + fn eq(&self, other: &MT) -> bool { + if self.len() != other.len() { + return false; + } + + self.iter() + .all(|(key, value)| other.get(key).map_or(false, |v| *value == *v)) + } +} + +impl Eq for FacadeHashMap +where + K: Eq, + V: Eq, + H: Hasher, +{ +} + +impl Debug for FacadeHashMap +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match &self.map_impl { + MapTypes::Hash(m) => m.fmt(f), + MapTypes::Scanning(m) => m.fmt(f), + } + } +} diff --git a/frozen-collections-core/src/facade_maps/facade_ordered_map.rs b/frozen-collections-core/src/facade_maps/facade_ordered_map.rs new file mode 100644 index 0000000..19e1091 --- /dev/null +++ b/frozen-collections-core/src/facade_maps/facade_ordered_map.rs @@ -0,0 +1,293 @@ +use crate::maps::{ + BinarySearchMap, EytzingerSearchMap, IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, + OrderedScanMap, Values, ValuesMut, +}; +use crate::traits::{Len, Map, MapIteration, MapQuery}; +use crate::utils::{dedup_by_keep_last, eytzinger_sort}; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Comparable; + +#[derive(Clone)] +enum MapTypes { + BinarySearch(BinarySearchMap), + EytzingerSearch(EytzingerSearchMap), + Scanning(OrderedScanMap), +} + +/// A map optimized for fast read access with ordered keys. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +/// # Alternate Choices +/// +/// If your keys are integers or enum variants, you should use the [`FacadeScalarMap`](crate::facade_maps::FacadeScalarMap) type instead. +/// If your keys are strings, you should use the [`FacadeStringMap`](crate::facade_maps::FacadeStringMap) type instead. Both of these will +/// deliver better performance since they are specifically optimized for those key types. +#[derive(Clone)] +pub struct FacadeOrderedMap { + map_impl: MapTypes, +} + +impl FacadeOrderedMap +where + K: Ord + Eq, +{ + /// Creates a frozen ordered map. + #[must_use] + pub fn new(mut entries: Vec<(K, V)>) -> Self { + entries.sort_by(|x, y| x.0.cmp(&y.0)); + dedup_by_keep_last(&mut entries, |x, y| x.0.eq(&y.0)); + + Self { + map_impl: if entries.len() < 5 { + MapTypes::Scanning(OrderedScanMap::new_raw(entries)) + } else if entries.len() < 64 { + MapTypes::BinarySearch(BinarySearchMap::new_raw(entries)) + } else { + eytzinger_sort(&mut entries); + MapTypes::EytzingerSearch(EytzingerSearchMap::new_raw(entries)) + }, + } + } +} + +impl Default for FacadeOrderedMap { + fn default() -> Self { + Self { + map_impl: MapTypes::Scanning(OrderedScanMap::default()), + } + } +} + +impl Map for FacadeOrderedMap +where + Q: ?Sized + Eq + Comparable, +{ + #[must_use] + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]> { + match &mut self.map_impl { + MapTypes::BinarySearch(m) => m.get_many_mut(keys), + MapTypes::EytzingerSearch(m) => m.get_many_mut(keys), + MapTypes::Scanning(m) => m.get_many_mut(keys), + } + } +} + +impl MapQuery for FacadeOrderedMap +where + Q: ?Sized + Eq + Comparable, +{ + #[inline] + #[must_use] + fn get(&self, key: &Q) -> Option<&V> { + match &self.map_impl { + MapTypes::BinarySearch(m) => m.get(key), + MapTypes::EytzingerSearch(m) => m.get(key), + MapTypes::Scanning(m) => m.get(key), + } + } + + #[inline] + #[must_use] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + match &self.map_impl { + MapTypes::BinarySearch(m) => m.get_key_value(key), + MapTypes::EytzingerSearch(m) => m.get_key_value(key), + MapTypes::Scanning(m) => m.get_key_value(key), + } + } + + #[inline] + #[must_use] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + match &mut self.map_impl { + MapTypes::BinarySearch(m) => m.get_mut(key), + MapTypes::EytzingerSearch(m) => m.get_mut(key), + MapTypes::Scanning(m) => m.get_mut(key), + } + } +} + +impl MapIteration for FacadeOrderedMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type IntoKeyIterator = IntoKeys; + type IntoValueIterator = IntoValues; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + fn iter(&self) -> Self::Iterator<'_> { + match &self.map_impl { + MapTypes::BinarySearch(m) => m.iter(), + MapTypes::EytzingerSearch(m) => m.iter(), + MapTypes::Scanning(m) => m.iter(), + } + } + + fn keys(&self) -> Self::KeyIterator<'_> { + match &self.map_impl { + MapTypes::BinarySearch(m) => m.keys(), + MapTypes::EytzingerSearch(m) => m.keys(), + MapTypes::Scanning(m) => m.keys(), + } + } + + fn values(&self) -> Self::ValueIterator<'_> { + match &self.map_impl { + MapTypes::BinarySearch(m) => m.values(), + MapTypes::EytzingerSearch(m) => m.values(), + MapTypes::Scanning(m) => m.values(), + } + } + + fn into_keys(self) -> Self::IntoKeyIterator { + match self.map_impl { + MapTypes::BinarySearch(m) => m.into_keys(), + MapTypes::EytzingerSearch(m) => m.into_keys(), + MapTypes::Scanning(m) => m.into_keys(), + } + } + + fn into_values(self) -> Self::IntoValueIterator { + match self.map_impl { + MapTypes::BinarySearch(m) => m.into_values(), + MapTypes::EytzingerSearch(m) => m.into_values(), + MapTypes::Scanning(m) => m.into_values(), + } + } + + fn iter_mut(&mut self) -> Self::MutIterator<'_> { + match &mut self.map_impl { + MapTypes::BinarySearch(m) => m.iter_mut(), + MapTypes::EytzingerSearch(m) => m.iter_mut(), + MapTypes::Scanning(m) => m.iter_mut(), + } + } + + fn values_mut(&mut self) -> Self::ValueMutIterator<'_> { + match &mut self.map_impl { + MapTypes::BinarySearch(m) => m.values_mut(), + MapTypes::EytzingerSearch(m) => m.values_mut(), + MapTypes::Scanning(m) => m.values_mut(), + } + } +} + +impl Len for FacadeOrderedMap { + fn len(&self) -> usize { + match &self.map_impl { + MapTypes::BinarySearch(m) => m.len(), + MapTypes::EytzingerSearch(m) => m.len(), + MapTypes::Scanning(m) => m.len(), + } + } +} + +impl Index<&Q> for FacadeOrderedMap +where + Q: ?Sized + Eq + Comparable, +{ + type Output = V; + + fn index(&self, index: &Q) -> &Self::Output { + self.get(index).expect("index should be valid") + } +} + +impl<'a, K, V> IntoIterator for &'a FacadeOrderedMap { + type Item = (&'a K, &'a V); + type IntoIter = Iter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a, K, V> IntoIterator for &'a mut FacadeOrderedMap { + type Item = (&'a K, &'a mut V); + type IntoIter = IterMut<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl IntoIterator for FacadeOrderedMap { + type Item = (K, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self.map_impl { + MapTypes::BinarySearch(m) => m.into_iter(), + MapTypes::EytzingerSearch(m) => m.into_iter(), + MapTypes::Scanning(m) => m.into_iter(), + } + } +} + +impl PartialEq for FacadeOrderedMap +where + K: Ord + Eq, + V: PartialEq, + MT: Map, +{ + fn eq(&self, other: &MT) -> bool { + if self.len() != other.len() { + return false; + } + + self.iter() + .all(|(key, value)| other.get(key).map_or(false, |v| *value == *v)) + } +} + +impl Eq for FacadeOrderedMap +where + K: Ord + Eq, + V: Eq, +{ +} + +impl Debug for FacadeOrderedMap +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match &self.map_impl { + MapTypes::BinarySearch(m) => m.fmt(f), + MapTypes::EytzingerSearch(m) => m.fmt(f), + MapTypes::Scanning(m) => m.fmt(f), + } + } +} diff --git a/frozen-collections-core/src/facade_maps/facade_scalar_map.rs b/frozen-collections-core/src/facade_maps/facade_scalar_map.rs new file mode 100644 index 0000000..e8838c1 --- /dev/null +++ b/frozen-collections-core/src/facade_maps/facade_scalar_map.rs @@ -0,0 +1,293 @@ +use crate::analyzers::{analyze_scalar_keys, ScalarKeyAnalysisResult}; +use crate::hashers::PassthroughHasher; +use crate::maps::{ + DenseScalarLookupMap, HashMap, IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, + SparseScalarLookupMap, Values, ValuesMut, +}; +use crate::traits::{LargeCollection, Len, Map, MapIteration, MapQuery, Scalar}; +use crate::utils::dedup_by_keep_last; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Equivalent; + +#[derive(Clone)] +enum MapTypes { + Hash(HashMap), + Dense(DenseScalarLookupMap), + Sparse(SparseScalarLookupMap), +} + +/// A map optimized for fast read access using scalar keys. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[derive(Clone)] +pub struct FacadeScalarMap { + map_impl: MapTypes, +} + +impl FacadeScalarMap +where + K: Scalar, +{ + /// Creates a frozen map. + #[must_use] + #[allow(clippy::missing_panics_doc)] + pub fn new(mut entries: Vec<(K, V)>) -> Self { + entries.sort_by(|x, y| x.0.cmp(&y.0)); + dedup_by_keep_last(&mut entries, |x, y| x.0.eq(&y.0)); + + Self { + map_impl: match analyze_scalar_keys(entries.iter().map(|x| x.0)) { + ScalarKeyAnalysisResult::DenseRange => { + MapTypes::Dense(DenseScalarLookupMap::new_raw(entries)) + } + ScalarKeyAnalysisResult::SparseRange => { + MapTypes::Sparse(SparseScalarLookupMap::new_raw(entries)) + } + ScalarKeyAnalysisResult::General => { + let h = PassthroughHasher::new(); + MapTypes::Hash(HashMap::new_half_baked(entries, h).unwrap()) + } + }, + } + } +} + +impl Default for FacadeScalarMap { + fn default() -> Self { + Self { + map_impl: MapTypes::Dense(DenseScalarLookupMap::default()), + } + } +} + +impl Map for FacadeScalarMap +where + K: Scalar + Eq + Equivalent, +{ + #[must_use] + fn get_many_mut(&mut self, keys: [&K; N]) -> Option<[&mut V; N]> { + match &mut self.map_impl { + MapTypes::Hash(m) => m.get_many_mut(keys), + MapTypes::Dense(m) => m.get_many_mut(keys), + MapTypes::Sparse(m) => m.get_many_mut(keys), + } + } +} + +impl MapQuery for FacadeScalarMap +where + K: Scalar + Eq + Equivalent, +{ + #[inline(always)] + #[must_use] + fn get(&self, key: &K) -> Option<&V> { + match &self.map_impl { + MapTypes::Hash(m) => m.get(key), + MapTypes::Dense(m) => m.get(key), + MapTypes::Sparse(m) => m.get(key), + } + } + + #[inline] + #[must_use] + fn get_key_value(&self, key: &K) -> Option<(&K, &V)> { + match &self.map_impl { + MapTypes::Hash(m) => m.get_key_value(key), + MapTypes::Dense(m) => m.get_key_value(key), + MapTypes::Sparse(m) => m.get_key_value(key), + } + } + + #[inline] + #[must_use] + fn get_mut(&mut self, key: &K) -> Option<&mut V> { + match &mut self.map_impl { + MapTypes::Hash(m) => m.get_mut(key), + MapTypes::Dense(m) => m.get_mut(key), + MapTypes::Sparse(m) => m.get_mut(key), + } + } +} + +impl MapIteration for FacadeScalarMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type IntoKeyIterator = IntoKeys; + type IntoValueIterator = IntoValues; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + fn iter(&self) -> Self::Iterator<'_> { + match &self.map_impl { + MapTypes::Hash(m) => m.iter(), + MapTypes::Dense(m) => m.iter(), + MapTypes::Sparse(m) => m.iter(), + } + } + + fn keys(&self) -> Self::KeyIterator<'_> { + match &self.map_impl { + MapTypes::Hash(m) => m.keys(), + MapTypes::Dense(m) => m.keys(), + MapTypes::Sparse(m) => m.keys(), + } + } + + fn values(&self) -> Self::ValueIterator<'_> { + match &self.map_impl { + MapTypes::Hash(m) => m.values(), + MapTypes::Dense(m) => m.values(), + MapTypes::Sparse(m) => m.values(), + } + } + + fn into_keys(self) -> Self::IntoKeyIterator { + match self.map_impl { + MapTypes::Hash(m) => m.into_keys(), + MapTypes::Dense(m) => m.into_keys(), + MapTypes::Sparse(m) => m.into_keys(), + } + } + + fn into_values(self) -> Self::IntoValueIterator { + match self.map_impl { + MapTypes::Hash(m) => m.into_values(), + MapTypes::Dense(m) => m.into_values(), + MapTypes::Sparse(m) => m.into_values(), + } + } + + fn iter_mut(&mut self) -> Self::MutIterator<'_> { + match &mut self.map_impl { + MapTypes::Hash(m) => m.iter_mut(), + MapTypes::Dense(m) => m.iter_mut(), + MapTypes::Sparse(m) => m.iter_mut(), + } + } + + fn values_mut(&mut self) -> Self::ValueMutIterator<'_> { + match &mut self.map_impl { + MapTypes::Hash(m) => m.values_mut(), + MapTypes::Dense(m) => m.values_mut(), + MapTypes::Sparse(m) => m.values_mut(), + } + } +} + +impl Len for FacadeScalarMap { + fn len(&self) -> usize { + match &self.map_impl { + MapTypes::Hash(m) => m.len(), + MapTypes::Dense(m) => m.len(), + MapTypes::Sparse(m) => m.len(), + } + } +} + +impl Index<&Q> for FacadeScalarMap +where + Q: Scalar + Eq + Equivalent, +{ + type Output = V; + + fn index(&self, index: &Q) -> &Self::Output { + self.get(index).expect("index should be valid") + } +} + +impl<'a, K, V> IntoIterator for &'a FacadeScalarMap { + type Item = (&'a K, &'a V); + type IntoIter = Iter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a, K, V> IntoIterator for &'a mut FacadeScalarMap { + type Item = (&'a K, &'a mut V); + type IntoIter = IterMut<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl IntoIterator for FacadeScalarMap { + type Item = (K, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self.map_impl { + MapTypes::Hash(m) => m.into_iter(), + MapTypes::Dense(m) => m.into_iter(), + MapTypes::Sparse(m) => m.into_iter(), + } + } +} + +impl PartialEq for FacadeScalarMap +where + K: Scalar, + V: PartialEq, + MT: Map, +{ + fn eq(&self, other: &MT) -> bool { + if self.len() != other.len() { + return false; + } + + self.iter() + .all(|(key, value)| other.get(key).map_or(false, |v| *value == *v)) + } +} + +impl Eq for FacadeScalarMap +where + K: Scalar, + V: Eq, +{ +} + +impl Debug for FacadeScalarMap +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match &self.map_impl { + MapTypes::Hash(m) => m.fmt(f), + MapTypes::Dense(m) => m.fmt(f), + MapTypes::Sparse(m) => m.fmt(f), + } + } +} diff --git a/frozen-collections-core/src/facade_maps/facade_string_map.rs b/frozen-collections-core/src/facade_maps/facade_string_map.rs new file mode 100644 index 0000000..43d6e5a --- /dev/null +++ b/frozen-collections-core/src/facade_maps/facade_string_map.rs @@ -0,0 +1,322 @@ +use crate::analyzers::{analyze_slice_keys, SliceKeyAnalysisResult}; +use crate::hashers::{BridgeHasher, LeftRangeHasher, RightRangeHasher}; +use crate::maps::{ + HashMap, IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut, +}; +use crate::traits::{Hasher, LargeCollection, Len, Map, MapIteration, MapQuery}; +use crate::utils::dedup_by_keep_last; +use ahash::RandomState; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::hash::{BuildHasher, Hash}; +use core::ops::Index; +use equivalent::Equivalent; + +#[derive(Clone)] +enum MapTypes { + LeftRange(HashMap>), + RightRange(HashMap>), + Hash(HashMap>), +} + +/// A map optimized for fast read access with string keys. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/hash_warning.md")] +#[derive(Clone)] +pub struct FacadeStringMap { + map_impl: MapTypes, +} + +impl<'a, V, BH> FacadeStringMap<&'a str, V, BH> +where + BH: BuildHasher, +{ + /// Creates a frozen map which uses the given hash builder to hash keys. + #[must_use] + #[allow(clippy::missing_panics_doc)] + pub fn new(mut entries: Vec<(&'a str, V)>, bh: BH) -> Self { + entries.sort_by(|x, y| x.0.cmp(y.0)); + dedup_by_keep_last(&mut entries, |x, y| x.0.eq(y.0)); + + Self { + map_impl: { + match analyze_slice_keys(entries.iter().map(|x| x.0.as_bytes()), &bh) { + SliceKeyAnalysisResult::General | SliceKeyAnalysisResult::Length => { + let h = BridgeHasher::new(bh); + MapTypes::Hash(HashMap::new_half_baked(entries, h).unwrap()) + } + + SliceKeyAnalysisResult::LeftHandSubslice(range) => { + let h = LeftRangeHasher::new(bh, range); + MapTypes::LeftRange(HashMap::new_half_baked(entries, h).unwrap()) + } + + SliceKeyAnalysisResult::RightHandSubslice(range) => { + let h = RightRangeHasher::new(bh, range); + MapTypes::RightRange(HashMap::new_half_baked(entries, h).unwrap()) + } + } + }, + } + } +} + +impl Default for FacadeStringMap +where + BH: Default, +{ + fn default() -> Self { + Self { + map_impl: MapTypes::Hash(HashMap::>::default()), + } + } +} + +impl Map for FacadeStringMap +where + Q: ?Sized + Hash + Eq + Len + Equivalent, + BH: BuildHasher, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + #[must_use] + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]> { + match &mut self.map_impl { + MapTypes::LeftRange(m) => m.get_many_mut(keys), + MapTypes::RightRange(m) => m.get_many_mut(keys), + MapTypes::Hash(m) => m.get_many_mut(keys), + } + } +} + +impl MapQuery for FacadeStringMap +where + Q: ?Sized + Hash + Eq + Len + Equivalent, + BH: BuildHasher, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + #[inline] + #[must_use] + fn get(&self, key: &Q) -> Option<&V> { + match &self.map_impl { + MapTypes::LeftRange(m) => m.get(key), + MapTypes::RightRange(m) => m.get(key), + MapTypes::Hash(m) => m.get(key), + } + } + + #[inline] + #[must_use] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + match &self.map_impl { + MapTypes::LeftRange(m) => m.get_key_value(key), + MapTypes::RightRange(m) => m.get_key_value(key), + MapTypes::Hash(m) => m.get_key_value(key), + } + } + + #[inline] + #[must_use] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + match &mut self.map_impl { + MapTypes::LeftRange(m) => m.get_mut(key), + MapTypes::RightRange(m) => m.get_mut(key), + MapTypes::Hash(m) => m.get_mut(key), + } + } +} + +impl MapIteration for FacadeStringMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type IntoKeyIterator = IntoKeys; + type IntoValueIterator = IntoValues; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + fn iter(&self) -> Self::Iterator<'_> { + match &self.map_impl { + MapTypes::LeftRange(m) => m.iter(), + MapTypes::RightRange(m) => m.iter(), + MapTypes::Hash(m) => m.iter(), + } + } + + fn keys(&self) -> Self::KeyIterator<'_> { + match &self.map_impl { + MapTypes::LeftRange(m) => m.keys(), + MapTypes::RightRange(m) => m.keys(), + MapTypes::Hash(m) => m.keys(), + } + } + + fn values(&self) -> Self::ValueIterator<'_> { + match &self.map_impl { + MapTypes::LeftRange(m) => m.values(), + MapTypes::RightRange(m) => m.values(), + MapTypes::Hash(m) => m.values(), + } + } + + fn into_keys(self) -> Self::IntoKeyIterator { + match self.map_impl { + MapTypes::LeftRange(m) => m.into_keys(), + MapTypes::RightRange(m) => m.into_keys(), + MapTypes::Hash(m) => m.into_keys(), + } + } + + fn into_values(self) -> Self::IntoValueIterator { + match self.map_impl { + MapTypes::LeftRange(m) => m.into_values(), + MapTypes::RightRange(m) => m.into_values(), + MapTypes::Hash(m) => m.into_values(), + } + } + + fn iter_mut(&mut self) -> Self::MutIterator<'_> { + match &mut self.map_impl { + MapTypes::LeftRange(m) => m.iter_mut(), + MapTypes::RightRange(m) => m.iter_mut(), + MapTypes::Hash(m) => m.iter_mut(), + } + } + + fn values_mut(&mut self) -> Self::ValueMutIterator<'_> { + match &mut self.map_impl { + MapTypes::LeftRange(m) => m.values_mut(), + MapTypes::RightRange(m) => m.values_mut(), + MapTypes::Hash(m) => m.values_mut(), + } + } +} + +impl Len for FacadeStringMap { + fn len(&self) -> usize { + match &self.map_impl { + MapTypes::LeftRange(m) => m.len(), + MapTypes::RightRange(m) => m.len(), + MapTypes::Hash(m) => m.len(), + } + } +} + +impl Index<&Q> for FacadeStringMap +where + Q: ?Sized + Hash + Eq + Len + Equivalent, + BH: BuildHasher, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + type Output = V; + + fn index(&self, index: &Q) -> &Self::Output { + self.get(index).expect("index should be valid") + } +} + +impl<'a, K, V, BH> IntoIterator for &'a FacadeStringMap { + type Item = (&'a K, &'a V); + type IntoIter = Iter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a, K, V, BH> IntoIterator for &'a mut FacadeStringMap { + type Item = (&'a K, &'a mut V); + type IntoIter = IterMut<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl IntoIterator for FacadeStringMap { + type Item = (K, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self.map_impl { + MapTypes::LeftRange(m) => m.into_iter(), + MapTypes::RightRange(m) => m.into_iter(), + MapTypes::Hash(m) => m.into_iter(), + } + } +} + +impl PartialEq for FacadeStringMap +where + K: Hash + Eq + Len + Equivalent, + V: PartialEq, + MT: Map, + BH: BuildHasher, +{ + fn eq(&self, other: &MT) -> bool { + if self.len() != other.len() { + return false; + } + + self.iter() + .all(|(key, value)| other.get(key).map_or(false, |v| *value == *v)) + } +} + +impl Eq for FacadeStringMap +where + K: Hash + Eq + Len + Equivalent, + V: Eq, + BH: BuildHasher, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ +} + +impl Debug for FacadeStringMap +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match &self.map_impl { + MapTypes::LeftRange(m) => m.fmt(f), + MapTypes::RightRange(m) => m.fmt(f), + MapTypes::Hash(m) => m.fmt(f), + } + } +} diff --git a/frozen-collections-core/src/facade_maps/mod.rs b/frozen-collections-core/src/facade_maps/mod.rs new file mode 100644 index 0000000..5c080d4 --- /dev/null +++ b/frozen-collections-core/src/facade_maps/mod.rs @@ -0,0 +1,11 @@ +//! Wrappers around other map types allowing runtime selection of implementation type based on input. + +pub use facade_hash_map::FacadeHashMap; +pub use facade_ordered_map::FacadeOrderedMap; +pub use facade_scalar_map::FacadeScalarMap; +pub use facade_string_map::FacadeStringMap; + +mod facade_hash_map; +mod facade_ordered_map; +mod facade_scalar_map; +mod facade_string_map; diff --git a/frozen-collections-core/src/facade_sets/facade_hash_set.rs b/frozen-collections-core/src/facade_sets/facade_hash_set.rs new file mode 100644 index 0000000..10fc6d3 --- /dev/null +++ b/frozen-collections-core/src/facade_sets/facade_hash_set.rs @@ -0,0 +1,152 @@ +use crate::facade_maps::FacadeHashMap; +use crate::hashers::BridgeHasher; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, into_iter_fn, into_iter_ref_fn, partial_eq_fn, + set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Hasher, Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Equivalent; + +/// A set optimized for fast read access with hashable values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/hash_warning.md")] +/// +/// # Alternate Choices +/// +/// If your values are integers or enum variants, you should use the [`FacadeScalarSet`](crate::facade_sets::FacadeScalarSet) type instead. +/// If your values are strings, you should use the [`FacadeStringSet`](crate::facade_sets::FacadeStringSet) type instead. Both of these will +/// deliver better performance since they are specifically optimized for those value types. +#[derive(Clone)] +pub struct FacadeHashSet { + map: FacadeHashMap, +} + +impl FacadeHashSet +where + T: Eq, + H: Hasher, +{ + /// Creates a new frozen set which uses the given hash builder to hash values. + #[must_use] + pub const fn new(map: FacadeHashMap) -> Self { + Self { map } + } +} + +impl Default for FacadeHashSet +where + H: Default, +{ + fn default() -> Self { + Self { + map: FacadeHashMap::default(), + } + } +} + +impl Set for FacadeHashSet +where + Q: Eq + Equivalent, + H: Hasher, +{ +} + +impl SetQuery for FacadeHashSet +where + Q: Eq + Equivalent, + H: Hasher, +{ + #[inline] + fn get(&self, value: &Q) -> Option<&T> { + Some(self.map.get_key_value(value)?.0) + } +} + +impl SetIteration for FacadeHashSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a, + H: 'a; + + set_iteration_funcs!(); +} + +impl Len for FacadeHashSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &FacadeHashSet +where + T: Hash + Eq + Clone, + ST: Set, + H: Hasher + Default, +{ + bitor_fn!(H); +} + +impl BitAnd<&ST> for &FacadeHashSet +where + T: Hash + Eq + Clone, + ST: Set, + H: Hasher + Default, +{ + bitand_fn!(H); +} + +impl BitXor<&ST> for &FacadeHashSet +where + T: Hash + Eq + Clone, + ST: Set, + H: Hasher + Default, +{ + bitxor_fn!(H); +} + +impl Sub<&ST> for &FacadeHashSet +where + T: Hash + Eq + Clone, + ST: Set, + H: Hasher + Default, +{ + sub_fn!(H); +} + +impl IntoIterator for FacadeHashSet { + into_iter_fn!(); +} + +impl<'a, T, H> IntoIterator for &'a FacadeHashSet { + into_iter_ref_fn!(); +} + +impl PartialEq for FacadeHashSet +where + T: Eq, + ST: Set, + H: Hasher, +{ + partial_eq_fn!(); +} + +impl Eq for FacadeHashSet +where + T: Eq, + H: Hasher, +{ +} + +impl Debug for FacadeHashSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/facade_sets/facade_ordered_set.rs b/frozen-collections-core/src/facade_sets/facade_ordered_set.rs new file mode 100644 index 0000000..fae6936 --- /dev/null +++ b/frozen-collections-core/src/facade_sets/facade_ordered_set.rs @@ -0,0 +1,127 @@ +use crate::facade_maps::FacadeOrderedMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Comparable; + +/// A set optimized for fast read access with ordered values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +/// # Alternate Choices +/// +/// If your values are integers or enum variants, you should use the [`FacadeScalarSet`](crate::facade_sets::FacadeScalarSet) type instead. +/// If your values are strings, you should use the [`FacadeStringSet`](crate::facade_sets::FacadeStringSet) type instead. Both of these will +/// deliver better performance since they are specifically optimized for those value types. +#[derive(Clone)] +pub struct FacadeOrderedSet { + map: FacadeOrderedMap, +} + +impl FacadeOrderedSet +where + T: Ord + Eq, +{ + /// Creates a new frozen ordered set. + #[must_use] + pub const fn new(map: FacadeOrderedMap) -> Self { + Self { map } + } +} + +impl Default for FacadeOrderedSet { + fn default() -> Self { + Self { + map: FacadeOrderedMap::default(), + } + } +} + +impl Set for FacadeOrderedSet where Q: ?Sized + Ord + Comparable {} + +impl SetQuery for FacadeOrderedSet +where + Q: ?Sized + Ord + Comparable, +{ + get_fn!(); +} + +impl SetIteration for FacadeOrderedSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for FacadeOrderedSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &FacadeOrderedSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &FacadeOrderedSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &FacadeOrderedSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &FacadeOrderedSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for FacadeOrderedSet { + into_iter_fn!(); +} + +impl<'a, T> IntoIterator for &'a FacadeOrderedSet { + into_iter_ref_fn!(); +} + +impl PartialEq for FacadeOrderedSet +where + T: Ord, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for FacadeOrderedSet where T: Ord {} + +impl Debug for FacadeOrderedSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/facade_sets/facade_scalar_set.rs b/frozen-collections-core/src/facade_sets/facade_scalar_set.rs new file mode 100644 index 0000000..834e072 --- /dev/null +++ b/frozen-collections-core/src/facade_sets/facade_scalar_set.rs @@ -0,0 +1,122 @@ +use crate::facade_maps::FacadeScalarMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, into_iter_fn, into_iter_ref_fn, partial_eq_fn, + set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Scalar, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; + +/// A set optimized for fast read access with integer or enum values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[derive(Clone)] +pub struct FacadeScalarSet { + map: FacadeScalarMap, +} + +impl FacadeScalarSet +where + T: Scalar, +{ + /// Creates a new frozen set. + #[must_use] + pub const fn new(map: FacadeScalarMap) -> Self { + Self { map } + } +} + +impl Default for FacadeScalarSet { + fn default() -> Self { + Self { + map: FacadeScalarMap::default(), + } + } +} + +impl Set for FacadeScalarSet where T: Scalar {} + +impl SetQuery for FacadeScalarSet +where + T: Scalar, +{ + #[inline] + fn get(&self, value: &T) -> Option<&T> { + Some(self.map.get_key_value(value)?.0) + } +} + +impl SetIteration for FacadeScalarSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for FacadeScalarSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &FacadeScalarSet +where + T: Hash + Eq + Scalar + Clone, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &FacadeScalarSet +where + T: Hash + Eq + Scalar + Clone, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &FacadeScalarSet +where + T: Hash + Eq + Scalar + Clone, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &FacadeScalarSet +where + T: Hash + Eq + Scalar + Clone, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for FacadeScalarSet { + into_iter_fn!(); +} + +impl<'a, T> IntoIterator for &'a FacadeScalarSet { + into_iter_ref_fn!(); +} + +impl PartialEq for FacadeScalarSet +where + T: Scalar, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for FacadeScalarSet where T: Scalar {} + +impl Debug for FacadeScalarSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/facade_sets/facade_string_set.rs b/frozen-collections-core/src/facade_sets/facade_string_set.rs new file mode 100644 index 0000000..7dbfeb8 --- /dev/null +++ b/frozen-collections-core/src/facade_sets/facade_string_set.rs @@ -0,0 +1,160 @@ +use crate::facade_maps::FacadeStringMap; +use crate::hashers::{LeftRangeHasher, RightRangeHasher}; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, into_iter_fn, into_iter_ref_fn, partial_eq_fn, + set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Hasher, Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; +use ahash::RandomState; +use core::fmt::Debug; +use core::hash::BuildHasher; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Equivalent; + +/// A set optimized for fast read access with string values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/hash_warning.md")] +#[derive(Clone)] +pub struct FacadeStringSet { + map: FacadeStringMap, +} + +impl<'a, BH> FacadeStringSet<&'a str, BH> { + /// Creates a new frozen set which uses the given hash builder to hash values. + #[must_use] + pub const fn new(map: FacadeStringMap<&'a str, (), BH>) -> Self { + Self { map } + } +} + +impl Default for FacadeStringSet +where + BH: Default, +{ + fn default() -> Self { + Self { + map: FacadeStringMap::default(), + } + } +} + +impl Set for FacadeStringSet +where + Q: Hash + Eq + Len + Equivalent, + BH: BuildHasher, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ +} + +impl SetQuery for FacadeStringSet +where + Q: Hash + Eq + Len + Equivalent, + BH: BuildHasher, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + #[inline] + fn get(&self, value: &Q) -> Option<&T> { + Some(self.map.get_key_value(value)?.0) + } +} + +impl SetIteration for FacadeStringSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a, + BH: 'a; + + set_iteration_funcs!(); +} + +impl Len for FacadeStringSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &FacadeStringSet +where + T: Hash + Eq + Len + Clone, + ST: Set, + BH: BuildHasher + Default, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + bitor_fn!(H); +} + +impl BitAnd<&ST> for &FacadeStringSet +where + T: Hash + Eq + Len + Clone, + ST: Set, + BH: BuildHasher + Default, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + bitand_fn!(H); +} + +impl BitXor<&ST> for &FacadeStringSet +where + T: Hash + Eq + Len + Clone, + ST: Set, + BH: BuildHasher + Default, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + bitxor_fn!(H); +} + +impl Sub<&ST> for &FacadeStringSet +where + T: Hash + Eq + Len + Clone, + ST: Set, + BH: BuildHasher + Default, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + sub_fn!(H); +} + +impl IntoIterator for FacadeStringSet { + into_iter_fn!(); +} + +impl<'a, T, BH> IntoIterator for &'a FacadeStringSet { + into_iter_ref_fn!(); +} + +impl PartialEq for FacadeStringSet +where + T: Hash + Eq + Len, + ST: Set, + BH: BuildHasher + Default, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ + partial_eq_fn!(); +} + +impl Eq for FacadeStringSet +where + T: Hash + Eq + Len, + BH: BuildHasher + Default, + LeftRangeHasher: Hasher, + RightRangeHasher: Hasher, +{ +} + +impl Debug for FacadeStringSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/facade_sets/mod.rs b/frozen-collections-core/src/facade_sets/mod.rs new file mode 100644 index 0000000..1519825 --- /dev/null +++ b/frozen-collections-core/src/facade_sets/mod.rs @@ -0,0 +1,11 @@ +//! Wrappers around other set types allowing runtime selection of implementation type based on input. + +pub use facade_hash_set::FacadeHashSet; +pub use facade_ordered_set::FacadeOrderedSet; +pub use facade_scalar_set::FacadeScalarSet; +pub use facade_string_set::FacadeStringSet; + +mod facade_hash_set; +mod facade_ordered_set; +mod facade_scalar_set; +mod facade_string_set; diff --git a/frozen-collections-core/src/hash_tables/hash_table.rs b/frozen-collections-core/src/hash_tables/hash_table.rs new file mode 100644 index 0000000..f8556e0 --- /dev/null +++ b/frozen-collections-core/src/hash_tables/hash_table.rs @@ -0,0 +1,149 @@ +use alloc::boxed::Box; +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; +use core::ops::Range; + +use crate::analyzers::analyze_hash_codes; +use crate::hash_tables::HashTableSlot; +use crate::traits::{CollectionMagnitude, Len, SmallCollection}; + +/// A general purpose hash table. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +/// +/// The `CM` type parameter is the collection magnitude, which +/// determines the maximum number of entries that can be stored in the hash table. +/// +/// This implementation always has a power-of-two number of hash slots. This speeds up +/// lookups by avoiding the need to perform a modulo operation. +#[derive(Clone)] +pub struct HashTable { + mask: u64, + pub(crate) slots: Box<[HashTableSlot]>, + pub(crate) entries: Box<[T]>, +} + +struct PrepItem { + pub hash_slot_index: usize, + pub entry: T, +} + +impl HashTable +where + CM: CollectionMagnitude, +{ + /// Creates a new hash table. + /// + /// This function assumes that there are no duplicates in the input vector. + #[allow(clippy::cast_possible_truncation)] + pub fn new(mut entries: Vec, hash: F) -> Result + where + F: Fn(&T) -> u64, + { + if entries.is_empty() { + return Ok(Self::default()); + } else if entries.len() > CM::MAX_CAPACITY { + return Err("too many entries for the selected collection magnitude".to_string()); + } + + let num_hash_slots = analyze_hash_codes(entries.iter().map(&hash)).num_hash_slots; + + let mut prep_items = Vec::with_capacity(entries.len()); + while let Some(entry) = entries.pop() { + let hash_code = hash(&entry); + let hash_slot_index = (hash_code % num_hash_slots as u64) as usize; + + prep_items.push(PrepItem { + hash_slot_index, + entry, + }); + } + + // sort items so hash collisions are contiguous. + prep_items.sort_unstable_by(|x, y| x.hash_slot_index.cmp(&y.hash_slot_index)); + + let mut entry_index = 0; + let mut slots = Vec::with_capacity(num_hash_slots); + let mut final_entries = entries; + + slots.resize_with(num_hash_slots, || HashTableSlot::new(CM::ZERO, CM::ZERO)); + + while let Some(mut item) = prep_items.pop() { + let hash_slot_index = item.hash_slot_index; + let mut num_entries_in_hash_slot = 0; + + loop { + final_entries.push(item.entry); + num_entries_in_hash_slot += 1; + + if let Some(last) = prep_items.last() { + if last.hash_slot_index == hash_slot_index { + item = prep_items.pop().unwrap(); + continue; + } + } + + break; + } + + slots[hash_slot_index] = HashTableSlot::new( + CM::try_from(entry_index).unwrap_or(CM::ZERO), + CM::try_from(entry_index + num_entries_in_hash_slot).unwrap_or(CM::ZERO), + ); + + entry_index += num_entries_in_hash_slot; + } + + Ok(Self { + mask: (slots.len() - 1) as u64, + slots: slots.into_boxed_slice(), + entries: final_entries.into_boxed_slice(), + }) + } +} + +impl HashTable +where + CM: CollectionMagnitude, +{ + #[inline] + #[allow(clippy::cast_possible_truncation)] + pub fn find(&self, hash_code: u64, mut eq: impl FnMut(&T) -> bool) -> Option<&T> { + let hash_slot_index = (hash_code & self.mask) as usize; + let hash_slot = unsafe { self.slots.get_unchecked(hash_slot_index) }; + let range: Range = hash_slot.min_index.into()..hash_slot.max_index.into(); + let entries = unsafe { self.entries.get_unchecked(range) }; + entries.iter().find(|entry| eq(entry)) + } + + #[inline] + #[allow(clippy::cast_possible_truncation)] + pub fn find_mut(&mut self, hash_code: u64, mut eq: impl FnMut(&T) -> bool) -> Option<&mut T> { + let hash_slot_index = (hash_code & self.mask) as usize; + let hash_slot = unsafe { self.slots.get_unchecked(hash_slot_index) }; + let range: Range = hash_slot.min_index.into()..hash_slot.max_index.into(); + let entries = unsafe { self.entries.get_unchecked_mut(range) }; + entries.iter_mut().find(|entry| eq(entry)) + } +} + +impl Len for HashTable { + #[inline] + fn len(&self) -> usize { + self.entries.len() + } +} + +impl Default for HashTable +where + CM: CollectionMagnitude, +{ + fn default() -> Self { + Self { + mask: 0, + slots: vec![HashTableSlot::new(CM::ZERO, CM::ZERO)].into_boxed_slice(), + entries: Box::new([]), + } + } +} diff --git a/frozen-collections-core/src/hash_tables/hash_table_slot.rs b/frozen-collections-core/src/hash_tables/hash_table_slot.rs new file mode 100644 index 0000000..6880c8d --- /dev/null +++ b/frozen-collections-core/src/hash_tables/hash_table_slot.rs @@ -0,0 +1,21 @@ +/// An individual slot in a hash table. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +/// +/// A slot contains the range of indices in the table's entry vector +/// that contain entries that hash to this slot. +#[derive(Clone)] +#[allow(clippy::module_name_repetitions)] +pub struct HashTableSlot { + pub(crate) min_index: CM, + pub(crate) max_index: CM, +} + +impl HashTableSlot { + pub const fn new(min_index: CM, max_index: CM) -> Self { + Self { + min_index, + max_index, + } + } +} diff --git a/frozen-collections-core/src/hash_tables/inline_hash_table.rs b/frozen-collections-core/src/hash_tables/inline_hash_table.rs new file mode 100644 index 0000000..869df3d --- /dev/null +++ b/frozen-collections-core/src/hash_tables/inline_hash_table.rs @@ -0,0 +1,65 @@ +use crate::hash_tables::HashTableSlot; +use crate::traits::{CollectionMagnitude, SmallCollection}; +use core::ops::Range; + +/// A hash table that stores its entries inline. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +/// +/// # Type Parameters +/// +/// - `T`: The data held in the hash table. +/// - `CM`: The magnitude of the collection. +/// - `SZ`: The length of the map. +/// - `NHS`: The number of hash table slots. +/// +/// This implementation always has a power-of-two number of hash slots. This speeds up +/// lookups by avoiding the need to perform a modulo operation. +#[derive(Clone)] +pub struct InlineHashTable { + mask: u64, + slots: [HashTableSlot; NHS], + pub(crate) entries: [T; SZ], +} + +impl InlineHashTable { + /// Creates a new hash table. + /// + /// This function assumes that the slots and processed entries are in proper order. + pub const fn new_raw(slots: [HashTableSlot; NHS], processed_entries: [T; SZ]) -> Self { + Self { + mask: (NHS - 1) as u64, + slots, + entries: processed_entries, + } + } +} + +impl InlineHashTable +where + CM: CollectionMagnitude, +{ + #[inline] + #[allow(clippy::cast_possible_truncation)] + pub(crate) fn find(&self, hash_code: u64, mut eq: impl FnMut(&T) -> bool) -> Option<&T> { + let hash_slot_index = (hash_code & self.mask) as usize; + let hash_slot = unsafe { self.slots.get_unchecked(hash_slot_index) }; + let range: Range = hash_slot.min_index.into()..hash_slot.max_index.into(); + let entries = unsafe { self.entries.get_unchecked(range) }; + entries.iter().find(|entry| eq(entry)) + } + + #[inline] + #[allow(clippy::cast_possible_truncation)] + pub(crate) fn find_mut( + &mut self, + hash_code: u64, + mut eq: impl FnMut(&T) -> bool, + ) -> Option<&mut T> { + let hash_slot_index = (hash_code & self.mask) as usize; + let hash_slot = unsafe { self.slots.get_unchecked(hash_slot_index) }; + let range: Range = hash_slot.min_index.into()..hash_slot.max_index.into(); + let entries = unsafe { self.entries.get_unchecked_mut(range) }; + entries.iter_mut().find(|entry| eq(entry)) + } +} diff --git a/frozen-collections-core/src/hash_tables/mod.rs b/frozen-collections-core/src/hash_tables/mod.rs new file mode 100644 index 0000000..e22a707 --- /dev/null +++ b/frozen-collections-core/src/hash_tables/mod.rs @@ -0,0 +1,11 @@ +//! Foundational hash table design. + +pub(crate) use crate::hash_tables::hash_table::HashTable; +pub use crate::hash_tables::hash_table_slot::HashTableSlot; +pub use crate::hash_tables::inline_hash_table::InlineHashTable; +// pub use crate::hash_tables::partially_inline_hash_table::PartiallyInlineHashTable; + +mod hash_table; +mod hash_table_slot; +mod inline_hash_table; +// mod partially_inline_hash_table; diff --git a/frozen-collections-core/src/hashers/bridge_hasher.rs b/frozen-collections-core/src/hashers/bridge_hasher.rs new file mode 100644 index 0000000..1bf173c --- /dev/null +++ b/frozen-collections-core/src/hashers/bridge_hasher.rs @@ -0,0 +1,37 @@ +use crate::traits::Hasher; +use ahash::RandomState; +use core::hash::{BuildHasher, Hash}; + +/// Wraps a normal [`BuildHasher`]. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[derive(Clone)] +pub struct BridgeHasher { + bh: BH, +} + +impl BridgeHasher { + #[must_use] + pub const fn new(bh: BH) -> Self { + Self { bh } + } +} + +impl Hasher for BridgeHasher +where + T: ?Sized + Hash, + BH: BuildHasher, +{ + fn hash(&self, value: &T) -> u64 { + self.bh.hash_one(value) + } +} + +impl Default for BridgeHasher +where + BH: Default, +{ + fn default() -> Self { + Self::new(BH::default()) + } +} diff --git a/frozen-collections-core/src/hashers/inline_left_range_hasher.rs b/frozen-collections-core/src/hashers/inline_left_range_hasher.rs new file mode 100644 index 0000000..6cf9661 --- /dev/null +++ b/frozen-collections-core/src/hashers/inline_left_range_hasher.rs @@ -0,0 +1,124 @@ +use crate::traits::Hasher; +use ahash::RandomState; +use alloc::string::String; +use core::hash::{BuildHasher, Hash}; + +/// Hashes a portion of a left-aligned slice. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[derive(Clone)] +pub struct InlineLeftRangeHasher +{ + bh: BH, +} + +impl + InlineLeftRangeHasher +{ + #[must_use] + pub const fn new(bh: BH) -> Self { + Self { bh } + } +} + +impl Hasher<[T]> + for InlineLeftRangeHasher +where + T: Hash, + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &[T]) -> u64 { + if value.len() < RANGE_END { + return 0; + } + + self.bh.hash_one(&value[RANGE_START..RANGE_END]) + } +} + +impl Hasher + for InlineLeftRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &String) -> u64 { + let b = value.as_bytes(); + if b.len() < RANGE_END { + return 0; + } + + self.bh.hash_one(&b[RANGE_START..RANGE_END]) + } +} + +impl Hasher<&str> + for InlineLeftRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &&str) -> u64 { + let b = value.as_bytes(); + if b.len() < RANGE_END { + return 0; + } + + self.bh.hash_one(&b[RANGE_START..RANGE_END]) + } +} + +impl Hasher + for InlineLeftRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &str) -> u64 { + let b = value.as_bytes(); + if b.len() < RANGE_END { + return 0; + } + + self.bh.hash_one(&b[RANGE_START..RANGE_END]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use alloc::vec; + + #[test] + fn test_left_range_hasher_hash_slice() { + let hasher = InlineLeftRangeHasher::<0, 3>::new(RandomState::default()); + assert_eq!( + hasher.hash(vec![1, 2, 3, 4].as_slice()), + hasher.bh.hash_one(vec![1, 2, 3].as_slice()) + ); + assert_eq!(hasher.hash(vec![1, 2].as_slice()), 0); + } + + #[test] + fn test_left_range_hasher_hash_string() { + let hasher = InlineLeftRangeHasher::<0, 3>::new(RandomState::default()); + assert_eq!(hasher.hash(&"abcd".to_string()), hasher.bh.hash_one(b"abc")); + assert_eq!(hasher.hash(&"ab".to_string()), 0); + } + + #[test] + fn test_left_range_hasher_hash_str_ref() { + let hasher = InlineLeftRangeHasher::<0, 3>::new(RandomState::default()); + assert_eq!(hasher.hash(&"abcd"), hasher.bh.hash_one(b"abc")); + assert_eq!(hasher.hash(&"ab"), 0); + } + + #[test] + fn test_left_range_hasher_hash_str() { + let hasher = InlineLeftRangeHasher::<0, 3>::new(RandomState::default()); + assert_eq!(hasher.hash("abcd"), hasher.bh.hash_one(b"abc")); + assert_eq!(hasher.hash("ab"), 0); + } +} diff --git a/frozen-collections-core/src/hashers/inline_right_range_hasher.rs b/frozen-collections-core/src/hashers/inline_right_range_hasher.rs new file mode 100644 index 0000000..a9b5714 --- /dev/null +++ b/frozen-collections-core/src/hashers/inline_right_range_hasher.rs @@ -0,0 +1,144 @@ +use crate::traits::Hasher; +use ahash::RandomState; +use alloc::string::String; +use core::hash::{BuildHasher, Hash}; + +/// Hashes a portion of a right-aligned slice. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[derive(Clone)] +pub struct InlineRightRangeHasher< + const RANGE_START: usize, + const RANGE_END: usize, + BH = RandomState, +> { + bh: BH, +} + +impl + InlineRightRangeHasher +{ + #[must_use] + pub const fn new(bh: BH) -> Self { + Self { bh } + } +} + +impl Hasher<[T]> + for InlineRightRangeHasher +where + T: Hash, + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &[T]) -> u64 { + if value.len() < RANGE_END { + return 0; + } + + let effective_range = value.len() - RANGE_END..value.len() - RANGE_START; + self.bh.hash_one(&value[effective_range]) + } +} + +impl Hasher + for InlineRightRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &String) -> u64 { + let b = value.as_bytes(); + if b.len() < RANGE_END { + return 0; + } + + let effective_range = value.len() - RANGE_END..value.len() - RANGE_START; + self.bh.hash_one(&b[effective_range]) + } +} + +impl Hasher<&str> + for InlineRightRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &&str) -> u64 { + let b = value.as_bytes(); + if b.len() < RANGE_END { + return 0; + } + + let effective_range = value.len() - RANGE_END..value.len() - RANGE_START; + self.bh.hash_one(&b[effective_range]) + } +} + +impl Hasher + for InlineRightRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &str) -> u64 { + let b = value.as_bytes(); + if b.len() < RANGE_END { + return 0; + } + + let effective_range = value.len() - RANGE_END..value.len() - RANGE_START; + self.bh.hash_one(&b[effective_range]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use alloc::vec; + + #[test] + fn test_right_range_hasher_hash_slice() { + let hasher = InlineRightRangeHasher::<1, 3>::new(RandomState::default()); + assert_eq!( + hasher.hash(vec![1, 2, 3, 4].as_slice()), + hasher.bh.hash_one(vec![2, 3].as_slice()) + ); + assert_eq!( + hasher.hash(vec![1, 2, 3, 4, 5, 6].as_slice()), + hasher.bh.hash_one(vec![4, 5].as_slice()) + ); + assert_eq!(hasher.hash(vec![1, 2].as_slice()), 0); + } + + #[test] + fn test_right_range_hasher_hash_string() { + let hasher = InlineRightRangeHasher::<3, 5>::new(RandomState::default()); + assert_eq!( + hasher.hash(&"abcdef".to_string()), + hasher.bh.hash_one(b"bc") + ); + assert_eq!( + hasher.hash(&"abcdefghijklmn".to_string()), + hasher.bh.hash_one(b"jk") + ); + assert_eq!(hasher.hash(&"a".to_string()), 0); + } + + #[test] + fn test_right_range_hasher_hash_str_ref() { + let hasher = InlineRightRangeHasher::<1, 3>::new(RandomState::default()); + assert_eq!(hasher.hash(&"abcd"), hasher.bh.hash_one(b"bc")); + assert_eq!(hasher.hash(&"abcdefghijklmn"), hasher.bh.hash_one(b"lm")); + assert_eq!(hasher.hash(&"a"), 0); + } + + #[test] + fn test_right_range_hasher_hash_str() { + let hasher = InlineRightRangeHasher::<1, 3>::new(RandomState::default()); + assert_eq!(hasher.hash("abcd"), hasher.bh.hash_one(b"bc")); + assert_eq!(hasher.hash("abcdefghijklmn"), hasher.bh.hash_one(b"lm")); + assert_eq!(hasher.hash("a"), 0); + } +} diff --git a/frozen-collections-core/src/hashers/left_range_hasher.rs b/frozen-collections-core/src/hashers/left_range_hasher.rs new file mode 100644 index 0000000..bd3faec --- /dev/null +++ b/frozen-collections-core/src/hashers/left_range_hasher.rs @@ -0,0 +1,134 @@ +use crate::traits::Hasher; +use ahash::RandomState; +use alloc::string::String; +use core::hash::{BuildHasher, Hash}; +use core::ops::Range; + +/// Hashes a portion of a left-aligned slice. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[derive(Clone)] +pub struct LeftRangeHasher { + bh: BH, + range: Range, +} + +impl LeftRangeHasher { + #[must_use] + pub const fn new(bh: BH, range: Range) -> Self { + Self { bh, range } + } +} + +impl Hasher<[T]> for LeftRangeHasher +where + T: Hash, + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &[T]) -> u64 { + if value.len() < self.range.end { + return 0; + } + + self.bh.hash_one(&value[self.range.clone()]) + } +} + +impl Hasher for LeftRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &String) -> u64 { + let b = value.as_bytes(); + if b.len() < self.range.end { + return 0; + } + + self.bh.hash_one(&b[self.range.clone()]) + } +} + +impl Hasher<&str> for LeftRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &&str) -> u64 { + let b = value.as_bytes(); + if b.len() < self.range.end { + return 0; + } + + self.bh.hash_one(&b[self.range.clone()]) + } +} + +impl Hasher for LeftRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &str) -> u64 { + let b = value.as_bytes(); + if b.len() < self.range.end { + return 0; + } + + self.bh.hash_one(&b[self.range.clone()]) + } +} + +impl Default for LeftRangeHasher +where + BH: Default, +{ + fn default() -> Self { + Self::new(BH::default(), 0..0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use alloc::vec; + + #[test] + fn test_left_range_hasher_hash_slice() { + let hasher = LeftRangeHasher::new(RandomState::default(), 0..3); + assert_eq!( + hasher.hash(vec![1, 2, 3, 4].as_slice()), + hasher.bh.hash_one(vec![1, 2, 3].as_slice()) + ); + assert_eq!(hasher.hash(vec![1, 2].as_slice()), 0); + } + + #[test] + fn test_left_range_hasher_hash_string() { + let hasher = LeftRangeHasher::new(RandomState::default(), 0..3); + assert_eq!(hasher.hash(&"abcd".to_string()), hasher.bh.hash_one(b"abc")); + assert_eq!(hasher.hash(&"ab".to_string()), 0); + } + + #[test] + fn test_left_range_hasher_hash_str_ref() { + let hasher = LeftRangeHasher::new(RandomState::default(), 0..3); + assert_eq!(hasher.hash(&"abcd"), hasher.bh.hash_one(b"abc")); + assert_eq!(hasher.hash(&"ab"), 0); + } + + #[test] + fn test_left_range_hasher_hash_str() { + let hasher = LeftRangeHasher::new(RandomState::default(), 0..3); + assert_eq!(hasher.hash("abcd"), hasher.bh.hash_one(b"abc")); + assert_eq!(hasher.hash("ab"), 0); + } + + #[test] + fn test_left_range_hasher_default() { + let hasher: LeftRangeHasher = LeftRangeHasher::default(); + assert_eq!(hasher.range, 0..0); + } +} diff --git a/frozen-collections-core/src/hashers/mixing_hasher.rs b/frozen-collections-core/src/hashers/mixing_hasher.rs new file mode 100644 index 0000000..2da896f --- /dev/null +++ b/frozen-collections-core/src/hashers/mixing_hasher.rs @@ -0,0 +1,67 @@ +// NOTE: Currently unused, keeping it around just in case... + +use crate::traits::{Hasher, Scalar}; + +/// A hasher for scalar values which performs mixing on the values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[derive(Clone)] +pub struct MixingHasher {} + +impl MixingHasher { + #[must_use] + pub const fn new() -> Self { + Self {} + } +} + +impl Hasher<&[T]> for MixingHasher { + fn hash(&self, value: &&[T]) -> u64 { + mix(value.len() as u64) + } +} + +impl Hasher for MixingHasher { + fn hash(&self, value: &String) -> u64 { + mix(value.len() as u64) + } +} + +impl Hasher for MixingHasher { + fn hash(&self, value: &str) -> u64 { + mix(value.len() as u64) + } +} + +impl Hasher<&str> for MixingHasher { + fn hash(&self, value: &&str) -> u64 { + mix(value.len() as u64) + } +} + +impl Hasher for MixingHasher +where + S: Scalar, +{ + fn hash(&self, value: &S) -> u64 { + mix(value.as_u64()) + } +} + +impl Default for MixingHasher { + fn default() -> Self { + Self::new() + } +} + +/// Take an integer value and 'mix' it into a hash code. +/// +/// This function is based on code from . +#[must_use] +#[inline] +const fn mix(mut value: u64) -> u64 { + value ^= value.wrapping_shr(23); + value = value.wrapping_mul(0x2127_599b_f432_5c37); + value ^= value.wrapping_shr(47); + value +} diff --git a/frozen-collections-core/src/hashers/mod.rs b/frozen-collections-core/src/hashers/mod.rs new file mode 100644 index 0000000..6165853 --- /dev/null +++ b/frozen-collections-core/src/hashers/mod.rs @@ -0,0 +1,16 @@ +//! Hasher implementations for various situations. + +pub use crate::hashers::bridge_hasher::BridgeHasher; +pub use crate::hashers::inline_left_range_hasher::InlineLeftRangeHasher; +pub use crate::hashers::inline_right_range_hasher::InlineRightRangeHasher; +pub use crate::hashers::left_range_hasher::LeftRangeHasher; +pub use crate::hashers::passthrough_hasher::PassthroughHasher; +pub use crate::hashers::right_range_hasher::RightRangeHasher; + +mod bridge_hasher; +mod inline_left_range_hasher; +mod inline_right_range_hasher; +mod left_range_hasher; +//mod mixing_hasher; +mod passthrough_hasher; +mod right_range_hasher; diff --git a/frozen-collections-core/src/hashers/passthrough_hasher.rs b/frozen-collections-core/src/hashers/passthrough_hasher.rs new file mode 100644 index 0000000..4c4e5c9 --- /dev/null +++ b/frozen-collections-core/src/hashers/passthrough_hasher.rs @@ -0,0 +1,94 @@ +use crate::traits::{Hasher, Scalar}; +use alloc::string::String; + +/// A hasher that simply returns the value as the hash. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[derive(Clone)] +pub struct PassthroughHasher {} + +impl PassthroughHasher { + #[must_use] + pub const fn new() -> Self { + Self {} + } +} + +impl Hasher<[T]> for PassthroughHasher { + fn hash(&self, value: &[T]) -> u64 { + value.len() as u64 + } +} + +impl Hasher for PassthroughHasher { + fn hash(&self, value: &String) -> u64 { + value.len() as u64 + } +} + +impl Hasher for PassthroughHasher { + fn hash(&self, value: &str) -> u64 { + value.len() as u64 + } +} + +impl Hasher<&str> for PassthroughHasher { + fn hash(&self, value: &&str) -> u64 { + value.len() as u64 + } +} + +impl Hasher for PassthroughHasher +where + S: Scalar, +{ + fn hash(&self, value: &S) -> u64 { + value.index() as u64 + } +} + +impl Default for PassthroughHasher { + fn default() -> Self { + Self::new() + } +} +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn hash_string_returns_length() { + let hasher = PassthroughHasher::new(); + let value = String::from("hello"); + assert_eq!(hasher.hash(&value), 5); + } + + #[test] + fn hash_str_returns_length() { + let hasher = PassthroughHasher::new(); + let value = "world"; + assert_eq!(hasher.hash(value), 5); + } + + #[test] + fn hash_slice_returns_length() { + let hasher = PassthroughHasher::new(); + let binding = vec![1, 2, 3, 4]; + let value = binding.as_slice(); + assert_eq!(hasher.hash(value), 4); + } + + #[test] + fn hash_scalar_returns_index() { + let hasher = PassthroughHasher::new(); + let index = Scalar::index(&42) as u64; + assert_eq!(hasher.hash(&42), index); + } + + #[test] + fn default_creates_instance() { + let hasher = PassthroughHasher::default(); + assert_eq!(hasher.hash(&"default"), 7); + } +} diff --git a/frozen-collections-core/src/hashers/right_range_hasher.rs b/frozen-collections-core/src/hashers/right_range_hasher.rs new file mode 100644 index 0000000..e8ed65c --- /dev/null +++ b/frozen-collections-core/src/hashers/right_range_hasher.rs @@ -0,0 +1,145 @@ +use crate::traits::Hasher; +use ahash::RandomState; +use alloc::string::String; +use core::hash::{BuildHasher, Hash}; +use core::ops::Range; + +/// Hashes a portion of a right-aligned slice. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[derive(Clone)] +pub struct RightRangeHasher { + bh: BH, + range: Range, +} + +impl RightRangeHasher { + #[must_use] + pub const fn new(bh: BH, range: Range) -> Self { + Self { bh, range } + } +} + +impl Hasher<[T]> for RightRangeHasher +where + T: Hash, + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &[T]) -> u64 { + if value.len() < self.range.end { + return 0; + } + + let effective_range = value.len() - self.range.end..value.len() - self.range.start; + self.bh.hash_one(&value[effective_range]) + } +} + +impl Hasher for RightRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &String) -> u64 { + let b = value.as_bytes(); + if b.len() < self.range.end { + return 0; + } + + let effective_range = value.len() - self.range.end..value.len() - self.range.start; + self.bh.hash_one(&b[effective_range]) + } +} + +impl Hasher<&str> for RightRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &&str) -> u64 { + let b = value.as_bytes(); + if b.len() < self.range.end { + return 0; + } + + let effective_range = value.len() - self.range.end..value.len() - self.range.start; + self.bh.hash_one(&b[effective_range]) + } +} + +impl Hasher for RightRangeHasher +where + BH: BuildHasher, +{ + #[inline] + fn hash(&self, value: &str) -> u64 { + let b = value.as_bytes(); + if b.len() < self.range.end { + return 0; + } + + let effective_range = value.len() - self.range.end..value.len() - self.range.start; + self.bh.hash_one(&b[effective_range]) + } +} + +impl Default for RightRangeHasher +where + BH: Default, +{ + fn default() -> Self { + Self::new(BH::default(), 0..0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use alloc::vec; + + #[test] + fn test_right_range_hasher_hash_slice() { + let hasher = RightRangeHasher::new(RandomState::default(), 1..3); + assert_eq!( + hasher.hash(vec![1, 2, 3, 4].as_slice()), + hasher.bh.hash_one(vec![2, 3].as_slice()) + ); + assert_eq!( + hasher.hash(vec![1, 2, 3, 4, 5, 6].as_slice()), + hasher.bh.hash_one(vec![4, 5].as_slice()) + ); + assert_eq!(hasher.hash(vec![1, 2].as_slice()), 0); + } + + #[test] + fn test_right_range_hasher_hash_string() { + let hasher = RightRangeHasher::new(RandomState::default(), 3..5); + assert_eq!( + hasher.hash(&"abcdef".to_string()), + hasher.bh.hash_one(b"bc") + ); + assert_eq!(hasher.hash(&"a".to_string()), 0); + } + + #[test] + fn test_right_range_hasher_hash_str_ref() { + let hasher = RightRangeHasher::new(RandomState::default(), 1..3); + assert_eq!(hasher.hash(&"abcd"), hasher.bh.hash_one(b"bc")); + assert_eq!(hasher.hash(&"a"), 0); + } + + #[test] + fn test_right_range_hasher_hash_str() { + let hasher = RightRangeHasher::new(RandomState::default(), 1..3); + assert_eq!(hasher.hash("abcd"), hasher.bh.hash_one(b"bc")); + assert_eq!(hasher.hash("a"), 0); + } + + #[test] + fn test_right_range_hasher_default() { + let hasher: RightRangeHasher = RightRangeHasher::default(); + assert_eq!(hasher.range, 0..0); + } +} diff --git a/frozen-collections-core/src/inline_maps/inline_dense_scalar_lookup_map.rs b/frozen-collections-core/src/inline_maps/inline_dense_scalar_lookup_map.rs new file mode 100644 index 0000000..43988d3 --- /dev/null +++ b/frozen-collections-core/src/inline_maps/inline_dense_scalar_lookup_map.rs @@ -0,0 +1,141 @@ +use crate::maps::decl_macros::{ + debug_fn, dense_scalar_lookup_query_funcs, get_many_mut_body, get_many_mut_fn, index_fn, + into_iter_fn, into_iter_mut_ref_fn, into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery, Scalar}; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; + +/// A map whose keys are a continuous range in a sequence of scalar values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +/// # Type Parameters +/// +/// - `K`: The key type. +/// - `V`: The value type. +/// - `SZ`: The number of entries in the map. +#[derive(Clone)] +pub struct InlineDenseScalarLookupMap { + min: usize, + max: usize, + entries: [(K, V); SZ], +} + +impl InlineDenseScalarLookupMap +where + K: Scalar, +{ + /// Creates a frozen map. + /// + /// This function assumes that `min` <= `max` and that the vector is sorted according to the + /// order of the [`Ord`] trait. + #[must_use] + pub const fn new_raw(processed_entries: [(K, V); SZ], min: usize, max: usize) -> Self { + Self { + min, + max, + entries: processed_entries, + } + } +} + +impl Map for InlineDenseScalarLookupMap +where + K: Scalar, +{ + get_many_mut_fn!("Scalar"); +} + +impl MapQuery for InlineDenseScalarLookupMap +where + K: Scalar, +{ + dense_scalar_lookup_query_funcs!(); +} + +impl MapIteration for InlineDenseScalarLookupMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for InlineDenseScalarLookupMap { + fn len(&self) -> usize { + SZ + } +} + +impl Index<&Q> for InlineDenseScalarLookupMap +where + Q: Scalar, +{ + index_fn!(); +} + +impl IntoIterator for InlineDenseScalarLookupMap { + into_iter_fn!(entries); +} + +impl<'a, K, V, const SZ: usize> IntoIterator for &'a InlineDenseScalarLookupMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V, const SZ: usize> IntoIterator for &'a mut InlineDenseScalarLookupMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for InlineDenseScalarLookupMap +where + K: Scalar, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Eq for InlineDenseScalarLookupMap +where + K: Scalar, + V: Eq, +{ +} + +impl Debug for InlineDenseScalarLookupMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_maps/inline_hash_map.rs b/frozen-collections-core/src/inline_maps/inline_hash_map.rs new file mode 100644 index 0000000..52041c0 --- /dev/null +++ b/frozen-collections-core/src/inline_maps/inline_hash_map.rs @@ -0,0 +1,186 @@ +use crate::hash_tables::InlineHashTable; +use crate::hashers::BridgeHasher; +use crate::maps::decl_macros::{ + get_many_mut_body, get_many_mut_fn, hash_query_funcs, index_fn, into_iter_fn, + into_iter_mut_ref_fn, into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{ + CollectionMagnitude, Hasher, Len, Map, MapIteration, MapQuery, SmallCollection, +}; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Equivalent; + +/// A general purpose map implemented using a hash table. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/hash_warning.md")] +/// +/// # Type Parameters +/// +/// - `K`: The key type. +/// - `V`: The value type. +/// - `CM`: The magnitude of the map, one of [`SmallCollection`](SmallCollection), [`MediumCollection`](crate::traits::MediumCollection), or [`LargeCollection`](crate::traits::LargeCollection). +/// - `SZ`: The number of entries in the map. +/// - `NHS`: The number of hash table slots. +/// - `H`: The hasher to generate hash codes. +#[derive(Clone)] +pub struct InlineHashMap< + K, + V, + const SZ: usize, + const NHS: usize, + CM = SmallCollection, + H = BridgeHasher, +> { + table: InlineHashTable<(K, V), SZ, NHS, CM>, + hasher: H, +} + +impl InlineHashMap +where + K: Eq, + CM: CollectionMagnitude, + H: Hasher, +{ + /// Creates a frozen map. + #[must_use] + pub const fn new_raw(table: InlineHashTable<(K, V), SZ, NHS, CM>, hasher: H) -> Self { + Self { table, hasher } + } +} + +impl Map + for InlineHashMap +where + CM: CollectionMagnitude, + Q: ?Sized + Eq + Equivalent, + H: Hasher, +{ + get_many_mut_fn!("Hash"); +} + +impl MapQuery + for InlineHashMap +where + CM: CollectionMagnitude, + Q: ?Sized + Eq + Equivalent, + H: Hasher, +{ + hash_query_funcs!(); +} + +impl MapIteration + for InlineHashMap +{ + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + map_iteration_funcs!(table entries); +} + +impl Len for InlineHashMap { + fn len(&self) -> usize { + SZ + } +} + +impl Index<&Q> + for InlineHashMap +where + CM: CollectionMagnitude, + Q: ?Sized + Eq + Equivalent, + H: Hasher, +{ + index_fn!(); +} + +impl IntoIterator + for InlineHashMap +{ + into_iter_fn!(table entries); +} + +impl<'a, K, V, const SZ: usize, const NHS: usize, CM, H> IntoIterator + for &'a InlineHashMap +{ + into_iter_ref_fn!(); +} + +impl<'a, K, V, const SZ: usize, const NHS: usize, CM, H> IntoIterator + for &'a mut InlineHashMap +{ + into_iter_mut_ref_fn!(); +} + +impl PartialEq + for InlineHashMap +where + K: Eq, + CM: CollectionMagnitude, + V: PartialEq, + MT: Map, + H: Hasher, +{ + partial_eq_fn!(); +} + +impl Eq for InlineHashMap +where + K: Eq, + CM: CollectionMagnitude, + V: Eq, + H: Hasher, +{ +} + +impl Debug for InlineHashMap +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let pairs = self.table.entries.iter().map(|x| (&x.0, &x.1)); + f.debug_map().entries(pairs).finish() + } +} diff --git a/frozen-collections-core/src/inline_maps/inline_ordered_scan_map.rs b/frozen-collections-core/src/inline_maps/inline_ordered_scan_map.rs new file mode 100644 index 0000000..9f2918e --- /dev/null +++ b/frozen-collections-core/src/inline_maps/inline_ordered_scan_map.rs @@ -0,0 +1,139 @@ +use crate::maps::decl_macros::{ + debug_fn, get_many_mut_body, get_many_mut_fn, index_fn, into_iter_fn, into_iter_mut_ref_fn, + into_iter_ref_fn, map_iteration_funcs, ordered_scan_query_funcs, partial_eq_fn, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery}; +use alloc::vec::Vec; +use core::cmp::Ordering; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Comparable; + +/// A general purpose map implemented using linear scanning. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +/// # Type Parameters +/// +/// - `K`: The key type. +/// - `V`: The value type. +/// - `SZ`: The number of entries in the map. +#[derive(Clone)] +pub struct InlineOrderedScanMap { + entries: [(K, V); SZ], +} + +impl InlineOrderedScanMap +where + K: Ord, +{ + /// Creates a frozen map. + /// + /// This function assumes the vector is sorted according to the ordering of the [`Ord`] trait. + #[must_use] + pub const fn new_raw(processed_entries: [(K, V); SZ]) -> Self { + Self { + entries: processed_entries, + } + } +} + +impl Map for InlineOrderedScanMap +where + Q: ?Sized + Eq + Comparable, +{ + get_many_mut_fn!(); +} + +impl MapQuery for InlineOrderedScanMap +where + Q: ?Sized + Eq + Comparable, +{ + ordered_scan_query_funcs!(); +} + +impl MapIteration for InlineOrderedScanMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for InlineOrderedScanMap { + fn len(&self) -> usize { + SZ + } +} + +impl Index<&Q> for InlineOrderedScanMap +where + Q: ?Sized + Eq + Comparable, +{ + index_fn!(); +} + +impl IntoIterator for InlineOrderedScanMap { + into_iter_fn!(entries); +} + +impl<'a, K, V, const SZ: usize> IntoIterator for &'a InlineOrderedScanMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V, const SZ: usize> IntoIterator for &'a mut InlineOrderedScanMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for InlineOrderedScanMap +where + K: Ord, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Eq for InlineOrderedScanMap +where + K: Ord, + V: PartialEq, +{ +} + +impl Debug for InlineOrderedScanMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_maps/inline_scan_map.rs b/frozen-collections-core/src/inline_maps/inline_scan_map.rs new file mode 100644 index 0000000..35df1e5 --- /dev/null +++ b/frozen-collections-core/src/inline_maps/inline_scan_map.rs @@ -0,0 +1,136 @@ +use crate::maps::decl_macros::get_many_mut_body; +use crate::maps::decl_macros::{ + debug_fn, get_many_mut_fn, index_fn, into_iter_fn, into_iter_mut_ref_fn, into_iter_ref_fn, + map_iteration_funcs, partial_eq_fn, scan_query_funcs, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery}; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Equivalent; + +/// A general purpose map implemented using linear scanning. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +/// # Type Parameters +/// +/// - `K`: The key type. +/// - `V`: The value type. +/// - `SZ`: The number of entries in the map. +#[derive(Clone)] +pub struct InlineScanMap { + entries: [(K, V); SZ], +} + +impl InlineScanMap +where + K: Eq, +{ + /// Creates a frozen map. + #[must_use] + pub const fn new_raw(processed_entries: [(K, V); SZ]) -> Self { + Self { + entries: processed_entries, + } + } +} + +impl Map for InlineScanMap +where + Q: ?Sized + Eq + Equivalent, +{ + get_many_mut_fn!(); +} + +impl MapQuery for InlineScanMap +where + Q: ?Sized + Eq + Equivalent, +{ + scan_query_funcs!(); +} + +impl MapIteration for InlineScanMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for InlineScanMap { + fn len(&self) -> usize { + SZ + } +} + +impl Index<&Q> for InlineScanMap +where + Q: ?Sized + Eq + Equivalent, +{ + index_fn!(); +} + +impl IntoIterator for InlineScanMap { + into_iter_fn!(entries); +} + +impl<'a, K, V, const SZ: usize> IntoIterator for &'a InlineScanMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V, const SZ: usize> IntoIterator for &'a mut InlineScanMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for InlineScanMap +where + K: Eq, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Eq for InlineScanMap +where + K: Eq, + V: PartialEq, +{ +} + +impl Debug for InlineScanMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_maps/inline_sparse_scalar_lookup_map.rs b/frozen-collections-core/src/inline_maps/inline_sparse_scalar_lookup_map.rs new file mode 100644 index 0000000..9c9de7f --- /dev/null +++ b/frozen-collections-core/src/inline_maps/inline_sparse_scalar_lookup_map.rs @@ -0,0 +1,186 @@ +use crate::maps::decl_macros::{ + debug_fn, get_many_mut_body, get_many_mut_fn, index_fn, into_iter_fn, into_iter_mut_ref_fn, + into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, sparse_scalar_lookup_query_funcs, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{ + CollectionMagnitude, Len, Map, MapIteration, MapQuery, Scalar, SmallCollection, +}; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; + +/// A map whose keys are a sparse range of integers. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +/// # Type Parameters +/// +/// - `K`: The key type. +/// - `V`: The value type. +/// - `CM`: The magnitude of the map, one of [`SmallCollection`](SmallCollection), [`MediumCollection`](crate::traits::MediumCollection), or [`LargeCollection`](crate::traits::LargeCollection). +/// - `SZ`: The number of entries in the map. +/// - `LTSZ`: The number of entries in the lookup table. +#[derive(Clone)] +pub struct InlineSparseScalarLookupMap< + K, + V, + const SZ: usize, + const LTSZ: usize, + CM = SmallCollection, +> { + min: usize, + max: usize, + lookup: [CM; LTSZ], + entries: [(K, V); SZ], +} + +impl InlineSparseScalarLookupMap +where + K: Scalar, + CM: CollectionMagnitude, +{ + /// Creates a frozen map. + #[must_use] + pub const fn new_raw( + processed_entries: [(K, V); SZ], + lookup: [CM; LTSZ], + min: usize, + max: usize, + ) -> Self { + Self { + min, + max, + lookup, + entries: processed_entries, + } + } +} + +impl Map + for InlineSparseScalarLookupMap +where + CM: CollectionMagnitude, + K: Scalar, +{ + get_many_mut_fn!("Scalar"); +} + +impl MapQuery + for InlineSparseScalarLookupMap +where + CM: CollectionMagnitude, + K: Scalar, +{ + sparse_scalar_lookup_query_funcs!(); +} + +impl MapIteration + for InlineSparseScalarLookupMap +{ + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a; + + map_iteration_funcs!(entries); +} + +impl Len + for InlineSparseScalarLookupMap +{ + fn len(&self) -> usize { + SZ + } +} + +impl Index<&Q> + for InlineSparseScalarLookupMap +where + Q: Scalar, + CM: CollectionMagnitude, +{ + index_fn!(); +} + +impl IntoIterator + for InlineSparseScalarLookupMap +{ + into_iter_fn!(entries); +} + +impl<'a, K, V, const SZ: usize, const LTSZ: usize, CM> IntoIterator + for &'a InlineSparseScalarLookupMap +where + CM: CollectionMagnitude, +{ + into_iter_ref_fn!(); +} + +impl<'a, K, V, const SZ: usize, const LTSZ: usize, CM> IntoIterator + for &'a mut InlineSparseScalarLookupMap +where + CM: CollectionMagnitude, +{ + into_iter_mut_ref_fn!(); +} + +impl PartialEq + for InlineSparseScalarLookupMap +where + K: Scalar, + V: PartialEq, + MT: Map, + CM: CollectionMagnitude, +{ + partial_eq_fn!(); +} + +impl Eq + for InlineSparseScalarLookupMap +where + K: Scalar, + V: Eq, + CM: CollectionMagnitude, +{ +} + +impl Debug + for InlineSparseScalarLookupMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_maps/mod.rs b/frozen-collections-core/src/inline_maps/mod.rs new file mode 100644 index 0000000..e3df2ca --- /dev/null +++ b/frozen-collections-core/src/inline_maps/mod.rs @@ -0,0 +1,13 @@ +//! Specialized static-friendly read-only map types. + +pub use inline_dense_scalar_lookup_map::InlineDenseScalarLookupMap; +pub use inline_hash_map::InlineHashMap; +pub use inline_ordered_scan_map::InlineOrderedScanMap; +pub use inline_scan_map::InlineScanMap; +pub use inline_sparse_scalar_lookup_map::InlineSparseScalarLookupMap; + +mod inline_dense_scalar_lookup_map; +mod inline_hash_map; +mod inline_ordered_scan_map; +mod inline_scan_map; +mod inline_sparse_scalar_lookup_map; diff --git a/frozen-collections-core/src/inline_sets/inline_dense_scalar_lookup_set.rs b/frozen-collections-core/src/inline_sets/inline_dense_scalar_lookup_set.rs new file mode 100644 index 0000000..c2f24c5 --- /dev/null +++ b/frozen-collections-core/src/inline_sets/inline_dense_scalar_lookup_set.rs @@ -0,0 +1,117 @@ +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; + +use crate::inline_maps::InlineDenseScalarLookupMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Scalar, Set, SetIteration, SetOps, SetQuery}; + +/// A set whose values are a continuous range in a sequence of scalar values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +/// # Type Parameters +/// +/// - `T`: The value type. +/// - `SZ`: The number of entries in the set. +#[derive(Clone)] +pub struct InlineDenseScalarLookupSet { + map: InlineDenseScalarLookupMap, +} + +impl InlineDenseScalarLookupSet +where + T: Scalar, +{ + /// Creates a frozen set. + #[must_use] + pub const fn new(map: InlineDenseScalarLookupMap) -> Self { + Self { map } + } +} + +impl Set for InlineDenseScalarLookupSet where T: Scalar {} + +impl SetQuery for InlineDenseScalarLookupSet +where + T: Scalar, +{ + get_fn!("Scalar"); +} + +impl SetIteration for InlineDenseScalarLookupSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for InlineDenseScalarLookupSet { + fn len(&self) -> usize { + SZ + } +} + +impl BitOr<&ST> for &InlineDenseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &InlineDenseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &InlineDenseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &InlineDenseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for InlineDenseScalarLookupSet { + into_iter_fn!(); +} + +impl<'a, T, const SZ: usize> IntoIterator for &'a InlineDenseScalarLookupSet { + into_iter_ref_fn!(); +} + +impl PartialEq for InlineDenseScalarLookupSet +where + T: Scalar, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for InlineDenseScalarLookupSet where T: Scalar {} + +impl Debug for InlineDenseScalarLookupSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_sets/inline_hash_set.rs b/frozen-collections-core/src/inline_sets/inline_hash_set.rs new file mode 100644 index 0000000..280d333 --- /dev/null +++ b/frozen-collections-core/src/inline_sets/inline_hash_set.rs @@ -0,0 +1,178 @@ +use crate::hashers::BridgeHasher; +use crate::inline_maps::InlineHashMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, into_iter_fn, into_iter_ref_fn, partial_eq_fn, + set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{ + CollectionMagnitude, Hasher, Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery, + SmallCollection, +}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Equivalent; + +/// A general purpose set implemented using a hash table. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/hash_warning.md")] +/// +/// # Type Parameters +/// +/// - `T`: The value type. +/// - `CM`: The magnitude of the set, one of [`SmallCollection`](SmallCollection), [`MediumCollection`](crate::traits::MediumCollection), or [`LargeCollection`](crate::traits::LargeCollection). +/// - `SZ`: The number of entries in the set. +/// - `NHS`: The number of hash table slots. +/// - `H`: The hasher to generate hash codes. +#[derive(Clone)] +pub struct InlineHashSet< + T, + const SZ: usize, + const NHS: usize, + CM = SmallCollection, + H = BridgeHasher, +> { + map: InlineHashMap, +} + +impl InlineHashSet +where + CM: CollectionMagnitude, + H: Hasher, +{ + /// Creates a frozen set. + #[must_use] + pub const fn new(map: InlineHashMap) -> Self { + Self { map } + } +} + +impl Set for InlineHashSet +where + Q: Eq + Equivalent, + CM: CollectionMagnitude, + H: Hasher, +{ +} + +impl SetQuery + for InlineHashSet +where + Q: Eq + Equivalent, + CM: CollectionMagnitude, + H: Hasher, +{ + #[inline] + fn get(&self, value: &Q) -> Option<&T> { + Some(self.map.get_key_value(value)?.0) + } +} + +impl SetIteration + for InlineHashSet +{ + type Iterator<'a> + = Iter<'a, T> + where + T: 'a, + CM: 'a, + H: 'a; + + set_iteration_funcs!(); +} + +impl Len for InlineHashSet { + fn len(&self) -> usize { + SZ + } +} + +impl BitOr<&ST> + for &InlineHashSet +where + T: Hash + Eq + Clone, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + bitor_fn!(H); +} + +impl BitAnd<&ST> + for &InlineHashSet +where + T: Hash + Eq + Clone, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + bitand_fn!(H); +} + +impl BitXor<&ST> + for &InlineHashSet +where + T: Hash + Eq + Clone, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + bitxor_fn!(H); +} + +impl Sub<&ST> for &InlineHashSet +where + T: Hash + Eq + Clone, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + sub_fn!(H); +} + +impl IntoIterator + for InlineHashSet +{ + into_iter_fn!(); +} + +impl<'a, T, const SZ: usize, const NHS: usize, CM, H> IntoIterator + for &'a InlineHashSet +where + T: Eq, + CM: CollectionMagnitude, + H: Hasher, +{ + into_iter_ref_fn!(); +} + +impl PartialEq + for InlineHashSet +where + T: Eq, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + partial_eq_fn!(); +} + +impl Eq for InlineHashSet +where + T: Eq, + CM: CollectionMagnitude, + H: Hasher, +{ +} + +impl Debug for InlineHashSet +where + T: Eq + Debug, + CM: CollectionMagnitude, + H: Hasher, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_sets/inline_ordered_scan_set.rs b/frozen-collections-core/src/inline_sets/inline_ordered_scan_set.rs new file mode 100644 index 0000000..25cfe1b --- /dev/null +++ b/frozen-collections-core/src/inline_sets/inline_ordered_scan_set.rs @@ -0,0 +1,118 @@ +use crate::inline_maps::InlineOrderedScanMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Comparable; + +/// A general purpose set implemented using linear scanning. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +/// # Type Parameters +/// +/// - `T`: The value type. +/// - `SZ`: The number of entries in the set. +#[derive(Clone)] +pub struct InlineOrderedScanSet { + map: InlineOrderedScanMap, +} + +impl InlineOrderedScanSet { + /// Creates a frozen set. + #[must_use] + pub const fn new(map: InlineOrderedScanMap) -> Self { + Self { map } + } +} + +impl Set for InlineOrderedScanSet where + Q: ?Sized + Ord + Comparable +{ +} + +impl SetQuery for InlineOrderedScanSet +where + Q: ?Sized + Ord + Comparable, +{ + get_fn!(); +} + +impl SetIteration for InlineOrderedScanSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for InlineOrderedScanSet { + fn len(&self) -> usize { + SZ + } +} + +impl BitOr<&ST> for &InlineOrderedScanSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &InlineOrderedScanSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &InlineOrderedScanSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &InlineOrderedScanSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for InlineOrderedScanSet { + into_iter_fn!(); +} + +impl<'a, T, const SZ: usize> IntoIterator for &'a InlineOrderedScanSet { + into_iter_ref_fn!(); +} + +impl PartialEq for InlineOrderedScanSet +where + T: Ord, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for InlineOrderedScanSet where T: Ord {} + +impl Debug for InlineOrderedScanSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_sets/inline_scan_set.rs b/frozen-collections-core/src/inline_sets/inline_scan_set.rs new file mode 100644 index 0000000..e95a4c7 --- /dev/null +++ b/frozen-collections-core/src/inline_sets/inline_scan_set.rs @@ -0,0 +1,114 @@ +use crate::inline_maps::InlineScanMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Equivalent; + +/// A general purpose set implemented using linear scanning. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +/// # Type Parameters +/// +/// - `T`: The value type. +/// - `SZ`: The number of entries in the set. +#[derive(Clone)] +pub struct InlineScanSet { + map: InlineScanMap, +} + +impl InlineScanSet { + /// Creates a frozen set. + #[must_use] + pub const fn new(map: InlineScanMap) -> Self { + Self { map } + } +} + +impl Set for InlineScanSet where Q: ?Sized + Eq + Equivalent {} + +impl SetQuery for InlineScanSet +where + Q: ?Sized + Eq + Equivalent, +{ + get_fn!(); +} + +impl SetIteration for InlineScanSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for InlineScanSet { + fn len(&self) -> usize { + SZ + } +} + +impl BitOr<&ST> for &InlineScanSet +where + T: Hash + Eq + Clone, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &InlineScanSet +where + T: Hash + Eq + Clone, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &InlineScanSet +where + T: Hash + Eq + Clone, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &InlineScanSet +where + T: Hash + Eq + Clone, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for InlineScanSet { + into_iter_fn!(); +} + +impl<'a, T, const SZ: usize> IntoIterator for &'a InlineScanSet { + into_iter_ref_fn!(); +} + +impl PartialEq for InlineScanSet +where + T: Eq, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for InlineScanSet where T: Eq {} + +impl Debug for InlineScanSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_sets/inline_sparse_scalar_lookup_set.rs b/frozen-collections-core/src/inline_sets/inline_sparse_scalar_lookup_set.rs new file mode 100644 index 0000000..35bae94 --- /dev/null +++ b/frozen-collections-core/src/inline_sets/inline_sparse_scalar_lookup_set.rs @@ -0,0 +1,153 @@ +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; + +use crate::inline_maps::InlineSparseScalarLookupMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{ + CollectionMagnitude, Len, MapIteration, MapQuery, Scalar, Set, SetIteration, SetOps, SetQuery, + SmallCollection, +}; + +/// A set whose values are scalars. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +/// # Type Parameters +/// +/// - `T`: The value type. +/// - `CM`: The magnitude of the set, one of [`SmallCollection`](SmallCollection), [`MediumCollection`](crate::traits::MediumCollection), or [`LargeCollection`](crate::traits::LargeCollection). +/// - `SZ`: The number of entries in the set. +/// - `LTSZ`: The number of entries in the lookup table. +#[derive(Clone)] +pub struct InlineSparseScalarLookupSet +{ + map: InlineSparseScalarLookupMap, +} + +impl InlineSparseScalarLookupSet { + /// Creates a frozen set. + #[must_use] + pub const fn new(map: InlineSparseScalarLookupMap) -> Self { + Self { map } + } +} + +impl Set + for InlineSparseScalarLookupSet +where + CM: CollectionMagnitude, + T: Scalar, +{ +} + +impl SetQuery + for InlineSparseScalarLookupSet +where + CM: CollectionMagnitude, + T: Scalar, +{ + get_fn!("Scalar"); +} + +impl SetIteration + for InlineSparseScalarLookupSet +{ + type Iterator<'a> + = Iter<'a, T> + where + T: 'a, + CM: 'a; + + set_iteration_funcs!(); +} + +impl Len + for InlineSparseScalarLookupSet +{ + fn len(&self) -> usize { + SZ + } +} + +impl BitOr<&ST> + for &InlineSparseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, + CM: CollectionMagnitude, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> + for &InlineSparseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, + CM: CollectionMagnitude, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> + for &InlineSparseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, + CM: CollectionMagnitude, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> + for &InlineSparseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, + CM: CollectionMagnitude, +{ + sub_fn!(RandomState); +} + +impl IntoIterator + for InlineSparseScalarLookupSet +{ + into_iter_fn!(); +} + +impl<'a, T, const SZ: usize, const LTSZ: usize, CM> IntoIterator + for &'a InlineSparseScalarLookupSet +{ + into_iter_ref_fn!(); +} + +impl PartialEq + for InlineSparseScalarLookupSet +where + T: Scalar, + ST: Set, + CM: CollectionMagnitude, +{ + partial_eq_fn!(); +} + +impl Eq for InlineSparseScalarLookupSet +where + T: Scalar, + CM: CollectionMagnitude, +{ +} + +impl Debug + for InlineSparseScalarLookupSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/inline_sets/mod.rs b/frozen-collections-core/src/inline_sets/mod.rs new file mode 100644 index 0000000..feaf86e --- /dev/null +++ b/frozen-collections-core/src/inline_sets/mod.rs @@ -0,0 +1,13 @@ +//! Specialized static-friendly read-only set types. + +pub use inline_dense_scalar_lookup_set::InlineDenseScalarLookupSet; +pub use inline_hash_set::InlineHashSet; +pub use inline_ordered_scan_set::InlineOrderedScanSet; +pub use inline_scan_set::InlineScanSet; +pub use inline_sparse_scalar_lookup_set::InlineSparseScalarLookupSet; + +mod inline_dense_scalar_lookup_set; +mod inline_hash_set; +mod inline_ordered_scan_set; +mod inline_scan_set; +mod inline_sparse_scalar_lookup_set; diff --git a/frozen-collections-core/src/lib.rs b/frozen-collections-core/src/lib.rs new file mode 100644 index 0000000..37cc204 --- /dev/null +++ b/frozen-collections-core/src/lib.rs @@ -0,0 +1,26 @@ +//! Implementation crate for the frozen collections. +//! +//!
+//! This crate is an implementation detail of the `frozen_collections` crate. +//! This crate's API is therefore not stable and may change at any time. Please do not +//! use this crate directly, and instead use the public API provided by the +//! `frozen_collections` crate. +//!
+ +#![cfg_attr(not(any(feature = "std")), no_std)] + +extern crate alloc; +extern crate core; + +mod analyzers; +pub mod facade_maps; +pub mod facade_sets; +pub mod hash_tables; +pub mod hashers; +pub mod inline_maps; +pub mod inline_sets; +pub mod macros; +pub mod maps; +pub mod sets; +pub mod traits; +mod utils; diff --git a/frozen-collections-core/src/macros/derive_scalar_macro.rs b/frozen-collections-core/src/macros/derive_scalar_macro.rs new file mode 100644 index 0000000..86ef2f7 --- /dev/null +++ b/frozen-collections-core/src/macros/derive_scalar_macro.rs @@ -0,0 +1,143 @@ +use alloc::vec::Vec; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Data, DeriveInput, Error, Fields}; + +/// Implementation logic for the `Scalar` derive macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +#[allow(clippy::module_name_repetitions)] +pub fn derive_scalar_macro(args: TokenStream) -> syn::Result { + let input: DeriveInput = syn::parse2(args)?; + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let Data::Enum(variants) = &input.data else { + return Err(Error::new_spanned( + name, + "Scalar can only be used with enums", + )); + }; + + if variants.variants.is_empty() { + return Err(Error::new_spanned( + name, + "Scalar can only be used with non-empty enums", + )); + } + + for v in &variants.variants { + if v.fields != Fields::Unit { + return Err(Error::new_spanned( + name, + "Scalar can only be used with enums that only contain unit variants", + )); + } + + if v.discriminant.is_some() { + return Err(Error::new_spanned( + name, + "Scalar can only be used with enums that do not have explicit discriminants", + )); + } + } + + let mut matches = Vec::new(); + for variant in &variants.variants { + let ident = &variant.ident; + + let index = matches.len(); + matches.push(quote! { #name::#ident => #index}); + } + + Ok(quote! { + #[automatically_derived] + impl #impl_generics ::frozen_collections::Scalar for #name #ty_generics #where_clause { + fn index(&self) -> usize { + match self { + #(#matches),* + } + } + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + + #[test] + fn basic() { + assert!(derive_scalar_macro(quote!( + enum Color { + Red, + Green, + Blue, + } + )) + .is_ok()); + } + + #[test] + fn only_with_enums() { + let r = derive_scalar_macro(quote!( + struct Color { + red: i32, + green: i32, + blue: i32, + } + )); + + assert_eq!( + "Scalar can only be used with enums", + r.unwrap_err().to_string() + ); + } + + #[test] + fn non_empty_enums() { + let r = derive_scalar_macro(quote!( + enum Color {} + )); + + assert_eq!( + "Scalar can only be used with non-empty enums", + r.unwrap_err().to_string() + ); + } + + #[test] + fn only_unit_variants() { + let r = derive_scalar_macro(quote!( + enum Color { + Red, + Green(i32), + Blue, + } + )); + + assert_eq!( + "Scalar can only be used with enums that only contain unit variants", + r.unwrap_err().to_string() + ); + } + + #[test] + fn no_explicit_discriminants() { + let r = derive_scalar_macro(quote!( + enum Color { + Red, + Green = 2, + Blue, + } + )); + + assert_eq!( + "Scalar can only be used with enums that do not have explicit discriminants", + r.unwrap_err().to_string() + ); + } +} diff --git a/frozen-collections-core/src/macros/generator.rs b/frozen-collections-core/src/macros/generator.rs new file mode 100644 index 0000000..078a997 --- /dev/null +++ b/frozen-collections-core/src/macros/generator.rs @@ -0,0 +1,902 @@ +use crate::analyzers::{ + analyze_scalar_keys, analyze_slice_keys, ScalarKeyAnalysisResult, SliceKeyAnalysisResult, +}; +use crate::hash_tables::HashTable; +use crate::hashers::{LeftRangeHasher, PassthroughHasher, RightRangeHasher}; +use crate::macros::parsing::entry::Entry; +use crate::macros::parsing::payload::Payload; +use crate::traits::{ + CollectionMagnitude, Hasher, LargeCollection, MediumCollection, Scalar, SmallCollection, +}; +use crate::utils::dedup_by_keep_last; +use ahash::RandomState; +use alloc::string::ToString; +use alloc::vec::Vec; +use alloc::{format, vec}; +use core::fmt::Display; +use core::ops::Range; +use core::str::FromStr; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use syn::{parse2, Expr, ExprLit, Lit, LitInt, LitStr}; + +struct ProcessedEntry { + base: Entry, + parsed_key: K, + hash_code: u64, +} + +impl ToTokens for ProcessedEntry { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.base.to_tokens(tokens); + } +} + +pub struct Output { + pub ctor: TokenStream, + pub type_sig: TokenStream, + pub constant: bool, +} + +impl Output { + const fn new(ctor: TokenStream, type_sig: TokenStream, constant: bool) -> Self { + Self { + ctor, + type_sig, + constant, + } + } +} + +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum MacroKind { + Scalar, + String, + Hashed, + Ordered, +} + +#[derive(Eq, PartialEq)] +enum ScalarType { + I8, + I16, + I32, + I64, + ISize, + U8, + U16, + U32, + U64, + USize, + Undecided, +} + +#[derive(Eq, PartialEq)] +enum DiscoveredKeyKind { + LiteralScalar(ScalarType), + LiteralString, + Expression, +} + +enum EffectiveKeyKind { + AllLiteralScalars(ScalarType), + LiteralAndExpressionScalars, + AllLiteralStrings, + LiteralAndExpressionStrings, + Hashed, + Ordered, +} + +const SCAN_THRESHOLD: usize = 4; +const ORDERED_SCAN_THRESHOLD: usize = 7; +const BINARY_SEARCH_THRESHOLD: usize = 64; + +struct Generator { + entries: Vec, + seeds: (u64, u64, u64, u64), + as_set: bool, + key_type: TokenStream, + value_type: TokenStream, +} + +pub fn generate( + payload: Payload, + seeds: (u64, u64, u64, u64), + as_set: bool, + key_type: TokenStream, + value_type: TokenStream, + macro_kind: MacroKind, +) -> syn::Result { + let mut gen = Generator { + entries: vec![], + seeds, + as_set, + key_type, + value_type, + }; + + match payload { + Payload::InlineEntries(entries) => { + gen.entries = entries; + + if gen.entries.is_empty() { + return if gen.key_type.to_string() == "_" { + Err(syn::Error::new( + Span::call_site(), + "no collection entries supplied", + )) + } else { + Ok(gen.emit_inline_scan::(&Vec::new())) + }; + } + + match gen.assess_keys(macro_kind)? { + EffectiveKeyKind::AllLiteralScalars(ScalarType::I8) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::I16) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::I32) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::I64) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::ISize) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::U8) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::U16) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::U32) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::U64) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::USize) => { + gen.handle_literal_scalar_keys::("") + } + EffectiveKeyKind::AllLiteralScalars(ScalarType::Undecided) => { + gen.handle_literal_scalar_keys::("i32") + } + EffectiveKeyKind::LiteralAndExpressionScalars => { + gen.handle_non_literal_scalar_keys() + } + EffectiveKeyKind::AllLiteralStrings => gen.handle_literal_string_keys(), + EffectiveKeyKind::LiteralAndExpressionStrings => { + gen.handle_non_literal_string_keys() + } + EffectiveKeyKind::Hashed => gen.handle_hashed_keys(), + EffectiveKeyKind::Ordered => gen.handle_ordered_keys(), + } + } + + Payload::Vector(expr) => match macro_kind { + MacroKind::Scalar => Ok(gen.emit_facade_for_vector(&expr, "Scalar")), + MacroKind::String => Ok(gen.emit_facade_for_vector(&expr, "String")), + MacroKind::Hashed => Ok(gen.emit_facade_for_vector(&expr, "Hash")), + MacroKind::Ordered => Ok(gen.emit_facade_for_vector(&expr, "Ordered")), + }, + } +} + +impl Generator { + fn assess_keys(&self, macro_kind: MacroKind) -> syn::Result { + let mut num_strings = 0; + let mut num_scalars = 0; + let mut scalar_type: ScalarType = ScalarType::Undecided; + + for entry in &self.entries { + let discovered_key_kind = match &entry.key { + Expr::Lit(expr) => Self::eval_literal_expr(expr)?, + Expr::Group(group) => match &*group.expr { + Expr::Lit(expr) => Self::eval_literal_expr(expr)?, + _ => DiscoveredKeyKind::Expression, + }, + _ => DiscoveredKeyKind::Expression, + }; + + if macro_kind == MacroKind::Scalar + && discovered_key_kind == DiscoveredKeyKind::LiteralString + { + return Err(syn::Error::new( + Span::call_site(), + "scalar macro cannot contain string keys", + )); + } else if macro_kind == MacroKind::String + && discovered_key_kind != DiscoveredKeyKind::LiteralString + && discovered_key_kind != DiscoveredKeyKind::Expression + { + return Err(syn::Error::new( + Span::call_site(), + "string macro cannot contain scalar keys", + )); + } + + match discovered_key_kind { + DiscoveredKeyKind::LiteralScalar(ScalarType::Undecided) => num_scalars += 1, + DiscoveredKeyKind::LiteralScalar(discovered_scalar_type) => { + num_scalars += 1; + if scalar_type == ScalarType::Undecided { + scalar_type = discovered_scalar_type; + } else if discovered_scalar_type != scalar_type { + return Err(syn::Error::new( + Span::call_site(), + "incompatible scalar literal type", + )); + } + } + + DiscoveredKeyKind::LiteralString => num_strings += 1, + DiscoveredKeyKind::Expression => {} + } + } + + Ok(if num_scalars == self.entries.len() { + EffectiveKeyKind::AllLiteralScalars(scalar_type) + } else if num_scalars > 0 && num_strings == 0 { + EffectiveKeyKind::LiteralAndExpressionScalars + } else if num_strings == self.entries.len() { + EffectiveKeyKind::AllLiteralStrings + } else if num_strings > 0 { + EffectiveKeyKind::LiteralAndExpressionStrings + } else { + match macro_kind { + MacroKind::Scalar => EffectiveKeyKind::LiteralAndExpressionScalars, + MacroKind::String => EffectiveKeyKind::LiteralAndExpressionStrings, + MacroKind::Hashed => EffectiveKeyKind::Hashed, + MacroKind::Ordered => EffectiveKeyKind::Ordered, + } + }) + } + + fn eval_literal_expr(expr: &ExprLit) -> syn::Result { + let kind = match &expr.lit { + Lit::Str(_) => DiscoveredKeyKind::LiteralString, + Lit::Int(expr) => match expr.suffix() { + "i8" => DiscoveredKeyKind::LiteralScalar(ScalarType::I8), + "i16" => DiscoveredKeyKind::LiteralScalar(ScalarType::I16), + "i32" => DiscoveredKeyKind::LiteralScalar(ScalarType::I32), + "i64" => DiscoveredKeyKind::LiteralScalar(ScalarType::I64), + "isize" => DiscoveredKeyKind::LiteralScalar(ScalarType::ISize), + "u8" => DiscoveredKeyKind::LiteralScalar(ScalarType::U8), + "u16" => DiscoveredKeyKind::LiteralScalar(ScalarType::U16), + "u32" => DiscoveredKeyKind::LiteralScalar(ScalarType::U32), + "u64" => DiscoveredKeyKind::LiteralScalar(ScalarType::U64), + "usize" => DiscoveredKeyKind::LiteralScalar(ScalarType::USize), + "" => DiscoveredKeyKind::LiteralScalar(ScalarType::Undecided), + _ => { + return Err(syn::Error::new_spanned( + expr, + format!("unknown suffix {} for scalar value", expr.suffix()), + )) + } + }, + _ => { + return Err(syn::Error::new_spanned( + expr, + "invalid literal, expecting a scalar or string value", + )) + } + }; + + Ok(kind) + } + + fn handle_literal_scalar_keys(self, suffix: &str) -> syn::Result + where + K: Scalar + Ord + FromStr, + K::Err: Display, + { + let mut processed_entries = Vec::with_capacity(self.entries.len()); + + let hasher = PassthroughHasher::new(); + for entry in &self.entries { + let lit = parse2::(entry.key.to_token_stream())?; + let k = lit.base10_parse::()?; + + let mut e = entry.clone(); + if !suffix.is_empty() { + e = Entry { + key: syn::parse_str::(&format!("{lit}{suffix}"))?, + value: e.value, + } + } + + processed_entries.push(ProcessedEntry { + base: e, + parsed_key: k, + hash_code: hasher.hash(&k), + }); + } + + processed_entries.sort_by_key(|x| x.parsed_key); + dedup_by_keep_last(&mut processed_entries, |x, y| x.parsed_key == y.parsed_key); + + let analysis = analyze_scalar_keys(processed_entries.iter().map(|x| x.parsed_key)); + Ok(match analysis { + ScalarKeyAnalysisResult::DenseRange => { + self.emit_inline_dense_scalar_lookup(&processed_entries) + } + ScalarKeyAnalysisResult::SparseRange => { + self.emit_inline_sparse_scalar_lookup(&processed_entries) + } + ScalarKeyAnalysisResult::General => { + if processed_entries.len() < SCAN_THRESHOLD { + self.emit_inline_scan(&processed_entries) + } else if processed_entries.len() < ORDERED_SCAN_THRESHOLD { + self.emit_inline_ordered_scan(&processed_entries) + } else { + self.emit_inline_hash_with_passthrough(processed_entries) + } + } + }) + } + + fn handle_literal_string_keys(self) -> syn::Result { + let bh = RandomState::with_seeds(self.seeds.0, self.seeds.1, self.seeds.2, self.seeds.3); + let mut processed_entries = Vec::with_capacity(self.entries.len()); + + for entry in &self.entries { + let ls = parse2::(entry.key.to_token_stream())?; + processed_entries.push(ProcessedEntry { + base: entry.clone(), + parsed_key: ls.value().to_string(), + hash_code: bh.hash_one(ls.value().to_string().as_str()), + }); + } + + processed_entries.sort_by(|x, y| x.parsed_key.cmp(&y.parsed_key)); + dedup_by_keep_last(&mut processed_entries, |x, y| x.parsed_key == y.parsed_key); + + if processed_entries.len() < SCAN_THRESHOLD { + return Ok(self.emit_inline_scan(&processed_entries)); + } else if processed_entries.len() < ORDERED_SCAN_THRESHOLD { + return Ok(self.emit_inline_ordered_scan(&processed_entries)); + } + + let iter = processed_entries.iter().map(|x| x.parsed_key.as_bytes()); + let analysis = analyze_slice_keys(iter, &bh); + + Ok(match analysis { + SliceKeyAnalysisResult::LeftHandSubslice(range) => { + let hasher = LeftRangeHasher::new(bh, range.clone()); + for entry in &mut processed_entries { + entry.hash_code = hasher.hash(&entry.parsed_key.as_str()); + } + + self.emit_inline_hash_with_range( + processed_entries, + range, + "e!(InlineLeftRangeHasher), + ) + } + + SliceKeyAnalysisResult::RightHandSubslice(range) => { + let hasher = RightRangeHasher::new(bh, range.clone()); + for entry in &mut processed_entries { + entry.hash_code = hasher.hash(&entry.parsed_key.as_str()); + } + + self.emit_inline_hash_with_range( + processed_entries, + range, + "e!(InlineRightRangeHasher), + ) + } + + SliceKeyAnalysisResult::Length => { + let hasher = PassthroughHasher::new(); + for entry in &mut processed_entries { + entry.hash_code = hasher.hash(&entry.parsed_key.as_str()); + } + + self.emit_inline_hash_with_passthrough(processed_entries) + } + + SliceKeyAnalysisResult::General => self.emit_inline_hash_with_bridge(processed_entries), + }) + } + + fn handle_non_literal_scalar_keys(self) -> syn::Result { + let mut processed_entries = Vec::with_capacity(self.entries.len()); + for entry in &self.entries { + processed_entries.push(ProcessedEntry { + base: entry.clone(), + parsed_key: (), + hash_code: 0, + }); + } + + if processed_entries.len() < SCAN_THRESHOLD { + Ok(self.emit_scan(&processed_entries)) + } else { + Ok(self.emit_facade_for_entries(&processed_entries, "Scalar")) + } + } + + fn handle_non_literal_string_keys(self) -> syn::Result { + let mut processed_entries = Vec::with_capacity(self.entries.len()); + for entry in &self.entries { + processed_entries.push(ProcessedEntry { + base: entry.clone(), + parsed_key: (), + hash_code: 0, + }); + } + + if processed_entries.len() < SCAN_THRESHOLD { + Ok(self.emit_scan(&processed_entries)) + } else { + Ok(self.emit_facade_for_entries(&processed_entries, "String")) + } + } + + fn handle_hashed_keys(self) -> syn::Result { + let mut processed_entries = Vec::with_capacity(self.entries.len()); + for entry in &self.entries { + processed_entries.push(ProcessedEntry { + base: entry.clone(), + parsed_key: (), + hash_code: 0, + }); + } + + if processed_entries.len() < SCAN_THRESHOLD { + Ok(self.emit_scan(&processed_entries)) + } else { + Ok(self.emit_hash_with_bridge(&processed_entries)) + } + } + + fn handle_ordered_keys(self) -> syn::Result { + let mut processed_entries = Vec::with_capacity(self.entries.len()); + for entry in &self.entries { + processed_entries.push(ProcessedEntry { + base: entry.clone(), + parsed_key: (), + hash_code: 0, + }); + } + + if processed_entries.len() < SCAN_THRESHOLD { + Ok(self.emit_scan(&processed_entries)) + } else if processed_entries.len() < ORDERED_SCAN_THRESHOLD { + Ok(self.emit_ordered_scan(&processed_entries)) + } else if processed_entries.len() < BINARY_SEARCH_THRESHOLD { + Ok(self.emit_binary_search(&processed_entries)) + } else { + Ok(self.emit_eytzinger_search(&processed_entries)) + } + } + + fn emit_scan(self, entries: &[ProcessedEntry]) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + + let mut ty = quote!(::frozen_collections::maps::ScanMap); + let mut generics = quote!(<#key_type, #value_type>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new(vec![ + #( + #entries, + )* + ])); + + if self.as_set { + ty = quote!(::frozen_collections::sets::ScanSet); + generics = quote!(<#key_type>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, false) + } + + fn emit_inline_scan(self, entries: &[ProcessedEntry]) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + let len = proc_macro2::Literal::usize_unsuffixed(entries.len()); + + let mut ty = quote!(::frozen_collections::inline_maps::InlineScanMap); + let mut generics = quote!(<#key_type, #value_type, #len>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new_raw([ + #( + #entries, + )* + ])); + + if self.as_set { + ty = quote!(::frozen_collections::inline_sets::InlineScanSet); + generics = quote!(<#key_type, #len>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, true) + } + + fn emit_inline_ordered_scan(self, entries: &[ProcessedEntry]) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + let len = proc_macro2::Literal::usize_unsuffixed(entries.len()); + + let mut ty = quote!(::frozen_collections::inline_maps::InlineOrderedScanMap); + let mut generics = quote!(<#key_type, #value_type, #len>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new_raw([ + #( + #entries, + )* + ])); + + if self.as_set { + ty = quote!(::frozen_collections::inline_sets::InlineOrderedScanSet); + generics = quote!(<#key_type, #len>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, true) + } + + fn emit_ordered_scan(self, entries: &[ProcessedEntry]) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + + let mut ty = quote!(::frozen_collections::maps::OrderedScanMap); + let mut generics = quote!(<#key_type, #value_type>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new(vec![ + #( + #entries, + )* + ])); + + if self.as_set { + ty = quote!(::frozen_collections::sets::OrderedScanSet); + generics = quote!(<#key_type>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, false) + } + + fn emit_inline_dense_scalar_lookup(self, entries: &[ProcessedEntry]) -> Output + where + K: Scalar + Ord + FromStr, + { + let key_type = &self.key_type; + let value_type = &self.value_type; + let len = proc_macro2::Literal::usize_unsuffixed(entries.len()); + + let min_key = &entries[0].parsed_key.index(); + let max_key = &entries[entries.len() - 1].parsed_key.index(); + + let mut ty = quote!(::frozen_collections::inline_maps::InlineDenseScalarLookupMap); + let mut generics = quote!(<#key_type, #value_type, #len>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new_raw([ + #( + #entries, + )* + ], #min_key, #max_key)); + + if self.as_set { + ty = quote!(::frozen_collections::inline_sets::InlineDenseScalarLookupSet); + generics = quote!(<#key_type, #len>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, true) + } + + fn emit_inline_sparse_scalar_lookup(self, entries: &[ProcessedEntry]) -> Output + where + K: Scalar + Ord + FromStr, + { + let min_key = &entries[0].parsed_key.index(); + let max_key = &entries[entries.len() - 1].parsed_key.index(); + + let count = max_key - min_key + 1; + let mut lookup = vec![0; count]; + + for (i, entry) in entries.iter().enumerate() { + let index_in_lookup = entry.parsed_key.index() - min_key; + let index_in_entries = i + 1; + lookup[index_in_lookup] = index_in_entries; + } + + let key_type = &self.key_type; + let value_type = &self.value_type; + let len = proc_macro2::Literal::usize_unsuffixed(entries.len()); + let magnitude = collection_magnitude(count); + let lookup = lookup + .iter() + .map(|x| proc_macro2::Literal::usize_unsuffixed(*x)); + let lookup_len = proc_macro2::Literal::usize_unsuffixed(lookup.len()); + + let mut ty = quote!(::frozen_collections::inline_maps::InlineSparseScalarLookupMap); + let mut generics = quote!(<#key_type, #value_type, #len, #lookup_len, #magnitude>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new_raw([ + #( + #entries, + )* + ], + [ + #( + #lookup, + )* + ], #min_key, #max_key)); + + if self.as_set { + ty = quote!(::frozen_collections::inline_sets::InlineSparseScalarLookupSet); + generics = quote!(<#key_type, #len, #lookup_len, #magnitude>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, true) + } + + fn emit_inline_hash_with_bridge(self, entries: Vec>) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + let len = proc_macro2::Literal::usize_unsuffixed(entries.len()); + let (ht, magnitude, num_slots) = self.hash_table(entries); + let (s0, s1, s2, s3) = self.seeds; + + let mut ty = quote!(::frozen_collections::inline_maps::InlineHashMap); + let mut generics = quote!(<#key_type, #value_type, #len, #num_slots, #magnitude, ::frozen_collections::hashers::BridgeHasher<::frozen_collections::ahash::RandomState>>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new_raw(#ht, ::frozen_collections::hashers::BridgeHasher::new(::frozen_collections::ahash::RandomState::with_seeds(#s0, #s1, #s2, #s3)))); + + if self.as_set { + ty = quote!(::frozen_collections::inline_sets::InlineHashSet); + generics = quote!(<#key_type, #len, #num_slots, #magnitude, ::frozen_collections::hashers::BridgeHasher<::frozen_collections::ahash::RandomState>>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, true) + } + + fn emit_inline_hash_with_range( + self, + entries: Vec>, + hash_range: Range, + hasher_type: &TokenStream, + ) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + let len = proc_macro2::Literal::usize_unsuffixed(entries.len()); + let (ht, magnitude, num_slots) = self.hash_table(entries); + let (s0, s1, s2, s3) = self.seeds; + let range_start = proc_macro2::Literal::usize_unsuffixed(hash_range.start); + let range_end = proc_macro2::Literal::usize_unsuffixed(hash_range.end); + + let mut ty = quote!(::frozen_collections::inline_maps::InlineHashMap); + let mut generics = quote!(<#key_type, #value_type, #len, #num_slots, #magnitude, ::frozen_collections::hashers::#hasher_type<#range_start, #range_end, ::frozen_collections::ahash::RandomState>>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new_raw(#ht, ::frozen_collections::hashers::#hasher_type::new(::frozen_collections::ahash::RandomState::with_seeds(#s0, #s1, #s2, #s3)))); + + if self.as_set { + ty = quote!(::frozen_collections::inline_sets::InlineHashSet); + generics = quote!(<#key_type, #len, #num_slots, #magnitude, ::frozen_collections::hashers::#hasher_type<#range_start, #range_end, ::frozen_collections::ahash::RandomState>>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, true) + } + + fn emit_inline_hash_with_passthrough(self, entries: Vec>) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + let len = proc_macro2::Literal::usize_unsuffixed(entries.len()); + let (ht, magnitude, num_slots) = self.hash_table(entries); + + let mut ty = quote!(::frozen_collections::inline_maps::InlineHashMap); + let mut generics = quote!(<#key_type, #value_type, #len, #num_slots, #magnitude, ::frozen_collections::hashers::PassthroughHasher>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new_raw(#ht, ::frozen_collections::hashers::PassthroughHasher::new())); + + if self.as_set { + ty = quote!(::frozen_collections::inline_sets::InlineHashSet); + generics = quote!(<#key_type, #len, #num_slots, #magnitude, ::frozen_collections::hashers::PassthroughHasher>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, true) + } + + fn emit_hash_with_bridge(self, entries: &[ProcessedEntry]) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + + let magnitude = collection_magnitude(entries.len()); + let mut ty = quote!(::frozen_collections::maps::HashMap); + let mut generics = quote!(<#key_type, #value_type, #magnitude>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new(vec![ + #( + #entries, + )* + ], ::frozen_collections::hashers::BridgeHasher::new(::frozen_collections::ahash::RandomState::new())).unwrap()); + + if self.as_set { + ty = quote!(::frozen_collections::sets::HashSet); + generics = quote!(<#key_type, #magnitude>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, false) + } + + fn emit_eytzinger_search(self, entries: &[ProcessedEntry]) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + + let mut ty = quote!(::frozen_collections::maps::EytzingerSearchMap); + let mut generics = quote!(<#key_type, #value_type>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new(vec![ + #( + #entries, + )* + ])); + + if self.as_set { + ty = quote!(::frozen_collections::sets::EytzingerSearchSet); + generics = quote!(<#key_type>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, false) + } + + fn emit_binary_search(self, entries: &[ProcessedEntry]) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + + let mut ty = quote!(::frozen_collections::maps::BinarySearchMap); + let mut generics = quote!(<#key_type, #value_type>); + let mut type_sig = quote!(#ty::#generics); + let mut ctor = quote!(#type_sig::new(vec![ + #( + #entries, + )* + ])); + + if self.as_set { + ty = quote!(::frozen_collections::sets::BinarySearchSet); + generics = quote!(<#key_type>); + type_sig = quote!(#ty::#generics); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, false) + } + + fn emit_facade_for_entries(self, entries: &[ProcessedEntry], variety: &str) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + + let type_name = format_ident!("Facade{}Map", variety); + let ty = quote!(::frozen_collections::facade_maps::#type_name); + + let mut type_sig = quote!(#ty::<#key_type, #value_type>); + + let mut ctor = if variety == "String" { + quote!(#type_sig::new(vec![ + #( + #entries, + )* + ]), ::frozen_collections::ahash::RandomState::new()) + } else { + quote!(#type_sig::new(vec![ + #( + #entries, + )* + ])) + }; + + if self.as_set { + let type_name = format_ident!("Facade{}Set", variety); + let ty = quote!(::frozen_collections::facade_sets::#type_name); + + type_sig = quote!(#ty); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, false) + } + + fn emit_facade_for_vector(self, expr: &Expr, variety: &str) -> Output { + let key_type = &self.key_type; + let value_type = &self.value_type; + + let mut type_name = format_ident!("Facade{}Map", variety); + let mut ty = quote!(::frozen_collections::facade_maps::#type_name); + + let mut type_sig = quote!(#ty::<#key_type, #value_type>); + + let converted_expr = if self.as_set { + quote!(#expr.into_iter().map(|x| (x, ())).collect()) + } else { + quote!(#expr) + }; + + let mut ctor = if variety == "Hash" { + quote!(#type_sig::new(#converted_expr, ::frozen_collections::hashers::BridgeHasher::new(::frozen_collections::ahash::RandomState::new()))) + } else if variety == "String" { + quote!(#type_sig::new(#converted_expr, ::frozen_collections::ahash::RandomState::new())) + } else { + quote!(#type_sig::new(#converted_expr)) + }; + + if self.as_set { + type_name = format_ident!("Facade{}Set", variety); + ty = quote!(::frozen_collections::facade_sets::#type_name); + + type_sig = quote!(#ty::<#key_type>); + ctor = quote!(#type_sig::new(#ctor)); + } + + Output::new(ctor, type_sig, false) + } + + fn hash_table( + &self, + entries: Vec>, + ) -> (TokenStream, TokenStream, TokenStream) { + let key_type = &self.key_type; + let value_type = &self.value_type; + let len = proc_macro2::Literal::usize_unsuffixed(entries.len()); + + let ht = HashTable::<_, LargeCollection>::new(entries, |x| x.hash_code).unwrap(); + let slots = ht.slots; + let num_slots = proc_macro2::Literal::usize_unsuffixed(slots.len()); + let entries = ht.entries; + let magnitude = collection_magnitude(entries.len()); + + ( + quote!(::frozen_collections::hash_tables::InlineHashTable::<(#key_type, #value_type), #len, #num_slots, #magnitude>::new_raw( + [ + #( + #slots, + )* + ], + [ + #( + #entries, + )* + ], + )), + magnitude, + quote!(#num_slots), + ) + } +} + +fn collection_magnitude(len: usize) -> TokenStream { + if len <= SmallCollection::MAX_CAPACITY { + quote!(::frozen_collections::SmallCollection) + } else if len <= MediumCollection::MAX_CAPACITY { + quote!(::frozen_collections::MediumCollection) + } else { + quote!(::frozen_collections::LargeCollection) + } +} diff --git a/frozen-collections-core/src/macros/hash_table.rs b/frozen-collections-core/src/macros/hash_table.rs new file mode 100644 index 0000000..467e8cc --- /dev/null +++ b/frozen-collections-core/src/macros/hash_table.rs @@ -0,0 +1,14 @@ +use crate::hash_tables::HashTableSlot; +use proc_macro2::Literal; +use quote::{quote, ToTokens}; + +impl ToTokens for HashTableSlot { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let min_index = Literal::usize_unsuffixed(self.min_index); + let max_index = Literal::usize_unsuffixed(self.max_index); + + tokens.extend( + quote!(::frozen_collections::hash_tables::HashTableSlot::new(#min_index, #max_index)), + ); + } +} diff --git a/frozen-collections-core/src/macros/map_macros.rs b/frozen-collections-core/src/macros/map_macros.rs new file mode 100644 index 0000000..eeb631f --- /dev/null +++ b/frozen-collections-core/src/macros/map_macros.rs @@ -0,0 +1,130 @@ +use crate::macros::generator; +use crate::macros::generator::MacroKind; +use crate::macros::parsing::long_form_map::LongFormMap; +use crate::macros::parsing::map::Map; +use crate::macros::parsing::short_form_map::ShortFormMap; +use crate::utils::pick_compile_time_random_seeds; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse2; + +/// Implementation logic for the `fz_hash_map!` macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +pub fn fz_hash_map_macro(args: TokenStream) -> syn::Result { + fz_map_macro(args, pick_compile_time_random_seeds(), MacroKind::Hashed) +} + +/// Implementation logic for the `fz_ordered_map!` macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +pub fn fz_ordered_map_macro(args: TokenStream) -> syn::Result { + fz_map_macro(args, pick_compile_time_random_seeds(), MacroKind::Ordered) +} + +/// Implementation logic for the `fz_string_map!` macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +pub fn fz_string_map_macro(args: TokenStream) -> syn::Result { + fz_map_macro(args, pick_compile_time_random_seeds(), MacroKind::String) +} + +/// Implementation logic for the `fz_scalar_map!` macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +pub fn fz_scalar_map_macro(args: TokenStream) -> syn::Result { + fz_map_macro(args, pick_compile_time_random_seeds(), MacroKind::Scalar) +} + +fn fz_map_macro( + args: TokenStream, + seeds: (u64, u64, u64, u64), + macro_kind: MacroKind, +) -> syn::Result { + let input = parse2::(args)?; + + match input { + Map::Short(map) => short_form_fz_map_macro(map, seeds, macro_kind), + Map::Long(map) => long_form_fz_map_macro(map, seeds, macro_kind), + } +} + +fn short_form_fz_map_macro( + map: ShortFormMap, + seeds: (u64, u64, u64, u64), + macro_kind: MacroKind, +) -> syn::Result { + Ok(generator::generate(map.payload, seeds, false, quote!(_), quote!(_), macro_kind)?.ctor) +} + +fn long_form_fz_map_macro( + map: LongFormMap, + seeds: (u64, u64, u64, u64), + macro_kind: MacroKind, +) -> syn::Result { + let key_type = map.key_type; + let value_type = map.value_type; + + let key_type = if map.key_type_amp { + quote!(&'static #key_type) + } else { + quote!(#key_type) + }; + + let value_type = quote!(#value_type); + + let output = generator::generate(map.payload, seeds, false, key_type, value_type, macro_kind)?; + + let type_sig = output.type_sig; + let ctor = output.ctor; + let var_name = &map.var_name; + let type_name = &map.type_name; + let visibility = &map.visibility; + + if !map.is_static { + let mutable = if map.is_mutable { + quote!(mut) + } else { + quote!() + }; + + Ok(quote!( + type #type_name = #type_sig; + let #mutable #var_name: #type_name = #ctor; + )) + } else if output.constant { + Ok(quote!( + #visibility type #type_name = #type_sig; + #visibility static #var_name: #type_name = #ctor; + )) + } else { + Ok(quote!( + #visibility type #type_name = #type_sig; + #visibility static #var_name: std::sync::LazyLock<#type_name> = std::sync::LazyLock::new(|| { #ctor }); + )) + } +} + +#[cfg(test)] +mod tests { + use crate::macros::fz_scalar_map_macro; + use alloc::string::ToString; + use quote::quote; + + #[test] + fn missing_static() { + let r = fz_scalar_map_macro(quote!( + pub Bar, { 1 : 2, 2 : 3 } + )); + + assert_eq!("expected `static`", r.unwrap_err().to_string()); + } +} diff --git a/frozen-collections-core/src/macros/mod.rs b/frozen-collections-core/src/macros/mod.rs new file mode 100644 index 0000000..1f77050 --- /dev/null +++ b/frozen-collections-core/src/macros/mod.rs @@ -0,0 +1,12 @@ +//! Implementation logic for frozen collection macros. + +pub use derive_scalar_macro::derive_scalar_macro; +pub use map_macros::*; +pub use set_macros::*; + +mod derive_scalar_macro; +mod generator; +mod hash_table; +mod map_macros; +mod parsing; +mod set_macros; diff --git a/frozen-collections-core/src/macros/parsing/entry.rs b/frozen-collections-core/src/macros/parsing/entry.rs new file mode 100644 index 0000000..2db0240 --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/entry.rs @@ -0,0 +1,31 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, Token}; + +#[derive(Clone)] +pub struct Entry { + pub key: Expr, + pub value: Option, +} + +impl Parse for Entry { + fn parse(input: ParseStream) -> syn::Result { + let key = input.parse::()?; + _ = input.parse::()?; + let value = Some(input.parse::()?); + + Ok(Self { key, value }) + } +} + +impl ToTokens for Entry { + fn to_tokens(&self, tokens: &mut TokenStream) { + let key = &self.key; + if let Some(value) = &self.value { + tokens.extend(quote!((#key, #value))); + } else { + tokens.extend(quote!((#key, ()))); + } + } +} diff --git a/frozen-collections-core/src/macros/parsing/long_form_map.rs b/frozen-collections-core/src/macros/parsing/long_form_map.rs new file mode 100644 index 0000000..531e5ee --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/long_form_map.rs @@ -0,0 +1,49 @@ +use crate::macros::parsing::payload::{parse_map_payload, Payload}; +use proc_macro2::Ident; +use syn::parse::{Parse, ParseStream}; +use syn::{Path, Token, Visibility}; + +pub struct LongFormMap { + pub var_name: Ident, + pub type_name: Ident, + pub key_type_amp: bool, + pub key_type: Path, + pub value_type: Path, + pub payload: Payload, + + pub visibility: Visibility, + pub is_static: bool, + pub is_mutable: bool, +} + +impl Parse for LongFormMap { + fn parse(input: ParseStream) -> syn::Result { + // var_name: type_name + let var_name = input.parse::()?; + input.parse::()?; + let type_name = input.parse::()?; + + // + input.parse::()?; + let key_type_amp = input.parse::().ok(); + let key_type = input.parse::()?; + input.parse::()?; + let value_type = input.parse::()?; + input.parse::]>()?; + input.parse::()?; + + Ok(Self { + var_name, + type_name, + key_type_amp: key_type_amp.is_some(), + key_type, + value_type, + payload: parse_map_payload(input)?, + + // these will be overridden by the caller + visibility: Visibility::Inherited, + is_static: false, + is_mutable: false, + }) + } +} diff --git a/frozen-collections-core/src/macros/parsing/long_form_set.rs b/frozen-collections-core/src/macros/parsing/long_form_set.rs new file mode 100644 index 0000000..20448dd --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/long_form_set.rs @@ -0,0 +1,57 @@ +use crate::macros::parsing::payload::{parse_set_payload, Payload}; +use proc_macro2::Ident; +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, Path, Token, Visibility}; + +pub struct LongFormSet { + pub var_name: Ident, + pub type_name: Ident, + pub value_type_amp: bool, + pub value_type: Path, + pub payload: Payload, + + pub visibility: Visibility, + pub is_static: bool, + pub is_mutable: bool, +} + +impl Parse for LongFormSet { + fn parse(input: ParseStream) -> syn::Result { + // var_name: type_name + let var_name = input.parse::()?; + input.parse::()?; + let type_name = input.parse::()?; + + // + input.parse::()?; + let value_type_amp = input.parse::().ok(); + let value_type = input.parse::()?; + input.parse::]>()?; + input.parse::()?; + + Ok(Self { + var_name, + type_name, + value_type_amp: value_type_amp.is_some(), + value_type, + payload: parse_set_payload(input)?, + + // these will be overridden by the caller + visibility: Visibility::Inherited, + is_static: false, + is_mutable: false, + }) + } +} + +pub struct SetEntry { + pub value: Expr, +} + +impl Parse for SetEntry { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + value: input.parse::()?, + }) + } +} diff --git a/frozen-collections-core/src/macros/parsing/map.rs b/frozen-collections-core/src/macros/parsing/map.rs new file mode 100644 index 0000000..fb6b30d --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/map.rs @@ -0,0 +1,43 @@ +use crate::macros::parsing::long_form_map::LongFormMap; +use crate::macros::parsing::short_form_map::ShortFormMap; +use syn::parse::Parse; +use syn::{Token, Visibility}; + +pub enum Map { + Short(ShortFormMap), + Long(LongFormMap), +} + +impl Parse for Map { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let visibility = input.parse::()?; + if visibility != Visibility::Inherited && !input.peek(Token![static]) { + return Err(input.error("expected `static`")); + } + + if input.peek(Token![static]) { + input.parse::()?; + let mut m = input.parse::()?; + m.visibility = visibility; + m.is_static = true; + Ok(Self::Long(m)) + } else if input.peek(Token![let]) { + input.parse::()?; + + let is_mutable = if input.peek(Token![mut]) { + input.parse::()?; + true + } else { + false + }; + + let mut m = input.parse::()?; + m.visibility = visibility; + m.is_static = false; + m.is_mutable = is_mutable; + Ok(Self::Long(m)) + } else { + Ok(Self::Short(input.parse()?)) + } + } +} diff --git a/frozen-collections-core/src/macros/parsing/mod.rs b/frozen-collections-core/src/macros/parsing/mod.rs new file mode 100644 index 0000000..5cfaeb7 --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/mod.rs @@ -0,0 +1,8 @@ +pub mod entry; +pub mod long_form_map; +pub mod long_form_set; +pub mod map; +pub mod payload; +pub mod set; +pub mod short_form_map; +pub mod short_form_set; diff --git a/frozen-collections-core/src/macros/parsing/payload.rs b/frozen-collections-core/src/macros/parsing/payload.rs new file mode 100644 index 0000000..b0ea189 --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/payload.rs @@ -0,0 +1,56 @@ +use crate::macros::parsing::entry::Entry; +use crate::macros::parsing::long_form_set::SetEntry; +use alloc::vec::Vec; +use syn::parse::Parse; +use syn::{braced, Expr, Token}; + +/// Data associated with a frozen collection macro. +pub enum Payload { + /// Entries supplied inline with the macro + InlineEntries(Vec), + + /// An expression producing a vector of values to insert into the collection. + Vector(Expr), +} + +#[allow(clippy::module_name_repetitions)] +pub fn parse_set_payload(input: syn::parse::ParseStream) -> syn::Result { + let payload = if input.peek(::syn::token::Brace) { + // { value, value, ... }; + let content; + _ = braced!(content in input); + Payload::InlineEntries( + content + .parse_terminated(SetEntry::parse, Token![,])? + .into_iter() + .map(|x| Entry { + key: x.value, + value: None, + }) + .collect(), + ) + } else { + Payload::Vector(input.parse::()?) + }; + + Ok(payload) +} + +#[allow(clippy::module_name_repetitions)] +pub fn parse_map_payload(input: syn::parse::ParseStream) -> syn::Result { + let payload = if input.peek(::syn::token::Brace) { + // { key: value, key: value, ... }; + let content; + _ = braced!(content in input); + Payload::InlineEntries( + content + .parse_terminated(Entry::parse, Token![,])? + .into_iter() + .collect(), + ) + } else { + Payload::Vector(input.parse::()?) + }; + + Ok(payload) +} diff --git a/frozen-collections-core/src/macros/parsing/set.rs b/frozen-collections-core/src/macros/parsing/set.rs new file mode 100644 index 0000000..4a138f4 --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/set.rs @@ -0,0 +1,43 @@ +use crate::macros::parsing::long_form_set::LongFormSet; +use crate::macros::parsing::short_form_set::ShortFormSet; +use syn::parse::Parse; +use syn::{Token, Visibility}; + +pub enum Set { + Short(ShortFormSet), + Long(LongFormSet), +} + +impl Parse for Set { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let visibility = input.parse::()?; + if visibility != Visibility::Inherited && !input.peek(Token![static]) { + return Err(input.error("expected `static`")); + } + + if input.peek(Token![static]) { + input.parse::()?; + let mut s = input.parse::()?; + s.visibility = visibility; + s.is_static = true; + Ok(Self::Long(s)) + } else if input.peek(Token![let]) { + input.parse::()?; + + let is_mutable = if input.peek(Token![mut]) { + input.parse::()?; + true + } else { + false + }; + + let mut s = input.parse::()?; + s.visibility = visibility; + s.is_static = false; + s.is_mutable = is_mutable; + Ok(Self::Long(s)) + } else { + Ok(Self::Short(input.parse()?)) + } + } +} diff --git a/frozen-collections-core/src/macros/parsing/short_form_map.rs b/frozen-collections-core/src/macros/parsing/short_form_map.rs new file mode 100644 index 0000000..dc720a6 --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/short_form_map.rs @@ -0,0 +1,14 @@ +use crate::macros::parsing::payload::{parse_map_payload, Payload}; +use syn::parse::{Parse, ParseStream}; + +pub struct ShortFormMap { + pub payload: Payload, +} + +impl Parse for ShortFormMap { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + payload: parse_map_payload(input)?, + }) + } +} diff --git a/frozen-collections-core/src/macros/parsing/short_form_set.rs b/frozen-collections-core/src/macros/parsing/short_form_set.rs new file mode 100644 index 0000000..155cb41 --- /dev/null +++ b/frozen-collections-core/src/macros/parsing/short_form_set.rs @@ -0,0 +1,14 @@ +use crate::macros::parsing::payload::{parse_set_payload, Payload}; +use syn::parse::{Parse, ParseStream}; + +pub struct ShortFormSet { + pub payload: Payload, +} + +impl Parse for ShortFormSet { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + payload: parse_set_payload(input)?, + }) + } +} diff --git a/frozen-collections-core/src/macros/set_macros.rs b/frozen-collections-core/src/macros/set_macros.rs new file mode 100644 index 0000000..c44dfac --- /dev/null +++ b/frozen-collections-core/src/macros/set_macros.rs @@ -0,0 +1,397 @@ +use crate::macros::generator; +use crate::macros::generator::MacroKind; +use crate::macros::parsing::long_form_set::LongFormSet; +use crate::macros::parsing::set::Set; +use crate::macros::parsing::short_form_set::ShortFormSet; +use crate::utils::pick_compile_time_random_seeds; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse2; + +/// Implementation logic for the `fz_hash_set!` macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +pub fn fz_hash_set_macro(args: TokenStream) -> syn::Result { + fz_set_macro(args, pick_compile_time_random_seeds(), MacroKind::Hashed) +} + +/// Implementation logic for the `fz_ordered_set!` macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +pub fn fz_ordered_set_macro(args: TokenStream) -> syn::Result { + fz_set_macro(args, pick_compile_time_random_seeds(), MacroKind::Ordered) +} + +/// Implementation logic for the `fz_string_set!` macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +pub fn fz_string_set_macro(args: TokenStream) -> syn::Result { + fz_set_macro(args, pick_compile_time_random_seeds(), MacroKind::String) +} + +/// Implementation logic for the `fz_scalar_set!` macro. +/// +/// # Errors +/// +/// Bad things happen to bad input +pub fn fz_scalar_set_macro(args: TokenStream) -> syn::Result { + fz_set_macro(args, pick_compile_time_random_seeds(), MacroKind::Scalar) +} + +fn fz_set_macro( + args: TokenStream, + seeds: (u64, u64, u64, u64), + macro_kind: MacroKind, +) -> syn::Result { + let input = parse2::(args)?; + + match input { + Set::Short(set) => short_form_fz_set_macro(set, seeds, macro_kind), + Set::Long(set) => long_form_fz_set_macro(set, seeds, macro_kind), + } +} + +fn short_form_fz_set_macro( + set: ShortFormSet, + seeds: (u64, u64, u64, u64), + macro_kind: MacroKind, +) -> syn::Result { + Ok(generator::generate(set.payload, seeds, true, quote!(_), quote!(_), macro_kind)?.ctor) +} + +fn long_form_fz_set_macro( + set: LongFormSet, + seeds: (u64, u64, u64, u64), + macro_kind: MacroKind, +) -> syn::Result { + let value_type = set.value_type; + + let value_type = if set.value_type_amp { + quote!(&'static #value_type) + } else { + quote!(#value_type) + }; + + let output = generator::generate(set.payload, seeds, true, value_type, quote!(_), macro_kind)?; + + let type_sig = output.type_sig; + let ctor = output.ctor; + let var_name = &set.var_name; + let type_name = &set.type_name; + let visibility = &set.visibility; + + if !set.is_static { + let mutable = if set.is_mutable { + quote!(mut) + } else { + quote!() + }; + + Ok(quote!( + type #type_name = #type_sig; + let #mutable #var_name: #type_name = #ctor; + )) + } else if output.constant { + Ok(quote!( + #visibility type #type_name = #type_sig; + #visibility static #var_name: #type_name = #ctor; + )) + } else { + Ok(quote!( + #visibility type #type_name = #type_sig; + #visibility static #var_name: std::sync::LazyLock<#type_name> = std::sync::LazyLock::new(|| { #ctor }); + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use proc_macro2::Delimiter::Brace; + use proc_macro2::{Group, TokenTree}; + use quote::{quote, ToTokens, TokenStreamExt}; + + #[test] + fn no_entries() { + let r = fz_scalar_set_macro(quote!({})); + + assert_eq!("no collection entries supplied", r.unwrap_err().to_string()); + } + + #[test] + fn invalid_suffix() { + let r = fz_scalar_set_macro(quote!( + { 123iXXX, 234iXXX } + )); + + assert_eq!( + "unknown suffix iXXX for scalar value", + r.unwrap_err().to_string() + ); + } + + #[test] + fn incompatible_literal_types() { + let r = fz_scalar_set_macro(quote!( + { 1i8, 2i16 } + )); + + assert_eq!( + "incompatible scalar literal type", + r.unwrap_err().to_string() + ); + } + + #[test] + fn invalid_literal() { + let r = fz_scalar_set_macro(quote!( + { 123.0, 234.0 } + )); + + assert_eq!( + "invalid literal, expecting a scalar or string value", + r.unwrap_err().to_string() + ); + + let r = fz_string_set_macro(quote!( + { 1, 2 } + )); + + assert_eq!( + "string macro cannot contain scalar keys", + r.unwrap_err().to_string() + ); + + let r = fz_scalar_set_macro(quote!( + { "1", "2" } + )); + + assert_eq!( + "scalar macro cannot contain string keys", + r.unwrap_err().to_string() + ); + } + + #[test] + fn missing_static() { + let r = fz_scalar_set_macro(quote!( + pub Bar, { 1, 2 } + )); + + assert_eq!("expected `static`", r.unwrap_err().to_string()); + } + + #[test] + fn magnitude() { + test_magnitude(255, "SmallCollection"); + test_magnitude(256, "MediumCollection"); + test_magnitude(65535, "MediumCollection"); + test_magnitude(65536, "LargeCollection"); + } + + fn test_magnitude(count: i32, expected: &str) { + let mut s = TokenStream::new(); + + for i in 1..count { + s.append_all(quote!(#i,)); + } + + s.append_all(quote!(12322225)); + let s = TokenTree::Group(Group::new(Brace, s)).to_token_stream(); + + let r = fz_scalar_set_macro(s).unwrap(); + assert!(r.to_string().contains(expected), "{}", expected); + } + + #[test] + fn test_selected_string_set_implementation_types() { + fn check_impl(expected: &str, ts: TokenStream) { + let r = fz_string_set_macro(ts).unwrap().to_string(); + assert!(r.contains(expected), "{r} doesn't contain {expected}"); + } + check_impl(":: InlineScanSet", quote!({ "1", "2", "3", })); + check_impl(":: InlineOrderedScanSet", quote!({ "1", "2", "3", "4" })); + check_impl( + ":: InlineOrderedScanSet", + quote!({ "1", "2", "3", "4", "5", "6"}), + ); + + check_impl( + ":: InlineHashSet", + quote!({ "1", "2", "3", "4", "5", "6", "7" }), + ); + + check_impl( + ":: BridgeHasher", + quote!({ "1", "2", "3", "4", "5", "6", "7" }), + ); + + check_impl( + ":: PassthroughHasher", + quote!({ "1", "22", "333", "4444", "55555", "666666", "7777777" }), + ); + + check_impl( + ":: InlineLeftRangeHasher", + quote!({ "1111", "1112", "1113", "1114", "1115", "1116", "1117" }), + ); + + check_impl(":: ScanSet", quote!({ x, "2", "3", })); + check_impl(":: FacadeStringSet", quote!({ x, "2", "3", "4" })); + check_impl(":: FacadeStringSet", quote!({ x, "2", "3", "4", "5", "6"})); + check_impl( + ":: FacadeStringSet", + quote!({ x, "2", "3", "4", "5", "6", "7" }), + ); + + check_impl(":: FacadeStringSet", quote!({ x, y, z, a, b, c, d })); + } + + #[test] + fn test_selected_scalar_set_implementation_types() { + fn check_impl(expected: &str, ts: TokenStream) { + let r = fz_scalar_set_macro(ts).unwrap().to_string(); + assert!(r.contains(expected), "{r} doesn't contain {expected}"); + } + + check_impl(":: InlineDenseScalarLookupSet", quote!({ 1, 2, 3, })); + check_impl(":: InlineSparseScalarLookupSet", quote!({ 1, 2, 3, 4, 6 })); + check_impl(":: InlineScanSet", quote!({ 1, 2, 10000 })); + check_impl(":: InlineOrderedScanSet", quote!({ 1, 2, 3, 4, 5, 10000 })); + check_impl(":: InlineHashSet", quote!({ 1, 2, 3, 4, 5, 6, 10000 })); + + check_impl(":: ScanSet", quote!({ x, 2, 3, })); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4 })); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4, 5, 6})); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4, 5, 6, 7 })); + } + + #[test] + fn test_selected_ordered_set_implementation_types() { + fn check_impl(expected: &str, ts: TokenStream) { + let r = fz_ordered_set_macro(ts).unwrap().to_string(); + assert!(r.contains(expected), "{r} doesn't contain {expected}"); + } + + check_impl(":: InlineDenseScalarLookupSet", quote!({ 1, 2, 3, })); + check_impl(":: InlineSparseScalarLookupSet", quote!({ 1, 2, 3, 4, 6 })); + check_impl(":: InlineScanSet", quote!({ 1, 2, 10000 })); + check_impl(":: InlineOrderedScanSet", quote!({ 1, 2, 3, 4, 5, 10000 })); + check_impl(":: InlineHashSet", quote!({ 1, 2, 3, 4, 5, 6, 10000 })); + + check_impl(":: InlineScanSet", quote!({ "1", "2", "3", })); + check_impl(":: InlineOrderedScanSet", quote!({ "1", "2", "3", "4" })); + check_impl( + ":: InlineOrderedScanSet", + quote!({ "1", "2", "3", "4", "5", "6"}), + ); + check_impl( + ":: InlineHashSet", + quote!({ "1", "2", "3", "4", "5", "6", "7" }), + ); + + check_impl(":: ScanSet", quote!({ Foo(1), Foo(2), Foo(3), })); + check_impl( + ":: OrderedScanSet", + quote!({ Foo(1), Foo(2), Foo(3), Foo(4) }), + ); + check_impl( + ":: OrderedScanSet", + quote!({ Foo(1), Foo(2), Foo(3), Foo(4), Foo(5), Foo(6)}), + ); + check_impl( + ":: BinarySearchSet", + quote!({ Foo(1), Foo(2), Foo(3), Foo(4), Foo(5), Foo(6), Foo(7) }), + ); + + check_impl( + ":: EytzingerSearchSet", + quote!({ Foo(0), Foo(1), Foo(2), Foo(3), Foo(4), Foo(5), Foo(6), Foo(7), Foo(8), Foo(9), + Foo(10), Foo(11), Foo(12), Foo(13), Foo(14), Foo(15), Foo(16), Foo(17), Foo(18), Foo(19), + Foo(20), Foo(21), Foo(22), Foo(23), Foo(24), Foo(25), Foo(26), Foo(27), Foo(28), Foo(29), + Foo(30), Foo(31), Foo(32), Foo(33), Foo(34), Foo(35), Foo(36), Foo(37), Foo(38), Foo(39), + Foo(40), Foo(41), Foo(42), Foo(43), Foo(44), Foo(45), Foo(46), Foo(47), Foo(48), Foo(49), + Foo(50), Foo(51), Foo(52), Foo(53), Foo(54), Foo(55), Foo(56), Foo(57), Foo(58), Foo(59), + Foo(60), Foo(61), Foo(62), Foo(63), Foo(64), Foo(65), Foo(66), Foo(67), Foo(68), Foo(69), + }), + ); + + check_impl(":: ScanSet", quote!({ x, 2, 3, })); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4 })); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4, 5, 6})); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4, 5, 6, 7 })); + + check_impl(":: ScanSet", quote!({ x, "2", "3", })); + check_impl(":: FacadeStringSet", quote!({ x, "2", "3", "4" })); + check_impl(":: FacadeStringSet", quote!({ x, "2", "3", "4", "5", "6"})); + check_impl( + ":: FacadeStringSet", + quote!({ x, "2", "3", "4", "5", "6", "7" }), + ); + } + + #[test] + fn test_selected_hash_set_implementation_types() { + fn check_impl(expected: &str, ts: TokenStream) { + let r = fz_hash_set_macro(ts).unwrap().to_string(); + assert!(r.contains(expected), "{r} doesn't contain {expected}"); + } + + check_impl(":: InlineDenseScalarLookupSet", quote!({ 1, 2, 3, })); + check_impl(":: InlineSparseScalarLookupSet", quote!({ 1, 2, 3, 4, 6 })); + check_impl(":: InlineScanSet", quote!({ 1, 2, 10000 })); + check_impl(":: InlineOrderedScanSet", quote!({ 1, 2, 3, 4, 5, 10000 })); + check_impl(":: InlineHashSet", quote!({ 1, 2, 3, 4, 5, 6, 10000 })); + + check_impl(":: InlineScanSet", quote!({ "1", "2", "3", })); + check_impl(":: InlineOrderedScanSet", quote!({ "1", "2", "3", "4" })); + check_impl( + ":: InlineOrderedScanSet", + quote!({ "1", "2", "3", "4", "5", "6"}), + ); + check_impl( + ":: InlineHashSet", + quote!({ "1", "2", "3", "4", "5", "6", "7" }), + ); + + check_impl(":: ScanSet", quote!({ Foo(1), Foo(2), Foo(3), })); + check_impl( + ":: HashSet", + quote!({ Foo(1), Foo(2), Foo(3), Foo(4), Foo(5), Foo(6), Foo(7) }), + ); + + check_impl(":: ScanSet", quote!({ x, 2, 3, })); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4 })); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4, 5, 6})); + check_impl(":: FacadeScalarSet", quote!({ x, 2, 3, 4, 5, 6, 7 })); + + check_impl(":: ScanSet", quote!({ x, "2", "3", })); + check_impl(":: FacadeStringSet", quote!({ x, "2", "3", "4" })); + check_impl(":: FacadeStringSet", quote!({ x, "2", "3", "4", "5", "6"})); + check_impl( + ":: FacadeStringSet", + quote!({ x, "2", "3", "4", "5", "6", "7" }), + ); + } + + #[test] + fn test_scalar_suffixes() { + let r = fz_scalar_set_macro(quote!({ 1i8, 2, 3, 4, 5, 6 })) + .unwrap() + .to_string(); + assert!(r.contains("1i8")); + + let r = fz_scalar_set_macro(quote!({ 1, 2, 3, 4, 5, 6 })) + .unwrap() + .to_string(); + assert!(!r.contains("1i8")); + assert!(r.contains("1i32")); + } +} diff --git a/frozen-collections-core/src/maps/binary_search_map.rs b/frozen-collections-core/src/maps/binary_search_map.rs new file mode 100644 index 0000000..6d282ad --- /dev/null +++ b/frozen-collections-core/src/maps/binary_search_map.rs @@ -0,0 +1,149 @@ +use crate::maps::decl_macros::{ + binary_search_query_funcs, debug_fn, get_many_mut_body, get_many_mut_fn, index_fn, + into_iter_fn, into_iter_mut_ref_fn, into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery}; +use crate::utils::dedup_by_keep_last; +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Comparable; + +/// A general purpose map implemented using binary search. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +#[derive(Clone)] +pub struct BinarySearchMap { + entries: Box<[(K, V)]>, +} + +impl BinarySearchMap +where + K: Ord, +{ + /// Creates a frozen map. + #[must_use] + pub fn new(mut entries: Vec<(K, V)>) -> Self { + entries.sort_by(|x, y| x.0.cmp(&y.0)); + dedup_by_keep_last(&mut entries, |x, y| x.0.eq(&y.0)); + Self::new_raw(entries) + } + + /// Creates a frozen map. + #[must_use] + pub(crate) fn new_raw(processed_entries: Vec<(K, V)>) -> Self { + Self { + entries: processed_entries.into_boxed_slice(), + } + } +} + +impl Default for BinarySearchMap { + fn default() -> Self { + Self { + entries: Box::default(), + } + } +} + +impl Map for BinarySearchMap +where + Q: ?Sized + Eq + Comparable, +{ + get_many_mut_fn!(); +} + +impl MapQuery for BinarySearchMap +where + Q: ?Sized + Eq + Comparable, +{ + binary_search_query_funcs!(); +} + +impl MapIteration for BinarySearchMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for BinarySearchMap { + fn len(&self) -> usize { + self.entries.len() + } +} + +impl Index<&Q> for BinarySearchMap +where + Q: ?Sized + Eq + Comparable, +{ + index_fn!(); +} + +impl IntoIterator for BinarySearchMap { + into_iter_fn!(entries); +} + +impl<'a, K, V> IntoIterator for &'a BinarySearchMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V> IntoIterator for &'a mut BinarySearchMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for BinarySearchMap +where + K: Ord, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Eq for BinarySearchMap +where + K: Ord, + V: Eq, +{ +} + +impl Debug for BinarySearchMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/maps/decl_macros.rs b/frozen-collections-core/src/maps/decl_macros.rs new file mode 100644 index 0000000..6a46254 --- /dev/null +++ b/frozen-collections-core/src/maps/decl_macros.rs @@ -0,0 +1,448 @@ +macro_rules! get_many_mut_fn { + ("Scalar") => { + fn get_many_mut(&mut self, keys: [&K; N]) -> Option<[&mut V; N]> { + if crate::utils::has_duplicates_with_hasher( + &keys, + &crate::hashers::PassthroughHasher::default(), + ) { + return None; + } + + get_many_mut_body!(self, keys); + } + }; + + ("Hash") => { + #[must_use] + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]> { + if crate::utils::has_duplicates_with_hasher(&keys, &self.hasher) { + return None; + } + + get_many_mut_body!(self, keys); + } + }; + + () => { + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]> { + if crate::utils::has_duplicates_slow(&keys) { + return None; + } + + get_many_mut_body!(self, keys); + } + }; +} + +macro_rules! get_many_mut_body { + ($self:ident, $keys:ident) => { + let mut result: core::mem::MaybeUninit<[&mut V; N]> = core::mem::MaybeUninit::uninit(); + let p = result.as_mut_ptr(); + let x: *mut Self = $self; + + unsafe { + for (i, key) in $keys.iter().enumerate() { + (*p)[i] = (*x).get_mut(*key)?; + } + + return Some(result.assume_init()); + } + }; +} + +macro_rules! index_fn { + () => { + type Output = V; + + #[inline] + fn index(&self, index: &Q) -> &Self::Output { + self.get(index).expect("index should be valid") + } + }; +} + +macro_rules! into_iter_fn { + ($($entries:ident)+) => { + type Item = (K, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter::new(self$(. $entries)+.into()) + } + }; +} + +macro_rules! into_iter_ref_fn { + () => { + type Item = (&'a K, &'a V); + type IntoIter = Iter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } + }; +} + +macro_rules! into_iter_mut_ref_fn { + () => { + type Item = (&'a K, &'a mut V); + type IntoIter = IterMut<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } + }; +} + +macro_rules! partial_eq_fn { + () => { + fn eq(&self, other: &MT) -> bool { + if self.len() != other.len() { + return false; + } + + return self + .iter() + .all(|(key, value)| other.get(key).map_or(false, |v| *value == *v)); + } + }; +} + +macro_rules! debug_fn { + () => { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let pairs = self.entries.iter().map(|x| (&x.0, &x.1)); + f.debug_map().entries(pairs).finish() + } + }; +} + +macro_rules! map_iteration_funcs { + ($($entries:ident)+) => { + type IntoKeyIterator = IntoKeys; + type IntoValueIterator = IntoValues; + + fn iter(&self) -> Self::Iterator<'_> { + Iter::new(&self$(. $entries)+) + } + + fn keys(&self) -> Self::KeyIterator<'_> { + Keys::new(&self$(. $entries)+) + } + + fn values(&self) -> Self::ValueIterator<'_> { + Values::new(&self$(. $entries)+) + } + + fn into_keys(self) -> Self::IntoKeyIterator { + // NOTE: this allocates and copies everything into a vector for the sake of iterating the vector. + // This is the best I could come up with, let me know if you see a way around the need to copy. + IntoKeys::new(Vec::from(self$(. $entries)+).into_boxed_slice()) + } + + fn into_values(self) -> Self::IntoValueIterator { + // NOTE: this allocates and copies everything into a vector for the sake of iterating the vector. + // This is the best I could come up with, let me know if you see a way around the need to copy. + IntoValues::new(Vec::from(self$(. $entries)+).into_boxed_slice()) + } + + fn iter_mut(&mut self) -> Self::MutIterator<'_> { + IterMut::new(self$(. $entries)+.as_mut()) + } + + fn values_mut(&mut self) -> Self::ValueMutIterator<'_> { + ValuesMut::new(self$(. $entries)+.as_mut()) + } + }; +} + +macro_rules! dense_scalar_lookup_query_funcs { + () => { + #[inline] + fn get(&self, key: &K) -> Option<&V> { + let index = key.index(); + if index >= self.min && index <= self.max { + let entry = unsafe { self.entries.get_unchecked(index - self.min) }; + Some(&entry.1) + } else { + None + } + } + + #[inline] + fn get_key_value(&self, key: &K) -> Option<(&K, &V)> { + let index = key.index(); + if index >= self.min && index <= self.max { + let entry = unsafe { self.entries.get_unchecked(index - self.min) }; + Some((&entry.0, &entry.1)) + } else { + None + } + } + + #[inline] + fn get_mut(&mut self, key: &K) -> Option<&mut V> { + let index = key.index(); + if index >= self.min && index <= self.max { + let entry = unsafe { self.entries.get_unchecked_mut(index - self.min) }; + Some(&mut entry.1) + } else { + None + } + } + }; +} + +macro_rules! binary_search_query_funcs { + () => { + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + self.entries + .binary_search_by(|entry| key.compare(&entry.0).reverse()) + .map(|index| { + let entry = unsafe { self.entries.get_unchecked(index) }; + &entry.1 + }) + .ok() + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + self.entries + .binary_search_by(|entry| key.compare(&entry.0).reverse()) + .map(|index| { + let entry = unsafe { self.entries.get_unchecked_mut(index) }; + &mut entry.1 + }) + .ok() + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + self.entries + .binary_search_by(|entry| key.compare(&entry.0).reverse()) + .map(|index| { + let entry = unsafe { self.entries.get_unchecked(index) }; + (&entry.0, &entry.1) + }) + .ok() + } + }; +} + +macro_rules! eytzinger_search_query_funcs { + () => { + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + if let Some(index) = + eytzinger_search_by(&self.entries, |entry| key.compare(&entry.0).reverse()) + { + let entry = unsafe { self.entries.get_unchecked(index) }; + Some(&entry.1) + } else { + None + } + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + if let Some(index) = + eytzinger_search_by(&self.entries, |entry| key.compare(&entry.0).reverse()) + { + let entry = unsafe { self.entries.get_unchecked_mut(index) }; + Some(&mut entry.1) + } else { + None + } + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + if let Some(index) = + eytzinger_search_by(&self.entries, |entry| key.compare(&entry.0).reverse()) + { + let entry = unsafe { self.entries.get_unchecked(index) }; + Some((&entry.0, &entry.1)) + } else { + None + } + } + }; +} + +macro_rules! sparse_scalar_lookup_query_funcs { + () => { + #[inline] + fn get(&self, key: &K) -> Option<&V> { + let index = key.index(); + if index >= self.min && index <= self.max { + let index_in_lookup = index - self.min; + let index_in_entries: usize = + unsafe { (*self.lookup.get_unchecked(index_in_lookup)).into() }; + if index_in_entries > 0 { + let entry = unsafe { self.entries.get_unchecked(index_in_entries - 1) }; + return Some(&entry.1); + } + } + + None + } + + #[inline] + fn get_key_value(&self, key: &K) -> Option<(&K, &V)> { + let index = key.index(); + if index >= self.min && index <= self.max { + let index_in_lookup = index - self.min; + let index_in_entries: usize = + unsafe { (*self.lookup.get_unchecked(index_in_lookup)).into() }; + if index_in_entries > 0 { + let entry = unsafe { self.entries.get_unchecked(index_in_entries - 1) }; + return Some((&entry.0, &entry.1)); + } + } + + None + } + + #[inline] + fn get_mut(&mut self, key: &K) -> Option<&mut V> { + let index = key.index(); + if index >= self.min && index <= self.max { + let index_in_lookup = index - self.min; + let index_in_entries: usize = + unsafe { (*self.lookup.get_unchecked(index_in_lookup)).into() }; + if index_in_entries > 0 { + let entry = unsafe { self.entries.get_unchecked_mut(index_in_entries - 1) }; + return Some(&mut entry.1); + } + } + + None + } + }; +} + +macro_rules! scan_query_funcs { + () => { + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + for entry in &self.entries { + if key.equivalent(&entry.0) { + return Some(&entry.1); + } + } + + None + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + for entry in &mut self.entries { + if key.equivalent(&entry.0) { + return Some(&mut entry.1); + } + } + + None + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + for entry in &self.entries { + if key.equivalent(&entry.0) { + return Some((&entry.0, &entry.1)); + } + } + + None + } + }; +} + +macro_rules! ordered_scan_query_funcs { + () => { + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + for entry in &self.entries { + let ord = key.compare(&entry.0); + if ord == Ordering::Equal { + return Some(&entry.1); + } else if ord == Ordering::Less { + break; + } + } + + None + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + for entry in &mut self.entries { + let ord = key.compare(&entry.0); + if ord == Ordering::Equal { + return Some(&mut entry.1); + } else if ord == Ordering::Less { + break; + } + } + + None + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + for entry in &self.entries { + let ord = key.compare(&entry.0); + if ord == Ordering::Equal { + return Some((&entry.0, &entry.1)); + } else if ord == Ordering::Less { + break; + } + } + + None + } + }; +} + +macro_rules! hash_query_funcs { + () => { + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + self.table + .find(self.hasher.hash(key), |entry| key.equivalent(&entry.0)) + .map(|(_, v)| v) + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + self.table + .find(self.hasher.hash(key), |entry| key.equivalent(&entry.0)) + .map(|(k, v)| (k, v)) + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + self.table + .find_mut(self.hasher.hash(key), |entry| key.equivalent(&entry.0)) + .map(|(_, v)| v) + } + }; +} + +pub(crate) use binary_search_query_funcs; +pub(crate) use debug_fn; +pub(crate) use dense_scalar_lookup_query_funcs; +pub(crate) use eytzinger_search_query_funcs; +pub(crate) use get_many_mut_body; +pub(crate) use get_many_mut_fn; +pub(crate) use hash_query_funcs; +pub(crate) use index_fn; +pub(crate) use into_iter_fn; +pub(crate) use into_iter_mut_ref_fn; +pub(crate) use into_iter_ref_fn; +pub(crate) use map_iteration_funcs; +pub(crate) use ordered_scan_query_funcs; +pub(crate) use partial_eq_fn; +pub(crate) use scan_query_funcs; +pub(crate) use sparse_scalar_lookup_query_funcs; diff --git a/frozen-collections-core/src/maps/dense_scalar_lookup_map.rs b/frozen-collections-core/src/maps/dense_scalar_lookup_map.rs new file mode 100644 index 0000000..e08022f --- /dev/null +++ b/frozen-collections-core/src/maps/dense_scalar_lookup_map.rs @@ -0,0 +1,189 @@ +use alloc::boxed::Box; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; + +use crate::maps::decl_macros::{ + debug_fn, dense_scalar_lookup_query_funcs, get_many_mut_body, get_many_mut_fn, index_fn, + into_iter_fn, into_iter_mut_ref_fn, into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery, Scalar}; +use crate::utils::dedup_by_keep_last; + +/// A map whose keys are a continuous range in a sequence of scalar values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +#[derive(Clone)] +pub struct DenseScalarLookupMap { + min: usize, + max: usize, + entries: Box<[(K, V)]>, +} + +impl DenseScalarLookupMap +where + K: Scalar, +{ + /// Creates a frozen map. + /// + /// # Errors + /// + /// Fails if the all the keys in the input vector, after sorting and dedupping, + /// don't represent a continuous range of values. + pub fn new(mut entries: Vec<(K, V)>) -> core::result::Result { + entries.sort_by_key(|x| x.0); + dedup_by_keep_last(&mut entries, |x, y| x.0.eq(&y.0)); + + if entries.is_empty() { + return Ok(Self::default()); + } + + let min = entries[0].0.index(); + let max = entries[entries.len() - 1].0.index(); + + if entries.len() == max - min + 1 { + Ok(Self::new_raw(entries)) + } else { + Err("keys must be in a contiguous range <= usize::MAX in size".to_string()) + } + } + + /// Creates a new frozen map. + /// + /// This function assumes that `min` <= `max` and that the vector is sorted according to the + /// order of the [`Ord`] trait. + #[must_use] + pub(crate) fn new_raw(processed_entries: Vec<(K, V)>) -> Self { + Self { + min: processed_entries[0].0.index(), + max: processed_entries[processed_entries.len() - 1].0.index(), + entries: processed_entries.into_boxed_slice(), + } + } +} + +impl Default for DenseScalarLookupMap { + fn default() -> Self { + Self { + min: 1, + max: 0, + entries: Box::new([]), + } + } +} + +impl Map for DenseScalarLookupMap +where + K: Scalar, +{ + get_many_mut_fn!("Scalar"); +} + +impl MapQuery for DenseScalarLookupMap +where + K: Scalar, +{ + dense_scalar_lookup_query_funcs!(); +} + +impl MapIteration for DenseScalarLookupMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for DenseScalarLookupMap { + fn len(&self) -> usize { + self.entries.len() + } +} + +impl Index<&Q> for DenseScalarLookupMap +where + Q: Scalar, +{ + index_fn!(); +} + +impl IntoIterator for DenseScalarLookupMap { + into_iter_fn!(entries); +} + +impl<'a, K, V> IntoIterator for &'a DenseScalarLookupMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V> IntoIterator for &'a mut DenseScalarLookupMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for DenseScalarLookupMap +where + K: Scalar, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Eq for DenseScalarLookupMap +where + K: Scalar, + V: Eq, +{ +} + +impl Debug for DenseScalarLookupMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn error_in_new() { + let map = DenseScalarLookupMap::::new(vec![(1, 1), (2, 2), (4, 3)]); + assert_eq!( + map, + Err("keys must be in a contiguous range <= usize::MAX in size".to_string()) + ); + } +} diff --git a/frozen-collections-core/src/maps/eytzinger_search_map.rs b/frozen-collections-core/src/maps/eytzinger_search_map.rs new file mode 100644 index 0000000..580ce34 --- /dev/null +++ b/frozen-collections-core/src/maps/eytzinger_search_map.rs @@ -0,0 +1,151 @@ +use crate::maps::decl_macros::{ + debug_fn, eytzinger_search_query_funcs, get_many_mut_body, get_many_mut_fn, index_fn, + into_iter_fn, into_iter_mut_ref_fn, into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery}; +use crate::utils::{dedup_by_keep_last, eytzinger_search_by, eytzinger_sort}; +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use core::option::Option; +use equivalent::Comparable; + +/// A general purpose map implemented using Eytzinger search. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +#[derive(Clone)] +pub struct EytzingerSearchMap { + entries: Box<[(K, V)]>, +} + +impl EytzingerSearchMap +where + K: Ord, +{ + /// Creates a frozen map. + #[must_use] + pub fn new(mut entries: Vec<(K, V)>) -> Self { + entries.sort_by(|x, y| x.0.cmp(&y.0)); + dedup_by_keep_last(&mut entries, |x, y| x.0.eq(&y.0)); + eytzinger_sort(&mut entries); + Self::new_raw(entries) + } + + /// Creates a frozen map. + #[must_use] + pub(crate) fn new_raw(processed_entries: Vec<(K, V)>) -> Self { + Self { + entries: processed_entries.into_boxed_slice(), + } + } +} + +impl Default for EytzingerSearchMap { + fn default() -> Self { + Self { + entries: Box::default(), + } + } +} + +impl Map for EytzingerSearchMap +where + Q: ?Sized + Eq + Comparable, +{ + get_many_mut_fn!(); +} + +impl MapQuery for EytzingerSearchMap +where + Q: ?Sized + Eq + Comparable, +{ + eytzinger_search_query_funcs!(); +} + +impl MapIteration for EytzingerSearchMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for EytzingerSearchMap { + fn len(&self) -> usize { + self.entries.len() + } +} + +impl Index<&Q> for EytzingerSearchMap +where + Q: ?Sized + Eq + Comparable, +{ + index_fn!(); +} + +impl IntoIterator for EytzingerSearchMap { + into_iter_fn!(entries); +} + +impl<'a, K, V> IntoIterator for &'a EytzingerSearchMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V> IntoIterator for &'a mut EytzingerSearchMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for EytzingerSearchMap +where + K: Ord, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Eq for EytzingerSearchMap +where + K: Ord, + V: Eq, +{ +} + +impl Debug for EytzingerSearchMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/maps/hash_map.rs b/frozen-collections-core/src/maps/hash_map.rs new file mode 100644 index 0000000..855fd4f --- /dev/null +++ b/frozen-collections-core/src/maps/hash_map.rs @@ -0,0 +1,234 @@ +use crate::hash_tables::HashTable; +use crate::hashers::BridgeHasher; +use crate::maps::decl_macros::{ + get_many_mut_body, get_many_mut_fn, hash_query_funcs, index_fn, into_iter_fn, + into_iter_mut_ref_fn, into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{ + CollectionMagnitude, Hasher, Len, Map, MapIteration, MapQuery, SmallCollection, +}; +use crate::utils::dedup_by_hash_keep_last; +use alloc::string::String; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Equivalent; + +/// A general purpose map implemented using a hash table. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/hash_warning.md")] +/// +#[derive(Clone)] +pub struct HashMap { + table: HashTable<(K, V), CM>, + hasher: H, +} + +impl HashMap +where + K: Eq, + CM: CollectionMagnitude, + H: Hasher, +{ + /// Creates a frozen map. + /// + /// # Errors + /// + /// Fails if the number of entries in the vector, after deduplication, exceeds the + /// magnitude of the collection as specified by the `CM` generic argument. + pub fn new(mut entries: Vec<(K, V)>, hasher: H) -> core::result::Result { + dedup_by_hash_keep_last(&mut entries, &hasher); + Self::new_half_baked(entries, hasher) + } + + /// Creates a frozen map. + /// + /// # Errors + /// + /// Fails if the number of entries in the vector, after deduplication, exceeds the + /// magnitude of the collection as specified by the `CM` generic argument. + pub(crate) fn new_half_baked( + processed_entries: Vec<(K, V)>, + hasher: H, + ) -> core::result::Result { + let c = &hasher; + let h = |entry: &(K, V)| c.hash(&entry.0); + Ok(Self::new_raw( + HashTable::<(K, V), CM>::new(processed_entries, h)?, + hasher, + )) + } + + /// Creates a frozen map. + pub(crate) const fn new_raw(table: HashTable<(K, V), CM>, hasher: H) -> Self { + Self { table, hasher } + } +} + +impl Default for HashMap +where + CM: CollectionMagnitude, + H: Default, +{ + fn default() -> Self { + Self { + table: HashTable::default(), + hasher: H::default(), + } + } +} + +impl Map for HashMap +where + CM: CollectionMagnitude, + Q: ?Sized + Eq + Equivalent, + H: Hasher, +{ + get_many_mut_fn!("Hash"); +} + +impl MapQuery for HashMap +where + CM: CollectionMagnitude, + Q: ?Sized + Eq + Equivalent, + H: Hasher, +{ + hash_query_funcs!(); +} + +impl MapIteration for HashMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a, + CM: 'a, + H: 'a; + + map_iteration_funcs!(table entries); +} + +impl Len for HashMap { + fn len(&self) -> usize { + self.table.len() + } +} + +impl Index<&Q> for HashMap +where + Q: ?Sized + Eq + Equivalent, + CM: CollectionMagnitude, + H: Hasher, +{ + index_fn!(); +} + +impl IntoIterator for HashMap { + into_iter_fn!(table entries); +} + +impl<'a, K, V, CM, H> IntoIterator for &'a HashMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V, CM, H> IntoIterator for &'a mut HashMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for HashMap +where + K: Eq, + V: PartialEq, + MT: Map, + CM: CollectionMagnitude, + H: Hasher, +{ + partial_eq_fn!(); +} + +impl Eq for HashMap +where + K: Eq, + V: Eq, + CM: CollectionMagnitude, + H: Hasher, +{ +} + +impl Debug for HashMap +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let pairs = self.table.entries.iter().map(|x| (&x.0, &x.1)); + f.debug_map().entries(pairs).finish() + } +} + +#[cfg(test)] +mod test { + use crate::hashers::BridgeHasher; + use crate::maps::HashMap; + use crate::traits::SmallCollection; + + #[test] + fn fails_when_not_in_magnitude() { + let mut input: Vec<(i32, i32)> = Vec::new(); + for i in 0..255 { + input.push((i, i)); + } + + assert!(HashMap::<_, _, SmallCollection, BridgeHasher>::new( + input, + BridgeHasher::default() + ) + .is_ok()); + + let mut input: Vec<(i32, i32)> = Vec::new(); + for i in 0..256 { + input.push((i, i)); + } + + assert!(HashMap::<_, _, SmallCollection, BridgeHasher>::new( + input, + BridgeHasher::default() + ) + .is_err()); + } +} diff --git a/frozen-collections-core/src/maps/iterators.rs b/frozen-collections-core/src/maps/iterators.rs new file mode 100644 index 0000000..4ed7184 --- /dev/null +++ b/frozen-collections-core/src/maps/iterators.rs @@ -0,0 +1,830 @@ +use alloc::boxed::Box; +use core::fmt::{Debug, Formatter}; +use core::iter::FusedIterator; + +/// An iterator over the entries of a map. +pub struct Iter<'a, K, V> { + inner: core::slice::Iter<'a, (K, V)>, +} + +impl<'a, K, V> Iter<'a, K, V> { + pub(crate) fn new(entries: &'a [(K, V)]) -> Self { + Self { + inner: entries.iter(), + } + } +} + +impl<'a, K, V> Iterator for Iter<'a, K, V> { + type Item = (&'a K, &'a V); + + fn next(&mut self) -> Option { + self.inner.next().map(|entry| (&entry.0, &entry.1)) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (k, v)| f(acc, (k, v))) + } +} + +impl ExactSizeIterator for Iter<'_, K, V> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl Clone for Iter<'_, K, V> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Debug for Iter<'_, K, V> +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries(self.clone()).finish() + } +} + +/// An iterator over the entries of a map providing mutable values. +pub struct IterMut<'a, K, V> { + inner: core::slice::IterMut<'a, (K, V)>, +} + +impl<'a, K, V> IterMut<'a, K, V> { + pub(crate) fn new(entries: &'a mut [(K, V)]) -> Self { + Self { + inner: entries.iter_mut(), + } + } +} + +impl<'a, K, V> Iterator for IterMut<'a, K, V> { + type Item = (&'a K, &'a mut V); + + fn next(&mut self) -> Option { + self.inner.next().map(|entry| (&entry.0, &mut entry.1)) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (k, v)| f(acc, (k, v))) + } +} + +impl ExactSizeIterator for IterMut<'_, K, V> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for IterMut<'_, K, V> {} + +impl Debug for IterMut<'_, K, V> +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } +} + +/// An iterator over the keys of a map. +pub struct Keys<'a, K, V> { + inner: Iter<'a, K, V>, +} + +impl<'a, K, V> Keys<'a, K, V> { + #[must_use] + pub fn new(entries: &'a [(K, V)]) -> Self { + Self { + inner: Iter::new(entries), + } + } +} + +impl<'a, K, V> Iterator for Keys<'a, K, V> { + type Item = &'a K; + + fn next(&mut self) -> Option { + self.inner.next().map(|x| x.0) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (k, _)| f(acc, k)) + } +} + +impl ExactSizeIterator for Keys<'_, K, V> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for Keys<'_, K, V> {} + +impl Clone for Keys<'_, K, V> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Debug for Keys<'_, K, V> +where + K: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries(self.clone()).finish() + } +} + +/// An iterator over the values of a map. +pub struct Values<'a, K, V> { + inner: Iter<'a, K, V>, +} + +impl<'a, K, V> Values<'a, K, V> { + #[must_use] + pub fn new(entries: &'a [(K, V)]) -> Self { + Self { + inner: Iter::new(entries), + } + } +} + +impl<'a, K, V> Iterator for Values<'a, K, V> { + type Item = &'a V; + + fn next(&mut self) -> Option { + self.inner.next().map(|x| x.1) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (_, v)| f(acc, v)) + } +} + +impl ExactSizeIterator for Values<'_, K, V> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for Values<'_, K, V> {} + +impl Clone for Values<'_, K, V> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Debug for Values<'_, K, V> +where + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries(self.clone()).finish() + } +} + +/// An iterator over the mutable values of a map. +pub struct ValuesMut<'a, K, V> { + inner: IterMut<'a, K, V>, +} + +impl<'a, K, V> ValuesMut<'a, K, V> { + pub(crate) fn new(entries: &'a mut [(K, V)]) -> Self { + Self { + inner: IterMut::new(entries), + } + } +} + +impl<'a, K, V> Iterator for ValuesMut<'a, K, V> { + type Item = &'a mut V; + + fn next(&mut self) -> Option { + self.inner.next().map(|entry| entry.1) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (_, v)| f(acc, v)) + } +} + +impl ExactSizeIterator for ValuesMut<'_, K, V> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for ValuesMut<'_, K, V> {} + +impl Debug for ValuesMut<'_, K, V> +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } +} + +/// A consuming iterator over the entries in a map. +pub struct IntoIter { + inner: alloc::vec::IntoIter<(K, V)>, +} + +impl IntoIter { + pub(crate) fn new(entries: Box<[(K, V)]>) -> Self { + Self { + inner: entries.into_vec().into_iter(), + } + } +} + +impl Iterator for IntoIter { + type Item = (K, V); + + fn next(&mut self) -> Option { + self.inner.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (k, v)| f(acc, (k, v))) + } +} + +impl ExactSizeIterator for IntoIter { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for IntoIter {} + +impl Debug for IntoIter +where + K: Debug, + V: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } +} + +/// A consuming iterator over the keys in a map. +pub struct IntoKeys { + inner: IntoIter, +} + +impl IntoKeys { + pub(crate) fn new(entries: Box<[(K, V)]>) -> Self { + Self { + inner: IntoIter::new(entries), + } + } +} + +impl Iterator for IntoKeys { + type Item = K; + + fn next(&mut self) -> Option { + self.inner.next().map(|x| x.0) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (k, _)| f(acc, k)) + } +} + +impl ExactSizeIterator for IntoKeys { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for IntoKeys {} + +/// A consuming iterator over the values in a map. +pub struct IntoValues { + inner: IntoIter, +} + +impl IntoValues { + pub(crate) fn new(entries: Box<[(K, V)]>) -> Self { + Self { + inner: IntoIter::new(entries), + } + } +} + +impl Iterator for IntoValues { + type Item = V; + + fn next(&mut self) -> Option { + self.inner.next().map(|x| x.1) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (_, v)| f(acc, v)) + } +} + +impl ExactSizeIterator for IntoValues { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for IntoValues {} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + use alloc::{format, vec}; + + #[test] + fn test_iter() { + let entries = vec![("Alice", 1), ("Bob", 2), ("Sandy", 3), ("Tom", 4)]; + let iter = Iter::new(&entries); + assert_eq!(entries.len(), iter.len()); + + let collected: Vec<_> = iter.collect(); + assert_eq!( + collected, + vec![(&"Alice", &1), (&"Bob", &2), (&"Sandy", &3), (&"Tom", &4)] + ); + } + + #[test] + fn test_iter_count() { + let entries = vec![("Alice", 1), ("Bob", 2), ("Sandy", 3), ("Tom", 4)]; + let iter = Iter::new(&entries); + assert_eq!(entries.len(), iter.len()); + assert_eq!(entries.len(), iter.count()); + } + + #[test] + fn test_iter_empty() { + let entries: Vec<(&str, i32)> = vec![]; + let mut iter = Iter::new(&entries); + assert_eq!(0, iter.len()); + assert!(iter.next().is_none()); + } + + #[test] + fn test_iter_debug() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let iter = Iter::new(&entries); + + let debug_str = format!("{iter:?}"); + assert!(debug_str.contains("Alice")); + assert!(debug_str.contains("Bob")); + } + + #[test] + fn test_iter_mut() { + let mut entries = vec![("Alice", 1), ("Bob", 2), ("Sandy", 3), ("Tom", 4)]; + let iter_mut = IterMut::new(&mut entries); + + for (_, v) in iter_mut { + *v += 1; + } + + let expected = vec![("Alice", 2), ("Bob", 3), ("Sandy", 4), ("Tom", 5)]; + assert_eq!(entries, expected); + } + + #[test] + fn test_iter_mut_count() { + let mut entries = vec![("Alice", 1), ("Bob", 2), ("Sandy", 3), ("Tom", 4)]; + let iter_mut = IterMut::new(&mut entries); + assert_eq!(4, iter_mut.count()); + } + + #[test] + fn test_iter_mut_empty() { + let mut entries: Vec<(&str, i32)> = vec![]; + let mut iter_mut = IterMut::new(&mut entries); + assert_eq!(0, iter_mut.len()); + assert!(iter_mut.next().is_none()); + } + + #[test] + fn test_iter_size_hint() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let iter = Iter::new(&entries); + + assert_eq!(iter.size_hint(), (2, Some(2))); + } + + #[test] + fn test_iter_mut_size_hint() { + let mut entries = vec![("Alice", 1), ("Bob", 2)]; + let iter_mut = IterMut::new(&mut entries); + + assert_eq!(iter_mut.size_hint(), (2, Some(2))); + } + + #[test] + fn test_iter_clone() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let iter = Iter::new(&entries); + let iter_clone = iter.clone(); + + let collected: Vec<_> = iter_clone.collect(); + assert_eq!(collected, vec![(&"Alice", &1), (&"Bob", &2)]); + } + + #[test] + fn test_iter_mut_debug() { + let mut entries = vec![("Alice", 1), ("Bob", 2)]; + let iter_mut = IterMut::new(&mut entries); + + let debug_str = format!("{iter_mut:?}"); + assert!(debug_str.contains("Alice")); + assert!(debug_str.contains("Bob")); + } + + #[test] + fn test_keys() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let keys = Keys::new(&entries); + assert_eq!(entries.len(), keys.len()); + + let collected: Vec<_> = keys.collect(); + assert_eq!(collected, vec![&"Alice", &"Bob"]); + } + + #[test] + fn test_keys_count() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let keys = Keys::new(&entries); + assert_eq!(2, keys.count()); + } + + #[test] + fn test_keys_empty() { + let entries: Vec<(&str, i32)> = vec![]; + let mut keys = Keys::new(&entries); + assert_eq!(0, keys.len()); + assert!(keys.next().is_none()); + } + + #[test] + fn test_keys_size_hint() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let keys = Keys::new(&entries); + + assert_eq!(keys.size_hint(), (2, Some(2))); + } + + #[test] + fn test_keys_clone() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let keys = Keys::new(&entries); + let keys_clone = keys.clone(); + + let collected: Vec<_> = keys_clone.collect(); + assert_eq!(collected, vec![&"Alice", &"Bob"]); + } + + #[test] + fn test_keys_debug() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let keys = Keys::new(&entries); + + let debug_str = format!("{keys:?}"); + assert!(debug_str.contains("Alice")); + assert!(debug_str.contains("Bob")); + } + + #[test] + fn test_values() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let values = Values::new(&entries); + assert_eq!(entries.len(), values.len()); + + let collected: Vec<_> = values.collect(); + assert_eq!(collected, vec![&1, &2]); + } + + #[test] + fn test_values_count() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let values = Values::new(&entries); + assert_eq!(2, values.count()); + } + + #[test] + fn test_values_empty() { + let entries: Vec<(&str, i32)> = vec![]; + let mut values = Values::new(&entries); + assert_eq!(0, values.len()); + assert!(values.next().is_none()); + } + + #[test] + fn test_values_size_hint() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let values = Values::new(&entries); + + assert_eq!(values.size_hint(), (2, Some(2))); + } + + #[test] + fn test_values_clone() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let values = Values::new(&entries); + let values_clone = values.clone(); + + let collected: Vec<_> = values_clone.collect(); + assert_eq!(collected, vec![&1, &2]); + } + + #[test] + fn test_values_debug() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let values = Values::new(&entries); + + let debug_str = format!("{values:?}"); + assert!(debug_str.contains('1')); + assert!(debug_str.contains('2')); + } + + #[test] + fn test_into_keys() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let into_keys = IntoKeys::new(entries.into_boxed_slice()); + assert_eq!(2, into_keys.len()); + + let collected: Vec<_> = into_keys.collect(); + assert_eq!(collected, vec!["Alice", "Bob"]); + } + + #[test] + fn test_into_keys_count() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let into_keys = IntoKeys::new(entries.into_boxed_slice()); + assert_eq!(2, into_keys.count()); + } + + #[test] + fn test_into_keys_empty() { + let entries: Vec<(&str, i32)> = vec![]; + let mut into_keys = IntoKeys::new(entries.into_boxed_slice()); + assert_eq!(0, into_keys.len()); + assert!(into_keys.next().is_none()); + } + + #[test] + fn test_into_keys_size_hint() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let into_keys = IntoKeys::new(entries.into_boxed_slice()); + + assert_eq!(into_keys.size_hint(), (2, Some(2))); + } + + #[test] + fn test_into_values() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let into_values = IntoValues::new(entries.into_boxed_slice()); + assert_eq!(2, into_values.len()); + + let collected: Vec<_> = into_values.collect(); + assert_eq!(collected, vec![1, 2]); + } + + #[test] + fn test_into_values_count() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let into_values = IntoValues::new(entries.into_boxed_slice()); + assert_eq!(2, into_values.count()); + } + + #[test] + fn test_into_values_empty() { + let entries: Vec<(&str, i32)> = vec![]; + let mut into_values = IntoValues::new(entries.into_boxed_slice()); + assert_eq!(0, into_values.len()); + assert!(into_values.next().is_none()); + } + + #[test] + fn test_into_values_size_hint() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let into_values = IntoValues::new(entries.into_boxed_slice()); + assert_eq!(into_values.size_hint(), (2, Some(2))); + } + + #[test] + fn test_values_mut() { + let mut entries = vec![("Alice", 1), ("Bob", 2)]; + let values_mut = ValuesMut::new(&mut entries); + assert_eq!(2, values_mut.len()); + + for v in values_mut { + *v += 1; + } + + let expected = vec![("Alice", 2), ("Bob", 3)]; + assert_eq!(entries, expected); + } + + #[test] + fn test_values_mut_count() { + let mut entries = vec![("Alice", 1), ("Bob", 2)]; + let values_mut = ValuesMut::new(&mut entries); + assert_eq!(2, values_mut.count()); + } + + #[test] + fn test_values_mut_empty() { + let mut entries: Vec<(&str, i32)> = vec![]; + let mut values_mut = ValuesMut::new(&mut entries); + assert_eq!(0, values_mut.len()); + assert!(values_mut.next().is_none()); + } + + #[test] + fn test_values_mut_size_hint() { + let mut entries = vec![("Alice", 1), ("Bob", 2)]; + let values_mut = ValuesMut::new(&mut entries); + assert_eq!(values_mut.size_hint(), (2, Some(2))); + } + + #[test] + fn test_values_mut_debug() { + let mut entries = vec![("Alice", 1), ("Bob", 2)]; + let values_mut = ValuesMut::new(&mut entries); + + let debug_str = format!("{values_mut:?}"); + assert!(debug_str.contains("Alice")); + assert!(debug_str.contains("Bob")); + } + + #[test] + fn test_into_iter() { + let entries = vec![("Alice", 1), ("Bob", 2), ("Sandy", 3), ("Tom", 4)]; + let into_iter = IntoIter::new(entries.into_boxed_slice()); + assert_eq!(4, into_iter.len()); + + let collected: Vec<_> = into_iter.collect(); + assert_eq!( + collected, + vec![("Alice", 1), ("Bob", 2), ("Sandy", 3), ("Tom", 4)] + ); + } + + #[test] + fn test_into_iter_count() { + let entries = vec![("Alice", 1), ("Bob", 2), ("Sandy", 3), ("Tom", 4)]; + let into_iter = IntoIter::new(entries.into_boxed_slice()); + assert_eq!(4, into_iter.count()); + } + + #[test] + fn test_into_iter_empty() { + let entries: Vec<(&str, i32)> = vec![]; + let mut into_iter = IntoIter::new(entries.into_boxed_slice()); + assert_eq!(0, into_iter.len()); + assert!(into_iter.next().is_none()); + } + + #[test] + fn test_into_iter_size_hint() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let into_iter = IntoIter::new(entries.into_boxed_slice()); + + assert_eq!(into_iter.size_hint(), (2, Some(2))); + } + + #[test] + fn test_into_iter_debug() { + let entries = vec![("Alice", 1), ("Bob", 2)]; + let into_iter = IntoIter::new(entries.into_boxed_slice()); + + let debug_str = format!("{into_iter:?}"); + assert!(debug_str.contains("Alice")); + assert!(debug_str.contains("Bob")); + } +} diff --git a/frozen-collections-core/src/maps/mod.rs b/frozen-collections-core/src/maps/mod.rs new file mode 100644 index 0000000..b7a5518 --- /dev/null +++ b/frozen-collections-core/src/maps/mod.rs @@ -0,0 +1,20 @@ +//! Specialized read-only map types. + +pub use binary_search_map::BinarySearchMap; +pub use dense_scalar_lookup_map::DenseScalarLookupMap; +pub use eytzinger_search_map::EytzingerSearchMap; +pub use hash_map::HashMap; +pub use iterators::*; +pub use ordered_scan_map::OrderedScanMap; +pub use scan_map::ScanMap; +pub use sparse_scalar_lookup_map::SparseScalarLookupMap; + +mod binary_search_map; +pub(crate) mod decl_macros; +mod dense_scalar_lookup_map; +mod eytzinger_search_map; +mod hash_map; +mod iterators; +mod ordered_scan_map; +mod scan_map; +mod sparse_scalar_lookup_map; diff --git a/frozen-collections-core/src/maps/ordered_scan_map.rs b/frozen-collections-core/src/maps/ordered_scan_map.rs new file mode 100644 index 0000000..5c8ecdc --- /dev/null +++ b/frozen-collections-core/src/maps/ordered_scan_map.rs @@ -0,0 +1,150 @@ +use crate::maps::decl_macros::{ + debug_fn, get_many_mut_body, get_many_mut_fn, index_fn, into_iter_fn, into_iter_mut_ref_fn, + into_iter_ref_fn, map_iteration_funcs, ordered_scan_query_funcs, partial_eq_fn, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery}; +use crate::utils::dedup_by_keep_last; +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::cmp::Ordering; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Comparable; + +/// A general purpose map implemented using linear scanning. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +#[derive(Clone)] +pub struct OrderedScanMap { + entries: Box<[(K, V)]>, +} + +impl OrderedScanMap +where + K: Ord, +{ + /// Creates a frozen map. + #[must_use] + pub fn new(mut entries: Vec<(K, V)>) -> Self { + entries.sort_by(|x, y| x.0.cmp(&y.0)); + dedup_by_keep_last(&mut entries, |x, y| x.0.eq(&y.0)); + Self::new_raw(entries) + } + + /// Creates a frozen map. + #[must_use] + pub(crate) fn new_raw(processed_entries: Vec<(K, V)>) -> Self { + Self { + entries: processed_entries.into_boxed_slice(), + } + } +} + +impl Default for OrderedScanMap { + fn default() -> Self { + Self { + entries: Box::default(), + } + } +} + +impl Map for OrderedScanMap +where + Q: ?Sized + Eq + Comparable, +{ + get_many_mut_fn!(); +} + +impl MapQuery for OrderedScanMap +where + Q: ?Sized + Eq + Comparable, +{ + ordered_scan_query_funcs!(); +} + +impl MapIteration for OrderedScanMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for OrderedScanMap { + fn len(&self) -> usize { + self.entries.len() + } +} + +impl Index<&Q> for OrderedScanMap +where + Q: ?Sized + Eq + Comparable, +{ + index_fn!(); +} + +impl IntoIterator for OrderedScanMap { + into_iter_fn!(entries); +} + +impl<'a, K, V> IntoIterator for &'a OrderedScanMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V> IntoIterator for &'a mut OrderedScanMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for OrderedScanMap +where + K: Ord, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Eq for OrderedScanMap +where + K: Ord, + V: Eq, +{ +} + +impl Debug for OrderedScanMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/maps/scan_map.rs b/frozen-collections-core/src/maps/scan_map.rs new file mode 100644 index 0000000..1c6ffad --- /dev/null +++ b/frozen-collections-core/src/maps/scan_map.rs @@ -0,0 +1,140 @@ +use crate::maps::decl_macros::{ + debug_fn, get_many_mut_body, get_many_mut_fn, index_fn, into_iter_fn, into_iter_mut_ref_fn, + into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, scan_query_funcs, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery}; +use crate::utils::dedup_by_keep_last_slow; +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; +use equivalent::Equivalent; + +/// A general purpose map implemented using linear scanning. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +#[derive(Clone, Eq)] +pub struct ScanMap { + entries: Box<[(K, V)]>, +} + +impl ScanMap +where + K: Eq, +{ + /// Creates a frozen map. + #[must_use] + pub fn new(mut entries: Vec<(K, V)>) -> Self { + dedup_by_keep_last_slow(&mut entries, |x, y| x.0.eq(&y.0)); + Self::new_raw(entries) + } + + /// Creates a frozen map. + #[must_use] + pub(crate) fn new_raw(processed_entries: Vec<(K, V)>) -> Self { + Self { + entries: processed_entries.into_boxed_slice(), + } + } +} + +impl Default for ScanMap { + fn default() -> Self { + Self { + entries: Box::default(), + } + } +} + +impl Map for ScanMap +where + Q: ?Sized + Eq + Equivalent, +{ + get_many_mut_fn!(); +} + +impl MapQuery for ScanMap +where + Q: ?Sized + Eq + Equivalent, +{ + scan_query_funcs!(); +} + +impl MapIteration for ScanMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for ScanMap { + fn len(&self) -> usize { + self.entries.len() + } +} + +impl Index<&Q> for ScanMap +where + Q: ?Sized + Eq + Equivalent, +{ + index_fn!(); +} + +impl IntoIterator for ScanMap { + into_iter_fn!(entries); +} + +impl<'a, K, V> IntoIterator for &'a ScanMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V> IntoIterator for &'a mut ScanMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for ScanMap +where + K: Eq, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Debug for ScanMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/maps/sparse_scalar_lookup_map.rs b/frozen-collections-core/src/maps/sparse_scalar_lookup_map.rs new file mode 100644 index 0000000..4682bb6 --- /dev/null +++ b/frozen-collections-core/src/maps/sparse_scalar_lookup_map.rs @@ -0,0 +1,174 @@ +use crate::maps::decl_macros::{ + debug_fn, get_many_mut_body, get_many_mut_fn, index_fn, into_iter_fn, into_iter_mut_ref_fn, + into_iter_ref_fn, map_iteration_funcs, partial_eq_fn, sparse_scalar_lookup_query_funcs, +}; +use crate::maps::{IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, Values, ValuesMut}; +use crate::traits::{Len, Map, MapIteration, MapQuery, Scalar}; +use crate::utils::dedup_by_keep_last; +use alloc::boxed::Box; +use alloc::vec; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter, Result}; +use core::ops::Index; + +/// A map whose keys are a sparse range of values from a scalar. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +#[derive(Clone)] +pub struct SparseScalarLookupMap { + min: usize, + max: usize, + lookup: Box<[usize]>, + entries: Box<[(K, V)]>, +} + +impl SparseScalarLookupMap +where + K: Scalar, +{ + /// Creates a new `IntegerSparseLookupMap` from a list of entries. + #[must_use] + pub fn new(mut entries: Vec<(K, V)>) -> Self { + entries.sort_by_key(|x| x.0); + dedup_by_keep_last(&mut entries, |x, y| x.0.eq(&y.0)); + + if entries.is_empty() { + return Self::default(); + } + + Self::new_raw(entries) + } + + /// Creates a new frozen map. + #[must_use] + pub(crate) fn new_raw(processed_entries: Vec<(K, V)>) -> Self { + let min = processed_entries[0].0.index(); + let max = processed_entries[processed_entries.len() - 1].0.index(); + let count = max - min + 1; + + let mut lookup = vec![0; count]; + + for (i, entry) in processed_entries.iter().enumerate() { + let index_in_lookup = entry.0.index() - min; + let index_in_entries = i + 1; + lookup[index_in_lookup] = index_in_entries; + } + + Self { + min, + max, + lookup: lookup.into_boxed_slice(), + entries: processed_entries.into_boxed_slice(), + } + } +} + +impl Default for SparseScalarLookupMap { + fn default() -> Self { + Self { + min: 1, + max: 0, + lookup: Box::new([]), + entries: Box::new([]), + } + } +} + +impl Map for SparseScalarLookupMap +where + K: Scalar, +{ + get_many_mut_fn!("Scalar"); +} + +impl MapQuery for SparseScalarLookupMap +where + K: Scalar, +{ + sparse_scalar_lookup_query_funcs!(); +} + +impl MapIteration for SparseScalarLookupMap { + type Iterator<'a> + = Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = Values<'a, K, V> + where + K: 'a, + V: 'a; + + type MutIterator<'a> + = IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + map_iteration_funcs!(entries); +} + +impl Len for SparseScalarLookupMap { + fn len(&self) -> usize { + self.entries.len() + } +} + +impl Index<&Q> for SparseScalarLookupMap +where + Q: Scalar, +{ + index_fn!(); +} + +impl IntoIterator for SparseScalarLookupMap { + into_iter_fn!(entries); +} + +impl<'a, K, V> IntoIterator for &'a SparseScalarLookupMap { + into_iter_ref_fn!(); +} + +impl<'a, K, V> IntoIterator for &'a mut SparseScalarLookupMap { + into_iter_mut_ref_fn!(); +} + +impl PartialEq for SparseScalarLookupMap +where + K: Scalar, + V: PartialEq, + MT: Map, +{ + partial_eq_fn!(); +} + +impl Eq for SparseScalarLookupMap +where + K: Scalar, + V: Eq, +{ +} + +impl Debug for SparseScalarLookupMap +where + K: Debug, + V: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/sets/binary_search_set.rs b/frozen-collections-core/src/sets/binary_search_set.rs new file mode 100644 index 0000000..b51c417 --- /dev/null +++ b/frozen-collections-core/src/sets/binary_search_set.rs @@ -0,0 +1,122 @@ +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; + +use crate::maps::BinarySearchMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; + +/// A general purpose set implemented using binary search. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +#[derive(Clone)] +pub struct BinarySearchSet { + map: BinarySearchMap, +} + +impl BinarySearchSet +where + T: Ord, +{ + /// Creates a frozen set. + #[must_use] + pub const fn new(map: BinarySearchMap) -> Self { + Self { map } + } +} + +impl Default for BinarySearchSet { + fn default() -> Self { + Self { + map: BinarySearchMap::default(), + } + } +} + +impl Set for BinarySearchSet where T: Ord {} + +impl SetQuery for BinarySearchSet +where + T: Ord, +{ + get_fn!("Scalar"); +} + +impl SetIteration for BinarySearchSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for BinarySearchSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &BinarySearchSet +where + T: Hash + Ord + Clone, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &BinarySearchSet +where + T: Hash + Ord + Clone, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &BinarySearchSet +where + T: Hash + Ord + Clone, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &BinarySearchSet +where + T: Hash + Ord + Clone, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for BinarySearchSet { + into_iter_fn!(); +} + +impl<'a, T> IntoIterator for &'a BinarySearchSet { + into_iter_ref_fn!(); +} + +impl PartialEq for BinarySearchSet +where + T: Ord, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for BinarySearchSet where T: Ord {} + +impl Debug for BinarySearchSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/sets/decl_macros.rs b/frozen-collections-core/src/sets/decl_macros.rs new file mode 100644 index 0000000..bfe067a --- /dev/null +++ b/frozen-collections-core/src/sets/decl_macros.rs @@ -0,0 +1,116 @@ +macro_rules! get_fn { + ("Scalar") => { + #[inline] + fn get(&self, value: &T) -> Option<&T> { + Some(self.map.get_key_value(value)?.0) + } + }; + + () => { + #[inline] + fn get(&self, value: &Q) -> Option<&T> { + Some(self.map.get_key_value(value)?.0) + } + }; +} + +macro_rules! partial_eq_fn { + () => { + fn eq(&self, other: &ST) -> bool { + if self.len() != other.len() { + return false; + } + + self.iter().all(|value| other.contains(value)) + } + }; +} + +macro_rules! into_iter_fn { + () => { + type Item = T; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter::new(self.map.into_iter()) + } + }; +} + +macro_rules! into_iter_ref_fn { + () => { + type Item = &'a T; + type IntoIter = Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } + }; +} + +macro_rules! bitor_fn { + ($build_hasher:ident) => { + type Output = hashbrown::HashSet; + + fn bitor(self, rhs: &ST) -> Self::Output { + Self::Output::from_iter(self.union(rhs).cloned()) + } + }; +} + +macro_rules! bitand_fn { + ($build_hasher:ident) => { + type Output = hashbrown::HashSet; + + fn bitand(self, rhs: &ST) -> Self::Output { + Self::Output::from_iter(self.intersection(rhs).cloned()) + } + }; +} + +macro_rules! bitxor_fn { + ($build_hasher:ident) => { + type Output = hashbrown::HashSet; + + fn bitxor(self, rhs: &ST) -> Self::Output { + self.symmetric_difference(rhs).cloned().collect() + } + }; +} + +macro_rules! sub_fn { + ($build_hasher:ident) => { + type Output = hashbrown::HashSet; + + fn sub(self, rhs: &ST) -> Self::Output { + self.difference(rhs).cloned().collect() + } + }; +} + +macro_rules! debug_fn { + () => { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_set().entries(self.iter()).finish() + } + }; +} + +macro_rules! set_iteration_funcs { + () => { + fn iter(&self) -> Iter<'_, T> { + Iter::new(self.map.iter()) + } + }; +} + +pub(crate) use bitand_fn; +pub(crate) use bitor_fn; +pub(crate) use bitxor_fn; +pub(crate) use debug_fn; +pub(crate) use get_fn; +pub(crate) use into_iter_fn; +pub(crate) use into_iter_ref_fn; +pub(crate) use partial_eq_fn; +pub(crate) use set_iteration_funcs; +pub(crate) use sub_fn; diff --git a/frozen-collections-core/src/sets/dense_scalar_lookup_set.rs b/frozen-collections-core/src/sets/dense_scalar_lookup_set.rs new file mode 100644 index 0000000..8ff7ceb --- /dev/null +++ b/frozen-collections-core/src/sets/dense_scalar_lookup_set.rs @@ -0,0 +1,128 @@ +use crate::maps::DenseScalarLookupMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Scalar, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; + +/// A set whose values are a continuous range in a sequence of scalar values. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +#[derive(Clone)] +pub struct DenseScalarLookupSet { + map: DenseScalarLookupMap, +} + +impl DenseScalarLookupSet +where + T: Scalar, +{ + /// Creates a frozen set. + /// + /// # Errors + /// + /// Fails if the all the values in the input vector, after sorting and dedupping, + /// don't represent a continuous range. + #[must_use] + pub const fn new(map: DenseScalarLookupMap) -> Self { + Self { map } + } +} + +impl Default for DenseScalarLookupSet +where + T: Scalar, +{ + fn default() -> Self { + Self { + map: DenseScalarLookupMap::default(), + } + } +} + +impl Set for DenseScalarLookupSet where T: Scalar {} + +impl SetQuery for DenseScalarLookupSet +where + T: Scalar, +{ + get_fn!("Scalar"); +} + +impl SetIteration for DenseScalarLookupSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for DenseScalarLookupSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &DenseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &DenseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &DenseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &DenseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for DenseScalarLookupSet { + into_iter_fn!(); +} + +impl<'a, T> IntoIterator for &'a DenseScalarLookupSet { + into_iter_ref_fn!(); +} + +impl PartialEq for DenseScalarLookupSet +where + T: Scalar, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for DenseScalarLookupSet where T: Scalar {} + +impl Debug for DenseScalarLookupSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/sets/eytzinger_search_set.rs b/frozen-collections-core/src/sets/eytzinger_search_set.rs new file mode 100644 index 0000000..5c9bf4b --- /dev/null +++ b/frozen-collections-core/src/sets/eytzinger_search_set.rs @@ -0,0 +1,122 @@ +use crate::maps::EytzingerSearchMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Comparable; + +/// A general purpose set implemented using Eytzinger search. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +#[derive(Clone)] +pub struct EytzingerSearchSet { + map: EytzingerSearchMap, +} + +impl EytzingerSearchSet +where + T: Ord, +{ + /// Creates a frozen set. + #[must_use] + pub const fn new(map: EytzingerSearchMap) -> Self { + Self { map } + } +} + +impl Default for EytzingerSearchSet { + fn default() -> Self { + Self { + map: EytzingerSearchMap::default(), + } + } +} + +impl Set for EytzingerSearchSet where Q: ?Sized + Eq + Comparable {} + +impl SetQuery for EytzingerSearchSet +where + Q: ?Sized + Eq + Comparable, +{ + get_fn!(); +} + +impl SetIteration for EytzingerSearchSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for EytzingerSearchSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &EytzingerSearchSet +where + T: Hash + Ord + Clone, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &EytzingerSearchSet +where + T: Hash + Ord + Clone, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &EytzingerSearchSet +where + T: Hash + Ord + Clone, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &EytzingerSearchSet +where + T: Hash + Ord + Clone, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for EytzingerSearchSet { + into_iter_fn!(); +} + +impl<'a, T> IntoIterator for &'a EytzingerSearchSet { + into_iter_ref_fn!(); +} + +impl PartialEq for EytzingerSearchSet +where + T: Ord, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for EytzingerSearchSet where T: Ord {} + +impl Debug for EytzingerSearchSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/sets/hash_set.rs b/frozen-collections-core/src/sets/hash_set.rs new file mode 100644 index 0000000..78aad81 --- /dev/null +++ b/frozen-collections-core/src/sets/hash_set.rs @@ -0,0 +1,163 @@ +use crate::hashers::BridgeHasher; +use crate::maps::HashMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, into_iter_fn, into_iter_ref_fn, partial_eq_fn, + set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{CollectionMagnitude, Len, SetOps, SetQuery, SmallCollection}; +use crate::traits::{Hasher, MapIteration, MapQuery, Set, SetIteration}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Equivalent; + +/// A general purpose set implemented using a hash table. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/hash_warning.md")] +/// +#[derive(Clone)] +pub struct HashSet { + map: HashMap, +} + +impl HashSet +where + T: Eq, + H: Hasher, +{ + /// Creates a frozen set. + /// + /// # Errors + /// + /// Fails if the number of entries in the vector, after deduplication, exceeds the + /// magnitude of the collection as specified by the `CM` generic argument. + #[must_use] + pub const fn new(map: HashMap) -> Self { + Self { map } + } +} + +impl Default for HashSet +where + CM: CollectionMagnitude, + H: Default, +{ + fn default() -> Self { + Self { + map: HashMap::default(), + } + } +} + +impl Set for HashSet +where + Q: ?Sized + Eq + Equivalent, + CM: CollectionMagnitude, + H: Hasher, +{ +} + +impl SetQuery for HashSet +where + Q: ?Sized + Eq + Equivalent, + CM: CollectionMagnitude, + H: Hasher, +{ + #[inline] + fn get(&self, value: &Q) -> Option<&T> { + Some(self.map.get_key_value(value)?.0) + } +} + +impl SetIteration for HashSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a, + CM: 'a, + H: 'a; + + set_iteration_funcs!(); +} + +impl Len for HashSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &HashSet +where + T: Hash + Eq + Clone, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + bitor_fn!(H); +} + +impl BitAnd<&ST> for &HashSet +where + T: Hash + Eq + Clone, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + bitand_fn!(H); +} + +impl BitXor<&ST> for &HashSet +where + T: Hash + Eq + Clone, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + bitxor_fn!(H); +} + +impl Sub<&ST> for &HashSet +where + T: Hash + Eq + Clone, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + sub_fn!(H); +} + +impl IntoIterator for HashSet { + into_iter_fn!(); +} + +impl<'a, T, CM, H> IntoIterator for &'a HashSet { + into_iter_ref_fn!(); +} + +impl PartialEq for HashSet +where + T: Eq, + ST: Set, + CM: CollectionMagnitude, + H: Hasher, +{ + partial_eq_fn!(); +} + +impl Eq for HashSet +where + T: Eq, + CM: CollectionMagnitude, + H: Hasher, +{ +} + +impl Debug for HashSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/sets/iterators.rs b/frozen-collections-core/src/sets/iterators.rs new file mode 100644 index 0000000..dd71839 --- /dev/null +++ b/frozen-collections-core/src/sets/iterators.rs @@ -0,0 +1,848 @@ +use crate::traits::{Set, SetIteration, SetOps}; +use core::cmp::{max, min}; +use core::fmt::{Debug, Formatter}; +use core::iter::{Chain, FusedIterator}; + +/// An iterator over the values of a set. +pub struct Iter<'a, T> { + inner: crate::maps::Iter<'a, T, ()>, +} + +impl<'a, T> Iter<'a, T> { + pub(crate) const fn new(inner: crate::maps::Iter<'a, T, ()>) -> Self { + Self { inner } + } +} + +impl<'a, T> Iterator for Iter<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + self.inner.next().map(|entry| entry.0) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (k, ())| f(acc, k)) + } +} + +impl ExactSizeIterator for Iter<'_, T> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for Iter<'_, T> {} + +impl Clone for Iter<'_, T> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Debug for Iter<'_, T> +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries((*self).clone()).finish() + } +} + +/// A consuming iterator over the values of a set. +pub struct IntoIter { + inner: crate::maps::IntoIter, +} + +impl IntoIter { + pub(crate) const fn new(inner: crate::maps::IntoIter) -> Self { + Self { inner } + } +} + +impl Iterator for IntoIter { + type Item = T; + + fn next(&mut self) -> Option { + self.inner.next().map(|entry| entry.0) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.inner.count() + } + + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.fold(init, |acc, (k, ())| f(acc, k)) + } +} + +impl ExactSizeIterator for IntoIter { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for IntoIter {} + +/// An iterator that returns the union between two sets. +pub struct Union<'a, S1, S2, T> +where + S1: Set, + S2: Set, + T: 'a, +{ + s1: &'a S1, + s1_iter: >::Iterator<'a>, + s2: &'a S2, + s2_iter: >::Iterator<'a>, +} + +impl<'a, S1, S2, T> Union<'a, S1, S2, T> +where + S1: Set, + S2: Set, +{ + pub(crate) fn new(s1: &'a S1, s2: &'a S2) -> Self { + Self { + s1_iter: s1.iter(), + s1, + s2_iter: s2.iter(), + s2, + } + } +} + +impl<'a, S1, S2, T> Iterator for Union<'a, S1, S2, T> +where + S1: Set, + S2: Set, +{ + type Item = &'a T; + + #[allow(clippy::needless_borrow)] + #[mutants::skip] + fn next(&mut self) -> Option { + if self.s1.len() > self.s2.len() { + let item = self.s1_iter.next(); + if item.is_some() { + return item; + } + + loop { + let item = self.s2_iter.next()?; + if !self.s1.contains(&item) { + return Some(item); + } + } + } else { + let item = self.s2_iter.next(); + if item.is_some() { + return item; + } + + loop { + let item = self.s1_iter.next()?; + if !self.s2.contains(&item) { + return Some(item); + } + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let h1 = self.s1_iter.size_hint(); + let h2 = self.s2_iter.size_hint(); + + let mut max_bound = None; + if let Some(h1x) = h1.1 { + if let Some(h2x) = h2.1 { + max_bound = h1x.checked_add(h2x); + } + } + + (max(h1.0, h2.0), max_bound) + } +} + +impl<'a, S1, S2, T> Clone for Union<'a, S1, S2, T> +where + S1: Set, + S2: Set, + >::Iterator<'a>: Clone, + >::Iterator<'a>: Clone, +{ + fn clone(&self) -> Self { + Self { + s1: self.s1, + s1_iter: self.s1_iter.clone(), + s2: self.s2, + s2_iter: self.s2_iter.clone(), + } + } +} + +impl FusedIterator for Union<'_, S1, S2, T> +where + S1: Set, + S2: Set, +{ +} + +impl<'a, S1, S2, T> Debug for Union<'a, S1, S2, T> +where + S1: Set, + S2: Set, + >::Iterator<'a>: Clone, + >::Iterator<'a>: Clone, + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries((*self).clone()).finish() + } +} + +/// An iterator that returns the symmetric difference between two sets. +pub struct SymmetricDifference<'a, S1, S2, T> +where + S1: Set, + S2: Set, + T: 'a, +{ + iter: Chain, Difference<'a, S2, S1, T>>, +} + +impl<'a, S1, S2, T> SymmetricDifference<'a, S1, S2, T> +where + S1: Set, + S2: Set, +{ + pub(crate) fn new(s1: &'a S1, s2: &'a S2) -> Self { + Self { + iter: s1.difference(s2).chain(s2.difference(s1)), + } + } +} + +impl<'a, S1, S2, T> Iterator for SymmetricDifference<'a, S1, S2, T> +where + S1: Set, + S2: Set, +{ + type Item = &'a T; + + fn next(&mut self) -> Option<&'a T> { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.iter.count() + } +} + +impl<'a, S1, S2, T> Clone for SymmetricDifference<'a, S1, S2, T> +where + S1: Set, + S2: Set, + >::Iterator<'a>: Clone, + >::Iterator<'a>: Clone, +{ + fn clone(&self) -> Self { + Self { + iter: self.iter.clone(), + } + } +} + +impl FusedIterator for SymmetricDifference<'_, S1, S2, T> +where + S1: Set, + S2: Set, +{ +} + +impl<'a, S1, S2, T> Debug for SymmetricDifference<'a, S1, S2, T> +where + S1: Set, + S2: Set, + >::Iterator<'a>: Clone, + >::Iterator<'a>: Clone, + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + self.iter.fmt(f) + } +} + +/// An iterator that returns the difference between two sets. +pub struct Difference<'a, S1, S2, T> +where + S1: Set, + S2: Set, + T: 'a, +{ + s1: &'a S1, + s1_iter: >::Iterator<'a>, + s2: &'a S2, +} + +impl<'a, S1, S2, T> Difference<'a, S1, S2, T> +where + S1: Set, + S2: Set, +{ + pub(crate) fn new(s1: &'a S1, s2: &'a S2) -> Self { + Self { + s1_iter: s1.iter(), + s1, + s2, + } + } +} + +impl<'a, S1, S2, T> Iterator for Difference<'a, S1, S2, T> +where + S1: Set, + S2: Set, +{ + type Item = &'a T; + + #[allow(clippy::needless_borrow)] + fn next(&mut self) -> Option { + loop { + let item = self.s1_iter.next()?; + if !self.s2.contains(&item) { + return Some(item); + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let (_, upper) = self.s1_iter.size_hint(); + (0, upper) + } +} + +impl<'a, S1, S2, T> Clone for Difference<'a, S1, S2, T> +where + S1: Set, + S2: Set, + >::Iterator<'a>: Clone, + >::Iterator<'a>: Clone, +{ + fn clone(&self) -> Self { + Self { + s1: self.s1, + s1_iter: self.s1_iter.clone(), + s2: self.s2, + } + } +} + +impl FusedIterator for Difference<'_, S1, S2, T> +where + S1: Set, + S2: Set, +{ +} + +impl<'a, S1, S2, T> Debug for Difference<'a, S1, S2, T> +where + S1: Set, + S2: Set, + >::Iterator<'a>: Clone, + >::Iterator<'a>: Clone, + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries((*self).clone()).finish() + } +} + +/// An iterator that returns the intersection between two sets. +pub struct Intersection<'a, S1, S2, T> +where + S1: Set, + S2: Set, + T: 'a, +{ + s1: &'a S1, + s1_iter: >::Iterator<'a>, + s2: &'a S2, + s2_iter: >::Iterator<'a>, +} + +impl<'a, S1, S2, T> Intersection<'a, S1, S2, T> +where + S1: Set, + S2: Set, +{ + pub(crate) fn new(s1: &'a S1, s2: &'a S2) -> Self { + Self { + s1_iter: s1.iter(), + s1, + s2_iter: s2.iter(), + s2, + } + } +} + +impl<'a, S1, S2, T> Iterator for Intersection<'a, S1, S2, T> +where + S1: Set, + S2: Set, +{ + type Item = &'a T; + + #[allow(clippy::needless_borrow)] + #[mutants::skip] + fn next(&mut self) -> Option { + if self.s1.len() < self.s2.len() { + loop { + let item = self.s1_iter.next()?; + if self.s2.contains(&item) { + return Some(item); + } + } + } else { + loop { + let item = self.s2_iter.next()?; + if self.s1.contains(&item) { + return Some(item); + } + } + } + } + + fn size_hint(&self) -> (usize, Option) { + (0, Some(min(self.s1.len(), self.s2.len()))) + } +} + +impl<'a, S1, S2, T> Clone for Intersection<'a, S1, S2, T> +where + S1: Set, + S2: Set, + >::Iterator<'a>: Clone, + >::Iterator<'a>: Clone, +{ + fn clone(&self) -> Self { + Self { + s1: self.s1, + s1_iter: self.s1_iter.clone(), + s2: self.s2, + s2_iter: self.s2_iter.clone(), + } + } +} + +impl FusedIterator for Intersection<'_, S1, S2, T> +where + S1: Set, + S2: Set, +{ +} + +impl<'a, S1, S2, T> Debug for Intersection<'a, S1, S2, T> +where + S1: Set, + S2: Set, + >::Iterator<'a>: Clone, + >::Iterator<'a>: Clone, + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries((*self).clone()).finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::maps::{IntoIter as MapIntoIter, Iter as MapIter}; + use alloc::string::String; + use alloc::vec::Vec; + use alloc::{format, vec}; + use hashbrown::HashSet as HashbrownSet; + + #[test] + fn test_iter() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_iter = MapIter::new(&entries); + let iter = Iter::new(map_iter); + + let collected: Vec<_> = iter.collect(); + assert_eq!(collected, vec![&"Alice", &"Bob"]); + } + + #[test] + fn test_iter_empty() { + let entries: Vec<(&str, ())> = vec![]; + let map_iter = MapIter::new(&entries); + let mut iter = Iter::new(map_iter); + + assert_eq!(iter.next(), None); + } + + #[test] + fn test_iter_size_hint() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_iter = MapIter::new(&entries); + let iter = Iter::new(map_iter); + + assert_eq!(iter.size_hint(), (2, Some(2))); + } + + #[test] + fn test_iter_clone() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_iter = MapIter::new(&entries); + let iter = Iter::new(map_iter); + let iter_clone = iter.clone(); + + let collected: Vec<_> = iter_clone.collect(); + assert_eq!(collected, vec![&"Alice", &"Bob"]); + } + + #[test] + fn test_iter_debug() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_iter = MapIter::new(&entries); + let iter = Iter::new(map_iter); + + let debug_str = format!("{iter:?}"); + assert!(debug_str.contains("Alice")); + assert!(debug_str.contains("Bob")); + } + + #[test] + fn test_into_iter() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_into_iter = MapIntoIter::new(entries.into_boxed_slice()); + let into_iter = IntoIter::new(map_into_iter); + + let collected: Vec<_> = into_iter.collect(); + assert_eq!(collected, vec!["Alice", "Bob"]); + } + + #[test] + fn test_into_iter_empty() { + let entries: Vec<(&str, ())> = vec![]; + let map_into_iter = MapIntoIter::new(entries.into_boxed_slice()); + let mut into_iter = IntoIter::new(map_into_iter); + + assert_eq!(into_iter.next(), None); + } + + #[test] + fn test_into_iter_size_hint() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_into_iter = MapIntoIter::new(entries.into_boxed_slice()); + let into_iter = IntoIter::new(map_into_iter); + + assert_eq!(into_iter.size_hint(), (2, Some(2))); + } + + #[test] + fn test_union() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let union = Union::new(&set1, &set2); + + assert_eq!((2, Some(4)), union.size_hint()); + assert_eq!(3, union.clone().count()); + + let mut collected: Vec<_> = union.collect(); + collected.sort(); + assert_eq!(collected, vec![&"Alice", &"Bob", &"Charlie"]); + } + + #[test] + fn test_union_empty() { + let set1: HashbrownSet<&str> = HashbrownSet::new(); + let set2: HashbrownSet<&str> = HashbrownSet::new(); + let union = Union::new(&set1, &set2); + + assert_eq!(union.count(), 0); + } + + #[test] + fn test_symmetric_difference() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let symmetric_difference = SymmetricDifference::new(&set1, &set2); + + assert_eq!((0, Some(4)), symmetric_difference.size_hint()); + assert_eq!(2, symmetric_difference.clone().count()); + + let collected: Vec<_> = symmetric_difference.collect(); + assert_eq!(collected, vec![&"Alice", &"Charlie"]); + } + + #[test] + fn test_symmetric_difference_empty() { + let set1: HashbrownSet<&str> = HashbrownSet::new(); + let set2: HashbrownSet<&str> = HashbrownSet::new(); + let symmetric_difference = SymmetricDifference::new(&set1, &set2); + + assert_eq!(symmetric_difference.count(), 0); + } + + #[test] + fn test_difference() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let difference = Difference::new(&set1, &set2); + + assert_eq!((0, Some(2)), difference.size_hint()); + assert_eq!(1, difference.clone().count()); + + let collected: Vec<_> = difference.collect(); + assert_eq!(collected, vec![&"Alice"]); + } + + #[test] + fn test_difference_empty() { + let set1: HashbrownSet<&str> = HashbrownSet::new(); + let set2: HashbrownSet<&str> = HashbrownSet::new(); + let difference = Difference::new(&set1, &set2); + + assert_eq!(difference.count(), 0); + } + + #[test] + fn test_intersection() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let intersection = Intersection::new(&set1, &set2); + + assert_eq!((0, Some(2)), intersection.size_hint()); + assert_eq!(1, intersection.clone().count()); + + let collected: Vec<_> = intersection.collect(); + assert_eq!(collected, vec![&"Bob"]); + } + + #[test] + fn test_intersection_empty() { + let set1: HashbrownSet<&str> = HashbrownSet::new(); + let set2: HashbrownSet<&str> = HashbrownSet::new(); + let intersection = Intersection::new(&set1, &set2); + + assert_eq!(intersection.count(), 0); + } + + #[test] + fn test_difference_clone() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let difference = Difference::new(&set1, &set2); + let difference_clone = difference.clone(); + + let collected: Vec<_> = difference_clone.collect(); + assert_eq!(collected, vec![&"Alice"]); + } + + #[test] + fn test_intersection_clone() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let intersection = Intersection::new(&set1, &set2); + let intersection_clone = intersection.clone(); + + let collected: Vec<_> = intersection_clone.collect(); + assert_eq!(collected, vec![&"Bob"]); + } + + #[test] + fn test_symmetric_difference_clone() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let symmetric_difference = SymmetricDifference::new(&set1, &set2); + let symmetric_difference_clone = symmetric_difference.clone(); + + let collected: Vec<_> = symmetric_difference_clone.collect(); + assert_eq!(collected, vec![&"Alice", &"Charlie"]); + } + + #[test] + fn test_union_clone() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let union = Union::new(&set1, &set2); + let union_clone = union.clone(); + + let mut collected: Vec<_> = union_clone.collect(); + collected.sort(); + assert_eq!(collected, vec![&"Alice", &"Bob", &"Charlie"]); + } + + #[test] + fn test_union_fmt() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let union = Union::new(&set1, &set2); + + let debug_str = format!("{union:?}"); + assert!(debug_str.contains("Alice")); + assert!(debug_str.contains("Bob")); + assert!(debug_str.contains("Charlie")); + } + + #[test] + fn test_symmetric_difference_fmt() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let symmetric_difference = SymmetricDifference::new(&set1, &set2); + + let debug_str = format!("{symmetric_difference:?}"); + assert!(debug_str.contains("Alice")); + assert!(debug_str.contains("Charlie")); + } + + #[test] + fn test_difference_fmt() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let difference = Difference::new(&set1, &set2); + + let debug_str = format!("{difference:?}"); + assert!(debug_str.contains("Alice")); + } + + #[test] + fn test_intersection_fmt() { + let set1 = vec!["Alice", "Bob"] + .into_iter() + .collect::>(); + let set2 = vec!["Bob", "Charlie"] + .into_iter() + .collect::>(); + let intersection = Intersection::new(&set1, &set2); + + let debug_str = format!("{intersection:?}"); + assert!(debug_str.contains("Bob")); + } + + #[test] + fn test_iter_fold() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_iter = MapIter::new(&entries); + let iter = Iter::new(map_iter); + + let result = iter.fold(String::new(), |mut acc, &name| { + acc.push_str(name); + acc + }); + assert!(result.eq("AliceBob") || result.eq("BobAlice")); + } + + #[test] + fn test_iter_len() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_iter = MapIter::new(&entries); + let iter = Iter::new(map_iter); + + assert_eq!(iter.len(), 2); + } + + #[test] + fn test_into_iter_fold() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_into_iter = MapIntoIter::new(entries.into_boxed_slice()); + let into_iter = IntoIter::new(map_into_iter); + + let result = into_iter.fold(String::new(), |mut acc, name| { + acc.push_str(name); + acc + }); + assert!(result.eq("AliceBob") || result.eq("BobAlice")); + } + + #[test] + fn test_into_iter_len() { + let entries = vec![("Alice", ()), ("Bob", ())]; + let map_into_iter = MapIntoIter::new(entries.into_boxed_slice()); + let into_iter = IntoIter::new(map_into_iter); + + assert_eq!(into_iter.len(), 2); + } +} diff --git a/frozen-collections-core/src/sets/mod.rs b/frozen-collections-core/src/sets/mod.rs new file mode 100644 index 0000000..5e35175 --- /dev/null +++ b/frozen-collections-core/src/sets/mod.rs @@ -0,0 +1,20 @@ +//! Specialized read-only set types. + +pub use binary_search_set::BinarySearchSet; +pub use dense_scalar_lookup_set::DenseScalarLookupSet; +pub use eytzinger_search_set::EytzingerSearchSet; +pub use hash_set::HashSet; +pub use iterators::*; +pub use ordered_scan_set::OrderedScanSet; +pub use scan_set::ScanSet; +pub use sparse_scalar_lookup_set::SparseScalarLookupSet; + +mod binary_search_set; +pub(crate) mod decl_macros; +mod dense_scalar_lookup_set; +mod eytzinger_search_set; +mod hash_set; +mod iterators; +mod ordered_scan_set; +mod scan_set; +mod sparse_scalar_lookup_set; diff --git a/frozen-collections-core/src/sets/ordered_scan_set.rs b/frozen-collections-core/src/sets/ordered_scan_set.rs new file mode 100644 index 0000000..f527e91 --- /dev/null +++ b/frozen-collections-core/src/sets/ordered_scan_set.rs @@ -0,0 +1,122 @@ +use crate::maps::OrderedScanMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Comparable; + +/// A general purpose set implemented with linear scanning. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +#[doc = include_str!("../doc_snippets/order_warning.md")] +/// +#[derive(Clone)] +pub struct OrderedScanSet { + map: OrderedScanMap, +} + +impl OrderedScanSet +where + T: Ord, +{ + /// Creates a frozen set. + #[must_use] + pub const fn new(map: OrderedScanMap) -> Self { + Self { map } + } +} + +impl Default for OrderedScanSet { + fn default() -> Self { + Self { + map: OrderedScanMap::default(), + } + } +} + +impl Set for OrderedScanSet where Q: ?Sized + Ord + Comparable {} + +impl SetQuery for OrderedScanSet +where + Q: ?Sized + Ord + Comparable, +{ + get_fn!(); +} + +impl SetIteration for OrderedScanSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for OrderedScanSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &OrderedScanSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &OrderedScanSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &OrderedScanSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &OrderedScanSet +where + T: Hash + Eq + Ord + Clone, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for OrderedScanSet { + into_iter_fn!(); +} + +impl<'a, T> IntoIterator for &'a OrderedScanSet { + into_iter_ref_fn!(); +} + +impl PartialEq for OrderedScanSet +where + T: Ord, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for OrderedScanSet where T: Ord {} + +impl Debug for OrderedScanSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/sets/scan_set.rs b/frozen-collections-core/src/sets/scan_set.rs new file mode 100644 index 0000000..b6c70b6 --- /dev/null +++ b/frozen-collections-core/src/sets/scan_set.rs @@ -0,0 +1,121 @@ +use crate::maps::ScanMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use equivalent::Equivalent; + +/// A general purpose set implemented with linear scanning. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +#[derive(Clone)] +pub struct ScanSet { + map: ScanMap, +} + +impl ScanSet +where + T: Eq, +{ + /// Creates a frozen set. + #[must_use] + pub const fn new(map: ScanMap) -> Self { + Self { map } + } +} + +impl Default for ScanSet { + fn default() -> Self { + Self { + map: ScanMap::default(), + } + } +} + +impl Set for ScanSet where Q: ?Sized + Eq + Equivalent {} + +impl SetQuery for ScanSet +where + Q: ?Sized + Eq + Equivalent, +{ + get_fn!(); +} + +impl SetIteration for ScanSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for ScanSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &ScanSet +where + T: Hash + Eq + Clone, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &ScanSet +where + T: Hash + Eq + Clone, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &ScanSet +where + T: Hash + Eq + Clone, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &ScanSet +where + T: Hash + Eq + Clone, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for ScanSet { + into_iter_fn!(); +} + +impl<'a, T> IntoIterator for &'a ScanSet { + into_iter_ref_fn!(); +} + +impl PartialEq for ScanSet +where + T: Eq, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for ScanSet where T: Eq {} + +impl Debug for ScanSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/sets/sparse_scalar_lookup_set.rs b/frozen-collections-core/src/sets/sparse_scalar_lookup_set.rs new file mode 100644 index 0000000..c0ac3c3 --- /dev/null +++ b/frozen-collections-core/src/sets/sparse_scalar_lookup_set.rs @@ -0,0 +1,131 @@ +use crate::maps::SparseScalarLookupMap; +use crate::sets::decl_macros::{ + bitand_fn, bitor_fn, bitxor_fn, debug_fn, get_fn, into_iter_fn, into_iter_ref_fn, + partial_eq_fn, set_iteration_funcs, sub_fn, +}; +use crate::sets::{IntoIter, Iter}; +use crate::traits::{Len, MapIteration, MapQuery, Scalar, Set, SetIteration, SetOps, SetQuery}; +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; + +/// A set whose values are a sparse range of values from a scalar. +/// +#[doc = include_str!("../doc_snippets/type_compat_warning.md")] +#[doc = include_str!("../doc_snippets/about.md")] +/// +#[derive(Clone)] +pub struct SparseScalarLookupSet { + map: SparseScalarLookupMap, +} + +impl SparseScalarLookupSet +where + T: Scalar, +{ + /// Creates a frozen set. + /// + /// # Errors + /// + /// Fails if the number of entries in the vector, after deduplication, exceeds the + /// magnitude of the collection as specified by the `CM` generic argument. + #[must_use] + pub const fn new(map: SparseScalarLookupMap) -> Self { + Self { map } + } +} + +impl Default for SparseScalarLookupSet +where + T: Scalar, +{ + fn default() -> Self { + Self { + map: SparseScalarLookupMap::default(), + } + } +} + +impl Set for SparseScalarLookupSet where T: Scalar {} + +impl SetQuery for SparseScalarLookupSet +where + T: Scalar, +{ + get_fn!("Scalar"); +} + +impl SetIteration for SparseScalarLookupSet { + type Iterator<'a> + = Iter<'a, T> + where + T: 'a; + + set_iteration_funcs!(); +} + +impl Len for SparseScalarLookupSet { + fn len(&self) -> usize { + self.map.len() + } +} + +impl BitOr<&ST> for &SparseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitor_fn!(RandomState); +} + +impl BitAnd<&ST> for &SparseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitand_fn!(RandomState); +} + +impl BitXor<&ST> for &SparseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + bitxor_fn!(RandomState); +} + +impl Sub<&ST> for &SparseScalarLookupSet +where + T: Scalar + Hash, + ST: Set, +{ + sub_fn!(RandomState); +} + +impl IntoIterator for SparseScalarLookupSet { + into_iter_fn!(); +} + +impl<'a, T> IntoIterator for &'a SparseScalarLookupSet +where + T: Scalar, +{ + into_iter_ref_fn!(); +} + +impl PartialEq for SparseScalarLookupSet +where + T: Scalar, + ST: Set, +{ + partial_eq_fn!(); +} + +impl Eq for SparseScalarLookupSet where T: Scalar {} + +impl Debug for SparseScalarLookupSet +where + T: Debug, +{ + debug_fn!(); +} diff --git a/frozen-collections-core/src/traits/collection_magnitude.rs b/frozen-collections-core/src/traits/collection_magnitude.rs new file mode 100644 index 0000000..a5887a7 --- /dev/null +++ b/frozen-collections-core/src/traits/collection_magnitude.rs @@ -0,0 +1,35 @@ +/// Controls the magnitude of collection types. +/// +/// This trait indicates that a collection's layout can be optimized at compile-time depending on +/// the max capacity that the collection can hold. +pub trait CollectionMagnitude: Copy + TryFrom + Into { + /// The maximum number of entries supported in the collection. + const MAX_CAPACITY: usize; + + /// The zero value for the magnitude. + const ZERO: Self; +} + +/// A small collection that can hold up to 255 entries. +pub type SmallCollection = u8; + +impl CollectionMagnitude for SmallCollection { + const MAX_CAPACITY: usize = Self::MAX as usize; + const ZERO: Self = 0; +} + +/// A medium collection that can hold up to 65,535 entries. +pub type MediumCollection = u16; + +impl CollectionMagnitude for MediumCollection { + const MAX_CAPACITY: usize = Self::MAX as usize; + const ZERO: Self = 0; +} + +/// A large collection that can hold up to [`usize::MAX`] entries. +pub type LargeCollection = usize; + +impl CollectionMagnitude for LargeCollection { + const MAX_CAPACITY: Self = Self::MAX; + const ZERO: Self = 0; +} diff --git a/frozen-collections-core/src/traits/hasher.rs b/frozen-collections-core/src/traits/hasher.rs new file mode 100644 index 0000000..fa7f22b --- /dev/null +++ b/frozen-collections-core/src/traits/hasher.rs @@ -0,0 +1,13 @@ +/// Hashes values of a specific type. +/// +/// This provides a hashing mechanism which is orthogonal to the normal +/// [`Hash`] trait. This allows for the creation of hashers that are +/// specialized for specific types, and can be used in contexts where the +/// standard `Hash` trait is not desirable. +pub trait Hasher +where + T: ?Sized, +{ + /// Produce a hash value for the given value. + fn hash(&self, value: &T) -> u64; +} diff --git a/frozen-collections-core/src/traits/len.rs b/frozen-collections-core/src/traits/len.rs new file mode 100644 index 0000000..0dff37b --- /dev/null +++ b/frozen-collections-core/src/traits/len.rs @@ -0,0 +1,400 @@ +use alloc::boxed::Box; +use alloc::collections::VecDeque; +use alloc::rc::Rc; +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; + +/// Types that can return a length. +pub trait Len { + /// Returns the length of the value. + fn len(&self) -> usize; + + /// Returns whether the value is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +#[cfg(feature = "std")] +impl Len for std::collections::HashSet { + fn len(&self) -> usize { + self.len() + } +} + +#[cfg(feature = "std")] +impl Len for std::collections::HashMap { + fn len(&self) -> usize { + self.len() + } +} + +impl Len for String { + fn len(&self) -> usize { + self.len() + } +} + +impl Len for str { + fn len(&self) -> usize { + self.len() + } +} + +impl Len for &str { + fn len(&self) -> usize { + str::len(self) + } +} + +impl Len for core::ffi::CStr { + fn len(&self) -> usize { + self.to_bytes().len() + } +} + +#[cfg(feature = "std")] +impl Len for std::ffi::CString { + fn len(&self) -> usize { + self.as_bytes().len() + } +} + +impl Len for [T] { + fn len(&self) -> usize { + self.len() + } +} + +impl Len for Box { + fn len(&self) -> usize { + T::len(self) + } +} + +impl Len for Rc { + fn len(&self) -> usize { + T::len(self) + } +} + +impl Len for Arc { + fn len(&self) -> usize { + T::len(self) + } +} + +#[cfg(feature = "std")] +impl Len for std::collections::BTreeMap { + fn len(&self) -> usize { + self.len() + } +} + +#[cfg(feature = "std")] +impl Len for std::collections::BTreeSet { + fn len(&self) -> usize { + self.len() + } +} + +#[cfg(feature = "std")] +impl Len for std::collections::BinaryHeap { + fn len(&self) -> usize { + self.len() + } +} + +#[cfg(feature = "std")] +impl Len for std::collections::LinkedList { + fn len(&self) -> usize { + self.len() + } +} + +impl Len for Vec { + fn len(&self) -> usize { + self.len() + } +} + +impl Len for VecDeque { + fn len(&self) -> usize { + self.len() + } +} + +#[cfg(feature = "std")] +impl Len for std::ffi::OsStr { + fn len(&self) -> usize { + self.len() + } +} + +#[cfg(feature = "std")] +impl Len for std::ffi::OsString { + fn len(&self) -> usize { + self.as_os_str().len() + } +} + +impl Len for hashbrown::HashMap { + fn len(&self) -> usize { + self.len() + } +} + +impl Len for hashbrown::HashSet { + fn len(&self) -> usize { + self.len() + } +} + +#[cfg(test)] +mod tests { + use crate::traits::Len; + use alloc::collections::VecDeque; + use alloc::rc::Rc; + use alloc::sync::Arc; + + fn get_len(value: &T) -> usize { + value.len() + } + + #[test] + #[cfg(feature = "std")] + fn hashset_len_and_is_empty() { + let mut set = std::collections::HashSet::new(); + assert_eq!(get_len(&set), 0); + assert!(set.is_empty()); + + set.insert(1); + assert_eq!(get_len(&set), 1); + assert!(!set.is_empty()); + } + + #[test] + #[cfg(feature = "std")] + fn hashmap_len_and_is_empty() { + let mut map = std::collections::HashMap::new(); + assert_eq!(get_len(&map), 0); + assert!(map.is_empty()); + + map.insert("key", "value"); + assert_eq!(get_len(&map), 1); + assert!(!map.is_empty()); + } + + #[test] + #[cfg(feature = "std")] + fn hashbrown_hashset_len_and_is_empty() { + let mut set = hashbrown::HashSet::new(); + assert_eq!(get_len(&set), 0); + assert!(set.is_empty()); + + set.insert(1); + assert_eq!(get_len(&set), 1); + assert!(!set.is_empty()); + } + + #[test] + #[cfg(feature = "std")] + fn hashbrown_hashmap_len_and_is_empty() { + let mut map = hashbrown::HashMap::new(); + assert_eq!(get_len(&map), 0); + assert!(map.is_empty()); + + map.insert("key", "value"); + assert_eq!(get_len(&map), 1); + assert!(!map.is_empty()); + } + + #[test] + fn string_len_and_is_empty() { + let s = String::new(); + assert_eq!(get_len(&s), 0); + assert!(s.is_empty()); + + let s = String::from("hello"); + assert_eq!(get_len(&s), 5); + assert!(!s.is_empty()); + } + + #[test] + fn str_len_and_is_empty() { + let s = ""; + assert_eq!(get_len(&s), 0); + assert!(s.is_empty()); + + let s = "hello"; + assert_eq!(get_len(&s), 5); + assert!(!s.is_empty()); + } + + #[test] + #[cfg(feature = "std")] + fn cstring_len_and_is_empty() { + use std::ffi::CString; + let s = CString::new("").unwrap(); + assert_eq!(get_len(&s), 0); + assert!(s.is_empty()); + + let s = CString::new("hello").unwrap(); + assert_eq!(get_len(&s), 5); + assert!(!s.is_empty()); + } + + #[test] + #[cfg(feature = "std")] + fn cstr_len_and_is_empty() { + let s = c""; + assert_eq!(get_len(s), 0); + assert!(s.is_empty()); + + let s = c"hello"; + assert_eq!(get_len(s), 5); + assert!(!s.is_empty()); + } + + #[test] + fn vec_len_and_is_empty() { + let v: Vec = Vec::new(); + assert_eq!(get_len(&v), 0); + assert!(v.is_empty()); + + let v = vec![1, 2, 3]; + assert_eq!(get_len(&v), 3); + assert!(!v.is_empty()); + } + + #[test] + fn vecdeque_len_and_is_empty() { + let v: VecDeque = VecDeque::new(); + assert_eq!(get_len(&v), 0); + assert!(v.is_empty()); + + let v: VecDeque = vec![1, 2, 3].into(); + assert_eq!(get_len(&v), 3); + assert!(!v.is_empty()); + } + + #[test] + fn box_len_and_is_empty() { + let b: Box = Box::from(""); + assert_eq!(get_len(&b), 0); + assert!(b.is_empty()); + + let b: Box = Box::from("hello"); + assert_eq!(get_len(&b), 5); + assert!(!b.is_empty()); + } + + #[test] + fn rc_len_and_is_empty() { + let r: Rc = Rc::from(""); + assert_eq!(get_len(&r), 0); + assert!(r.is_empty()); + + let r: Rc = Rc::from("hello"); + assert_eq!(get_len(&r), 5); + assert!(!r.is_empty()); + } + + #[test] + fn arc_len_and_is_empty() { + let a: Arc = Arc::from(""); + assert_eq!(get_len(&a), 0); + assert!(a.is_empty()); + + let a: Arc = Arc::from("hello"); + assert_eq!(get_len(&a), 5); + assert!(!a.is_empty()); + } + + #[test] + fn btreemap_len_and_is_empty() { + use std::collections::BTreeMap; + let mut map = BTreeMap::new(); + assert_eq!(get_len(&map), 0); + assert!(map.is_empty()); + + map.insert("key", "value"); + assert_eq!(get_len(&map), 1); + assert!(!map.is_empty()); + } + + #[test] + fn btreeset_len_and_is_empty() { + use std::collections::BTreeSet; + let mut set = BTreeSet::new(); + assert_eq!(get_len(&set), 0); + assert!(set.is_empty()); + + set.insert(1); + assert_eq!(get_len(&set), 1); + assert!(!set.is_empty()); + } + + #[test] + fn binaryheap_len_and_is_empty() { + use std::collections::BinaryHeap; + let mut heap = BinaryHeap::new(); + assert_eq!(get_len(&heap), 0); + assert!(heap.is_empty()); + + heap.push(1); + assert_eq!(get_len(&heap), 1); + assert!(!heap.is_empty()); + } + + #[test] + fn linkedlist_len_and_is_empty() { + use std::collections::LinkedList; + let mut list = LinkedList::new(); + assert_eq!(get_len(&list), 0); + assert!(list.is_empty()); + + list.push_back(1); + assert_eq!(get_len(&list), 1); + assert!(!list.is_empty()); + } + + #[test] + #[cfg(feature = "std")] + fn osstr_len_and_is_empty() { + use std::ffi::OsStr; + let s = OsStr::new(""); + assert_eq!(get_len(s), 0); + assert!(s.is_empty()); + + let s = OsStr::new("hello"); + assert_eq!(get_len(s), 5); + assert!(!s.is_empty()); + } + + #[test] + #[cfg(feature = "std")] + fn osstring_len_and_is_empty() { + use std::ffi::OsString; + let s = OsString::new(); + assert_eq!(get_len(&s), 0); + assert!(s.is_empty()); + + let s = OsString::from("hello"); + assert_eq!(get_len(&s), 5); + assert!(!s.is_empty()); + } + + #[test] + #[cfg(feature = "std")] + fn slice_len_and_is_empty() { + let s: &[u8] = [].as_slice(); + assert_eq!(get_len(s), 0); + assert!(s.is_empty()); + + let s = [0, 1, 2, 3, 4].as_slice(); + assert_eq!(get_len(s), 5); + assert!(!s.is_empty()); + } +} diff --git a/frozen-collections-core/src/traits/map.rs b/frozen-collections-core/src/traits/map.rs new file mode 100644 index 0000000..dd451b6 --- /dev/null +++ b/frozen-collections-core/src/traits/map.rs @@ -0,0 +1,85 @@ +use crate::traits::{Len, MapIteration, MapQuery}; +use crate::utils::has_duplicates; +use core::borrow::Borrow; +use core::hash::{BuildHasher, Hash}; +use core::mem::MaybeUninit; + +/// Common abstractions for maps. +pub trait Map: MapQuery + MapIteration + Len { + /// Gets multiple mutable values from the map. + #[must_use] + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]>; +} + +#[cfg(feature = "std")] +impl Map for std::collections::HashMap +where + K: Hash + Eq + Borrow, + Q: ?Sized + Hash + Eq, + BH: BuildHasher, +{ + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]> { + if has_duplicates(keys.iter()) { + return None; + } + + let mut result: MaybeUninit<[&mut V; N]> = MaybeUninit::uninit(); + let p = result.as_mut_ptr(); + let x: *mut Self = self; + unsafe { + for (i, key) in keys.iter().enumerate() { + (*p)[i] = (*x).get_mut(key)?; + } + + Some(result.assume_init()) + } + } +} + +#[cfg(feature = "std")] +impl Map for std::collections::BTreeMap +where + K: Ord + Borrow, + Q: Ord, +{ + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]> { + if crate::utils::has_duplicates_slow(&keys) { + return None; + } + + let mut result: MaybeUninit<[&mut V; N]> = MaybeUninit::uninit(); + let p = result.as_mut_ptr(); + let x: *mut Self = self; + unsafe { + for (i, key) in keys.iter().enumerate() { + (*p)[i] = (*x).get_mut(key)?; + } + + Some(result.assume_init()) + } + } +} + +impl Map for hashbrown::HashMap +where + K: Hash + Eq + Borrow, + Q: Hash + Eq, + BH: BuildHasher, +{ + fn get_many_mut(&mut self, keys: [&Q; N]) -> Option<[&mut V; N]> { + if has_duplicates(keys.iter()) { + return None; + } + + let mut result: MaybeUninit<[&mut V; N]> = MaybeUninit::uninit(); + let p = result.as_mut_ptr(); + let x: *mut Self = self; + unsafe { + for (i, key) in keys.iter().enumerate() { + (*p)[i] = (*x).get_mut(*key)?; + } + + Some(result.assume_init()) + } + } +} diff --git a/frozen-collections-core/src/traits/map_iteration.rs b/frozen-collections-core/src/traits/map_iteration.rs new file mode 100644 index 0000000..79b54aa --- /dev/null +++ b/frozen-collections-core/src/traits/map_iteration.rs @@ -0,0 +1,266 @@ +use core::hash::BuildHasher; + +/// Common iteration abstractions for maps. +pub trait MapIteration: IntoIterator { + type Iterator<'a>: Iterator + where + Self: 'a, + K: 'a, + V: 'a; + + type KeyIterator<'a>: Iterator + where + Self: 'a, + K: 'a; + + type ValueIterator<'a>: Iterator + where + Self: 'a, + V: 'a; + + type IntoKeyIterator: Iterator; + type IntoValueIterator: Iterator; + + type MutIterator<'a>: Iterator + where + Self: 'a, + K: 'a, + V: 'a; + + type ValueMutIterator<'a>: Iterator + where + Self: 'a, + V: 'a; + + /// An iterator visiting all entries in arbitrary order. + #[must_use] + fn iter(&self) -> Self::Iterator<'_>; + + /// An iterator visiting all keys in arbitrary order. + #[must_use] + fn keys(&self) -> Self::KeyIterator<'_>; + + /// An iterator visiting all values in arbitrary order. + #[must_use] + fn values(&self) -> Self::ValueIterator<'_>; + + /// A consuming iterator visiting all keys in arbitrary order. + #[must_use] + fn into_keys(self) -> Self::IntoKeyIterator; + + /// A consuming iterator visiting all values in arbitrary order. + #[must_use] + fn into_values(self) -> Self::IntoValueIterator; + + /// An iterator producing mutable references to all entries in arbitrary order. + #[must_use] + fn iter_mut(&mut self) -> Self::MutIterator<'_>; + + /// An iterator visiting all values mutably in arbitrary order. + #[must_use] + fn values_mut(&mut self) -> Self::ValueMutIterator<'_>; +} + +#[cfg(feature = "std")] +impl MapIteration for std::collections::HashMap +where + BH: BuildHasher, +{ + type Iterator<'a> + = std::collections::hash_map::Iter<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type KeyIterator<'a> + = std::collections::hash_map::Keys<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type ValueIterator<'a> + = std::collections::hash_map::Values<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type IntoKeyIterator = std::collections::hash_map::IntoKeys; + type IntoValueIterator = std::collections::hash_map::IntoValues; + type MutIterator<'a> + = std::collections::hash_map::IterMut<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type ValueMutIterator<'a> + = std::collections::hash_map::ValuesMut<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + fn iter(&self) -> Self::Iterator<'_> { + Self::iter(self) + } + + fn keys(&self) -> Self::KeyIterator<'_> { + Self::keys(self) + } + + fn values(&self) -> Self::ValueIterator<'_> { + Self::values(self) + } + + fn into_keys(self) -> Self::IntoKeyIterator { + Self::into_keys(self) + } + + fn into_values(self) -> Self::IntoValueIterator { + Self::into_values(self) + } + + fn iter_mut(&mut self) -> Self::MutIterator<'_> { + Self::iter_mut(self) + } + + fn values_mut(&mut self) -> Self::ValueMutIterator<'_> { + self.values_mut() + } +} + +#[cfg(feature = "std")] +impl MapIteration for std::collections::BTreeMap { + type Iterator<'a> + = std::collections::btree_map::Iter<'a, K, V> + where + K: 'a, + V: 'a; + + type KeyIterator<'a> + = std::collections::btree_map::Keys<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueIterator<'a> + = std::collections::btree_map::Values<'a, K, V> + where + K: 'a, + V: 'a; + + type IntoKeyIterator = std::collections::btree_map::IntoKeys; + type IntoValueIterator = std::collections::btree_map::IntoValues; + type MutIterator<'a> + = std::collections::btree_map::IterMut<'a, K, V> + where + K: 'a, + V: 'a; + + type ValueMutIterator<'a> + = std::collections::btree_map::ValuesMut<'a, K, V> + where + K: 'a, + V: 'a; + + fn iter(&self) -> Self::Iterator<'_> { + Self::iter(self) + } + + fn keys(&self) -> Self::KeyIterator<'_> { + Self::keys(self) + } + + fn values(&self) -> Self::ValueIterator<'_> { + Self::values(self) + } + + fn into_keys(self) -> Self::IntoKeyIterator { + Self::into_keys(self) + } + + fn into_values(self) -> Self::IntoValueIterator { + Self::into_values(self) + } + + fn iter_mut(&mut self) -> Self::MutIterator<'_> { + Self::iter_mut(self) + } + + fn values_mut(&mut self) -> Self::ValueMutIterator<'_> { + self.values_mut() + } +} + +impl MapIteration for hashbrown::HashMap +where + BH: BuildHasher, +{ + type Iterator<'a> + = hashbrown::hash_map::Iter<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type KeyIterator<'a> + = hashbrown::hash_map::Keys<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type ValueIterator<'a> + = hashbrown::hash_map::Values<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type IntoKeyIterator = hashbrown::hash_map::IntoKeys; + type IntoValueIterator = hashbrown::hash_map::IntoValues; + type MutIterator<'a> + = hashbrown::hash_map::IterMut<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + type ValueMutIterator<'a> + = hashbrown::hash_map::ValuesMut<'a, K, V> + where + K: 'a, + V: 'a, + BH: 'a; + + fn iter(&self) -> Self::Iterator<'_> { + Self::iter(self) + } + + fn keys(&self) -> Self::KeyIterator<'_> { + Self::keys(self) + } + + fn values(&self) -> Self::ValueIterator<'_> { + Self::values(self) + } + + fn into_keys(self) -> Self::IntoKeyIterator { + Self::into_keys(self) + } + + fn into_values(self) -> Self::IntoValueIterator { + Self::into_values(self) + } + + fn iter_mut(&mut self) -> Self::MutIterator<'_> { + Self::iter_mut(self) + } + + fn values_mut(&mut self) -> Self::ValueMutIterator<'_> { + self.values_mut() + } +} diff --git a/frozen-collections-core/src/traits/map_query.rs b/frozen-collections-core/src/traits/map_query.rs new file mode 100644 index 0000000..fe23ff7 --- /dev/null +++ b/frozen-collections-core/src/traits/map_query.rs @@ -0,0 +1,118 @@ +use core::hash::{BuildHasher, Hash}; + +/// Common query abstractions for maps. +pub trait MapQuery { + /// Checks whether a particular value is present in the map. + #[inline] + #[must_use] + fn contains_key(&self, key: &Q) -> bool { + self.get(key).is_some() + } + + /// Gets a value from the map. + #[must_use] + fn get(&self, key: &Q) -> Option<&V>; + + /// Gets a key and value from the map. + #[must_use] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)>; + + /// Gets a mutable value from the map. + #[must_use] + fn get_mut(&mut self, key: &Q) -> Option<&mut V>; +} + +#[cfg(feature = "std")] +impl MapQuery for std::collections::HashMap +where + K: Hash + Eq + core::borrow::Borrow, + Q: ?Sized + Hash + Eq, + BH: BuildHasher, +{ + #[inline] + fn contains_key(&self, key: &Q) -> bool { + Self::contains_key(self, key) + } + + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + Self::get(self, key) + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + Self::get_key_value(self, key) + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + Self::get_mut(self, key) + } +} + +#[cfg(feature = "std")] +impl MapQuery for std::collections::BTreeMap +where + K: Ord + core::borrow::Borrow, + Q: Ord, +{ + #[inline] + fn contains_key(&self, key: &Q) -> bool { + Self::contains_key(self, key) + } + + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + Self::get(self, key) + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + Self::get_key_value(self, key) + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + Self::get_mut(self, key) + } +} + +impl MapQuery for hashbrown::HashMap +where + K: Hash + Eq + core::borrow::Borrow, + Q: Hash + Eq, + BH: BuildHasher, +{ + #[inline] + fn contains_key(&self, key: &Q) -> bool { + Self::contains_key(self, key) + } + + #[inline] + fn get(&self, key: &Q) -> Option<&V> { + Self::get(self, key) + } + + #[inline] + fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> { + Self::get_key_value(self, key) + } + + #[inline] + fn get_mut(&mut self, key: &Q) -> Option<&mut V> { + Self::get_mut(self, key) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hashbrown::HashMap; + + #[test] + fn test_object_safe() { + let m: &dyn MapQuery<_, _, _> = &HashMap::from([(1, 1), (2, 2), (3, 3)]); + + assert!(m.contains_key(&1)); + } +} diff --git a/frozen-collections-core/src/traits/mod.rs b/frozen-collections-core/src/traits/mod.rs new file mode 100644 index 0000000..706d74f --- /dev/null +++ b/frozen-collections-core/src/traits/mod.rs @@ -0,0 +1,27 @@ +//! Traits to support frozen collections. + +pub use crate::traits::collection_magnitude::{ + CollectionMagnitude, LargeCollection, MediumCollection, SmallCollection, +}; +pub use crate::traits::hasher::Hasher; +pub use crate::traits::len::Len; +pub use crate::traits::map::Map; +pub use crate::traits::map_iteration::MapIteration; +pub use crate::traits::map_query::MapQuery; +pub use crate::traits::scalar::Scalar; +pub use crate::traits::set::Set; +pub use crate::traits::set_iteration::SetIteration; +pub use crate::traits::set_ops::SetOps; +pub use crate::traits::set_query::SetQuery; + +mod collection_magnitude; +mod hasher; +mod len; +mod map; +mod map_iteration; +mod map_query; +mod scalar; +mod set; +mod set_iteration; +mod set_ops; +mod set_query; diff --git a/frozen-collections-core/src/traits/scalar.rs b/frozen-collections-core/src/traits/scalar.rs new file mode 100644 index 0000000..48b241c --- /dev/null +++ b/frozen-collections-core/src/traits/scalar.rs @@ -0,0 +1,193 @@ +use core::num::{ + NonZeroI16, NonZeroI32, NonZeroI8, NonZeroIsize, NonZeroU16, NonZeroU32, NonZeroU8, + NonZeroUsize, +}; + +/// A scalar value with an index in its sequence of possible values. +pub trait Scalar: Ord + Clone + Copy { + /// Returns a value's index into its containing sequence. + fn index(&self) -> usize; +} + +macro_rules! impl_unsigned_scalar { + ($($t:ty),*) => { + $( + impl Scalar for $t { + #[inline] + #[allow(clippy::cast_possible_truncation)] + fn index(&self) -> usize { + *self as usize + } + } + )* + }; +} + +macro_rules! impl_signed_scalar { + ($($t:ty: $unsigned_ty:ty: $mask:expr),*) => { + $( + impl Scalar for $t { + #[inline] + #[allow(clippy::cast_sign_loss)] + #[allow(clippy::cast_possible_truncation)] + fn index(&self) -> usize { + ((*self as $unsigned_ty) ^ $mask) as usize + } + } + )* + }; +} + +macro_rules! impl_unsigned_nz_scalar { + ($($t:ty),*) => { + $( + impl Scalar for $t { + #[inline] + #[allow(clippy::cast_possible_truncation)] + fn index(&self) -> usize { + (*self).get() as usize + } + } + )* + }; +} + +macro_rules! impl_signed_nz_scalar { + ($($t:ty: $unsigned_ty:ty: $mask:expr),*) => { + $( + impl Scalar for $t { + #[inline] + #[allow(clippy::cast_sign_loss)] + #[allow(clippy::cast_possible_truncation)] + fn index(&self) -> usize { + (((*self).get() as $unsigned_ty) ^ $mask) as usize + } + } + )* + }; +} + +#[cfg(target_pointer_width = "64")] +impl_unsigned_scalar!(u8, u16, u32, u64, usize); +#[cfg(target_pointer_width = "64")] +impl_signed_scalar!(i8:u8:0x80, i16:u16:0x8000, i32:u32:0x8000_0000, i64:u64:0x8000_0000_0000_0000, isize:usize:0x8000_0000_0000_0000); +#[cfg(target_pointer_width = "64")] +impl_unsigned_nz_scalar!( + NonZeroU8, + NonZeroU16, + NonZeroU32, + core::num::NonZeroU64, + NonZeroUsize +); +#[cfg(target_pointer_width = "64")] +impl_signed_nz_scalar!(NonZeroI8:u8:0x80, NonZeroI16:u16:0x8000, NonZeroI32:u32:0x8000_0000, core::num::NonZeroI64:u64:0x8000_0000_0000_0000, NonZeroIsize:usize:0x8000_0000_0000_0000); + +#[cfg(target_pointer_width = "32")] +impl_unsigned_scalar!(u8, u16, u32, usize); +#[cfg(target_pointer_width = "32")] +impl_signed_scalar!(i8:u8:0x80, i16:u16:0x8000, i32:u32:0x8000_0000, isize:usize:0x8000_0000); +#[cfg(target_pointer_width = "32")] +impl_unsigned_nz_scalar!(NonZeroU8, NonZeroU16, NonZeroU32, NonZeroUsize); +#[cfg(target_pointer_width = "32")] +impl_signed_nz_scalar!(NonZeroI8:u8:0x80, NonZeroI16:u16:0x8000, NonZeroI32:u32:0x8000_0000, NonZeroIsize:usize:0x8000_0000); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unsigned_scalar() { + assert_eq!(5u8.index(), 5); + assert_eq!(10u16.index(), 10); + assert_eq!(20u32.index(), 20); + assert_eq!(30usize.index(), 30); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn test_unsigned_scalar_64() { + assert_eq!(40u64.index(), 40); + } + + #[test] + fn test_signed_scalar() { + assert_eq!((-5i8).index(), 0x80 - 5); + assert_eq!((-10i16).index(), 0x8000 - 10); + assert_eq!((-20i32).index(), 0x8000_0000 - 20); + + assert_eq!(5i8.index(), 0x80 + 5); + assert_eq!(10i16.index(), 0x8000 + 10); + assert_eq!(20i32.index(), 0x8000_0000 + 20); + } + + #[test] + #[cfg(target_pointer_width = "32")] + fn test_signed_scalar_32() { + assert_eq!((-30isize).index(), 0x8000_0000 - 30); + assert_eq!(30isize.index(), 0x8000_0000 + 30); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn test_signed_scalar_64() { + assert_eq!((-30isize).index(), 0x8000_0000_0000_0000 - 30); + assert_eq!((-40i64).index(), 0x8000_0000_0000_0000 - 40); + + assert_eq!(30isize.index(), 0x8000_0000_0000_0000 + 30); + assert_eq!((40i64).index(), 0x8000_0000_0000_0000 + 40); + } + + #[test] + fn test_unsigned_nz_scalar() { + assert_eq!(NonZeroU8::new(5).unwrap().index(), 5); + assert_eq!(NonZeroU16::new(10).unwrap().index(), 10); + assert_eq!(NonZeroU32::new(20).unwrap().index(), 20); + assert_eq!(NonZeroUsize::new(30).unwrap().index(), 30); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn test_unsigned_nz_scalar_64() { + assert_eq!(core::num::NonZeroU64::new(40).unwrap().index(), 40); + } + + #[test] + fn test_signed_nz_scalar() { + assert_eq!(NonZeroI8::new(-5).unwrap().index(), 0x80 - 5); + assert_eq!(NonZeroI16::new(-10).unwrap().index(), 0x8000 - 10); + assert_eq!(NonZeroI32::new(-20).unwrap().index(), 0x8000_0000 - 20); + + assert_eq!(NonZeroI8::new(5).unwrap().index(), 0x80 + 5); + assert_eq!(NonZeroI16::new(10).unwrap().index(), 0x8000 + 10); + assert_eq!(NonZeroI32::new(20).unwrap().index(), 0x8000_0000 + 20); + } + + #[test] + #[cfg(target_pointer_width = "32")] + fn test_signed_nz_scalar_32() { + assert_eq!(NonZeroIsize::new(-30).unwrap().index(), 0x8000_0000 - 30); + assert_eq!(NonZeroIsize::new(30).unwrap().index(), 0x8000_0000 + 30); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn test_signed_nz_scalar_64() { + assert_eq!( + NonZeroIsize::new(-30).unwrap().index(), + 0x8000_0000_0000_0000 - 30 + ); + assert_eq!( + core::num::NonZeroI64::new(-40).unwrap().index(), + 0x8000_0000_0000_0000 - 40 + ); + + assert_eq!( + NonZeroIsize::new(30).unwrap().index(), + 0x8000_0000_0000_0000 + 30 + ); + assert_eq!( + core::num::NonZeroI64::new(40).unwrap().index(), + 0x8000_0000_0000_0000 + 40 + ); + } +} diff --git a/frozen-collections-core/src/traits/set.rs b/frozen-collections-core/src/traits/set.rs new file mode 100644 index 0000000..b730feb --- /dev/null +++ b/frozen-collections-core/src/traits/set.rs @@ -0,0 +1,31 @@ +use crate::traits::{Len, SetIteration, SetQuery}; +use core::borrow::Borrow; +use core::hash::{BuildHasher, Hash}; + +/// Common abstractions for sets. +pub trait Set: SetQuery + SetIteration + Len {} + +#[cfg(feature = "std")] +impl Set for std::collections::HashSet +where + T: Eq + Hash + Borrow, + Q: Hash + Eq, + BH: BuildHasher, +{ +} + +#[cfg(feature = "std")] +impl Set for std::collections::BTreeSet +where + T: Ord + Borrow, + Q: Ord, +{ +} + +impl Set for hashbrown::hash_set::HashSet +where + T: Hash + Eq + Borrow, + Q: Hash + Eq, + BH: BuildHasher, +{ +} diff --git a/frozen-collections-core/src/traits/set_iteration.rs b/frozen-collections-core/src/traits/set_iteration.rs new file mode 100644 index 0000000..cca0e5e --- /dev/null +++ b/frozen-collections-core/src/traits/set_iteration.rs @@ -0,0 +1,59 @@ +use core::hash::BuildHasher; + +/// Common iteration abstractions for sets. +pub trait SetIteration: IntoIterator { + type Iterator<'a>: Iterator + where + Self: 'a, + T: 'a; + + /// An iterator visiting all entries in arbitrary order. + #[must_use] + fn iter(&self) -> Self::Iterator<'_>; +} + +#[cfg(feature = "std")] +impl SetIteration for std::collections::HashSet +where + BH: BuildHasher, +{ + type Iterator<'a> + = std::collections::hash_set::Iter<'a, T> + where + T: 'a, + BH: 'a; + + #[inline] + fn iter(&self) -> Self::Iterator<'_> { + Self::iter(self) + } +} + +#[cfg(feature = "std")] +impl SetIteration for std::collections::BTreeSet { + type Iterator<'a> + = std::collections::btree_set::Iter<'a, T> + where + T: 'a; + + #[inline] + fn iter(&self) -> Self::Iterator<'_> { + Self::iter(self) + } +} + +impl SetIteration for hashbrown::hash_set::HashSet +where + BH: BuildHasher, +{ + type Iterator<'a> + = hashbrown::hash_set::Iter<'a, T> + where + T: 'a, + BH: 'a; + + #[inline] + fn iter(&self) -> Self::Iterator<'_> { + Self::iter(self) + } +} diff --git a/frozen-collections-core/src/traits/set_ops.rs b/frozen-collections-core/src/traits/set_ops.rs new file mode 100644 index 0000000..4e9fd23 --- /dev/null +++ b/frozen-collections-core/src/traits/set_ops.rs @@ -0,0 +1,103 @@ +use crate::sets::{Difference, Intersection, SymmetricDifference, Union}; +use crate::traits::Set; + +/// Common operations on sets. +pub trait SetOps { + /// Visits the values representing the union, + /// i.e., all the values in `self` or `other`, without duplicates. + #[must_use] + fn union<'a, ST>(&'a self, other: &'a ST) -> Union<'a, Self, ST, T> + where + ST: Set, + Self: Sized + Set, + { + Union::new(self, other) + } + + /// Visits the values representing the symmetric difference, + /// i.e., the values that are in `self` or in `other` but not in both. + #[must_use] + fn symmetric_difference<'a, ST>(&'a self, other: &'a ST) -> SymmetricDifference<'a, Self, ST, T> + where + ST: Set, + Self: Sized + Set, + { + SymmetricDifference::new(self, other) + } + + /// Visits the values representing the difference, + /// i.e., the values that are in `self` but not in `other`. + #[must_use] + fn difference<'a, ST>(&'a self, other: &'a ST) -> Difference<'a, Self, ST, T> + where + ST: Set, + Self: Sized + Set, + { + Difference::new(self, other) + } + + /// Visits the values representing the intersection, + /// i.e., the values that are both in `self` and `other`. + /// + /// When an equal element is present in `self` and `other` + /// then the resulting `Intersection` may yield references to + /// one or the other. This can be relevant if `T` contains fields which + /// are not compared by its `Eq` implementation, and may hold different + /// value between the two equal copies of `T` in the two sets. + #[must_use] + fn intersection<'a, ST>(&'a self, other: &'a ST) -> Intersection<'a, Self, ST, T> + where + ST: Set, + Self: Sized + Set, + { + Intersection::new(self, other) + } + + /// Returns `true` if `self` has no entries in common with `other`. + /// This is equivalent to checking for an empty intersection. + #[must_use] + #[mutants::skip] + fn is_disjoint<'a, ST>(&'a self, other: &'a ST) -> bool + where + ST: Set, + Self: Sized + Set, + { + if self.len() <= other.len() { + self.iter().all(|v| !other.contains(v)) + } else { + other.iter().all(|v| !self.contains(v)) + } + } + + /// Returns `true` if the set is a subset of another, + /// i.e., `other` contains at least all the values in `self`. + #[must_use] + fn is_subset<'a, ST>(&'a self, other: &'a ST) -> bool + where + ST: Set, + Self: Sized + Set, + { + if self.len() <= other.len() { + self.iter().all(|v| other.contains(v)) + } else { + false + } + } + + /// Returns `true` if the set is a superset of another, + /// i.e., `self` contains at least all the values in `other`. + #[must_use] + fn is_superset<'a, ST>(&'a self, other: &'a ST) -> bool + where + ST: Set, + Self: Sized + Set, + { + if other.len() <= self.len() { + other.iter().all(|v| self.contains(v)) + } else { + false + } + } +} + +impl SetOps for ST where ST: Set {} diff --git a/frozen-collections-core/src/traits/set_query.rs b/frozen-collections-core/src/traits/set_query.rs new file mode 100644 index 0000000..1f44116 --- /dev/null +++ b/frozen-collections-core/src/traits/set_query.rs @@ -0,0 +1,65 @@ +use core::borrow::Borrow; +use core::hash::{BuildHasher, Hash}; + +/// Common query abstractions for sets. +pub trait SetQuery { + /// Checks whether a particular value is present in the set. + #[must_use] + fn contains(&self, value: &Q) -> bool { + self.get(value).is_some() + } + + /// Gets a reference to a value in the set. + #[must_use] + fn get(&self, value: &Q) -> Option<&T>; +} + +#[cfg(feature = "std")] +impl SetQuery for std::collections::HashSet +where + T: Eq + Hash + Borrow, + Q: Hash + Eq, + BH: BuildHasher, +{ + #[inline] + fn get(&self, value: &Q) -> Option<&T> { + self.get(value) + } +} + +#[cfg(feature = "std")] +impl SetQuery for std::collections::BTreeSet +where + T: Ord + Borrow, + Q: Ord, +{ + #[inline] + fn get(&self, value: &Q) -> Option<&T> { + self.get(value) + } +} + +impl SetQuery for hashbrown::hash_set::HashSet +where + T: Hash + Eq + Borrow, + Q: Hash + Eq, + BH: BuildHasher, +{ + #[inline] + fn get(&self, value: &Q) -> Option<&T> { + self.get(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hashbrown::HashSet; + + #[test] + fn test_object_safe() { + let s: &dyn SetQuery<_, _> = &HashSet::from([1, 2, 3]); + + assert!(s.contains(&1)); + } +} diff --git a/frozen-collections-core/src/utils/bitvec.rs b/frozen-collections-core/src/utils/bitvec.rs new file mode 100644 index 0000000..fed4034 --- /dev/null +++ b/frozen-collections-core/src/utils/bitvec.rs @@ -0,0 +1,92 @@ +//! Simple bit vectors. + +use alloc::boxed::Box; + +pub struct BitVec { + bits: Box<[u64]>, + len: usize, +} + +impl BitVec { + pub fn with_capacity(capacity: usize) -> Self { + Self { + bits: (0..((capacity as u64) + 63) / 64).collect(), + len: capacity, + } + } + + pub fn fill(&mut self, value: bool) { + if value { + self.bits.fill(0xffff_ffff_ffff_ffff); + } else { + self.bits.fill(0); + } + } + + pub fn set(&mut self, index: usize, value: bool) { + debug_assert!(index < self.len, "Out of bounds"); + + if value { + self.bits[index / 64] |= 1 << (index % 64); + } else { + self.bits[index / 64] &= !(1 << (index % 64)); + } + } + + pub fn get(&self, index: usize) -> bool { + debug_assert!(index < self.len, "Out of bounds"); + + (self.bits[index / 64] & 1 << (index % 64)) != 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bitvec() { + const LEN: usize = 125; + + let mut bitvec = BitVec::with_capacity(LEN); + assert_eq!(2, bitvec.bits.len()); + + bitvec.fill(false); + for i in 0..LEN { + assert!(!bitvec.get(i)); + } + + bitvec.fill(true); + for i in 0..LEN { + assert!(bitvec.get(i)); + } + + for i in 0..LEN { + bitvec.set(i, false); + bitvec.set(i, false); + assert!(!bitvec.get(i)); + } + + for i in 0..LEN { + bitvec.set(i, true); + bitvec.set(i, true); + assert!(bitvec.get(i)); + } + } + + #[test] + #[should_panic] + #[allow(clippy::should_panic_without_expect)] + fn get_panic() { + let bitvec = BitVec::with_capacity(12); + bitvec.get(12); + } + + #[test] + #[should_panic] + #[allow(clippy::should_panic_without_expect)] + fn set_panic() { + let mut bitvec = BitVec::with_capacity(12); + bitvec.set(12, false); + } +} diff --git a/frozen-collections-core/src/utils/dedup.rs b/frozen-collections-core/src/utils/dedup.rs new file mode 100644 index 0000000..dbe1710 --- /dev/null +++ b/frozen-collections-core/src/utils/dedup.rs @@ -0,0 +1,295 @@ +//! Duplicate removal utility functions for frozen collections. + +use crate::traits::Hasher; +use alloc::vec::Vec; +use core::hash::Hash; +use hashbrown::HashSet as HashbrownSet; + +/// Remove duplicates from a vector, keeping the last occurrence of each duplicate. +/// +/// This assumes the input vector is fairly short as time complexity is very high. +#[mutants::skip] +#[allow(clippy::module_name_repetitions)] +pub fn dedup_by_keep_last_slow(unsorted_entries: &mut Vec, mut cmp: F) +where + F: FnMut(&mut T, &mut T) -> bool, +{ + if unsorted_entries.len() < 2 { + return; + } + + let mut dupes = HashbrownSet::new(); + for i in 0..unsorted_entries.len() { + for j in (i + 1)..unsorted_entries.len() { + let (s0, s1) = unsorted_entries.split_at_mut(j); + if cmp(&mut s0[i], &mut s1[0]) { + dupes.insert(i); + break; + } + } + } + + if dupes.is_empty() { + return; + } + + let mut index = 0; + unsorted_entries.retain(|_| { + let result = !dupes.contains(&index); + index += 1; + result + }); +} + +/// Remove duplicates from a vector, keeping the last occurrence of each duplicate. +/// +/// This assumes the input vector is sorted. +#[allow(clippy::module_name_repetitions)] +pub fn dedup_by_keep_last(sorted_entries: &mut Vec, mut cmp: F) +where + F: FnMut(&mut T, &mut T) -> bool, +{ + if sorted_entries.len() < 2 { + return; + } + + let mut dupes = HashbrownSet::new(); + for i in 0..sorted_entries.len() - 1 { + let (s0, s1) = sorted_entries.split_at_mut(i + 1); + if cmp(&mut s0[i], &mut s1[0]) { + dupes.insert(i); + } + } + + if dupes.is_empty() { + return; + } + + let mut index = 0; + sorted_entries.retain(|_| { + let result = !dupes.contains(&index); + index += 1; + result + }); +} + +struct Entry<'a, K> { + pub hash: u64, + pub index: usize, + pub key: &'a K, +} + +impl Hash for Entry<'_, K> +where + K: Eq, +{ + #[mutants::skip] + fn hash(&self, state: &mut H) { + state.write_u64(self.hash); + } +} + +impl PartialEq for Entry<'_, K> { + fn eq(&self, other: &Self) -> bool { + self.key.eq(other.key) + } +} + +impl Eq for Entry<'_, K> {} + +/// Remove duplicates from a vector, keeping the last occurrence of each duplicate. +#[allow(clippy::module_name_repetitions)] +#[mutants::skip] +pub fn dedup_by_hash_keep_last(unsorted_entries: &mut Vec<(K, V)>, hasher: &H) +where + K: Eq, + H: Hasher, +{ + if unsorted_entries.len() < 2 { + return; + } + + let mut dupes = Vec::new(); + { + let mut keep = HashbrownSet::with_capacity(unsorted_entries.len()); + for (i, pair) in unsorted_entries.iter().enumerate() { + let hash = hasher.hash(&pair.0); + + let entry = Entry { + hash, + index: i, + key: &pair.0, + }; + + let old = keep.replace(entry); + if let Some(old) = old { + dupes.push(old.index); + } + } + } + + if dupes.is_empty() { + // no duplicates found, we're done + return; + } + + // remove the duplicates from the input vector + let mut index = 0; + unsorted_entries.retain(|_| { + let result = !dupes.contains(&index); + index += 1; + result + }); +} + +/// Look for the first duplicate value if any. +pub fn has_duplicates(values: I) -> bool +where + I: Iterator, +{ + let mut s = HashbrownSet::new(); + for v in values { + if !s.insert(v) { + return true; + } + } + + false +} + +#[derive(PartialEq, Eq)] +struct Wrapper { + value: T, + hash_code: u64, +} + +impl Hash for Wrapper { + fn hash(&self, state: &mut H) { + state.write_u64(self.hash_code); + } +} + +/// Look for the first duplicate value if any. +pub fn has_duplicates_with_hasher(values: &[&T], hasher: &H) -> bool +where + H: Hasher, + T: ?Sized + Eq, +{ + let mut s = HashbrownSet::new(); + for value in values { + let hash_code = hasher.hash(value); + if !s.insert(Wrapper { value, hash_code }) { + return true; + } + } + + false +} + +/// Look for the first duplicate value if any (assumes `values` is a relatively small array). +pub fn has_duplicates_slow(values: &[T]) -> bool +where + T: Eq, +{ + for i in 0..values.len() { + for j in 0..i { + if values[j].eq(&values[i]) { + return true; + } + } + } + + false +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_slow_dedup_by_keep_last_no_duplicates() { + let mut vec = vec![(1, "one"), (2, "two"), (3, "three")]; + dedup_by_keep_last_slow(&mut vec, |x, y| x.0.eq(&y.0)); + assert_eq!(vec, vec![(1, "one"), (2, "two"), (3, "three")]); + } + + #[test] + fn test_slow_dedup_by_keep_last_with_duplicates() { + let mut vec = vec![ + (1, "one"), + (2, "two"), + (2, "two duplicate"), + (3, "three"), + (3, "three duplicate"), + (3, "three last"), + ]; + dedup_by_keep_last_slow(&mut vec, |x, y| x.0.eq(&y.0)); + assert_eq!( + vec, + vec![(1, "one"), (2, "two duplicate"), (3, "three last")] + ); + } + + #[test] + fn test_slow_dedup_by_keep_last_empty_vector() { + let mut vec: Vec<(u8, &str)> = Vec::new(); + dedup_by_keep_last_slow(&mut vec, |x, y| x.0.eq(&y.0)); + assert!(vec.is_empty()); + } + + #[test] + fn test_slow_dedup_by_key_keep_last_all_same_entries() { + let mut vec = vec![(1, "one"), (1, "one duplicate"), (1, "one last")]; + dedup_by_keep_last_slow(&mut vec, |x, y| x.0.eq(&y.0)); + assert_eq!(vec, vec![(1, "one last")]); + } + + #[test] + fn test_find_duplicate_no_duplicates() { + let vec = [1, 2, 3]; + assert!(!has_duplicates(vec.iter())); + } + + #[test] + fn test_find_duplicate_with_duplicates() { + let vec = [1, 2, 2, 3]; + assert!(has_duplicates(vec.iter())); + } + + #[test] + fn test_find_duplicate_empty_slice() { + let vec: Vec = Vec::new(); + assert!(!has_duplicates(vec.iter())); + } + + #[test] + fn test_find_duplicate_all_same_entries() { + let vec = [1, 1, 1]; + assert!(has_duplicates(vec.iter())); + } + + #[test] + fn test_find_duplicate_slow_no_duplicates() { + let vec = vec![1, 2, 3]; + assert!(!has_duplicates_slow(&vec)); + } + + #[test] + fn test_find_duplicate_slow_with_duplicates() { + let vec = vec![1, 2, 2, 3]; + assert!(has_duplicates_slow(&vec)); + } + + #[test] + fn test_find_duplicate_slow_empty_slice() { + let vec: Vec = Vec::new(); + assert!(!has_duplicates_slow(&vec)); + } + + #[test] + fn test_find_duplicate_slow_all_same_entries() { + let vec = vec![1, 1, 1]; + assert!(has_duplicates_slow(&vec)); + } +} diff --git a/frozen-collections-core/src/utils/eytzinger.rs b/frozen-collections-core/src/utils/eytzinger.rs new file mode 100644 index 0000000..da81112 --- /dev/null +++ b/frozen-collections-core/src/utils/eytzinger.rs @@ -0,0 +1,69 @@ +//! Implements Eytzinger search over slices. +//! Refer to this research paper for more information: [Eytzinger layout](https://arxiv.org/pdf/1509.05053.pdf) +//! +//! This code is adapted and heavily modified from + +use core::cmp::Ordering; + +/// Sorts the slice in-place using the Eytzinger layout. +#[allow(clippy::module_name_repetitions)] +pub fn eytzinger_sort(data: &mut [T]) { + const fn get_eytzinger_index(original_index: usize, slice_len: usize) -> usize { + let ipk = (original_index + 2).next_power_of_two().trailing_zeros() as usize; + let li = original_index + 1 - (1 << (ipk - 1)); + let zk = li * 2 + 1; + let last_power_of_two = (slice_len + 2).next_power_of_two() / 2; + let y = (last_power_of_two >> (ipk - 1)) * zk; + let kp = y >> 1; + let x = kp + last_power_of_two; // (1+k) * last_power_of_two + let x = x.saturating_sub(slice_len + 1); + y - x - 1 + } + + let mut map = hashbrown::HashMap::new(); + for mut i in 0..data.len() { + let mut target = get_eytzinger_index(i, data.len()); + if target < i { + target = map.remove(&target).unwrap(); + } + + data.swap(i, target); + + if let Some(x) = map.remove(&i) { + i = x; + } + + if target != i { + _ = map.insert(target, i); + _ = map.insert(i, target); + } + } +} + +/// Searches for a given key in the slice. +/// +/// The slice must have been previously sorted with the `eytzinger` method. +#[inline] +#[allow(clippy::module_name_repetitions)] +pub fn eytzinger_search_by<'a, T: 'a, F>(data: &'a [T], mut f: F) -> Option +where + F: FnMut(&'a T) -> Ordering, +{ + let mut i = 0; + loop { + match data.get(i) { + Some(v) => { + let order = f(v); + if order == Ordering::Equal { + return Some(i); + } + + // Leverage the fact Ordering is defined as -1/0/1 + let o = order as usize; + let o = (o >> 1) & 1; + i = 2 * i + 1 + o; + } + None => return None, + } + } +} diff --git a/frozen-collections-core/src/utils/mod.rs b/frozen-collections-core/src/utils/mod.rs new file mode 100644 index 0000000..9a02ee2 --- /dev/null +++ b/frozen-collections-core/src/utils/mod.rs @@ -0,0 +1,11 @@ +//! Utility functions and types for internal use. + +pub use bitvec::*; +pub use dedup::*; +pub use eytzinger::*; +pub use random::*; + +mod bitvec; +mod dedup; +mod eytzinger; +mod random; diff --git a/frozen-collections-core/src/utils/random.rs b/frozen-collections-core/src/utils/random.rs new file mode 100644 index 0000000..4cdb9cd --- /dev/null +++ b/frozen-collections-core/src/utils/random.rs @@ -0,0 +1,15 @@ +//! Random # utilities for frozen collections. + +use const_random::const_random; + +/// Pick four random seeds at compile time. +#[must_use] +#[mutants::skip] +pub const fn pick_compile_time_random_seeds() -> (u64, u64, u64, u64) { + ( + const_random!(u64), + const_random!(u64), + const_random!(u64), + const_random!(u64), + ) +} diff --git a/frozen-collections-macros/Cargo.toml b/frozen-collections-macros/Cargo.toml new file mode 100644 index 0000000..528b76b --- /dev/null +++ b/frozen-collections-macros/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "frozen-collections-macros" +description = "Macros to support frozen collections" +readme = "README.md" +authors.workspace = true +version.workspace = true +edition.workspace = true +categories.workspace = true +keywords.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +name = "frozen_collections_macros" +path = "src/lib.rs" +proc-macro = true + +[dependencies] +proc-macro-error = "1.0.4" + +[dependencies.frozen-collections-core] +path = "../frozen-collections-core" +version = "0.1.0" +default-features = false + +[lints] +workspace = true diff --git a/frozen-collections-macros/README.md b/frozen-collections-macros/README.md new file mode 100644 index 0000000..f51a5c3 --- /dev/null +++ b/frozen-collections-macros/README.md @@ -0,0 +1,6 @@ +# frozen-collections-macros + +This crate contains some of the implementation logic for the +frozen-collections crate. Users of frozen collections +should take a dependency on the [`frozen-collections`](https://docs.rs/frozen-collections) crate +instead of this one. diff --git a/frozen-collections-macros/src/lib.rs b/frozen-collections-macros/src/lib.rs new file mode 100644 index 0000000..b818bd4 --- /dev/null +++ b/frozen-collections-macros/src/lib.rs @@ -0,0 +1,84 @@ +//! Implementation crate for the frozen collections. +//! +//!
+//! This crate is an implementation detail of the `frozen_collections` crate. +//! This crate's API is therefore not stable and may change at any time. Please do not +//! use this crate directly, and instead use the public API provided by the +//! `frozen_collections` crate. +//!
+ +use frozen_collections_core::macros::*; +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; + +#[proc_macro] +#[proc_macro_error] +pub fn fz_hash_map(item: TokenStream) -> TokenStream { + fz_hash_map_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn fz_hash_set(item: TokenStream) -> TokenStream { + fz_hash_set_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn fz_ordered_map(item: TokenStream) -> TokenStream { + fz_ordered_map_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn fz_ordered_set(item: TokenStream) -> TokenStream { + fz_ordered_set_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn fz_string_map(item: TokenStream) -> TokenStream { + fz_string_map_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn fz_string_set(item: TokenStream) -> TokenStream { + fz_string_set_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn fz_scalar_map(item: TokenStream) -> TokenStream { + fz_scalar_map_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn fz_scalar_set(item: TokenStream) -> TokenStream { + fz_scalar_set_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +#[proc_macro_derive(Scalar)] +#[proc_macro_error] +pub fn derive_scalar(item: TokenStream) -> TokenStream { + derive_scalar_macro(item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} diff --git a/frozen-collections/Cargo.toml b/frozen-collections/Cargo.toml new file mode 100644 index 0000000..bbb7e0b --- /dev/null +++ b/frozen-collections/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "frozen-collections" +description = "Fast partially-immutable collections." +readme.workspace = true +authors.workspace = true +version.workspace = true +edition.workspace = true +categories.workspace = true +keywords.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +ahash = { version = "0.8.11" } +rand = "0.9.0-beta.1" + +[dev-dependencies] +quote = { version = "1.0.37" } +hashbrown = { version = "0.15.2" } +criterion = "0.5.1" +ahash = "0.8.11" + +[build-dependencies] +rand = "0.9.0-beta.1" + +[dependencies.frozen-collections-macros] +path = "../frozen-collections-macros" +version = "0.1.0" + +[dependencies.frozen-collections-core] +path = "../frozen-collections-core" +default-features = false +version = "0.1.0" + +[[bench]] +name = "string_keys" +harness = false + +[[bench]] +name = "scalar_keys" +harness = false + +[[bench]] +name = "hashed_keys" +harness = false + +[[bench]] +name = "ordered_keys" +harness = false + +[lints] +workspace = true + +[features] +default = ["std"] +std = ["frozen-collections-core/std"] diff --git a/frozen-collections/benches/hashed_keys.rs b/frozen-collections/benches/hashed_keys.rs new file mode 100644 index 0000000..b458ba2 --- /dev/null +++ b/frozen-collections/benches/hashed_keys.rs @@ -0,0 +1,14 @@ +extern crate alloc; + +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; +use core::hash::Hash; +use core::hint::black_box; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use frozen_collections::{fz_hash_set, SetQuery}; + +include!(concat!(env!("OUT_DIR"), "/hashed.rs")); + +criterion_group!(benches, hashed); +criterion_main!(benches); diff --git a/frozen-collections/benches/ordered_keys.rs b/frozen-collections/benches/ordered_keys.rs new file mode 100644 index 0000000..8d3ea62 --- /dev/null +++ b/frozen-collections/benches/ordered_keys.rs @@ -0,0 +1,8 @@ +use core::hint::black_box; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use frozen_collections::{fz_ordered_set, SetQuery}; + +include!(concat!(env!("OUT_DIR"), "/ordered.rs")); + +criterion_group!(benches, ordered); +criterion_main!(benches); diff --git a/frozen-collections/benches/scalar_keys.rs b/frozen-collections/benches/scalar_keys.rs new file mode 100644 index 0000000..687a7dd --- /dev/null +++ b/frozen-collections/benches/scalar_keys.rs @@ -0,0 +1,14 @@ +extern crate alloc; + +use alloc::vec::Vec; +use core::hint::black_box; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use frozen_collections::{fz_scalar_set, SetQuery}; + +include!(concat!(env!("OUT_DIR"), "/dense_scalar.rs")); +include!(concat!(env!("OUT_DIR"), "/sparse_scalar.rs")); +include!(concat!(env!("OUT_DIR"), "/random_scalar.rs")); + +criterion_group!(benches, dense_scalar, sparse_scalar, random_scalar,); + +criterion_main!(benches); diff --git a/frozen-collections/benches/string_keys.rs b/frozen-collections/benches/string_keys.rs new file mode 100644 index 0000000..c2fc4ba --- /dev/null +++ b/frozen-collections/benches/string_keys.rs @@ -0,0 +1,14 @@ +extern crate alloc; + +use alloc::vec::Vec; +use core::hint::black_box; +use core::ops::Add; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use frozen_collections::{fz_string_set, SetQuery}; + +include!(concat!(env!("OUT_DIR"), "/random_string.rs")); +include!(concat!(env!("OUT_DIR"), "/prefixed_string.rs")); + +criterion_group!(benches, random_string, prefixed_string,); + +criterion_main!(benches); diff --git a/frozen-collections/build.rs b/frozen-collections/build.rs new file mode 100644 index 0000000..f9b7f8d --- /dev/null +++ b/frozen-collections/build.rs @@ -0,0 +1,422 @@ +use rand::Rng; +use std::env; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::Path; + +const SMALL: usize = 3; +const MEDIUM: usize = 16; +const LARGE: usize = 256; +const HUGE: usize = 1000; + +fn emit_benchmark_preamble(name: &str) -> BufWriter { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join(format!("{name}.rs")); + let mut file = BufWriter::new(File::create(dest_path).unwrap()); + + writeln!(file, "#[allow(clippy::unreadable_literal)]").unwrap(); + writeln!(file, "#[allow(clippy::items_after_statements)]").unwrap(); + writeln!(file, "#[allow(clippy::explicit_auto_deref)]").unwrap(); + writeln!(file, "#[allow(clippy::redundant_closure_for_method_calls)]").unwrap(); + writeln!(file, "fn {name}(c: &mut Criterion) {{").unwrap(); + writeln!(file, " let mut group = c.benchmark_group(\"{name}\");").unwrap(); + + file +} + +fn emit_benchmark_postamble(mut file: BufWriter) { + writeln!(file, " group.finish();").unwrap(); + writeln!(file, "}}").unwrap(); +} + +fn emit_loop(file: &mut BufWriter, name: &str) { + writeln!( + file, + " group.bench_with_input(BenchmarkId::new(\"{name}\", size), &size, |b, _| {{" + ) + .unwrap(); + writeln!(file, " b.iter(|| {{").unwrap(); + writeln!(file, " for key in &probe {{").unwrap(); + writeln!(file, " _ = black_box(s.contains(key));").unwrap(); + writeln!(file, " }}").unwrap(); + writeln!(file, " }});").unwrap(); + writeln!(file, " }});").unwrap(); +} + +fn emit_scalar_suite(file: &mut BufWriter, size: usize, literal_producer: F) +where + F: Fn(&mut BufWriter, usize), +{ + writeln!(file, " let frozen = fz_scalar_set!({{").unwrap(); + literal_producer(file, size); + writeln!(file, " }});").unwrap(); + + writeln!( + file, + " let input: Vec = frozen.clone().into_iter().collect();" + ) + .unwrap(); + writeln!(file, " let size = input.len();").unwrap(); + writeln!(file, " let mut probe = Vec::new();").unwrap(); + writeln!(file, " for i in &input {{").unwrap(); + writeln!(file, " probe.push(*i);").unwrap(); + writeln!(file, " probe.push(-(*i));").unwrap(); + writeln!(file, " }}").unwrap(); + + writeln!(file, " let s = std::collections::HashSet::<_, std::hash::RandomState>::from_iter(input.clone());").unwrap(); + emit_loop(file, "HashSet(classic)"); + + writeln!( + file, + " let s = std::collections::HashSet::<_, ahash::RandomState>::from_iter(input.clone());" + ) + .unwrap(); + emit_loop(file, "HashSet(ahash)"); + + writeln!(file, " let s = fz_scalar_set!(input);").unwrap(); + emit_loop(file, "fz_scalar_set(vector)"); + + writeln!(file, " let s = frozen;").unwrap(); + emit_loop(file, "fz_scalar_set(literals)"); +} + +fn emit_string_suite(file: &mut BufWriter, size: usize, literal_producer: F) +where + F: Fn(&mut BufWriter, usize), +{ + writeln!(file, " let frozen = fz_string_set!({{").unwrap(); + literal_producer(file, size); + writeln!(file, " }});").unwrap(); + + writeln!( + file, + " let input: Vec = frozen.clone().into_iter().map(|x| x.to_string()).collect();" + ) + .unwrap(); + writeln!(file, " let size = input.len();").unwrap(); + writeln!(file, " let mut probe = Vec::new();").unwrap(); + writeln!(file, " for s in &input {{").unwrap(); + writeln!(file, " probe.push((*s).clone());").unwrap(); + writeln!(file, " probe.push((*s).clone().add(\"Hello\"));").unwrap(); + writeln!(file, " }}").unwrap(); + + writeln!(file, " let mut tmp: Vec<&str> = Vec::new();").unwrap(); + writeln!(file, " for x in &input {{").unwrap(); + writeln!(file, " tmp.push(x.as_str());").unwrap(); + writeln!(file, " }}").unwrap(); + writeln!(file, " let input = tmp;").unwrap(); + + writeln!(file, " let mut tmp: Vec<&str> = Vec::new();").unwrap(); + writeln!(file, " for x in &probe {{").unwrap(); + writeln!(file, " tmp.push(x.as_str());").unwrap(); + writeln!(file, " }}").unwrap(); + writeln!(file, " let probe = tmp;").unwrap(); + + writeln!(file, " let s = std::collections::HashSet::<_, std::hash::RandomState>::from_iter(input.clone());").unwrap(); + emit_loop(file, "HashSet(classic)"); + + writeln!( + file, + " let s = std::collections::HashSet::<_, ahash::RandomState>::from_iter(input.clone());" + ) + .unwrap(); + emit_loop(file, "HashSet(ahash)"); + + writeln!(file, " let s = fz_string_set!(input);").unwrap(); + emit_loop(file, "fz_string_set(vector)"); + + writeln!(file, " let s = frozen;").unwrap(); + emit_loop(file, "fz_string_set(literals)"); +} + +fn emit_hashed_suite(file: &mut BufWriter, size: usize, literal_producer: F) +where + F: Fn(&mut BufWriter, usize), +{ + writeln!(file, " let frozen = fz_hash_set!({{").unwrap(); + literal_producer(file, size); + writeln!(file, " }});").unwrap(); + + writeln!( + file, + " let input: Vec<_> = frozen.clone().into_iter().collect();" + ) + .unwrap(); + writeln!(file, " let size = input.len();").unwrap(); + writeln!(file, " let mut probe = Vec::new();").unwrap(); + writeln!(file, " for i in &input {{").unwrap(); + writeln!( + file, + " probe.push(Record {{ name: (*i).name.clone(), age: (*i).age }});" + ) + .unwrap(); + writeln!( + file, + " probe.push(Record {{ name: (*i).name.clone(), age: -(*i).age }});" + ) + .unwrap(); + writeln!(file, " }}").unwrap(); + + writeln!(file, " let s = std::collections::HashSet::<_, std::hash::RandomState>::from_iter(input.clone());").unwrap(); + emit_loop(file, "HashSet(classic)"); + + writeln!( + file, + " let s = std::collections::HashSet::<_, ahash::RandomState>::from_iter(input.clone());" + ) + .unwrap(); + emit_loop(file, "HashSet(ahash)"); + + writeln!(file, " let s = fz_hash_set!(input);").unwrap(); + emit_loop(file, "fz_hash_set(vector)"); + + writeln!(file, " let s = frozen;").unwrap(); + emit_loop(file, "fz_hash_set(literals)"); +} + +fn emit_ordered_suite(file: &mut BufWriter, size: usize, literal_producer: F) +where + F: Fn(&mut BufWriter, usize), +{ + writeln!(file, " let frozen = fz_ordered_set!({{").unwrap(); + literal_producer(file, size); + writeln!(file, " }});").unwrap(); + + writeln!( + file, + " let input: Vec<_> = frozen.clone().into_iter().collect();" + ) + .unwrap(); + writeln!(file, " let size = input.len();").unwrap(); + writeln!(file, " let mut probe = Vec::new();").unwrap(); + writeln!(file, " for i in &input {{").unwrap(); + writeln!( + file, + " probe.push(Record {{ name: (*i).name.clone(), age: (*i).age }});" + ) + .unwrap(); + writeln!( + file, + " probe.push(Record {{ name: (*i).name.clone(), age: -(*i).age }});" + ) + .unwrap(); + writeln!(file, " }}").unwrap(); + + writeln!( + file, + " let s = std::collections::BTreeSet::<_>::from_iter(input.clone());" + ) + .unwrap(); + emit_loop(file, "BTreeSet"); + + writeln!(file, " let s = fz_ordered_set!(input);").unwrap(); + emit_loop(file, "fz_hash_set(vector)"); + + writeln!(file, " let s = frozen;").unwrap(); + emit_loop(file, "fz_ordered_set(literals)"); +} + +fn emit_dense_scalar_benchmark() { + fn dense_producer(file: &mut BufWriter, size: usize) { + for i in 0..size { + writeln!(file, " {i},",).unwrap(); + } + } + + let mut file = emit_benchmark_preamble("dense_scalar"); + + emit_scalar_suite(&mut file, SMALL, dense_producer); + emit_scalar_suite(&mut file, MEDIUM, dense_producer); + emit_scalar_suite(&mut file, LARGE, dense_producer); + emit_scalar_suite(&mut file, HUGE, dense_producer); + + emit_benchmark_postamble(file); +} + +fn emit_sparse_scalar_benchmark() { + fn sparse_producer(file: &mut BufWriter, size: usize) { + for i in 0..size { + let x = i * 2; + writeln!(file, " {x},",).unwrap(); + } + } + + let mut file = emit_benchmark_preamble("sparse_scalar"); + + emit_scalar_suite(&mut file, SMALL, sparse_producer); + emit_scalar_suite(&mut file, MEDIUM, sparse_producer); + emit_scalar_suite(&mut file, LARGE, sparse_producer); + emit_scalar_suite(&mut file, HUGE, sparse_producer); + + emit_benchmark_postamble(file); +} + +fn emit_random_scalar_benchmark() { + fn random_producer(file: &mut BufWriter, size: usize) { + let mut rng = rand::rng(); + + for _ in 0..size { + let x: i32 = rng.random(); + writeln!(file, " {x},",).unwrap(); + } + } + + let mut file = emit_benchmark_preamble("random_scalar"); + + emit_scalar_suite(&mut file, SMALL, random_producer); + emit_scalar_suite(&mut file, MEDIUM, random_producer); + emit_scalar_suite(&mut file, LARGE, random_producer); + emit_scalar_suite(&mut file, HUGE, random_producer); + + emit_benchmark_postamble(file); +} + +fn emit_prefixed_string_benchmark() { + fn prefixed_producer(file: &mut BufWriter, size: usize) { + let mut rng = rand::rng(); + + for _ in 0..size { + let len: u32 = rng.random(); + let len = (len % 10) + 5; + let mut s = String::new(); + for _ in 0..len { + let x: u8 = rng.random(); + let x = (x % 26) + 97; + s.push(x as char); + } + + writeln!(file, " \"Color-{s}\",",).unwrap(); + } + } + + let mut file = emit_benchmark_preamble("prefixed_string"); + + emit_string_suite(&mut file, SMALL, prefixed_producer); + emit_string_suite(&mut file, MEDIUM, prefixed_producer); + emit_string_suite(&mut file, LARGE, prefixed_producer); + emit_string_suite(&mut file, HUGE, prefixed_producer); + + emit_benchmark_postamble(file); +} + +fn emit_random_string_benchmark() { + fn random_producer(file: &mut BufWriter, size: usize) { + let mut rng = rand::rng(); + + for _ in 0..size { + let len: u32 = rng.random(); + let len = (len % 10) + 5; + let mut s = String::new(); + for _ in 0..len { + let x: u8 = rng.random(); + let x = (x % 26) + 97; + s.push(x as char); + } + + writeln!(file, " \"{s}\",",).unwrap(); + } + } + + let mut file = emit_benchmark_preamble("random_string"); + + emit_string_suite(&mut file, SMALL, random_producer); + emit_string_suite(&mut file, MEDIUM, random_producer); + emit_string_suite(&mut file, LARGE, random_producer); + emit_string_suite(&mut file, HUGE, random_producer); + + emit_benchmark_postamble(file); +} + +fn emit_hashed_benchmark() { + fn hashed_producer(file: &mut BufWriter, size: usize) { + let mut rng = rand::rng(); + + for _ in 0..size { + let len: u32 = rng.random(); + let len = (len % 10) + 5; + let mut s = String::new(); + for _ in 0..len { + let x: u8 = rng.random(); + let x = (x % 26) + 97; + s.push(x as char); + } + + let age: i32 = rng.random(); + writeln!( + file, + " Record {{ name: \"{s}\".to_string(), age: {age} }}," + ) + .unwrap(); + } + } + + let mut file = emit_benchmark_preamble("hashed"); + + writeln!(file, "#[derive(Clone, Debug, Eq, Hash, PartialEq)]").unwrap(); + writeln!(file, "struct Record {{").unwrap(); + writeln!(file, " name: String,").unwrap(); + writeln!(file, " age: i32,").unwrap(); + writeln!(file, "}}").unwrap(); + + emit_hashed_suite(&mut file, SMALL, hashed_producer); + emit_hashed_suite(&mut file, MEDIUM, hashed_producer); + emit_hashed_suite(&mut file, LARGE, hashed_producer); + emit_hashed_suite(&mut file, HUGE, hashed_producer); + + emit_benchmark_postamble(file); +} + +fn emit_ordered_benchmark() { + fn ordered_producer(file: &mut BufWriter, size: usize) { + let mut rng = rand::rng(); + + for _ in 0..size { + let len: u32 = rng.random(); + let len = (len % 10) + 5; + let mut s = String::new(); + for _ in 0..len { + let x: u8 = rng.random(); + let x = (x % 26) + 97; + s.push(x as char); + } + + let age: i32 = rng.random(); + writeln!( + file, + " Record {{ name: \"{s}\".to_string(), age: {age} }}," + ) + .unwrap(); + } + } + + let mut file = emit_benchmark_preamble("ordered"); + + writeln!( + file, + "#[derive(Clone, Debug, Eq, Ord, PartialOrd, PartialEq)]" + ) + .unwrap(); + writeln!(file, "struct Record {{").unwrap(); + writeln!(file, " name: String,").unwrap(); + writeln!(file, " age: i32,").unwrap(); + writeln!(file, "}}").unwrap(); + + emit_ordered_suite(&mut file, SMALL, ordered_producer); + emit_ordered_suite(&mut file, MEDIUM, ordered_producer); + emit_ordered_suite(&mut file, LARGE, ordered_producer); + emit_ordered_suite(&mut file, HUGE, ordered_producer); + + emit_benchmark_postamble(file); +} + +fn main() { + emit_dense_scalar_benchmark(); + emit_sparse_scalar_benchmark(); + emit_random_scalar_benchmark(); + emit_prefixed_string_benchmark(); + emit_random_string_benchmark(); + emit_ordered_benchmark(); + emit_hashed_benchmark(); + + println!("cargo::rerun-if-changed=build.rs"); +} diff --git a/frozen-collections/src/lib.rs b/frozen-collections/src/lib.rs new file mode 100644 index 0000000..3001a65 --- /dev/null +++ b/frozen-collections/src/lib.rs @@ -0,0 +1,1279 @@ +#![cfg_attr(not(any(test, feature = "std")), no_std)] + +//! Frozen collections: fast _partially_ immutable collections +//! +//! Frozen collections are designed to deliver improved +//! read performance relative to the standard [`HashMap`](std::collections::HashMap) and +//! [`HashSet`](std::collections::HashSet) types. They are ideal for use with long-lasting collections +//! which get initialized when an application starts and remain unchanged +//! permanently, or at least extended periods of time. This is a common +//! pattern in service applications. +//! +//! As part of creating a frozen collection, analysis is performed over the data that the collection +//! will hold to determine the best layout and algorithm to use to deliver optimal performance. +//! Depending on the situation, sometimes the analysis is done at compile-time whereas in +//! other cases it is done at runtime when the collection is initialized. +//! This analysis can take some time, but the value in spending this time up front +//! is that the collections provide faster read-time performance. +//! +//! Frozen maps are only partially immutable. The keys associated with a frozen map are determined +//! at creation time and cannot change, but the values can be updated at will if you have a +//! mutable reference to the map. Frozen sets however are completely immutable and so never +//! change after creation. +//! +//! See [BENCHMARKS.md](https://github.com/geeknoid/frozen-collections/blob/main/BENCHMARKS.md) for +//! current benchmark numbers. +//! +//! # Creation +//! +//! Frozen collections are created with one of eight macros: [`fz_hash_map!`], [`fz_ordered_map!`], +//! [`fz_scalar_map!`], [`fz_string_map!`], [`fz_hash_set!`], [`fz_ordered_set!`], +//! [`fz_scalar_set!`], or [`fz_string_set!`]. These macros analyze the data you provide +//! and return a custom implementation type that's optimized for the data. All the +//! possible implementations implement the [`Map`] or [`Set`] traits. +//! +//! The macros exist in a short form and a long form, described below. +//! +//! ## Short Form +//! +//! With the short form, you supply the data that +//! goes into the collection and get in return an initialized collection of an unnamed +//! type. For example: +//! +//! ```rust +//! use frozen_collections::*; +//! +//! let m = fz_string_map!({ +//! "Alice": 1, +//! "Bob": 2, +//! "Sandy": 3, +//! "Tom": 4, +//! }); +//! ``` +//! +//! At build time, the macro analyzes the data supplied and determines the best map +//! implementation type to use. As such, the type of `m` is not known to this code. `m` will +//! always implement the [`Map`] trait however, so you can leverage type inference even though +//! you don't know the actual type of `m`: +//! +//! ```rust +//! use frozen_collections::*; +//! +//! fn main() { +//! let m = fz_string_map!({ +//! "Alice": 1, +//! "Bob": 2, +//! "Sandy": 3, +//! "Tom": 4, +//! }); +//! +//! more(m); +//! } +//! +//! fn more(m: M) +//! where +//! M: Map<&'static str, i32> +//! { +//! assert!(m.contains_key(&"Alice")); +//! } +//! ``` +//! +//! Rather than specifying all the data inline, you can also create a frozen collection by passing +//! a vector as input: +//! +//! ```rust +//! use frozen_collections::*; +//! +//! let v = vec![ +//! ("Alice", 1), +//! ("Bob", 2), +//! ("Sandy", 3), +//! ("Tom", 4), +//! ]; +//! +//! let m = fz_string_map!(v); +//! ``` +//! +//! The inline form is preferred however since it results in faster code. However, whereas the inline form +//! requires all the data to be provided at compile time, the vector form enables the content of the +//! frozen collection to be determined at runtime. +//! +//! ## Long Form +//! +//! The long form lets you provide a type alias name which will be created to +//! correspond to the collection implementation type chosen by the macro invocation. +//! Note that you must use the long form if you want to declare a static frozen collection. +//! +//! ```rust +//! use frozen_collections::*; +//! +//! fz_string_map!(static MAP: MyMapType<&str, i32>, { +//! "Alice": 1, +//! "Bob": 2, +//! "Sandy": 3, +//! "Tom": 4, +//! }); +//! ``` +//! +//! The above creates a static variable called `MAP` with keys that are strings and values which are +//! integers. As before, you don't know the specific implementation type selected by the macro, but +//! this time you have a type alias (i.e. `MyMapType`) representing that type. You can then use this alias +//! anywhere you'd like to in your code where you'd like to mention the type explicitly. +//! +//! To use the long form for non-static uses, replace `static` with `let`: +//! +//! ```rust +//! use frozen_collections::*; +//! +//! fz_string_map!(let m: MyMapType<&str, i32>, { +//! "Alice": 1, +//! "Bob": 2, +//! "Sandy": 3, +//! "Tom": 4, +//! }); +//! +//! more(m); +//! +//! struct S { +//! m: MyMapType, +//! } +//! +//! fn more(m: MyMapType) { +//! assert!(m.contains_key("Alice")); +//! } +//! ``` +//! +//! And like in the short form, you can also supply the collection's data via a vector: +//! +//! ```rust +//! use frozen_collections::*; +//! +//! let v = vec![ +//! ("Alice", 1), +//! ("Bob", 2), +//! ("Sandy", 3), +//! ("Tom", 4), +//! ]; +//! +//! fz_string_map!(let m: MyMapType<&str, i32>, v); +//! ``` +//! +//! # Traits +//! +//! The maps created by the frozen collections macros implement the following traits: +//! +//! - [`Map`]. The primary representation of a map. This trait has [`MapQuery`] and +//! [`MapIterations`] as super-traits. +//! - [`MapQuery`]. A trait for querying maps. This is an object-safe trait. +//! - [`MapIteration`]. A trait for iterating over maps. +//! +//! The sets created by the frozen collection macros implement the following traits: +//! +//! - [`Set`]. The primary representation of a set. This trait has [`SetQuery`], +//! [`SetIteration`] and [`SetOps`] as super-traits. +//! - [`SetQuery`]. A trait for querying sets. This is an object-safe trait. +//! - [`SetIteration`]. A trait for iterating over sets. +//! - [`SetOps`]. A trait for set operations like union and intersections. +//! +//! # Performance Considerations +//! +//! The analysis performed when creating maps tries to find the best concrete implementation type +//! given the data at hand. If all the data is visible to the macro at compile time, then you get +//! the best possible performance. If you supply a vector instead, then the analysis can only be +//! done at runtime and the resulting collection types are slightly slower. +//! +//! When creating static collections, the collections produced can often be embedded directly as constant data +//! into the binary of the application, thus requiring no initialization time and no heap space. at +//! This also happens to be the fastest form for these collections. If possible, this happens +//! automatically, you don't need to do anything special to enable this behavior. +//! +//! # Analysis and Optimizations +//! +//! Unlike normal collections, the frozen collections require you to provide all the data for +//! the collection when you create the collection. The data you supply is analyzed which determines +//! which specific underlying implementation strategy to use and how to lay out data internally. +//! +//! The available implementation strategies are: +//! +//! - **Scalar as Hash**. When the keys are of an integer or enum type, this uses the keys themselves +//! as hash codes, avoiding the overhead of hashing. +//! +//! - **Length as Hash**. When the keys are of a string type, the length of the keys +//! are used as hash code, avoiding the overhead of hashing. +//! +//! - **Dense Scalar Lookup**. When the keys represent a contiguous range of integer or enum values, +//! lookups use a simple array access instead of hashing. +//! +//! - **Sparse Scalar Lookup**. When the keys represent a sparse range of integer or enum values, +//! lookups use a sparse array access instead of hashing. +//! +//! - **Left Hand Substring Hashing**. When the keys are of a string type, this uses sub-slices of +//! the keys for hashing, reducing the overhead of hashing. +//! +//! - **Right Hand Substring Hashing**. Similar to the Left Hand Substring Hashing from above, but +//! using right-aligned sub-slices instead. +//! +//! - **Linear Scan**. For very small collections, this avoids hashing completely by scanning through +//! the entries in linear order. +//! +//! - **Ordered Scan**. For very small collections where the keys implement the [`Ord`] trait, +//! this avoids hashing completely by scanning through the entries in linear order. Unlike the +//! Linear Scan strategy, this one can early exit when keys are not found during the scan. +//! +//! - **Classic Hashing**. This is the fallback when none of the previous strategies apply. The +//! frozen implementations are generally faster than [`std::collections::HashMap`] and +//! [`std::collections::HashSet`]. +//! +//! - **Binary Search**. For relatively small collections where the keys implement the [`Ord`] trait, +//! classic binary searching is used. +//! +//! - **Eytzinger Search**. For larger collections where the keys implement the [`Ord`] trait, +//! a cache-friendly Eytzinger search is used. +//! +//! # Cargo Features +//! +//! You can specify the following features when you include the `frozen_collections` crate in your +//! `Cargo.toml` file: +//! +//! - **`std`**. Enables small features only available when building with the standard library. +//! +//! The `std` feature is enabled by default. + +extern crate alloc; + +pub use frozen_collections_core::traits::{ + Map, MapIteration, MapQuery, Scalar, Set, SetIteration, SetOps, SetQuery, +}; + +#[doc(hidden)] +pub use frozen_collections_core::traits::{ + CollectionMagnitude, Hasher, LargeCollection, Len, MediumCollection, SmallCollection, +}; + +/// Creates an efficient map with a fixed set of hashable keys. +/// +/// The concrete type used to implement the map is based on an analysis of the input you +/// provide. Although the types vary, they all implement the [`Map`] trait, so refer to the +/// trait for API documentation. +/// +/// # Alternate Choices +/// +/// If your keys are integers or enum variants, you should use the [`fz_scalar_map!`] macro instead. +/// If your keys are strings, you should use the [`fz_string_map`] macro instead. Both of these will +/// deliver better performance since they are specifically optimized for those key types. +/// +/// # Example +/// +/// ``` +/// use frozen_collections::*; +/// +/// // The key type we use to index into our example maps +/// #[derive(Eq, PartialEq, Hash, Clone, Debug)] +/// struct Key { +/// pub name: &'static str, +/// pub age: i32, +/// } +/// +/// // Declare a global static map. This results in a static variable called MY_MAP_0 of type MyMapType0. +/// fz_hash_map!(static MY_MAP_0: MyMapType0, { +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// fn variables() { +/// // Declare a local static map. This results in a local variable called MY_MAP_1 of type MyMapType1. +/// fz_hash_map!(static MY_MAP_1: MyMapType1, { +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// // Declare a local map. This results in a local variable called my_map_2 of type MyMapType2. +/// fz_hash_map!(let my_map_2: MyMapType2, { +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// // Declare a mutable local map. This results in a local variable called my_map_3 of type MyMapType3. +/// fz_hash_map!(let mut my_map_3: MyMapType3, { +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// // Declare a local map. This results in a local variable called my_map_4 of an unknown type. +/// let my_map_4 = fz_hash_map!({ +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// let v = vec![ +/// (Key { name: "Alice", age: 30}, 1), +/// (Key { name: "Bob", age: 40}, 2), +/// ]; +/// +/// // Declare a local map. This results in a local variable called my_map_5 of type MyMapType4. +/// fz_hash_map!(let my_map_5: MyMapType4, v); +/// +/// let v = vec![ +/// (Key { name: "Alice", age: 30}, 1), +/// (Key { name: "Bob", age: 40}, 2), +/// ]; +/// +/// // Declare a local map. This results in a local variable called my_map_6 of an unknown type. +/// let my_map_6 = fz_hash_map!(v); +/// +/// // no matter how the maps are declared, no matter the type selected to implement the map, +/// // they all work the same way and have the same API surface and implement the `Map` trait. +/// +/// assert_eq!( +/// Some(&1), +/// MY_MAP_0.get(&Key { +/// name: "Alice", +/// age: 30 +/// }) +/// ); +/// +/// assert_eq!( +/// Some(&2), +/// MY_MAP_0.get(&Key { +/// name: "Bob", +/// age: 40 +/// }) +/// ); +/// +/// assert_eq!( +/// None, +/// MY_MAP_0.get(&Key { +/// name: "Fred", +/// age: 50 +/// }) +/// ); +/// } +/// +/// // How to embed a map into a struct using the map's type. +/// struct MyStruct0 { +/// map: MyMapType0, +/// } +/// +/// // how to embed a map into a struct using the `Map` trait. +/// struct MyStruct1 +/// where +/// M: Map, +/// { +/// map: M, +/// } +/// +/// // You may need to have specific instances of a map in your process where the keys are the same, but +/// // the values differ. For that case, you can create a static instance with placeholder values. +/// // Then you clone this static instance when needed and set the values that you need in each case. +/// // So you'll have a single set of keys, but different values in each map instance. +/// fn structs() { +/// let mut ms0 = MyStruct0 { +/// map: MY_MAP_0.clone(), +/// }; +/// +/// let mut ms1 = MyStruct1 { +/// map: MY_MAP_0.clone(), +/// }; +/// +/// // set a custom value in ms0 +/// if let Some(v) = ms0.map.get_mut(&Key { +/// name: "Alice", +/// age: 30, +/// }) { +/// *v = 3; +/// } +/// +/// // set a different custom value in ms1 +/// if let Some(v) = ms1.map.get_mut(&Key { +/// name: "Alice", +/// age: 30, +/// }) { +/// *v = 4; +/// } +/// +/// assert_eq!( +/// Some(&3), +/// ms0.map.get(&Key { +/// name: "Alice", +/// age: 30 +/// }) +/// ); +/// +/// assert_eq!( +/// Some(&4), +/// ms1.map.get(&Key { +/// name: "Alice", +/// age: 30 +/// }) +/// ); +/// } +/// +/// fn main() { +/// variables(); +/// structs(); +/// } +/// ``` +pub use frozen_collections_macros::fz_hash_map; + +/// Creates an efficient set with a fixed set of hashable values. +/// +/// The concrete type used to implement the set is based on an analysis of the input you +/// provide. Although the types vary, they all implement the [`Set`] trait, so refer to the +/// trait for API documentation. +/// +/// # Alternate Choices +/// +/// If your values are integers or enum variants, you should use the [`fz_scalar_set!`] macro instead. +/// If your values are strings, you should use the [`fz_string_set`] macro instead. Both of these will +/// deliver better performance since they are specifically optimized for those value types. +/// +/// # Example +/// +/// ``` +/// use frozen_collections::*; +/// +/// // The value type we use to index into our example sets +/// #[derive(Eq, PartialEq, Hash, Clone, Debug)] +/// struct Key { +/// pub name: &'static str, +/// pub age: i32, +/// } +/// +/// // Declare a global static set. This results in a static variable called MY_SET_0 of type MySetType0. +/// fz_hash_set!(static MY_SET_0: MySetType0, { +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// fn variables() { +/// // Declare a local static set. This results in a local variable called MY_SET_1 of type MySetType1. +/// fz_hash_set!(static MY_SET_1: MySetType1, { +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// // Declare a local set. This results in a local variable called my_set_2 of type MySetType2. +/// fz_hash_set!(let my_set_2: MySetType2, { +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// // Declare a mutable local set. This results in a local variable called my_set_3 of type MySetType3. +/// fz_hash_set!(let mut my_set_3: MySetType3, { +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// // Declare a local set. This results in a local variable called my_set_4 of an unknown type. +/// let my_set_4 = fz_hash_set!({ +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// let v = vec![ +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// ]; +/// +/// // Declare a local set. This results in a local variable called my_set_5 of type MySetType4. +/// fz_hash_set!(let my_set_5: MySetType4, v); +/// +/// let v = vec![ +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// ]; +/// +/// // Declare a local set. This results in a local variable called my_set_6 of an unknown type. +/// let my_set_6 = fz_hash_set!(v); +/// +/// // no matter how the sets are declared, no matter the type selected to implement the set, +/// // they all work the same way and have the same API surface and implement the `Set` trait. +/// +/// assert!( +/// MY_SET_0.contains(&Key { +/// name: "Alice", +/// age: 30 +/// }) +/// ); +/// +/// assert!( +/// MY_SET_0.contains(&Key { +/// name: "Bob", +/// age: 40 +/// }) +/// ); +/// +/// assert!( +/// !MY_SET_0.contains(&Key { +/// name: "Fred", +/// age: 50 +/// }) +/// ); +/// } +/// +/// fn main() { +/// variables(); +/// } +/// ``` +pub use frozen_collections_macros::fz_hash_set; + +/// Creates an efficient map with a fixed set of ordered keys. +/// +/// The concrete type used to implement the map is based on an analysis of the input you +/// provide. Although the types vary, they all implement the [`Map`] trait, so refer to the +/// trait for API documentation. +/// +/// # Alternate Choices +/// +/// If your keys are integers or enum variants, you should use the [`fz_scalar_map!`] macro instead. +/// If your keys are strings, you should use the [`fz_string_map`] macro instead. Both of these will +/// deliver better performance since they are specifically optimized for those key types. +/// +/// # Example +/// +/// ``` +/// use frozen_collections::*; +/// +/// // The key type we use to index into our example maps +/// #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)] +/// struct Key { +/// pub name: &'static str, +/// pub age: i32, +/// } +/// +/// // Declare a global static map. This results in a static variable called MY_MAP_0 of type MyMapType0. +/// fz_ordered_map!(static MY_MAP_0: MyMapType0, { +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// fn variables() { +/// // Declare a local static map. This results in a local variable called MY_MAP_1 of type MyMapType1. +/// fz_ordered_map!(static MY_MAP_1: MyMapType1, { +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// // Declare a local map. This results in a local variable called my_map_2 of type MyMapType2. +/// fz_ordered_map!(let my_map_2: MyMapType2, { +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// // Declare a mutable local map. This results in a local variable called my_map_3 of type MyMapType3. +/// fz_ordered_map!(let mut my_map_3: MyMapType3, { +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// // Declare a local map. This results in a local variable called my_map_4 of an unknown type. +/// let my_map_4 = fz_ordered_map!({ +/// Key { name: "Alice", age: 30}: 1, +/// Key { name: "Bob", age: 40}: 2, +/// }); +/// +/// let v = vec![ +/// (Key { name: "Alice", age: 30}, 1), +/// (Key { name: "Bob", age: 40}, 2), +/// ]; +/// +/// // Declare a local map. This results in a local variable called my_map_5 of type MyMapType4. +/// fz_ordered_map!(let my_map_5: MyMapType4, v); +/// +/// let v = vec![ +/// (Key { name: "Alice", age: 30}, 1), +/// (Key { name: "Bob", age: 40}, 2), +/// ]; +/// +/// // Declare a local map. This results in a local variable called my_map_6 of an unknown type. +/// let my_map_6 = fz_ordered_map!(v); +/// +/// // no matter how the maps are declared, no matter the type selected to implement the map, +/// // they all work the same way and have the same API surface and implement the `Map` trait. +/// +/// assert_eq!( +/// Some(&1), +/// MY_MAP_0.get(&Key { +/// name: "Alice", +/// age: 30 +/// }) +/// ); +/// +/// assert_eq!( +/// Some(&2), +/// MY_MAP_0.get(&Key { +/// name: "Bob", +/// age: 40 +/// }) +/// ); +/// +/// assert_eq!( +/// None, +/// MY_MAP_0.get(&Key { +/// name: "Fred", +/// age: 50 +/// }) +/// ); +/// } +/// +/// // How to embed a map into a struct using the map's type. +/// struct MyStruct0 { +/// map: MyMapType0, +/// } +/// +/// // how to embed a map into a struct using the `Map` trait. +/// struct MyStruct1 +/// where +/// M: Map, +/// { +/// map: M, +/// } +/// +/// // You may need to have specific instances of a map in your process where the keys are the same, but +/// // the values differ. For that case, you can create a static instance with placeholder values. +/// // Then you clone this static instance when needed and set the values that you need in each case. +/// // So you'll have a single set of keys, but different values in each map instance. +/// fn structs() { +/// let mut ms0 = MyStruct0 { +/// map: MY_MAP_0.clone(), +/// }; +/// +/// let mut ms1 = MyStruct1 { +/// map: MY_MAP_0.clone(), +/// }; +/// +/// // set a custom value in ms0 +/// if let Some(v) = ms0.map.get_mut(&Key { +/// name: "Alice", +/// age: 30, +/// }) { +/// *v = 3; +/// } +/// +/// // set a different custom value in ms1 +/// if let Some(v) = ms1.map.get_mut(&Key { +/// name: "Alice", +/// age: 30, +/// }) { +/// *v = 4; +/// } +/// +/// assert_eq!( +/// Some(&3), +/// ms0.map.get(&Key { +/// name: "Alice", +/// age: 30 +/// }) +/// ); +/// +/// assert_eq!( +/// Some(&4), +/// ms1.map.get(&Key { +/// name: "Alice", +/// age: 30 +/// }) +/// ); +/// } +/// +/// fn main() { +/// variables(); +/// structs(); +/// } +/// ``` +pub use frozen_collections_macros::fz_ordered_map; + +/// Creates an efficient set with a fixed set of ordered values. +/// +/// The concrete type used to implement the set is based on an analysis of the input you +/// provide. Although the types vary, they all implement the [`Set`] trait, so refer to the +/// trait for API documentation. +/// +/// # Alternate Choices +/// +/// If your values are integers or enum variants, you should use the [`fz_scalar_set!`] macro instead. +/// If your values are strings, you should use the [`fz_string_set`] macro instead. Both of these will +/// deliver better performance since they are specifically optimized for those value types. +/// +/// # Example +/// +/// ``` +/// use frozen_collections::*; +/// +/// // The value type we use to index into our example sets +/// #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)] +/// struct Key { +/// pub name: &'static str, +/// pub age: i32, +/// } +/// +/// // Declare a global static set. This results in a static variable called MY_SET_0 of type MySetType0. +/// fz_ordered_set!(static MY_SET_0: MySetType0, { +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// fn variables() { +/// // Declare a local static set. This results in a local variable called MY_SET_1 of type MySetType1. +/// fz_ordered_set!(static MY_SET_1: MySetType1, { +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// // Declare a local set. This results in a local variable called my_set_2 of type MySetType2. +/// fz_ordered_set!(let my_set_2: MySetType2, { +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// // Declare a mutable local set. This results in a local variable called my_set_3 of type MySetType3. +/// fz_ordered_set!(let mut my_set_3: MySetType3, { +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// // Declare a local set. This results in a local variable called my_set_4 of an unknown type. +/// let my_set_4 = fz_ordered_set!({ +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// }); +/// +/// let v = vec![ +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// ]; +/// +/// // Declare a local set. This results in a local variable called my_set_5 of type MySetType4. +/// fz_ordered_set!(let my_set_5: MySetType4, v); +/// +/// let v = vec![ +/// Key { name: "Alice", age: 30}, +/// Key { name: "Bob", age: 40}, +/// ]; +/// +/// // Declare a local set. This results in a local variable called my_set_6 of an unknown type. +/// let my_set_6 = fz_ordered_set!(v); +/// +/// // no matter how the sets are declared, no matter the type selected to implement the set, +/// // they all work the same way and have the same API surface and implement the `Set` trait. +/// +/// assert!( +/// MY_SET_0.contains(&Key { +/// name: "Alice", +/// age: 30 +/// }) +/// ); +/// +/// assert!( +/// MY_SET_0.contains(&Key { +/// name: "Bob", +/// age: 40 +/// }) +/// ); +/// +/// assert!( +/// !MY_SET_0.contains(&Key { +/// name: "Fred", +/// age: 50 +/// }) +/// ); +/// } +/// +/// fn main() { +/// variables(); +/// } +/// ``` +pub use frozen_collections_macros::fz_ordered_set; + +/// Creates an efficient map with a fixed set of scalar keys. +/// +/// The concrete type used to implement the map is based on an analysis of the input you +/// provide. Although the types vary, they all implement the [`Map`] trait, so refer to the +/// trait for API documentation. +/// +/// # Example +/// +/// ``` +/// use frozen_collections::*; +/// +/// // The enum type we use to index into our example maps +/// #[derive(Scalar, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Debug)] +/// enum Person { +/// Alice, +/// Bob, +/// Fred, +/// } +/// +/// // Declare a global static map. This results in a static variable called MY_MAP_0 of type MyMapType0. +/// fz_scalar_map!(static MY_MAP_0: MyMapType0, { +/// Person::Alice: 1, +/// Person::Bob: 2, +/// }); +/// +/// // Scalar maps can also be used with any integer type as key. +/// // +/// // This declares a global static map. This results in a static variable called MY_INT_MAP of type MyIntMapType. +/// fz_scalar_map!(static MY_INT_MAP: MyIntMapType, { +/// 10: 1, +/// 20: 2, +/// }); +/// +/// fn variables() { +/// // Declare a local static map. This results in a local variable called MY_MAP_1 of type MyMapType1. +/// fz_scalar_map!(static MY_MAP_1: MyMapType1, { +/// Person::Alice: 1, +/// Person::Bob: 2, +/// }); +/// +/// // Declare a local map. This results in a local variable called my_map_2 of type MyMapType2. +/// fz_scalar_map!(let my_map_2: MyMapType2, { +/// Person::Alice: 1, +/// Person::Bob: 2, +/// }); +/// +/// // Declare a mutable local map. This results in a local variable called my_map_3 of type MyMapType3. +/// fz_scalar_map!(let mut my_map_3: MyMapType3, { +/// Person::Alice: 1, +/// Person::Bob: 2, +/// }); +/// +/// // Declare a local map. This results in a local variable called my_map_4 of an unknown type. +/// let my_map_4 = fz_scalar_map!({ +/// Person::Alice: 1, +/// Person::Bob: 2, +/// }); +/// +/// let v = vec![ +/// (Person::Alice, 1), +/// (Person::Bob, 2), +/// ]; +/// +/// // Declare a local map. This results in a local variable called my_map_5 of type MyMapType4. +/// fz_scalar_map!(let my_map_5: MyMapType4, v); +/// +/// let v = vec![ +/// (Person::Alice, 1), +/// (Person::Bob, 2), +/// ]; +/// +/// // Declare a local map. This results in a local variable called my_map_6 of an unknown type. +/// let my_map_6 = fz_scalar_map!(v); +/// +/// // no matter how the maps are declared, no matter the type selected to implement the map, +/// // they all work the same way and have the same API surface and implement the `Map` trait. +/// +/// assert_eq!(Some(&1), MY_MAP_0.get(&Person::Alice)); +/// assert_eq!(Some(&2), MY_MAP_0.get(&Person::Bob)); +/// assert_eq!(None, MY_MAP_0.get(&Person::Fred)); +/// } +/// +/// // How to embed a map into a struct using the map's type. +/// struct MyStruct0 { +/// map: MyMapType0, +/// } +/// +/// // how to embed a map into a struct using the `Map` trait. +/// struct MyStruct1 +/// where +/// M: Map, +/// { +/// map: M, +/// } +/// +/// // You may need to have specific instances of a map in your process where the keys are the same, but +/// // the values differ. For that case, you can create a static instance with placeholder values. +/// // Then you clone this static instance when needed and set the values that you need in each case. +/// // So you'll have a single set of keys, but different values in each map instance. +/// fn structs() { +/// let mut ms0 = MyStruct0 { +/// map: MY_MAP_0.clone(), +/// }; +/// +/// let mut ms1 = MyStruct1 { +/// map: MY_MAP_0.clone(), +/// }; +/// +/// // set a custom value in ms0 +/// if let Some(v) = ms0.map.get_mut(&Person::Alice) { +/// *v = 3; +/// } +/// +/// // set a different custom value in ms1 +/// if let Some(v) = ms1.map.get_mut(&Person::Alice) { +/// *v = 4; +/// } +/// +/// assert_eq!( +/// Some(&3), +/// ms0.map.get(&Person::Alice) +/// ); +/// +/// assert_eq!( +/// Some(&4), +/// ms1.map.get(&Person::Alice) +/// ); +/// } +/// +/// fn main() { +/// variables(); +/// structs(); +/// } +/// ``` +pub use frozen_collections_macros::fz_scalar_map; + +/// Creates an efficient set with a fixed set of scalar values. +/// +/// The concrete type used to implement the set is based on an analysis of the input you +/// provide. Although the types vary, they all implement the [`Set`] trait, so refer to the +/// trait for API documentation. +/// +/// # Example +/// +/// ``` +/// use frozen_collections::*; +/// +/// // The enum type we use to index into our example sets +/// #[derive(Scalar, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Debug)] +/// enum Person { +/// Alice, +/// Bob, +/// Fred, +/// } +/// +/// // Declare a global static set. This results in a static variable called MY_SET_0 of type MySetType0. +/// fz_scalar_set!(static MY_SET_0: MySetType0, { +/// Person::Alice, +/// Person::Bob, +/// }); +/// +/// // Scalar sets can also be used with any integer type as value. +/// // +/// // This declares a global static set. This results in a static variable called MY_INT_SET of type MyIntSetType. +/// fz_scalar_set!(static MY_INT_SET: MyIntMapType, { +/// 10, +/// 20, +/// }); +/// +/// fn variables() { +/// // Declare a local static set. This results in a local variable called MY_SET_1 of type MySetType1. +/// fz_scalar_set!(static MY_SET_1: MySetType1, { +/// Person::Alice, +/// Person::Bob, +/// }); +/// +/// // Declare a local set. This results in a local variable called my_set_2 of type MySetType2. +/// fz_scalar_set!(let my_set_2: MySetType2, { +/// Person::Alice, +/// Person::Bob, +/// }); +/// +/// // Declare a mutable local set. This results in a local variable called my_set_3 of type MySetType3. +/// fz_scalar_set!(let mut my_set_3: MySetType3, { +/// Person::Alice, +/// Person::Bob, +/// }); +/// +/// // Declare a local set. This results in a local variable called my_set_4 of an unknown type. +/// let my_set_4 = fz_scalar_set!({ +/// Person::Alice, +/// Person::Bob, +/// }); +/// +/// let v = vec![ +/// Person::Alice, +/// Person::Bob, +/// ]; +/// +/// // Declare a local set. This results in a local variable called my_set_5 of type MySetType4. +/// fz_scalar_set!(let my_set_5: MySetType4, v); +/// +/// let v = vec![ +/// Person::Alice, +/// Person::Bob, +/// ]; +/// +/// // Declare a local set. This results in a local variable called my_set_6 of an unknown type. +/// let my_set_6 = fz_scalar_set!(v); +/// +/// // no matter how the sets are declared, no matter the type selected to implement the set, +/// // they all work the same way and have the same API surface and implement the `Set` trait. +/// +/// assert!(MY_SET_0.contains(&Person::Alice)); +/// assert!(MY_SET_0.contains(&Person::Bob)); +/// assert!(!MY_SET_0.contains(&Person::Fred)); +/// } +/// +/// fn main() { +/// variables(); +/// } +/// ``` +pub use frozen_collections_macros::fz_scalar_set; + +/// Creates an efficient map with a fixed set of string keys. +/// +/// The concrete type used to implement the map is based on an analysis of the input you +/// provide. Although the types vary, they all implement the [`Map`] trait, so refer to the +/// trait for API documentation. +/// +/// # Example +/// +/// ``` +/// use frozen_collections::*; +/// +/// // Declare a global static map. This results in a static variable called MY_MAP_0 of type MyMapType0. +/// fz_string_map!(static MY_MAP_0: MyMapType0<&str, i32>, { +/// "Alice": 1, +/// "Bob": 2, +/// }); +/// +/// fn variables() { +/// // Declare a local static map. This results in a local variable called MY_MAP_1 of type MyMapType1. +/// fz_string_map!(static MY_MAP_1: MyMapType1<&str, i32>, { +/// "Alice": 1, +/// "Bob": 2, +/// }); +/// +/// // Declare a local map. This results in a local variable called my_map_2 of type MyMapType2. +/// fz_string_map!(let my_map_2: MyMapType2<&str, i32>, { +/// "Alice": 1, +/// "Bob": 2, +/// }); +/// +/// // Declare a mutable local map. This results in a local variable called my_map_3 of type MyMapType3. +/// fz_string_map!(let mut my_map_3: MyMapType3<&str, i32>, { +/// "Alice": 1, +/// "Bob": 2, +/// }); +/// +/// // Declare a local map. This results in a local variable called my_map_4 of an unknown type. +/// let my_map_4 = fz_string_map!({ +/// "Alice": 1, +/// "Bob": 2, +/// }); +/// +/// let v = vec![ +/// ("Alice", 1), +/// ("Bob", 2), +/// ]; +/// +/// // Declare a local map. This results in a local variable called my_map_5 of type MyMapType4. +/// fz_string_map!(let my_map_5: MyMapType4<&str, i32>, v); +/// +/// let v = vec![ +/// ("Alice", 1), +/// ("Bob", 2), +/// ]; +/// +/// // Declare a local map. This results in a local variable called my_map_6 of an unknown type. +/// let my_map_6 = fz_string_map!(v); +/// +/// let _ = my_map_5; +/// let _ = my_map_6; +/// +/// // no matter how the maps are declared, no matter the type selected to implement the map, +/// // they all work the same way and have the same API surface and implement the `Map` trait. +/// +/// assert_eq!( +/// Some(&1), +/// MY_MAP_0.get("Alice") +/// ); +/// +/// assert_eq!( +/// Some(&2), +/// MY_MAP_0.get("Bob") +/// ); +/// +/// assert_eq!( +/// None, +/// MY_MAP_0.get("Fred") +/// ); +/// } +/// +/// // How to embed a map into a struct using the map's type. +/// struct MyStruct0 { +/// map: MyMapType0, +/// } +/// +/// // how to embed a map into a struct using the `Map` trait. +/// struct MyStruct1 +/// where +/// M: Map<&'static str, i32>, +/// { +/// map: M, +/// } +/// +/// // You may need to have specific instances of a map in your process where the keys are the same, but +/// // the values differ. For that case, you can create a static instance with placeholder values. +/// // Then you clone this static instance when needed and set the values that you need in each case. +/// // So you'll have a single set of keys, but different values in each map instance. +/// fn structs() { +/// let mut ms0 = MyStruct0 { +/// map: MY_MAP_0.clone(), +/// }; +/// +/// let mut ms1 = MyStruct1 { +/// map: MY_MAP_0.clone(), +/// }; +/// +/// // set a custom value in ms0 +/// if let Some(v) = ms0.map.get_mut("Alice") { +/// *v = 3; +/// } +/// +/// // set a different custom value in ms1 +/// if let Some(v) = ms1.map.get_mut("Alice") { +/// *v = 4; +/// } +/// +/// assert_eq!( +/// Some(&3), +/// ms0.map.get(&"Alice") +/// ); +/// +/// assert_eq!( +/// Some(&4), +/// ms1.map.get("Alice") +/// ); +/// } +/// +/// fn main() { +/// variables(); +/// structs(); +/// } +/// ``` +pub use frozen_collections_macros::fz_string_map; + +/// Creates an efficient set with a fixed set of string values. +/// +/// The concrete type used to implement the set is based on an analysis of the input you +/// provide. Although the types vary, they all implement the [`Set`] trait, so refer to the +/// trait for API documentation. +/// +/// # Example +/// +/// ``` +/// use frozen_collections::*; +/// +/// // This example shows the various uses of a frozen set whose values are strings. +/// +/// // Declare a global static set. This results in a static variable called MY_SET_0 of type MySetType0. +/// fz_string_set!(static MY_SET_0: MySetType0<&str>, { +/// "Alice", +/// "Bob", +/// }); +/// +/// fn variables() { +/// // Declare a local static set. This results in a local variable called MY_SET_1 of type MySetType1. +/// fz_string_set!(static MY_SET_1: MySetType1<&str>, { +/// "Alice", +/// "Bob", +/// }); +/// +/// // Declare a local set. This results in a local variable called my_set_2 of type MySetType2. +/// fz_string_set!(let my_set_2: MySetType2<&str>, { +/// "Alice", +/// "Bob", +/// }); +/// +/// // Declare a mutable local set. This results in a local variable called my_set_3 of type MySetType3. +/// fz_string_set!(let mut my_set_3: MySetType3<&str>, { +/// "Alice", +/// "Bob", +/// }); +/// +/// // Declare a local set. This results in a local variable called my_set_4 of an unknown type. +/// let my_set_4 = fz_string_set!({ +/// "Alice", +/// "Bob", +/// }); +/// +/// let v = vec![ +/// "Alice", +/// "Bob", +/// ]; +/// +/// // Declare a local set. This results in a local variable called my_set_5 of type MySetType4. +/// fz_string_set!(let my_set_5: MySetType4<&str>, v); +/// +/// let v = vec![ +/// "Alice", +/// "Bob", +/// ]; +/// +/// // Declare a local set. This results in a local variable called my_set_6 of an unknown type. +/// let my_set_6 = fz_string_set!(v); +/// +/// let _ = my_set_5; +/// let _ = my_set_6; +/// +/// // no matter how the sets are declared, no matter the type selected to implement the set, +/// // they all work the same way and have the same API surface and implement the `Set` trait. +/// +/// assert!( +/// MY_SET_0.contains("Alice")); +/// +/// assert!( +/// MY_SET_0.contains("Bob") +/// ); +/// +/// assert!( +/// !MY_SET_0.contains("Fred") +/// ); +/// } +/// +/// fn main() { +/// variables(); +/// } +/// ``` +pub use frozen_collections_macros::fz_string_set; + +/// Implements the `Scalar` trait for an enum. +/// +/// Implementing the `Scalar` trait for an enum allows you to use the enum with the [`fz_scalar_map`] +/// and [`fz_scalar_set`] macros. The `Scalar` macro can only be used with enums that only include +/// unit variants without explicit discriminants. +pub use frozen_collections_macros::Scalar; + +#[doc(hidden)] +pub mod sets { + pub use frozen_collections_core::sets::*; +} + +#[doc(hidden)] +pub mod maps { + pub use frozen_collections_core::maps::*; +} + +#[doc(hidden)] +pub mod inline_maps { + pub use frozen_collections_core::inline_maps::*; +} + +#[doc(hidden)] +pub mod inline_sets { + pub use frozen_collections_core::inline_sets::*; +} + +#[doc(hidden)] +pub mod facade_maps { + pub use frozen_collections_core::facade_maps::*; +} + +#[doc(hidden)] +pub mod facade_sets { + pub use frozen_collections_core::facade_sets::*; +} + +#[doc(hidden)] +pub mod hashers { + pub use frozen_collections_core::hashers::*; +} + +#[doc(hidden)] +pub mod hash_tables { + pub use frozen_collections_core::hash_tables::*; +} + +#[doc(hidden)] +pub mod ahash { + pub use ahash::RandomState; +} diff --git a/frozen-collections/tests/common/map_testing.rs b/frozen-collections/tests/common/map_testing.rs new file mode 100644 index 0000000..f2d9f79 --- /dev/null +++ b/frozen-collections/tests/common/map_testing.rs @@ -0,0 +1,199 @@ +use core::fmt::Debug; +use frozen_collections_core::traits::Map; +use std::collections::HashMap as StdHashMap; +use std::collections::HashSet as StdHashSet; +use std::hash::Hash; +use std::ops::Index; + +pub fn test_map( + map: &MT, + reference: &StdHashMap, + other: &StdHashMap, +) where + K: Hash + Eq + Clone + Debug + Default, + V: Hash + Eq + Clone + Debug + Default, + MT: Map + Debug + Clone + Eq, +{ + assert_same(map, reference); + + let formatted_map = format!("{map:?}"); + for key in map.keys() { + let key_str = format!("{key:?}"); + assert!(formatted_map.contains(&key_str)); + } + + let m2 = map.clone(); + let r2 = reference.clone(); + assert_same(&m2, &r2); + + let v1: StdHashMap = map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let v2: StdHashMap = reference + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + assert_eq!(v1, v2); + + let v1: StdHashMap = map + .clone() + .iter_mut() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let v2: StdHashMap = reference + .clone() + .iter_mut() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + assert_eq!(v1, v2); + + let v1: StdHashMap = map.clone().into_iter().collect(); + let v2: StdHashMap = reference.clone().into_iter().collect(); + assert_eq!(v1, v2); + + let v1: StdHashSet<&K> = map.keys().collect(); + let v2: StdHashSet<&K> = reference.keys().collect(); + assert_eq!(v1, v2); + + let v1: StdHashSet = map.clone().into_keys().collect(); + let v2: StdHashSet = reference.clone().into_keys().collect(); + assert_eq!(v1, v2); + + let v1: StdHashSet<&V> = map.values().collect(); + let v2: StdHashSet<&V> = reference.values().collect(); + assert_eq!(v1, v2); + + let v1: StdHashSet = map.clone().values_mut().map(|v| v.clone()).collect(); + let v2: StdHashSet = reference.clone().values_mut().map(|v| v.clone()).collect(); + assert_eq!(v1, v2); + + let v1: StdHashSet = map.clone().into_values().collect(); + let v2: StdHashSet = reference.clone().into_values().collect(); + assert_eq!(v1, v2); + + for pair in other { + assert_eq!(map.contains_key(pair.0), reference.contains_key(pair.0)); + assert_eq!(map.get(pair.0), reference.get(pair.0)); + assert_eq!(map.get_key_value(pair.0), reference.get_key_value(pair.0)); + assert_eq!( + map.clone().get_mut(pair.0), + reference.clone().get_mut(pair.0) + ); + } + + if map.len() >= 2 { + let keys: Vec<_> = map.keys().collect(); + let mut cloned_map = map.clone(); + let values_from_map = cloned_map.get_many_mut([keys[0], keys[1]]).unwrap(); + + assert_eq!(values_from_map[0], &reference[keys[0]]); + assert_eq!(values_from_map[1], &reference[keys[1]]); + + let mut cloned_map = map.clone(); + let r = cloned_map.get_many_mut([keys[0], keys[0]]); + assert!(r.is_none()); + } +} + +pub fn test_map_default() +where + K: Hash + Eq + Clone + Debug + Default, + MT: Map + Debug + Clone + Default + Eq, +{ + let m = MT::default(); + let r = StdHashMap::default(); + assert_same(&m, &r); + assert!(!m.contains_key(&K::default())); + assert_eq!(0, m.len()); + assert!(m.is_empty()); +} + +pub fn test_map_ops<'a, MT, K, V>(map: &'a MT, reference: &'a StdHashMap) +where + K: 'a + Hash + Eq, + V: 'a + Hash + Eq + Debug, + MT: 'a + + Map + + Debug + + Clone + + PartialEq> + + Index<&'a K, Output = V>, +{ + assert!(map.eq(reference)); + + if !map.is_empty() { + assert!(!map.eq(&StdHashMap::default())); + + for pair in reference { + assert_eq!(&map[pair.0], pair.1); + } + } +} + +pub fn test_map_iter<'a, MT, K, V>(map: &'a MT, reference: &'a StdHashMap) +where + K: 'a + Hash + Eq + Clone + Debug, + V: Eq, + MT: 'a + Map + Debug + Clone, + &'a MT: IntoIterator, +{ + // operates on an &MT + assert_eq!(map.len(), map.iter().count()); + for pair in map.iter() { + assert!(reference.contains_key(pair.0)); + } + + // operates on an &MT + assert_eq!(map.len(), map.into_iter().count()); + for pair in map { + assert!(reference.contains_key(pair.0)); + } + + // operates on an MT + assert_eq!(map.len(), map.clone().into_iter().count()); + for pair in map.clone() { + assert!(map.contains_key(&pair.0)); + } +} + +pub fn test_map_iter_mut<'a, MT, K, V>( + map: &'a mut MT, + reference: &'a StdHashMap, +) where + K: 'a + Hash + Eq + Clone + Debug, + V: Eq, + MT: 'a + Map + Debug + Clone, + &'a mut MT: IntoIterator, +{ + // operates on a &mut MT + for pair in map { + assert!(reference.contains_key(pair.0)); + } +} + +pub fn test_map_empty(map: &MT) +where + MT: Map, + K: Default, +{ + assert_eq!(0, map.len()); + assert!(map.is_empty()); + assert_eq!(0, map.iter().count()); + assert!(!map.contains_key(&K::default())); +} + +fn assert_same(map: &MT, reference: &StdHashMap) +where + K: Hash + Eq + Clone + Debug, + V: Clone + Eq + Debug, + MT: Map + Clone + IntoIterator, +{ + assert_eq!(map.len(), reference.len()); + assert_eq!(map.is_empty(), reference.is_empty()); + + for pair in reference { + assert!(map.contains_key(pair.0)); + } + + for pair in map.iter() { + assert!(reference.contains_key(pair.0)); + } +} diff --git a/frozen-collections/tests/common/mod.rs b/frozen-collections/tests/common/mod.rs new file mode 100644 index 0000000..1726566 --- /dev/null +++ b/frozen-collections/tests/common/mod.rs @@ -0,0 +1,5 @@ +pub use map_testing::*; +pub use set_testing::*; + +mod map_testing; +mod set_testing; diff --git a/frozen-collections/tests/common/set_testing.rs b/frozen-collections/tests/common/set_testing.rs new file mode 100644 index 0000000..5375d7f --- /dev/null +++ b/frozen-collections/tests/common/set_testing.rs @@ -0,0 +1,157 @@ +use core::fmt::Debug; +use core::hash::Hash; +use core::ops::{BitAnd, BitOr, BitXor, Sub}; +use frozen_collections::Set; +use frozen_collections_core::traits::SetOps; +use hashbrown::HashSet as HashbrownSet; + +pub fn test_set(set: &ST, reference: &HashbrownSet, other: &HashbrownSet) +where + T: Hash + Eq + Clone + Debug + Default, + ST: Set + Debug + Clone, +{ + assert_same(set, reference); + + let s2: HashbrownSet<&T> = set.symmetric_difference(other).collect(); + let r2: HashbrownSet<&T> = reference.symmetric_difference(other).collect(); + assert_same(&s2, &r2); + + let s2: HashbrownSet<&T> = set.difference(other).collect(); + let r2: HashbrownSet<&T> = reference.difference(other).collect(); + assert_same(&s2, &r2); + + let s2: HashbrownSet<&T> = set.union(other).collect(); + let r2: HashbrownSet<&T> = reference.union(other).collect(); + assert_same(&s2, &r2); + + let s2: HashbrownSet<&T> = set.intersection(other).collect(); + let r2: HashbrownSet<&T> = reference.intersection(other).collect(); + assert_same(&s2, &r2); + + assert_eq!(set.is_disjoint(other), reference.is_disjoint(other)); + assert_eq!(set.is_subset(other), reference.is_subset(other)); + assert_eq!(set.is_superset(other), reference.is_superset(other)); + + let formatted_set = format!("{set:?}"); + for value in set.iter() { + let value_str = format!("{value:?}"); + assert!(formatted_set.contains(&value_str)); + } + + let s2 = set.clone(); + let r2 = reference.clone(); + assert_same(&s2, &r2); + + let s2 = set.clone(); + let mut r2 = reference.clone(); + for value in s2 { + r2.remove(&value); + } + + assert!(r2.is_empty()); +} + +pub fn test_set_default() +where + T: Hash + Eq + Clone + Debug + Default, + ST: Set + Debug + Clone + Default, +{ + let s = ST::default(); + let r = HashbrownSet::default(); + assert_same(&s, &r); + assert!(!s.contains(&T::default())); + assert_eq!(0, s.len()); + assert!(s.is_empty()); +} + +pub fn test_set_ops<'a, ST, T>( + set: &'a ST, + reference: &'a HashbrownSet, + other: &'a HashbrownSet, +) where + T: 'a + Hash + Eq + Clone + Debug, + ST: 'a + Set + Debug + Clone + PartialEq>, + &'a ST: BitOr<&'a HashbrownSet, Output = HashbrownSet> + + BitAnd<&'a HashbrownSet, Output = HashbrownSet> + + BitXor<&'a HashbrownSet, Output = HashbrownSet> + + Sub<&'a HashbrownSet, Output = HashbrownSet>, +{ + assert!(set.eq(reference)); + + if !set.is_empty() { + assert!(!set.eq(&HashbrownSet::default())); + } + + let s2 = set.bitor(other); + let r2 = reference.bitor(other); + assert_eq!(&s2, &r2); + assert!(s2.eq(&r2)); + + let s2 = set.bitand(other); + let r2 = reference.bitand(other); + assert_eq!(s2, r2); + assert!(s2.eq(&r2)); + + let s2 = set.bitxor(other); + let r2 = reference.bitxor(other); + assert_eq!(s2, r2); + assert!(s2.eq(&r2)); + + let s2 = set.sub(other); + let r2 = reference.sub(other); + assert_eq!(s2, r2); + assert!(s2.eq(&r2)); +} + +pub fn test_set_iter<'a, ST, T>(set: &'a ST, reference: &'a HashbrownSet) +where + T: 'a + Hash + Eq + Clone + Debug, + ST: 'a + Set + Debug + Clone, + &'a ST: IntoIterator, +{ + // operates on an &ST + assert_eq!(set.len(), set.iter().count()); + for v in set.iter() { + assert!(reference.contains(v)); + } + + // operates on an &ST + assert_eq!(set.len(), set.into_iter().count()); + for v in set { + assert!(reference.contains(v)); + } + + // operates on an ST + assert_eq!(set.len(), set.clone().into_iter().count()); + for v in set.clone() { + assert!(set.contains(&v)); + } +} + +pub fn test_set_empty(set: &ST) +where + ST: Set, + T: Default, +{ + assert_eq!(0, set.len()); + assert!(set.is_empty()); + assert_eq!(0, set.iter().count()); + assert!(!set.contains(&T::default())); +} + +fn assert_same(set: &ST, reference: &HashbrownSet) +where + T: Hash + Eq + Clone, + ST: Set + Clone, +{ + assert_eq!(set.len(), reference.len()); + assert_eq!(set.is_empty(), reference.is_empty()); + + for value in reference { + assert!(set.contains(value)); + } + + for value in set.iter() { + assert!(reference.contains(value)); + } +} diff --git a/frozen-collections/tests/derive_scalar_macro_tests.rs b/frozen-collections/tests/derive_scalar_macro_tests.rs new file mode 100644 index 0000000..14e5e1d --- /dev/null +++ b/frozen-collections/tests/derive_scalar_macro_tests.rs @@ -0,0 +1,26 @@ +use frozen_collections::*; +use frozen_collections_core::macros::derive_scalar_macro; +use quote::quote; + +#[derive(Scalar, Copy, Ord, PartialOrd, Eq, PartialEq, Clone)] +enum Color { + Red, + Green, + Blue, +} + +#[test] +fn test_derive_scalar() { + _ = derive_scalar_macro(quote!( + enum Color { + Red, + Green, + Blue, + } + )) + .unwrap(); + + assert_eq!(0, Color::index(&Color::Red)); + assert_eq!(1, Color::index(&Color::Green)); + assert_eq!(2, Color::index(&Color::Blue)); +} diff --git a/frozen-collections/tests/hash_macro_tests.rs b/frozen-collections/tests/hash_macro_tests.rs new file mode 100644 index 0000000..da0b1e6 --- /dev/null +++ b/frozen-collections/tests/hash_macro_tests.rs @@ -0,0 +1,484 @@ +use frozen_collections::*; +use frozen_collections_core::macros::{fz_hash_map_macro, fz_hash_set_macro}; +use quote::quote; +use std::collections::HashMap as StdHashMap; +use std::collections::HashSet as StdHashSet; + +#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] +struct Person { + name: &'static str, + age: i32, +} + +macro_rules! test_hash { + ( $type:ty, $( $arg:expr ),* $(,)?) => { + { + _ = fz_hash_set_macro(quote!({ + $( + $arg, + )* + })).unwrap(); + + let s0 = fz_hash_set!({ + $( + $arg, + )* + }); + + let v = vec![ + $( + $arg, + )* + ]; + + _ = fz_hash_set_macro(quote!(v)).unwrap(); + + let s1 = fz_hash_set!(v); + + let v = vec![ + $( + $arg, + )* + ]; + + let mut s2 = StdHashSet::new(); + for x in v.into_iter() { + s2.insert(x); + } + + _ = fz_hash_set_macro(quote!(static _S3: Foo< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_hash_set!(static _S3: Foo< $type >, { + $( + $arg, + )* + }); + + _ = fz_hash_set_macro(quote!(let s4: Bar< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_hash_set!(let s4: Bar< $type >, { + $( + $arg, + )* + }); + + assert_eq!(s0, s1); + assert_eq!(s0, s2); + // assert_eq!(s0, S3); + assert_eq!(s0, s4); + } + + { + _ = fz_hash_map_macro(quote!({ + $( + $arg:42, + )* + })).unwrap(); + + let m0 = fz_hash_map!({ + $( + $arg: 42, + )* + }); + + let v = vec![ + $( + ($arg, 42), + )* + ]; + + _ = fz_hash_map_macro(quote!(v)).unwrap(); + + let m1 = fz_hash_map!(v); + + let v = vec![ + $( + ($arg, 42), + )* + ]; + + let mut m2 = StdHashMap::new(); + for x in v.into_iter() { + m2.insert(x.0, x.1); + } + + _ = fz_hash_map_macro(quote!(static _M3: Foo< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_hash_map!(static _M3: Foo< $type, i32 >, { + $( + $arg: 42, + )* + }); + + _ = fz_hash_map_macro(quote!(let m4: Bar< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_hash_map!(let m4: Bar< $type, i32 >, { + $( + $arg: 42, + )* + }); + + assert_eq!(m0, m1); + assert_eq!(m0, m2); + // assert_eq!(m0, M3); + assert_eq!(m0, m4); + } + } +} + +#[test] +fn hash_complex() { + test_hash!(Person, Person { name: "A", age: 1 },); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + Person { name: "K", age: 11 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + Person { name: "K", age: 11 }, + Person { name: "L", age: 12 }, + ); + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + Person { name: "K", age: 11 }, + Person { name: "L", age: 12 }, + Person { name: "M", age: 13 }, + ); + + // test duplicate logic + test_hash!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "A", age: 3 }, + Person { name: "A", age: 4 }, + ); +} + +#[test] +fn hash_i8() { + test_hash!(i8, 0i8); + test_hash!(i8, 0i8, 1,); + test_hash!(i8, 0i8, 1, 2,); + test_hash!(i8, 0i8, 1, 2, 3,); + test_hash!(i8, 0i8, 1, 2, 3, 4,); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5,); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5, 6,); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); + + // test duplicate logic + test_hash!(i8, 0i8, 1, 2, 1, 1); +} + +#[test] +fn hash_u8() { + test_hash!(u8, 0u8); + test_hash!(u8, 0u8, 1,); + test_hash!(u8, 0u8, 1, 2,); + test_hash!(u8, 0u8, 1, 2, 3,); + test_hash!(u8, 0u8, 1, 2, 3, 4,); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5,); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5, 6,); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_i16() { + test_hash!(i16, 0i16); + test_hash!(i16, 0i16, 1,); + test_hash!(i16, 0i16, 1, 2,); + test_hash!(i16, 0i16, 1, 2, 3,); + test_hash!(i16, 0i16, 1, 2, 3, 4,); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5,); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5, 6,); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_u16() { + test_hash!(u16, 0u16); + test_hash!(u16, 0u16, 1,); + test_hash!(u16, 0u16, 1, 2,); + test_hash!(u16, 0u16, 1, 2, 3,); + test_hash!(u16, 0u16, 1, 2, 3, 4,); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5,); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5, 6,); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_i32() { + test_hash!(i32, 0i32); + test_hash!(i32, 0i32, 1,); + test_hash!(i32, 0i32, 1, 2,); + test_hash!(i32, 0i32, 1, 2, 3,); + test_hash!(i32, 0i32, 1, 2, 3, 4,); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5,); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5, 6,); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_u32() { + test_hash!(u32, 0u32); + test_hash!(u32, 0u32, 1,); + test_hash!(u32, 0u32, 1, 2,); + test_hash!(u32, 0u32, 1, 2, 3,); + test_hash!(u32, 0u32, 1, 2, 3, 4,); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5,); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5, 6,); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_i64() { + test_hash!(i64, 0i64); + test_hash!(i64, 0i64, 1,); + test_hash!(i64, 0i64, 1, 2,); + test_hash!(i64, 0i64, 1, 2, 3,); + test_hash!(i64, 0i64, 1, 2, 3, 4,); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5,); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5, 6,); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_u64() { + test_hash!(u64, 0u64); + test_hash!(u64, 0u64, 1,); + test_hash!(u64, 0u64, 1, 2,); + test_hash!(u64, 0u64, 1, 2, 3,); + test_hash!(u64, 0u64, 1, 2, 3, 4,); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5,); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5, 6,); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_isize() { + test_hash!(isize, 0isize); + test_hash!(isize, 0isize, 1,); + test_hash!(isize, 0isize, 1, 2,); + test_hash!(isize, 0isize, 1, 2, 3,); + test_hash!(isize, 0isize, 1, 2, 3, 4,); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5,); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5, 6,); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_usize() { + test_hash!(usize, 0usize); + test_hash!(usize, 0usize, 1,); + test_hash!(usize, 0usize, 1, 2,); + test_hash!(usize, 0usize, 1, 2, 3,); + test_hash!(usize, 0usize, 1, 2, 3, 4,); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5,); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5, 6,); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7,); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8,); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_hash!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn hash_string() { + test_hash!(&str, "0"); + test_hash!(&str, "0", "1"); + test_hash!(&str, "0", "1", "2"); + test_hash!(&str, "0", "1", "2", "3"); + test_hash!(&str, "0", "1", "2", "3", "4"); + test_hash!(&str, "0", "1", "2", "3", "4", "5"); + test_hash!(&str, "0", "1", "2", "3", "4", "5", "6"); + test_hash!(&str, "0", "1", "2", "3", "4", "5", "6", "7"); + test_hash!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8"); + test_hash!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"); + test_hash!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); + test_hash!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"); + test_hash!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"); + test_hash!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"); +} diff --git a/frozen-collections/tests/omni_tests.rs b/frozen-collections/tests/omni_tests.rs new file mode 100644 index 0000000..be2dd04 --- /dev/null +++ b/frozen-collections/tests/omni_tests.rs @@ -0,0 +1,520 @@ +mod common; + +use common::*; +use frozen_collections::*; +use frozen_collections_core::facade_maps::*; +use frozen_collections_core::facade_sets::*; +use frozen_collections_core::hashers::BridgeHasher; +use frozen_collections_core::macros::fz_scalar_map_macro; +use frozen_collections_core::maps::*; +use frozen_collections_core::sets::*; +use hashbrown::HashSet as HashbrownSet; +use quote::quote; +use std::collections::HashMap as StdHashMap; + +macro_rules! test_str { + ( $( $input:expr ),* ; $( $other:literal ),*) => { + // handle &str cases + + let set_reference = HashbrownSet::<&str>::from_iter(vec![ $( $input, )* ].into_iter()); + let set_other = HashbrownSet::<&str>::from_iter(vec![ $( $other, )* ].into_iter()); + + let map_reference = StdHashMap::<_, _, ahash::RandomState>::from_iter(vec![ $( ($input, ()), )* ].into_iter()); + let map_other = StdHashMap::<_, _, ahash::RandomState>::from_iter(vec![ $( ($other, ()), )* ].into_iter()); + + let mut m = fz_string_map!({ $( $input: (),)* }); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = fz_string_set!({ $( $input,)* }); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + } +} + +macro_rules! test_all { + ( $( $input:expr ),* ; $( $other:literal ),*) => { + let set_reference = HashbrownSet::from_iter(vec![ $( $input, )* ].into_iter()); + let set_other = HashbrownSet::from_iter(vec![ $( $other, )* ].into_iter()); + let set_input = vec![ $($input,)* ]; + + let map_reference = StdHashMap::<_, _, ahash::RandomState>::from_iter(vec![ $( ($input, ()), )* ].into_iter()); + let map_other = StdHashMap::<_, _, ahash::RandomState>::from_iter(vec![ $( ($other, ()), )* ].into_iter()); + let map_input = vec![ $( ($input, ()), )* ]; + + let mut m = fz_scalar_map!({ $( $input: (), )* }); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = fz_scalar_set!({ $($input,)* }); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = fz_scalar_map!(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = fz_scalar_set!(set_input.clone()); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = fz_hash_map!({ $( $input: (), )* }); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = fz_hash_set!({ $($input,)* }); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = fz_hash_map!(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = fz_hash_set!(set_input.clone()); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = fz_ordered_map!({ $( $input: (), )* }); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = fz_ordered_set!({ $($input,)* }); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = fz_ordered_map!(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = fz_ordered_set!(set_input.clone()); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = EytzingerSearchMap::new(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = EytzingerSearchSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = BinarySearchMap::new(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = BinarySearchSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = OrderedScanMap::new(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = OrderedScanSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = ScanMap::new(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = ScanSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + if let Ok(mut m) = DenseScalarLookupMap::new(map_input.clone()) { + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = DenseScalarLookupSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + } + + let mut m = SparseScalarLookupMap::<_, _>::new(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = SparseScalarLookupSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = HashMap::<_, _>::new(map_input.clone(), BridgeHasher::default()).unwrap(); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = HashSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = FacadeOrderedMap::new(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = FacadeOrderedSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = FacadeScalarMap::new(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = FacadeScalarSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = FacadeHashMap::<_, _>::new(map_input.clone(), BridgeHasher::default()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = FacadeHashSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let m = std::collections::HashMap::<_, _, ahash::RandomState>::from_iter(map_input.clone().into_iter()); + test_map(&m, &map_reference, &map_other); + + let s = std::collections::HashSet::<_, ahash::RandomState>::from_iter(set_input.clone().into_iter()); + test_set(&s, &set_reference, &set_other); + + let m = std::collections::BTreeMap::from_iter(map_input.clone().into_iter()); + test_map(&m, &map_reference, &map_other); + + let s = std::collections::BTreeSet::from_iter(set_input.clone().into_iter()); + test_set(&s, &set_reference, &set_other); + + let m = hashbrown::HashMap::<_, _, ahash::RandomState>::from_iter(map_input.clone().into_iter()); + test_map(&m, &map_reference, &map_other); + + let s = hashbrown::HashSet::<_, ahash::RandomState>::from_iter(set_input.clone().into_iter()); + test_set(&s, &set_reference, &set_other); + + // handle String cases + + let set_reference = HashbrownSet::<&str>::from_iter(vec![ $( stringify!($input), )* ].into_iter()); + let set_other = HashbrownSet::<&str>::from_iter(vec![ $( stringify!($other), )* ].into_iter()); + let set_input = vec![ $( stringify!($input), )* ]; + + let map_reference = StdHashMap::<_, _, ahash::RandomState>::from_iter(vec![ $( (stringify!($input), ()), )* ].into_iter()); + let map_other = StdHashMap::<_, _, ahash::RandomState>::from_iter(vec![ $( (stringify!($other), ()), )* ].into_iter()); + let map_input = vec![ $( (stringify!($input), ()), )* ]; + + let mut m = fz_string_map!(map_input.clone()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = fz_string_set!(set_input.clone()); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let mut m = FacadeStringMap::new(map_input.clone(), ahash::RandomState::default()); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); + test_map_iter_mut(&mut m, &map_reference); + + let s = FacadeStringSet::new(m); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + } +} + +#[test] +#[allow(clippy::unreadable_literal)] +fn test_common() { + test_all!(1, 2, 3 ; 3, 4, 5); + test_all!(0, 1 ; 0, 1); + test_all!(3, 1, 2, 3, 3 ; 3, 4, 5); + test_all!(1, 2, 3 ; 1, 2, 3, 4, 5); + test_all!(1, 2, 3 ; 1, 2); + test_all!(1, 2, 3 ; 2); + test_all!(1, 2, 4 ; 2); + test_all!(1, 2, 4 ; 3); + test_all!(1, 2, 4, 1500 ; 3); + test_all!(1, 2, 4, 1500 ; 2500); + test_all!(1 ; 3); + test_all!(1, 2 ; 3); + test_all!(1, 2, 3 ; 3); + test_all!(1, 2, 3, 4 ; 3); + test_all!(1, 2, 3, 4, 5 ; 3); + test_all!(1, 2, 3, 4, 5, 6 ; 3); + test_all!(1, 2, 3, 4, 5, 6, 7 ; 3, 5); + test_all!(1, 2, 3, 4, 5, 6, 7, 8 ; 3); + test_all!(1, 2, 3, 4, 5, 6, 7, 8, 9 ; 3, 10); + test_all!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ; 3); + test_all!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ; 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20); + test_all!(11111, 11112, 11114, 11115, 111165, 111175 ; 2500, 333333333); + + // trigger the eytzinger facade code + test_all!( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, + 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, + 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, + 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, + 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, + 660, 661, 662, 663, 664, 665, 666, 667, 668, 669; 2500, 333333333); + + test_str!("1", "2", "3" ; "3", "4", "5"); + test_str!("0", "1" ; "0", "1"); + test_str!("3", "1", "2", "3", "3" ; "3", "4", "5"); + test_str!("1", "2", "3" ; "1", "2", "3", "4", "5"); + test_str!("1", "2", "3" ; "1", "2"); + test_str!("1", "2", "3" ; "2"); + test_str!("1", "2", "4" ; "2"); + test_str!("1", "2", "4" ; "3"); + test_str!("1", "2", "4", "1500" ; "3"); + test_str!("1", "2", "4", "1500" ; "2500"); + test_str!("1" ; "3"); + test_str!("1", "2" ; "3"); + test_str!("1", "2", "3" ; "3"); + test_str!("1", "2", "3", "4" ; "3"); + test_str!("1", "2", "3", "4", "5" ; "3"); + test_str!("1", "2", "3", "4", "5", "6" ; "3"); + test_str!("1", "2", "3", "4", "5", "6", "7" ; "3", "5"); + test_str!("1", "2", "3", "4", "5", "6", "7", "8" ; "3"); + test_str!("1", "2", "3", "4", "5", "6", "7", "8", "9" ; "3", "10"); + test_str!("1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ; "3"); + test_str!("1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ; "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "20"); + test_str!("11111", "11112", "11114", "11115", "111165", "111175" ; "2500", "333333333"); + test_str!("11111", "11112", "11114", "11115", "111165", "111175", "111185" ; "2500", "333333333"); + test_str!("1", "22", "333", "4444", "55555", "666666", "7777777" ; "2500", "333333333"); +} + +#[test] +fn test_set_defaults() { + test_set_default::, i32>(); + test_set_default::, i32>(); + test_set_default::, i32>(); + test_set_default::, i32>(); + test_set_default::, i32>(); + test_set_default::, i32>(); + + test_set_default::, i32>(); + test_set_default::, i32>(); + test_set_default::, i32>(); + test_set_default::, &str>(); +} + +#[test] +fn test_map_defaults() { + test_map_default::, i32>(); + test_map_default::, i32>(); + test_map_default::, i32>(); + test_map_default::, i32>(); + test_map_default::, i32>(); + test_map_default::, i32>(); + + test_map_default::, i32>(); + test_map_default::, i32>(); + test_map_default::, i32>(); + test_map_default::, &str>(); +} + +#[test] +fn test_set_empties() { + test_set_empty(&std::collections::HashSet::::default()); + test_set_empty(&std::collections::HashSet::::from_iter(vec![])); + + test_set_empty(&std::collections::BTreeSet::::default()); + test_set_empty(&std::collections::BTreeSet::::from_iter(vec![])); + + test_set_empty(&hashbrown::HashSet::::default()); + test_set_empty(&hashbrown::HashSet::::from_iter(vec![])); + + test_set_empty(&EytzingerSearchSet::::default()); + test_set_empty(&EytzingerSearchSet::::new(EytzingerSearchMap::new( + vec![], + ))); + + test_set_empty(&BinarySearchSet::::default()); + test_set_empty(&BinarySearchSet::::new(BinarySearchMap::new(vec![]))); + + test_set_empty(&OrderedScanSet::::default()); + test_set_empty(&OrderedScanSet::::new(OrderedScanMap::new(vec![]))); + + test_set_empty(&ScanSet::::default()); + test_set_empty(&ScanSet::::new(ScanMap::new(vec![]))); + + test_set_empty(&DenseScalarLookupSet::::default()); + test_set_empty(&DenseScalarLookupSet::::new( + DenseScalarLookupMap::new(vec![]).unwrap(), + )); + + test_set_empty(&SparseScalarLookupSet::::default()); + test_set_empty(&SparseScalarLookupSet::::new( + SparseScalarLookupMap::new(vec![]), + )); + + test_set_empty(&HashSet::::default()); + test_set_empty(&HashSet::::new( + HashMap::new(vec![], BridgeHasher::default()).unwrap(), + )); + + test_set_empty(&FacadeHashSet::::default()); + test_set_empty(&FacadeHashSet::::new(FacadeHashMap::new( + vec![], + BridgeHasher::default(), + ))); + + test_set_empty(&FacadeOrderedSet::::default()); + test_set_empty(&FacadeOrderedSet::::new(FacadeOrderedMap::new(vec![]))); + + test_set_empty(&FacadeScalarSet::::default()); + test_set_empty(&FacadeScalarSet::::new(FacadeScalarMap::new(vec![]))); + + test_set_empty(&FacadeStringSet::<&str, ahash::RandomState>::default()); + test_set_empty(&FacadeStringSet::new(FacadeStringMap::new( + vec![], + ahash::RandomState::default(), + ))); +} + +#[test] +fn test_map_empties() { + test_map_empty(&std::collections::HashMap::::default()); + test_map_empty(&std::collections::HashMap::::from_iter(vec![])); + + test_map_empty(&std::collections::BTreeMap::::default()); + test_map_empty(&std::collections::BTreeMap::::from_iter(vec![])); + + test_map_empty(&hashbrown::HashMap::::default()); + test_map_empty(&hashbrown::HashMap::::from_iter(vec![])); + + test_map_empty(&EytzingerSearchMap::::default()); + test_map_empty(&EytzingerSearchMap::::new(vec![])); + + test_map_empty(&BinarySearchMap::::default()); + test_map_empty(&BinarySearchMap::::new(vec![])); + + test_map_empty(&OrderedScanMap::::default()); + test_map_empty(&OrderedScanMap::::new(vec![])); + + test_map_empty(&ScanMap::::default()); + test_map_empty(&ScanMap::::new(vec![])); + + test_map_empty(&DenseScalarLookupMap::::default()); + test_map_empty(&DenseScalarLookupMap::::new(vec![]).unwrap()); + + test_map_empty(&SparseScalarLookupMap::::default()); + test_map_empty(&SparseScalarLookupMap::::new(vec![])); + + test_map_empty(&HashMap::::default()); + test_map_empty(&HashMap::::new(vec![], BridgeHasher::default()).unwrap()); + + test_map_empty(&FacadeHashMap::::default()); + test_map_empty(&FacadeHashMap::::new( + vec![], + BridgeHasher::default(), + )); + + test_map_empty(&FacadeOrderedMap::::default()); + test_map_empty(&FacadeOrderedMap::::new(vec![])); + + test_map_empty(&FacadeScalarMap::::default()); + test_map_empty(&FacadeScalarMap::::new(vec![])); + + test_map_empty(&FacadeStringMap::<&str, i32, ahash::RandomState>::default()); + test_map_empty(&FacadeStringMap::<&str, i32, ahash::RandomState>::new( + vec![], + ahash::RandomState::default(), + )); + + fz_hash_map!(let m: MyHashMap, {}); + test_map_empty(&m); + + fz_ordered_map!(let m: MyOrderedMap, {}); + test_map_empty(&m); + + fz_string_map!(let m: MyStringMap<&str, i32>, {}); + test_map_empty(&m); + + fz_scalar_map!(let m: MyScalarMap, {}); + test_map_empty(&m); +} + +#[test] +fn edge_cases() { + let b = "B"; + let set_reference = HashbrownSet::from_iter(vec!["A", b, "C"]); + let set_other = HashbrownSet::from_iter(vec!["A", b, "C"]); + + let s = fz_string_set!({ "A", b, "C", }); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let a = 1; + let b = 2; + let set_reference = HashbrownSet::from_iter(vec![a, b]); + let set_other = HashbrownSet::from_iter(vec![a, b]); + + let s = fz_scalar_set!({ a, b, }); + test_set(&s, &set_reference, &set_other); + test_set_ops(&s, &set_reference, &set_other); + test_set_iter(&s, &set_reference); + + let map_reference = StdHashMap::from_iter(vec![(a, 1), (b, 2), (32, 3), (42, 4), (55, 5)]); + let map_other = StdHashMap::from_iter(vec![(a, 2), (b, 3)]); + + _ = fz_scalar_map_macro(quote!({ a:1, b:2, 32: 3, 42: 4, 55: 5})); + let m = fz_scalar_map!({ a:1, b:2, 32: 3, 42: 4, 55: 5}); + test_map(&m, &map_reference, &map_other); + test_map_ops(&m, &map_reference); + test_map_iter(&m, &map_reference); +} diff --git a/frozen-collections/tests/ordered_macro_tests.rs b/frozen-collections/tests/ordered_macro_tests.rs new file mode 100644 index 0000000..a0240b0 --- /dev/null +++ b/frozen-collections/tests/ordered_macro_tests.rs @@ -0,0 +1,565 @@ +use frozen_collections::*; +use frozen_collections_core::macros::{fz_ordered_map_macro, fz_ordered_set_macro}; +use quote::quote; +use std::collections::BTreeMap as StdBTreeMap; +use std::collections::BTreeSet as StdBTreeSet; + +#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] +struct Person { + name: &'static str, + age: i32, +} + +macro_rules! test_ordered { + ( $type:ty, $( $arg:expr ),* $(,)?) => { + { + _ = fz_ordered_set_macro(quote!({ + $( + $arg, + )* + })).unwrap(); + + let s0 = fz_ordered_set!({ + $( + $arg, + )* + }); + + let v = vec![ + $( + $arg, + )* + ]; + + _ = fz_ordered_set_macro(quote!(v)).unwrap(); + + let s1 = fz_ordered_set!(v); + + let v = vec![ + $( + $arg, + )* + ]; + + let mut s2 = StdBTreeSet::new(); + for x in v.into_iter() { + s2.insert(x); + } + + _ = fz_ordered_set_macro(quote!(static _S3: Foo< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_ordered_set!(static _S3: Foo< $type >, { + $( + $arg, + )* + }); + + _ = fz_ordered_set_macro(quote!(let s4: Bar< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_ordered_set!(let s4: Bar< $type >, { + $( + $arg, + )* + }); + + assert_eq!(s0, s1); + assert_eq!(s0, s2); + // assert_eq!(s0, S3); + assert_eq!(s0, s4); + } + + { + _ = fz_ordered_map_macro(quote!({ + $( + $arg: 42, + )* + })).unwrap(); + + let m0 = fz_ordered_map!({ + $( + $arg: 42, + )* + }); + + let v = vec![ + $( + ($arg, 42), + )* + ]; + + _ = fz_ordered_map_macro(quote!(v)).unwrap(); + + let m1 = fz_ordered_map!(v); + + let v = vec![ + $( + ($arg, 42), + )* + ]; + + let mut m2 = StdBTreeMap::new(); + for x in v.into_iter() { + m2.insert(x.0, x.1); + } + + _ = fz_ordered_map_macro(quote!(static _M3: Foo< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_ordered_map!(static _M3: Foo< $type, i32 >, { + $( + $arg: 42, + )* + }); + + _ = fz_ordered_map_macro(quote!(let m4: Bar< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_ordered_map!(let m4: Bar< $type, i32 >, { + $( + $arg: 42, + )* + }); + + assert_eq!(m0, m1); + assert_eq!(m0, m2); + // assert_eq!(m0, M3); + assert_eq!(m0, m4); + } + } +} + +#[test] +fn ordered_complex() { + test_ordered!(Person, Person { name: "A", age: 1 },); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + Person { name: "K", age: 11 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + Person { name: "K", age: 11 }, + Person { name: "L", age: 12 }, + ); + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + Person { name: "K", age: 11 }, + Person { name: "L", age: 12 }, + Person { name: "M", age: 13 }, + ); + + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "C", age: 3 }, + Person { name: "D", age: 4 }, + Person { name: "E", age: 5 }, + Person { name: "F", age: 6 }, + Person { name: "G", age: 7 }, + Person { name: "H", age: 8 }, + Person { name: "I", age: 9 }, + Person { name: "J", age: 10 }, + Person { name: "K", age: 11 }, + Person { name: "L", age: 12 }, + Person { name: "M", age: 13 }, + Person { name: "xA", age: 1 }, + Person { name: "xB", age: 2 }, + Person { name: "xC", age: 3 }, + Person { name: "xD", age: 4 }, + Person { name: "xE", age: 5 }, + Person { name: "xF", age: 6 }, + Person { name: "xG", age: 7 }, + Person { name: "xH", age: 8 }, + Person { name: "xI", age: 9 }, + Person { + name: "xJ", + age: 10 + }, + Person { + name: "xK", + age: 11 + }, + Person { + name: "xL", + age: 12 + }, + Person { + name: "xM", + age: 13 + }, + Person { name: "aA", age: 1 }, + Person { name: "aB", age: 2 }, + Person { name: "aC", age: 3 }, + Person { name: "aD", age: 4 }, + Person { name: "aE", age: 5 }, + Person { name: "aF", age: 6 }, + Person { name: "aG", age: 7 }, + Person { name: "aH", age: 8 }, + Person { name: "aI", age: 9 }, + Person { + name: "aJ", + age: 10 + }, + Person { + name: "aK", + age: 11 + }, + Person { + name: "aL", + age: 12 + }, + Person { + name: "aM", + age: 13 + }, + Person { name: "zA", age: 1 }, + Person { name: "zB", age: 2 }, + Person { name: "zC", age: 3 }, + Person { name: "zD", age: 4 }, + Person { name: "zE", age: 5 }, + Person { name: "zF", age: 6 }, + Person { name: "zG", age: 7 }, + Person { name: "zH", age: 8 }, + Person { name: "zI", age: 9 }, + Person { + name: "zJ", + age: 10 + }, + Person { + name: "zK", + age: 11 + }, + Person { + name: "zL", + age: 12 + }, + Person { + name: "zM", + age: 13 + }, + Person { name: "vA", age: 1 }, + Person { name: "vB", age: 2 }, + Person { name: "vC", age: 3 }, + Person { name: "vD", age: 4 }, + Person { name: "vE", age: 5 }, + Person { name: "vF", age: 6 }, + Person { name: "vG", age: 7 }, + Person { name: "vH", age: 8 }, + Person { name: "vI", age: 9 }, + Person { + name: "vJ", + age: 10 + }, + Person { + name: "vK", + age: 11 + }, + Person { + name: "vL", + age: 12 + }, + Person { + name: "vM", + age: 13 + }, + ); + + // test duplicate logic + test_ordered!( + Person, + Person { name: "A", age: 1 }, + Person { name: "B", age: 2 }, + Person { name: "A", age: 3 }, + Person { name: "A", age: 4 }, + ); +} + +#[test] +fn ordered_i8() { + test_ordered!(i8, 0i8); + test_ordered!(i8, 0i8, 1,); + test_ordered!(i8, 0i8, 1, 2,); + test_ordered!(i8, 0i8, 1, 2, 3,); + test_ordered!(i8, 0i8, 1, 2, 3, 4,); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5,); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5, 6,); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7,); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8,); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_ordered!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); + + // test duplicate logic + test_ordered!(i8, 0i8, 1, 2, 1, 1); +} + +#[test] +fn ordered_u8() { + test_ordered!(u8, 0u8); + test_ordered!(u8, 0u8, 1,); + test_ordered!(u8, 0u8, 1, 2,); + test_ordered!(u8, 0u8, 1, 2, 3,); + test_ordered!(u8, 0u8, 1, 2, 3, 4,); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5,); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5, 6,); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7,); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8,); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_ordered!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn ordered_i16() { + test_ordered!(i16, 0i16); + test_ordered!(i16, 0i16, 1,); + test_ordered!(i16, 0i16, 1, 2,); + test_ordered!(i16, 0i16, 1, 2, 3,); + test_ordered!(i16, 0i16, 1, 2, 3, 4,); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5,); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5, 6,); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7,); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8,); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_ordered!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn ordered_u16() { + test_ordered!(u16, 0u16); + test_ordered!(u16, 0u16, 1,); + test_ordered!(u16, 0u16, 1, 2,); + test_ordered!(u16, 0u16, 1, 2, 3,); + test_ordered!(u16, 0u16, 1, 2, 3, 4,); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5,); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5, 6,); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7,); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8,); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_ordered!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn ordered_i32() { + test_ordered!(i32, 0i32); + test_ordered!(i32, 0i32, 1,); + test_ordered!(i32, 0i32, 1, 2,); + test_ordered!(i32, 0i32, 1, 2, 3,); + test_ordered!(i32, 0i32, 1, 2, 3, 4,); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5,); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5, 6,); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7,); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8,); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_ordered!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn ordered_u32() { + test_ordered!(u32, 0u32); + test_ordered!(u32, 0u32, 1,); + test_ordered!(u32, 0u32, 1, 2,); + test_ordered!(u32, 0u32, 1, 2, 3,); + test_ordered!(u32, 0u32, 1, 2, 3, 4,); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5,); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5, 6,); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7,); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8,); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_ordered!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn ordered_i64() { + test_ordered!(i64, 0i64); + test_ordered!(i64, 0i64, 1,); + test_ordered!(i64, 0i64, 1, 2,); + test_ordered!(i64, 0i64, 1, 2, 3,); + test_ordered!(i64, 0i64, 1, 2, 3, 4,); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5,); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5, 6,); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7,); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8,); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_ordered!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn ordered_u64() { + test_ordered!(u64, 0u64); + test_ordered!(u64, 0u64, 1,); + test_ordered!(u64, 0u64, 1, 2,); + test_ordered!(u64, 0u64, 1, 2, 3,); + test_ordered!(u64, 0u64, 1, 2, 3, 4,); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5,); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5, 6,); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7,); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8,); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_ordered!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn ordered_string() { + test_ordered!(&str, "0"); + test_ordered!(&str, "0", "1"); + test_ordered!(&str, "0", "1", "2"); + test_ordered!(&str, "0", "1", "2", "3"); + test_ordered!(&str, "0", "1", "2", "3", "4"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5", "6"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5", "6", "7"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"); + test_ordered!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"); +} diff --git a/frozen-collections/tests/scalar_macro_tests.rs b/frozen-collections/tests/scalar_macro_tests.rs new file mode 100644 index 0000000..f989be5 --- /dev/null +++ b/frozen-collections/tests/scalar_macro_tests.rs @@ -0,0 +1,345 @@ +use frozen_collections::*; +use frozen_collections_core::macros::{fz_scalar_map_macro, fz_scalar_set_macro}; +use quote::quote; +use std::collections::BTreeMap as StdBTreeMap; +use std::collections::BTreeSet as StdBTreeSet; + +macro_rules! test_scalar { + ( $type:ty, $( $arg:expr ),* $(,)?) => { + { + _ = fz_scalar_set_macro(quote!({ + $( + $arg, + )* + })).unwrap(); + + let s0 = fz_scalar_set!({ + $( + $arg, + )* + }); + + let v = vec![ + $( + $arg, + )* + ]; + + _ = fz_scalar_set_macro(quote!(v)).unwrap(); + + let s1 = fz_scalar_set!(v); + + let v = vec![ + $( + $arg, + )* + ]; + + let mut s2 = StdBTreeSet::new(); + for x in v.into_iter() { + s2.insert(x); + } + + _ = fz_scalar_set_macro(quote!(static _S3: Foo< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_scalar_set!(static _S3: Foo< $type >, { + $( + $arg, + )* + }); + + _ = fz_scalar_set_macro(quote!(let S4: Bar< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_scalar_set!(let s4: Bar< $type >, { + $( + $arg, + )* + }); + + assert_eq!(s0, s1); + assert_eq!(s0, s2); + // assert_eq!(s0, S3); + assert_eq!(s0, s4); + } + + { + _ = fz_scalar_map_macro(quote!({ + $( + $arg: 42, + )* + })).unwrap(); + + let m0 = fz_scalar_map!({ + $( + $arg: 42, + )* + }); + + let v = vec![ + $( + ($arg, 42), + )* + ]; + + _ = fz_scalar_map_macro(quote!(v)).unwrap(); + + let m1 = fz_scalar_map!(v); + + let v = vec![ + $( + ($arg, 42), + )* + ]; + + let mut m2 = StdBTreeMap::new(); + for x in v.into_iter() { + m2.insert(x.0, x.1); + } + + _ = fz_scalar_map_macro(quote!(static _M3: Foo< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_scalar_map!(static _M3: Foo< $type, i32 >, { + $( + $arg: 42, + )* + }); + + _ = fz_scalar_map_macro(quote!(let m4: Bar< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_scalar_map!(let m4: Bar< $type, i32 >, { + $( + $arg: 42, + )* + }); + + assert_eq!(m0, m1); + assert_eq!(m0, m2); + // assert_eq!(m0, M3); + assert_eq!(m0, m4); + } + } +} + +#[test] +fn scalar_i8() { + test_scalar!(i8, 0i8); + test_scalar!(i8, 0i8, 1,); + test_scalar!(i8, 0i8, 1, 2,); + test_scalar!(i8, 0i8, 1, 2, 3,); + test_scalar!(i8, 0i8, 1, 2, 3, 4,); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5,); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5, 6,); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(i8, 0i8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); + + // test duplicate logic + test_scalar!(i8, 0i8, 1, 2, 1, 1); +} + +#[test] +fn scalar_u8() { + test_scalar!(u8, 0u8); + test_scalar!(u8, 0u8, 1,); + test_scalar!(u8, 0u8, 1, 2,); + test_scalar!(u8, 0u8, 1, 2, 3,); + test_scalar!(u8, 0u8, 1, 2, 3, 4,); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5,); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5, 6,); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(u8, 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_i16() { + test_scalar!(i16, 0i16); + test_scalar!(i16, 0i16, 1,); + test_scalar!(i16, 0i16, 1, 2,); + test_scalar!(i16, 0i16, 1, 2, 3,); + test_scalar!(i16, 0i16, 1, 2, 3, 4,); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5,); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5, 6,); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(i16, 0i16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_u16() { + test_scalar!(u16, 0u16); + test_scalar!(u16, 0u16, 1,); + test_scalar!(u16, 0u16, 1, 2,); + test_scalar!(u16, 0u16, 1, 2, 3,); + test_scalar!(u16, 0u16, 1, 2, 3, 4,); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5,); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5, 6,); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(u16, 0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_i32() { + test_scalar!(i32, 0i32); + test_scalar!(i32, 0i32, 1,); + test_scalar!(i32, 0i32, 1, 2,); + test_scalar!(i32, 0i32, 1, 2, 3,); + test_scalar!(i32, 0i32, 1, 2, 3, 4,); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5,); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5, 6,); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(i32, 0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_u32() { + test_scalar!(u32, 0u32); + test_scalar!(u32, 0u32, 1,); + test_scalar!(u32, 0u32, 1, 2,); + test_scalar!(u32, 0u32, 1, 2, 3,); + test_scalar!(u32, 0u32, 1, 2, 3, 4,); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5,); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5, 6,); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(u32, 0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_i64() { + test_scalar!(i64, 0i64); + test_scalar!(i64, 0i64, 1,); + test_scalar!(i64, 0i64, 1, 2,); + test_scalar!(i64, 0i64, 1, 2, 3,); + test_scalar!(i64, 0i64, 1, 2, 3, 4,); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5,); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5, 6,); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(i64, 0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_u64() { + test_scalar!(u64, 0u64); + test_scalar!(u64, 0u64, 1,); + test_scalar!(u64, 0u64, 1, 2,); + test_scalar!(u64, 0u64, 1, 2, 3,); + test_scalar!(u64, 0u64, 1, 2, 3, 4,); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5,); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6,); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_isize() { + test_scalar!(isize, 0isize); + test_scalar!(isize, 0isize, 1,); + test_scalar!(isize, 0isize, 1, 2,); + test_scalar!(isize, 0isize, 1, 2, 3,); + test_scalar!(isize, 0isize, 1, 2, 3, 4,); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5,); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5, 6,); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(isize, 0isize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_usize() { + test_scalar!(usize, 0usize); + test_scalar!(usize, 0usize, 1,); + test_scalar!(usize, 0usize, 1, 2,); + test_scalar!(usize, 0usize, 1, 2, 3,); + test_scalar!(usize, 0usize, 1, 2, 3, 4,); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5,); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5, 6,); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7,); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(usize, 0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); +} + +#[test] +fn scalar_extra() { + // test sparse case + test_scalar!(u64, 0u64); + test_scalar!(u64, 0u64, 1,); + test_scalar!(u64, 0u64, 2,); + test_scalar!(u64, 0u64, 2, 3,); + test_scalar!(u64, 0u64, 2, 3, 4,); + test_scalar!(u64, 0u64, 2, 3, 4, 5,); + test_scalar!(u64, 0u64, 2, 3, 4, 5, 6,); + test_scalar!(u64, 0u64, 2, 3, 4, 5, 6, 7,); + test_scalar!(u64, 0u64, 2, 3, 4, 5, 6, 7, 8,); + test_scalar!(u64, 0u64, 2, 3, 4, 5, 6, 7, 8, 9,); + test_scalar!(u64, 0u64, 2, 3, 4, 5, 6, 7, 8, 9, 10,); + test_scalar!(u64, 0u64, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + test_scalar!(u64, 0u64, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + test_scalar!(u64, 0u64, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); + + // test defaulting to hash table + test_scalar!(u64, 0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1500); + + // test default to scan + test_scalar!(u64, 0u64, 1500); +} diff --git a/frozen-collections/tests/string_macro_tests.rs b/frozen-collections/tests/string_macro_tests.rs new file mode 100644 index 0000000..ad3eda2 --- /dev/null +++ b/frozen-collections/tests/string_macro_tests.rs @@ -0,0 +1,216 @@ +use frozen_collections::*; +use frozen_collections_core::macros::{fz_string_map_macro, fz_string_set_macro}; +use quote::quote; +use std::collections::BTreeMap as StdBTreeMap; +use std::collections::BTreeSet as StdBTreeSet; + +macro_rules! test_string { + ( $type:ty, $( $arg:expr ),* $(,)?) => { + { + _ = fz_string_set_macro(quote!({ + $( + $arg, + )* + })).unwrap(); + + let s0 = fz_string_set!({ + $( + $arg, + )* + }); + + let v = vec![ + $( + $arg, + )* + ]; + + _ = fz_string_set_macro(quote!(v)).unwrap(); + + let _s1 = fz_string_set!(v); + + let v = vec![ + $( + $arg, + )* + ]; + + let mut s2 = StdBTreeSet::new(); + for x in v.into_iter() { + s2.insert(x); + } + + _ = fz_string_set_macro(quote!(static _S3: Foo< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_string_set!(static _S3: Foo< $type >, { + $( + $arg, + )* + }); + + _ = fz_string_set_macro(quote!(let s4: Bar< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_string_set!(let s4: Bar< $type >, { + $( + $arg, + )* + }); + + _ = fz_string_set_macro(quote!(let mut s5: Baz< $type >, { + $( + $arg, + )* + })).unwrap(); + + fz_string_set!(let mut s5: Baz< $type >, { + $( + $arg, + )* + }); + + // assert_eq!(s0, s1); + assert_eq!(s0, s2); + // assert_eq!(s0, S3); + assert_eq!(s0, s4); + assert_eq!(s0, s5); + } + + { + _ = fz_string_map_macro(quote!({ + $( + $arg: 42, + )* + })).unwrap(); + + let m0 = fz_string_map!({ + $( + $arg: 42, + )* + }); + + let v = vec![ + $( + ($arg, 42), + )* + ]; + + _ = fz_string_map_macro(quote!(v)).unwrap(); + + let _m1 = fz_string_map!(v); + + let v = vec![ + $( + ($arg, 42), + )* + ]; + + let mut m2 = StdBTreeMap::new(); + for x in v.into_iter() { + m2.insert(x.0, x.1); + } + + _ = fz_string_map_macro(quote!(static _M3: Foo< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_string_map!(static _M3: Foo< $type, i32 >, { + $( + $arg: 42, + )* + }); + + _ = fz_string_map_macro(quote!(let m4: Bar< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_string_map!(let m4: Bar< $type, i32 >, { + $( + $arg: 42, + )* + }); + + _ = fz_string_map_macro(quote!(let mut m5: Baz< $type, i32 >, { + $( + $arg: 42, + )* + })).unwrap(); + + fz_string_map!(let mut m5: Baz< $type, i32 >, { + $( + $arg: 42, + )* + }); + + // assert_eq!(m0, m1); + assert_eq!(m0, m2); + // assert_eq!(m0, M3); + assert_eq!(m0, m4); + assert_eq!(m0, m5); + } + } +} + +#[test] +fn string() { + test_string!(&str, "0"); + test_string!(&str, "0", "1"); + test_string!(&str, "0", "1", "2"); + test_string!(&str, "0", "1", "2", "3"); + test_string!(&str, "0", "1", "2", "3", "4"); + test_string!(&str, "0", "1", "2", "3", "4", "5"); + test_string!(&str, "0", "1", "2", "3", "4", "5", "6"); + test_string!(&str, "0", "1", "2", "3", "4", "5", "6", "7"); + test_string!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8"); + test_string!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"); + test_string!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); + test_string!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"); + test_string!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"); + test_string!(&str, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"); + + // test duplicate logic + test_string!(&str, "0", "1", "0", "0"); + + test_string!( + &str, + "ColorRed", + "ColorGreen", + "ColorBlue", + "ColorYellow", + "ColorCyan", + "ColorMagenta" + ); + + test_string!( + &str, + "RedColor", + "GreenColor", + "BlueColor", + "YellowColor", + "CyanColor", + "MagentaColor" + ); + + test_string!( + &str, + "ColorRed1111", + "ColorGreen22", + "ColorBlue333", + "ColorYellow4", + "ColorCyan555", + "ColorMagenta" + ); + + test_string!(&str, "XXA", "XXB", "XXC", "XXD", "XXE", "XXF", "XXG", "XXH", "XXHI"); +} diff --git a/mutate.ps1 b/mutate.ps1 new file mode 100644 index 0000000..48af295 --- /dev/null +++ b/mutate.ps1 @@ -0,0 +1 @@ +cargo mutants --test-workspace=true --colors=never --jobs=2 --build-timeout=120 --cap-lints=true diff --git a/tables.toml b/tables.toml new file mode 100644 index 0000000..0df68be --- /dev/null +++ b/tables.toml @@ -0,0 +1,48 @@ +[top_comments] +Overview = """ +These benchmarks compare the performance of the frozen collecitons relative +to the classic Rust collections. + +The frozen collections have different optimizations depending on the type of data they +storeta and how it is declared. The benchmarks probe those different features to show +the effect of the different optimizations on effective performance. + +When you see `HashSet(classic)` vs. `HashSet(ahash)` this reflects the performance difference between the +normal hasher used by the standard collections as opposed to the performnace that the +`ahash` hasher provides. + +The benchmarks assume a 50% hit rate when probing for lookup, meaning that +half the queries are for non-existing data. Some algorithms perform differently between +present vs. non-existing cases, so real world performance of these algorithms depends on the +real world hit rate you experience. +""" + +[table_comments] + +dense_scalar = """ +Scalar sets where the values are in a contiguous range. +""" + +sparse_scalar = """ +Scalar sets where the values are in a non-contiguous range. +""" + +random_scalar = """ +Scalar sets where the values are randomly distributed. +""" + +random_string = """ +String sets where the values are random. +""" + +prefixed_string = """ +String sets where the values are random, but share a common prefix. +""" + +hashed = """ +Sets with a complex key type that is hashable. +""" + +ordered = """ +Sets with a complex key type that is ordered. +"""