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. +"""