diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..923b07c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,106 @@ +name: CI + +# Run on any push or any PR (both for any branch) +on: [push, pull_request] + +env: + CARGO_TERM_COLOR: always # Pretty colors +jobs: + check_fmt: + name: Cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Cargo fmt + run: ./ci.sh check_fmt + + check_docs: + name: Cargo docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@nightly + - name: Cargo docs + run: ./ci.sh check_docs + + lint: + name: Lint (clippy) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Lint (clippy) + run: ./ci.sh lint + + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Build + run: ./ci.sh build + + build_nostd: + name: Build (nostd) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: thumbv6m-none-eabi + - name: Build (nostd) + run: ./ci.sh build_nostd + + run_tests_stable: + name: Tests (stable) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Tests (stable) + run: ./ci.sh run_tests_stable + + run_tests_beta: + name: Tests (beta) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@beta + - name: Tests (beta) + run: ./ci.sh run_tests_beta + + run_tests_msrv: + name: Tests (MSRV) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.85.0 + - name: Tests (MSRV) + run: ./ci.sh run_tests_msrv + + run_tests_leak_sanitizer: + name: Tests (leak sanitizer) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - name: Tests (leak sanitizer) + run: ./ci.sh run_tests_leak_sanitizer + + run_tests_miri: + name: Tests (MIRI) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + with: + components: miri + - name: Tests (MIRI) + run: ./ci.sh run_tests_miri diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..50837fd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Major Version 0 + +## v0.2.0 + +Implemented the core functionality of this crate: +- Defined `Pair` struct +- Defined `Owner` and `HasDependent` traits +- Defined `Dependent` type alias + +## v0.1.0 + +Initial (empty) release. diff --git a/Cargo.lock b/Cargo.lock index 82dbb46..f4557aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,432 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "generator" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +dependencies = [ + "cfg-if", + "libc", + "log", + "rustversion", + "windows", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pair" +version = "0.2.0" +dependencies = [ + "loom", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 9f5efe5..cb24b6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,85 @@ +# # # # # # # # # # # # # # # # # # # # +# # +# PACKAGE # +# # +# # # # # # # # # # # # # # # # # # # # + [package] name = "pair" -version = "0.1.0" +version = "0.2.0" authors = ["Isaac Chen"] +edition = "2024" +rust-version = "1.85.0" description = "Safe API for generic self-referential pairs of owner and dependent." -license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/pair" readme = "README.md" repository = "https://github.com/ijchen/pair" -documentation = "https://docs.rs/pair" -edition = "2021" -include = ["/src/", "/Cargo.toml", "/README.md", "/LICENSE-*"] -publish = false +license = "MIT OR Apache-2.0" +keywords = ["self-referential", "data-structures", "memory", "ownership", "no_std"] +categories = ["memory-management", "rust-patterns", "no-std", "data-structures"] +include = ["/src/", "/Cargo.toml", "/README.md", "/CHANGELOG.md", "/LICENSE-*"] + +# # # # # # # # # # # # # # # # # # # # +# # +# DEPENDENCIES # +# # +# # # # # # # # # # # # # # # # # # # # [dependencies] +# No dependencies + +[dev-dependencies] +loom = "0.7.2" + +# # # # # # # # # # # # # # # # # # # # +# # +# LINTS # +# # +# # # # # # # # # # # # # # # # # # # # + +[lints.rust] + +# "deprecated_safe" lint group +deprecated_safe = { level = "warn", priority = -1 } + +# "future_incompatible" lint group +future_incompatible = { level = "warn", priority = -1 } + +# "keyword_idents" lint group +keyword_idents = { level = "warn", priority = -1 } + +# Cherry-picked lint overrides +unstable_features = "forbid" # do not permit nightly-only features +elided_lifetimes_in_paths = "warn" # elided lifetimes are unclear +missing_debug_implementations = "warn" # all public types should impl Debug +missing_docs = "warn" # all public items should be documented +non_ascii_idents = "warn" # non-ASCII identifiers could be confusing +redundant_imports = "warn" # redundant imports are unnecessary +redundant_lifetimes = "warn" # duplicate identical lifetimes are unnecessary +single_use_lifetimes = "warn" # single-use lifetimes are unnecessary +unnameable_types = "warn" # unnameable types should be considered case-by-case +unused_qualifications = "warn" # overly-qualified paths decrease readability + + + +[lints.clippy] + +# "pedantic" lint group +pedantic = { level = "warn", priority = -1 } +must_use_candidate = "allow" # too many false positives + +# "cargo" lint group +cargo = { level = "warn", priority = -1 } + +# Cherry-picked lint overrides +multiple_unsafe_ops_per_block = "forbid" # enforce all unsafe ops have a SAFETY comment +undocumented_unsafe_blocks = "forbid" # enforce all unsafe ops have a SAFETY comment +unnecessary_safety_comment = "warn" # unnecessary SAFETY comments would be confusing +unnecessary_safety_doc = "warn" # unnecessary "# Safety" sections would be confusing +allow_attributes = "warn" # prefer #[expect(..)] +allow_attributes_without_reason = "warn" # should always include a reason +dbg_macro = "warn" # this macro should only be used during development +todo = "warn" # this macro should only be used during development +cognitive_complexity = "warn" # can be #[expect(..)]ed, but useful as a warning +too_long_first_doc_paragraph = "warn" # first doc paragraph should be brief +use_self = "warn" # `Self` is more clear when applicable diff --git a/README.md b/README.md index 1858d95..5d1b25a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,137 @@ +# Pair + + +[![GitHub](https://img.shields.io/badge/Source-ijchen/pair-FFD639?labelColor=555555&logo=github)](https://github.com/ijchen/pair) +[![crates.io](https://img.shields.io/crates/v/pair?logo=rust)](https://crates.io/crates/pair) +[![docs.rs](https://img.shields.io/docsrs/pair?logo=docs.rs)](https://docs.rs/pair) +[![License](https://img.shields.io/crates/l/pair)](#) + Safe API for generic self-referential pairs of owner and dependent. -# DO NOT USE THIS LIBRARY +You define how to construct a dependent type from a reference to an owning type, +and [`Pair`] will carefully bundle them together in a safe and freely movable +self-referential struct. + +# Example Usage + +A typical use case might look something like this: +```rust +use pair::{Dependent, HasDependent, Owner, Pair}; + +// Let's say you have some buffer type that contains a string +#[derive(Debug)] +pub struct MyBuffer { + data: String, +} + +// And you have some borrowing "parsed" representation, containing string slices +#[derive(Debug)] +pub struct Parsed<'a> { + tokens: Vec<&'a str>, +} + +// And you have some expensive parsing function you only want to run once +fn parse(buffer: &MyBuffer) -> Parsed<'_> { + let tokens = buffer.data.split_whitespace().collect(); + Parsed { tokens } +} + + + +// You would then implement HasDependent and Owner for MyBuffer: + +// Defines the owner/dependent relationship between MyBuffer and Parsed<'_> +impl<'owner> HasDependent<'owner> for MyBuffer { + type Dependent = Parsed<'owner>; +} + +// Define how to make a Parsed<'_> from a &MyBuffer +impl Owner for MyBuffer { + type Context<'a> = (); // We don't need any extra args to `make_dependent` + type Error = std::convert::Infallible; // Our example parsing can't fail + + fn make_dependent(&self, _: ()) -> Result, Self::Error> { + Ok(parse(self)) + } +} -As of right now, I have absolutely no idea whether or not this API is sound. You -should *absolutely not* use this library for anything that matters at this point -in time. + + +// You can now use MyBuffer in a Pair: +fn main() { + // A Pair can be constructed from an owner value (MyBuffer, in this example) + let mut pair = Pair::new(MyBuffer { + data: String::from("this is an example"), + }); + + // You can obtain a reference to the owner via a reference to the pair + let owner: &MyBuffer = pair.owner(); + assert_eq!(owner.data, "this is an example"); + + // You can access a reference to the dependent via a reference to the pair, + // but only within a provided closure. + // See the documentation of `Pair::with_dependent` for details. + let kebab = pair.with_dependent(|parsed: &Parsed<'_>| parsed.tokens.join("-")); + assert_eq!(kebab, "this-is-an-example"); + + // However, if the dependent is covariant over its lifetime (as our example + // Parsed<'_> is) you can trivially extract the dependent from the closure. + // This will not compile if the dependent is not covariant. + let parsed: &Parsed<'_> = pair.with_dependent(|parsed| parsed); + assert_eq!(parsed.tokens, ["this", "is", "an", "example"]); + + // You can obtain a mutable reference to the dependent via a mutable + // reference to the pair, but only within a provided closure. + // See the documentation of `Pair::with_dependent_mut` for details. + pair.with_dependent_mut(|parsed| parsed.tokens.pop()); + assert_eq!(pair.with_dependent(|parsed| parsed.tokens.len()), 3); + + // If you're done with the dependent, you can recover the owner. + // This will drop the dependent. + let my_buffer: MyBuffer = pair.into_owner(); +} +``` + +# How it Works + +*Note: the implementation details described in this section are not part of the +crate's public API, and are subject to change.* + +Under the hood, [`Pair`] moves the owner onto the heap, giving it a stable +memory address. It is then borrowed and used to construct the dependent, which +is also moved onto the heap. The dependent is type-erased, so that its +inexpressible self-referential lifetime goes away. All exposed APIs are careful +to ensure type and aliasing rules are upheld, regardless of anything safe user +code could do. When the owner needs to be dropped or recovered, the dependent +will first be recovered and dropped, ending the borrow of the owner. At that +point, the owner can safely be recovered and the `Pair` deconstructed. + +# Related Projects + +| Crate | Macro free | No `alloc` | Maintained | Soundness | +| ----- | :--------: | :-------------: | :--------: | :-------: | +| `pair` | ✅ | ❌ | ✅ | No known issues | +| [`ouroboros`](https://crates.io/crates/ouroboros) | ❌ | ❌ | ✅ | ⚠️ [Unsound](https://github.com/someguynamedjosh/ouroboros/issues/122) ⚠️ | +| [`self_cell`](https://crates.io/crates/self_cell) | ❌ | ❌ | ✅ | No known issues | +| [`yoke`](https://crates.io/crates/yoke) | ❌ | ✅ | ✅ | ⚠️ [Unsound](https://github.com/unicode-org/icu4x/issues/2095) ⚠️ | +| [`selfie`](https://crates.io/crates/selfie) | ✅ | ✅ | ❌ | ⚠️ [Unsound](https://github.com/prokopyl/selfie?tab=readme-ov-file#abandoned-this-crate-is-unsound-and-no-longer-maintained_) ⚠️ | +| [`nolife`](https://crates.io/crates/nolife) | ❌ | ❌ | ✅ | No known issues | +| [`owning_ref`](https://crates.io/crates/owning_ref) | ✅ | ✅ | ❌ | ⚠️ [Unsound](https://github.com/Kimundi/owning-ref-rs/issues/77) ⚠️ | +| [`rental`](https://crates.io/crates/rental) | ❌ | ✅ | ❌ | ⚠️ [Unsound](https://github.com/Voultapher/self_cell?tab=readme-ov-file#related-projects) ⚠️ | +| [`fortify`](https://crates.io/crates/fortify) | ❌ | ❌ | ✅ | No known issues | +| [`loaned`](https://crates.io/crates/loaned) | ❌ | ✅ | ✅ | No known issues | +| [`selfref`](https://crates.io/crates/selfref) | ❌ | ✅ | ✅ | No known issues | +| [`self-reference`](https://crates.io/crates/self-reference) | ❌ | ✅ | ❌ | ⚠️ [Unsound](https://github.com/ArtBlnd/self-reference/issues/1) ⚠️ | +| [`zc`](https://crates.io/crates/zc) | ❌ | ✅ | ✅ | No known issues | # License Licensed under either of: - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or - http://www.apache.org/licenses/LICENSE-2.0) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or + ) at your option. @@ -21,3 +140,9 @@ at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + + +[`Pair`]: https://docs.rs/pair/0.2.0/pair/struct.Pair.html diff --git a/ci.sh b/ci.sh new file mode 100755 index 0000000..01be4b6 --- /dev/null +++ b/ci.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Prints text formatted as a header (colors and arrows and stuff, very cool) +print_header() { + echo -e "\e[1;34m==>\e[0m \e[1m$1\e[0m" +} + +check_fmt() { + print_header 'Checking code formatting...' + RUSTFLAGS='-D warnings' cargo +stable fmt --check +} + +check_docs() { + print_header 'Building documentation (stable)...' + RUSTDOCFLAGS='-D warnings' cargo +stable doc --document-private-items --no-deps + + print_header 'Building documentation (nightly)...' + RUSTDOCFLAGS='-D warnings' cargo +nightly doc --document-private-items --no-deps +} + +lint() { + print_header 'Linting with cargo clippy...' + cargo +stable clippy --no-deps --all-targets -- -D warnings +} + +build() { + print_header 'Running cargo build...' + RUSTFLAGS='-D warnings' cargo +stable build --all-targets +} + +build_nostd() { + print_header 'Building on no_std target...' + RUSTFLAGS='-D warnings' cargo +stable build --target thumbv6m-none-eabi +} + +run_tests_stable() { + print_header 'Running tests (stable compiler)...' + RUSTFLAGS='-D warnings' cargo +stable test +} + +run_tests_beta() { + print_header 'Running tests (beta compiler)...' + RUSTFLAGS='-D warnings' cargo +beta test +} + +run_tests_msrv() { + local msrv="1.85.0" + + print_header "Running tests (MSRV compiler ($msrv))..." + RUSTFLAGS='-D warnings' cargo "+$msrv" test +} + +run_tests_leak_sanitizer() { + # NOTE: loom seems to make the leak sanitizer unhappy. I don't think that + # combination of tests is important, so we just skip loom tests here. + + print_header 'Running tests with leak sanitizer...' + RUSTFLAGS='-D warnings -Z sanitizer=leak' cargo +nightly test -- --skip loom +} + +run_tests_miri() { + # NOTE: some tests (containing `nomiri`) can't run under MIRI, and are + # skipped here. + print_header 'Running tests with MIRI...' + RUSTFLAGS='-D warnings' MIRIFLAGS='-Zmiri-strict-provenance' cargo +nightly miri test -- --skip nomiri +} + +# Run all checks +all_checks() { + check_fmt + check_docs + build + build_nostd + lint + run_tests_stable + run_tests_beta + run_tests_msrv + run_tests_leak_sanitizer + run_tests_miri + + print_header "All checks passed! 🎉" +} + +# Main function to handle command line arguments +main() { + local command="${1:-"all"}" + + case "$command" in + "all") all_checks ;; + "check_fmt") check_fmt ;; + "check_docs") check_docs ;; + "lint") lint ;; + "build") build ;; + "build_nostd") build_nostd ;; + "run_tests_stable") run_tests_stable ;; + "run_tests_beta") run_tests_beta ;; + "run_tests_msrv") run_tests_msrv ;; + "run_tests_leak_sanitizer") run_tests_leak_sanitizer ;; + "run_tests_miri") run_tests_miri ;; + *) + echo "Unknown command: $command" + echo "Available commands: all (default), check_fmt, check_docs, lint, build, build_nostd, run_tests_stable, run_tests_beta, run_tests_msrv, run_tests_leak_sanitizer, run_tests_miri" + exit 1 + ;; + esac +} + +main "$@" diff --git a/src/drop_guard.rs b/src/drop_guard.rs new file mode 100644 index 0000000..8bbeddf --- /dev/null +++ b/src/drop_guard.rs @@ -0,0 +1,9 @@ +/// A simple struct that runs a closure on drop. Used to clean up resources +/// during panic unwinding within [`Pair`](crate::pair). +pub struct DropGuard(pub F); + +impl Drop for DropGuard { + fn drop(&mut self) { + (self.0)(); + } +} diff --git a/src/lib.rs b/src/lib.rs index ffe2342..6aafd3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,15 @@ -//! Not very interesting yet +// These markdown links are used to override the docs.rs web links present at +// the bottom of README.md. These two lists must be kept in sync, or the links +// included here will be hard-coded links to docs.rs, not relative doc links. +//! [`Pair`]: Pair +#![cfg_attr(any(doc, test), doc = include_str!("../README.md"))] +#![no_std] + +extern crate alloc; + +mod drop_guard; +mod owner; +mod pair; + +pub use owner::{Dependent, HasDependent, Owner}; +pub use pair::Pair; diff --git a/src/owner.rs b/src/owner.rs new file mode 100644 index 0000000..5cd36a3 --- /dev/null +++ b/src/owner.rs @@ -0,0 +1,83 @@ +//! Defines the [`Owner`] and [`HasDependent`] traits, the common interface for +//! types stored in a [`Pair`](crate::Pair). + +/// Defines the dependent type for the [`Owner`] trait. +/// +/// Semantically, you can think of this like a lifetime Generic Associated Type +/// (GAT) in the `Owner` trait - the two behave very similarly, and serve the +/// same role in defining a [`Dependent`](HasDependent::Dependent) type, generic +/// over some lifetime. +/// +/// A real GAT is not used due to limitations in the Rust compiler. For the +/// technical details on this, I recommend Sabrina Jewson's blog post on +/// [The Better Alternative to Lifetime GATs]. +/// +/// [The Better Alternative to Lifetime GATs]: https://sabrinajewson.org/blog/the-better-alternative-to-lifetime-gats +pub trait HasDependent<'owner, ForImpliedBound: Sealed = Bounds<&'owner Self>> { + /// The dependent type, borrowing from an owner. This type is what is + /// returned from [`Owner::make_dependent`]. + type Dependent; +} + +/// A type alias for the [`Dependent`](HasDependent::Dependent) of some +/// [`Owner`] with a specific lifetime `'owner`. +pub type Dependent<'owner, O> = >::Dependent; + +#[expect( + clippy::missing_errors_doc, + reason = "failure modes are specific to the trait's implementation" +)] +/// A type which can act as the "owner" of some data, and can produce some +/// dependent type which borrows from `Self`. Used for the [`Pair`](crate::Pair) +/// struct. +/// +/// The supertrait [`HasDependent`] defines the dependent type, acting as a sort +/// of generic associated type - see its documentation for more information. The +/// [`make_dependent`](Owner::make_dependent) function defines how to create a +/// dependent from a reference to an owner. +pub trait Owner: for<'any> HasDependent<'any> { + /// Additional context provided to [`make_dependent`](Owner::make_dependent) + /// as an argument. + /// + /// If additional context is not necessary, this should be set to + /// [`()`](prim@unit). + // + // TODO(ichen): default this to () when associated type defaults are + // stabilized (https://github.com/rust-lang/rust/issues/29661) + type Context<'a>; + + /// The error type returned by [`make_dependent`](Owner::make_dependent) in + /// the event of an error. + /// + /// If `make_dependent` can't fail, this should be set to + /// [`Infallible`](core::convert::Infallible). + // + // TODO(ichen): default this to core::convert::Infallible (or preferably !) + // when associated type defaults are stabilized + // (https://github.com/rust-lang/rust/issues/29661) + type Error; + + /// Attempts to construct a [`Dependent`](HasDependent::Dependent) from a + /// reference to an owner and some context. + fn make_dependent<'owner>( + &'owner self, + context: Self::Context<'_>, + ) -> Result, Self::Error>; +} + +/// Used to prevent implementors of [`HasDependent`] from overriding the +/// `ForImpliedBounds` generic type from its default. +mod sealed { + #![expect(unnameable_types, reason = "...kinda the point")] + /// The `ForImpliedBounds` generic type for + /// [`HasDependent`](super::HasDependent) should not be overridden from its + /// default. + pub trait Sealed {} + #[derive(Debug)] + /// The `ForImpliedBounds` generic type for + /// [`HasDependent`](super::HasDependent) should not be overridden from its + /// default. + pub struct Bounds(core::marker::PhantomData); + impl Sealed for Bounds {} +} +use sealed::{Bounds, Sealed}; diff --git a/src/pair.rs b/src/pair.rs new file mode 100644 index 0000000..0b6394e --- /dev/null +++ b/src/pair.rs @@ -0,0 +1,595 @@ +//! Defines [`Pair`], the primary abstraction provided by this crate. + +use core::{convert::Infallible, fmt::Debug, marker::PhantomData, mem::ManuallyDrop, ptr::NonNull}; + +use alloc::boxed::Box; + +use crate::{Dependent, Owner, drop_guard::DropGuard}; + +/// A self-referential pair containing both some [`Owner`] and its [`Dependent`]. +/// +/// The owner must be provided to construct a [`Pair`], and the dependent is +/// immediately created (borrowing from the owner). Both are stored together in +/// the pair, which heap-allocates the owner so that the pair itself may be +/// moved freely without invalidating any references stored inside the +/// dependent. +/// +/// Conceptually, the pair itself has ownership over the owner `O`, the owner is +/// immutably borrowed by the dependent for the lifetime of the pair, and the +/// dependent is owned by the pair and valid for the pair's lifetime. +/// +/// # Constructors +/// +/// There are many different constructors for `Pair`, each serving a different +/// use case. There are three relevant factors to consider when deciding which +/// constructor to use: +/// +/// 1. Can [`make_dependent`](Owner::make_dependent) fail (return [`Err`])? +/// 2. Does `make_dependent` require additional context? +/// 3. Is your owner already stored in a [`Box`]? +/// +/// The simplest constructor, which you should use if you answered "no" to all +/// of the above questions, is [`Pair::new`]. It takes an `O: Owner`, and gives +/// you a `Pair` - doesn't get much easier than that! +/// +/// If your `make_dependent` can fail (meaning [`Owner::Error`] is not +/// [`Infallible`]), you should use one of the `try_*` constructors. +/// +/// If your `make_dependent` requires additional context (meaning +/// [`Owner::Context`] is not [`()`](prim@unit)), you should use one of the +/// `*_with_context` constructors. +/// +/// If your owner is already stored in a `Box`, you should use one of the +/// `*_from_box` constructors. +/// +/// Every combination of these is supported, up to the most powerful (and least +/// ergonomic) [`Pair::try_new_from_box_with_context`]. You should use the +/// simplest constructor you can for your implementation of `Owner`. +/// +/// [`Dependent`]: crate::HasDependent::Dependent +pub struct Pair { + // Derived from a Box + // Immutably borrowed by `self.dependent` from construction until drop + owner: NonNull, + + // Type-erased Box> + dependent: NonNull<()>, + + // Need invariance over O - if we were covariant or contravariant, two + // different `O`s with two different `Owner` impls (and importantly, two + // different associated types in HasDependent) which have a sub/supertype + // relationship could be incorrectly treated as substitutable in a Pair. + // That would lead to effectively an unchecked transmute of each field, + // which would obviously be unsound. + // + // Coherence doesn't help us here, since there are types which are able to + // have different impls of the same trait, but also have a subtype/supertype + // relationship (namely, `fn(&'static T)` and `for<'a> fn(&'a T)` ) + prevent_covariance: PhantomData<*mut O>, +} + +/// Creates a [`NonNull`] from [`Box`]. The returned `NonNull` is the same +/// pointer as the Box, and therefore comes with all of Box's representation +/// guarantees: +/// - The returned `NonNull` will be suitably aligned for T +/// - The returned `NonNull` will point to a valid T +/// - The returned `NonNull` was allocated with the +/// [`Global`](alloc::alloc::Global) allocator and a valid +/// [`Layout`](alloc::alloc::Layout) for `T`. +fn non_null_from_box(value: Box) -> NonNull { + // See: https://github.com/rust-lang/rust/issues/47336#issuecomment-586578713 + NonNull::from(Box::leak(value)) +} + +impl Pair { + /// Constructs a new [`Pair`] with the given [`Owner`]. The dependent will + /// be computed through [`Owner::make_dependent`] during this construction. + /// + /// See the "Constructors" section in the documentation of [`Pair`] for + /// information on the differences between constructors. + /// + /// # Errors + /// If [`::make_dependent`](Owner::make_dependent) returns an + /// error. + pub fn try_new_with_context(owner: O, context: O::Context<'_>) -> Result + where + O: Sized, + { + Self::try_new_from_box_with_context(Box::new(owner), context) + .map_err(|(owner, err)| (*owner, err)) + } + + /// Constructs a new [`Pair`] with the given [`Owner`]. The dependent will + /// be computed through [`Owner::make_dependent`] during this construction. + /// + /// See the "Constructors" section in the documentation of [`Pair`] for + /// information on the differences between constructors. + /// + /// # Errors + /// If [`::make_dependent`](Owner::make_dependent) returns an + /// error. + pub fn try_new_from_box_with_context( + owner: Box, + context: O::Context<'_>, + ) -> Result, O::Error)> { + // Convert owner into a NonNull, so we are no longer restricted by the + // aliasing requirements of Box + let owner = non_null_from_box(owner); + + // Borrow `owner` to construct `dependent`. This borrow conceptually + // lasts from now until drop, where we will drop `dependent` and then + // drop owner. + + // We're about to call `make_dependent(..)` - if it panics, we want to + // be able to drop the boxed owner before unwinding the rest of the + // stack to avoid unnecessarily leaking memory (and potentially other + // resources). + let panic_drop_guard = DropGuard(|| { + // If this code is executed, it means make_dependent panicked and we + // never `mem::forget(..)`'d this drop guard. Recover and drop the + // boxed owner. + + // SAFETY: `owner` was just created from a Box earlier in + // `try_new_from_box_with_context`, and not invalidated since then. + // Because we haven't given away access to a `Self`, and the one + // borrow we took of the owner to pass to `make_dependent` has + // expired (since it panicked), we know there are no outstanding + // borrows to owner. Therefore, reconstructing the original Box + // is okay. + let owner: Box = unsafe { Box::from_raw(owner.as_ptr()) }; + + // If the owner's drop *also* panics, that will be a double-panic. + // This will cause an abort, which is fine - drops generally + // shouldn't panic, and if the user *really* wants to handle this, + // they can check if the thread is panicking within owner's drop + // before performing any operations which could panic. + drop(owner); + }); + + let maybe_dependent = { + // SAFETY: `owner` was just converted from a valid Box, and inherits + // the alignment and validity guarantees of Box. Additionally, the + // value behind the pointer is currently not borrowed at all - this + // marks the beginning of a shared borrow which will last until the + // returned `Pair` is dropped (or ends immediately if make_dependent + // panics or returns an error). + unsafe { owner.as_ref() }.make_dependent(context) + }; + + // The call to `make_dependent` didn't panic - disarm our drop guard + core::mem::forget(panic_drop_guard); + + // If `make_dependent(..)` failed, early return out from this function. + let dependent = match maybe_dependent { + Ok(dependent) => dependent, + Err(err) => { + // SAFETY: `owner` was just created from a Box earlier in this + // function, and not invalidated since then. Because we haven't + // given away access to a `Self`, and the one borrow we took of + // the dependent to pass to `make_dependent` has expired, we + // know there are no outstanding borrows to owner. Therefore, + // reconstructing the original Box is okay. + let owner: Box = unsafe { Box::from_raw(owner.as_ptr()) }; + + return Err((owner, err)); + } + }; + + // We're about to call `Box::new(..)` - if it panics, we want to be able + // to drop the boxed owner before unwinding the rest of the stack to + // avoid unnecessarily leaking memory (and potentially other resources). + let panic_drop_guard = DropGuard(|| { + // If this code is executed, it means `Box::new(..)` panicked and we + // never `mem::forget(..)`'d this drop guard. Recover and drop the + // boxed owner. + + // SAFETY: `owner` was just created from a Box earlier in + // `try_new_from_box_with_context`, and not invalidated since then. + // Because we haven't given away access to a `Self`, and the one + // borrow of the owner stored in the dependent has expired (since we + // gave ownership of the dependent to the `Box::new(..)` call that + // panicked), we know there are no outstanding borrows to owner. + // Therefore, reconstructing the original Box is okay. + let owner: Box = unsafe { Box::from_raw(owner.as_ptr()) }; + + // If the owner's drop *also* panics, that will be a double-panic. + // This will cause an abort, which is fine - drops generally + // shouldn't panic, and if the user *really* wants to handle this, + // they can check if the thread is panicking within owner's drop + // before performing any operations which could panic. + drop(owner); + }); + + // Move `dependent` to the heap, so we can store it as a type-erased + // pointer. + let dependent = Box::new(dependent); + + // The call to `Box::new(..)` didn't panic - disarm our drop guard + core::mem::forget(panic_drop_guard); + + // Type-erase dependent so its inexpressible self-referential lifetime + // goes away (we know that it's borrowing self.owner immutably from + // construction (now) until drop) + let dependent: NonNull> = non_null_from_box(dependent); + let dependent: NonNull<()> = dependent.cast(); + + Ok(Self { + owner, + dependent, + prevent_covariance: PhantomData, + }) + } + + /// Returns a reference to the owner. + pub fn owner(&self) -> &O { + // SAFETY: `self.owner` was originally converted from a valid Box, and + // inherited the alignment and validity guarantees of Box - and neither + // our code nor any of our exposed APIs could have invalidated those + // since construction. Additionally, the value behind the pointer is + // currently in a shared borrow state (no exclusive borrows, no other + // code assuming unique ownership), and will be until the Pair is + // dropped. Here, we only add another shared borrow. + unsafe { self.owner.as_ref() } + } + + /// Calls the given closure, providing shared access to the dependent, and + /// returns the value computed by the closure. + /// + /// The closure must be able to work with a [`Dependent`] with any arbitrary + /// lifetime that lives at least as long as the borrow of `self`. This is + /// important because the dependent may be invariant over its lifetime, and + /// the correct lifetime (lasting from the construction of `self` until + /// drop) is inexpressible. For dependent types covariant over their + /// lifetime, the closure may simply return the reference to the dependent, + /// which may then be used as if this function directly returned a + /// reference. + /// + /// [`Dependent`]: crate::HasDependent::Dependent + pub fn with_dependent<'self_borrow, F, T>(&'self_borrow self, f: F) -> T + where + F: for<'any> FnOnce(&'self_borrow Dependent<'_, O>) -> T, + { + // SAFETY: `self.dependent` was originally converted from a valid + // Box>, and type-erased to a NonNull<()>. As such, it + // inherited the alignment and validity guarantees of Box (for a + // Dependent<'_, O>) - and neither our code nor any of our exposed APIs + // could have invalidated those since construction. Additionally, + // because we have a shared reference to self, we know that the value + // behind the pointer is currently either not borrowed at all, or in a + // shared borrow state (no exclusive borrows, no other code assuming + // unique ownership). Here, we only either create the first shared + // borrow, or add another. + let dependent = unsafe { self.dependent.cast::>().as_ref() }; + + f(dependent) + } + + /// Calls the given closure, providing exclusive access to the dependent, + /// and returns the value computed by the closure. + /// + /// The closure must be able to work with a [`Dependent`] with any arbitrary + /// lifetime that lives at least as long as the borrow of `self`. This is + /// important because mutable references are invariant over their type `T`, + /// and the exact T here (a `Dependent` with a very specific lifetime + /// lasting from the construction of `self` until drop) is inexpressible. + /// + /// [`Dependent`]: crate::HasDependent::Dependent + pub fn with_dependent_mut<'self_borrow, F, T>(&'self_borrow mut self, f: F) -> T + where + F: for<'any> FnOnce(&'self_borrow mut Dependent<'_, O>) -> T, + { + self.with_both_mut(|_, dependent| f(dependent)) + } + + /// Calls the given closure, providing shared access to both the owner and + /// the dependent, and returns the value computed by the closure. + /// + /// The closure must be able to work with a [`Dependent`] with any arbitrary + /// lifetime that lives at least as long as the borrow of `self`. See the + /// documentation of [`with_dependent`](Pair::with_dependent) for more + /// information on this. + /// + /// [`Dependent`]: crate::HasDependent::Dependent + pub fn with_both<'self_borrow, F, T>(&'self_borrow self, f: F) -> T + where + F: for<'any> FnOnce(&'self_borrow O, &'self_borrow Dependent<'_, O>) -> T, + { + self.with_dependent(|dependent| f(self.owner(), dependent)) + } + + /// Calls the given closure, providing shared access to the owner and + /// exclusive access to the dependent, and returns the value computed by the + /// closure. + /// + /// The closure must be able to work with a [`Dependent`] with any arbitrary + /// lifetime that lives at least as long as the borrow of `self`. See the + /// documentation of [`with_dependent_mut`](Pair::with_dependent_mut) for + /// more information on this. + /// + /// [`Dependent`]: crate::HasDependent::Dependent + pub fn with_both_mut<'self_borrow, F, T>(&'self_borrow mut self, f: F) -> T + where + F: for<'any> FnOnce(&'self_borrow O, &'self_borrow mut Dependent<'_, O>) -> T, + { + let owner: &O = self.owner(); + + // SAFETY: `self.dependent` was originally converted from a valid + // Box>, and type-erased to a NonNull<()>. As such, it + // inherited the alignment and validity guarantees of Box (for a + // Dependent<'_, O>) - and neither our code nor any of our exposed APIs + // could have invalidated those since construction. Additionally, + // because we have an exclusive reference to self (and Pair::owner(..) + // doesn't borrow the dependent), we know that the value behind the + // pointer is currently not borrowed at all, and can't be until our + // exclusive borrow of `self` expires. + let dependent = unsafe { self.dependent.cast::>().as_mut() }; + + f(owner, dependent) + } + + /// Consumes the [`Pair`], dropping the dependent and returning the owner. + /// + /// If you don't need the returned owner in a [`Box`], consider the + /// convenience method [`Pair::into_owner`], which moves the owner out of + /// the box for you. + pub fn into_boxed_owner(self) -> Box { + // Prevent dropping `self` at the end of this scope - otherwise, the + // Pair drop implementation would attempt to drop the owner and + // dependent again, which would be... not good (unsound). + // + // It's important that we do this before calling the dependent's drop, + // since a panic in that drop would otherwise cause a double free when + // we attempt to drop the dependent again when dropping `self`. + let this = ManuallyDrop::new(self); + + // SAFETY: `this.dependent` was originally created from a Box, and never + // invalidated since then. Because we took ownership of `self`, we know + // there are no outstanding borrows to the dependent. Therefore, + // reconstructing the original Box> is okay. + let dependent: Box> = + unsafe { Box::from_raw(this.dependent.cast::>().as_ptr()) }; + + // We're about to drop the dependent - if it panics, we want to be able + // to drop the boxed owner before unwinding the rest of the stack to + // avoid unnecessarily leaking memory (and potentially other resources). + let panic_drop_guard = DropGuard(|| { + // If this code is executed, it means the dependent's drop panicked + // and we never `mem::forget(..)`'d this drop guard. Recover and + // drop the boxed owner. + + // SAFETY: `this.owner` was originally created from a Box, and never + // invalidated since then. Because we took ownership of `self`, and + // we just dropped the dependent (well, the drop panicked - but its + // borrow of the owner has certainly expired), we know there are no + // outstanding borrows to owner. Therefore, reconstructing the + // original Box is okay. + let owner: Box = unsafe { Box::from_raw(this.owner.as_ptr()) }; + + // If the owner's drop *also* panics, that will be a double-panic. + // This will cause an abort, which is fine - drops generally + // shouldn't panic, and if the user *really* wants to handle this, + // they can check if the thread is panicking within owner's drop + // before performing any operations which could panic. + drop(owner); + }); + + // Drop the dependent + drop(dependent); + + // The dependent's drop didn't panic - disarm our drop guard + core::mem::forget(panic_drop_guard); + + // SAFETY: `this.owner` was originally created from a Box, and never + // invalidated since then. Because we took ownership of `self`, and we + // just dropped the dependent, we know there are no outstanding borrows + // to owner. Therefore, reconstructing the original Box is okay. + unsafe { Box::from_raw(this.owner.as_ptr()) } + } + + /// Consumes the [`Pair`], dropping the dependent and returning the owner. + /// + /// If you manually box the returned owner for your own purposes, consider + /// [`Pair::into_boxed_owner`] to avoid redundant reallocation. + pub fn into_owner(self) -> O + where + O: Sized, + { + *self.into_boxed_owner() + } +} + +impl Owner = (), Error = Infallible> + ?Sized> Pair { + /// Constructs a new [`Pair`] with the given [`Owner`]. The dependent will + /// be computed through [`Owner::make_dependent`] during this construction. + /// + /// See the "Constructors" section in the documentation of [`Pair`] for + /// information on the differences between constructors. + pub fn new(owner: O) -> Self + where + O: Sized, + { + Self::new_with_context(owner, ()) + } + + /// Constructs a new [`Pair`] with the given [`Owner`]. The dependent will + /// be computed through [`Owner::make_dependent`] during this construction. + /// + /// See the "Constructors" section in the documentation of [`Pair`] for + /// information on the differences between constructors. + pub fn new_from_box(owner: Box) -> Self { + Self::new_from_box_with_context(owner, ()) + } +} + +impl Owner = ()> + ?Sized> Pair { + /// Constructs a new [`Pair`] with the given [`Owner`]. The dependent will + /// be computed through [`Owner::make_dependent`] during this construction. + /// + /// See the "Constructors" section in the documentation of [`Pair`] for + /// information on the differences between constructors. + /// + /// # Errors + /// If [`::make_dependent`](Owner::make_dependent) returns an + /// error. + pub fn try_new(owner: O) -> Result + where + O: Sized, + { + Self::try_new_with_context(owner, ()) + } + + /// Constructs a new [`Pair`] with the given [`Owner`]. The dependent will + /// be computed through [`Owner::make_dependent`] during this construction. + /// + /// See the "Constructors" section in the documentation of [`Pair`] for + /// information on the differences between constructors. + /// + /// # Errors + /// If [`::make_dependent`](Owner::make_dependent) returns an + /// error. + pub fn try_new_from_box(owner: Box) -> Result, O::Error)> { + Self::try_new_from_box_with_context(owner, ()) + } +} + +impl + ?Sized> Pair { + /// Constructs a new [`Pair`] with the given [`Owner`]. The dependent will + /// be computed through [`Owner::make_dependent`] during this construction. + /// + /// See the "Constructors" section in the documentation of [`Pair`] for + /// information on the differences between constructors. + pub fn new_with_context(owner: O, context: O::Context<'_>) -> Self + where + O: Sized, + { + let Ok(pair) = Self::try_new_with_context(owner, context); + pair + } + + /// Constructs a new [`Pair`] with the given [`Owner`]. The dependent will + /// be computed through [`Owner::make_dependent`] during this construction. + /// + /// See the "Constructors" section in the documentation of [`Pair`] for + /// information on the differences between constructors. + pub fn new_from_box_with_context(owner: Box, context: O::Context<'_>) -> Self { + let Ok(pair) = Self::try_new_from_box_with_context(owner, context); + pair + } +} + +/// The [`Drop`] implementation for [`Pair`] will drop both the dependent and +/// the owner, in that order. +// +// NOTE(ichen): There are definitely some weird dropck things going on, but I do +// not believe they can lead to any unsoundness. Because of the signature of +// Pair, dropck thinks we access an O and do nothing with O::Dependent. It's +// right about O - we don't access it directly, but the dependent (which we do +// drop) might access an O in its drop. Unfortunately, the compiler is wrong +// about O::Dependent. It doesn't see any indication of O::Dependent in the +// signature for Pair (because we've type erased it), so dropck has no idea that +// we will drop an O::Dependent in our drop. +// +// This sounds like a problem, but I believe it is not. The signature of Owner +// and HasDependent enforce that the dependent only borrows the owner, or things +// which the owner also borrows. Additionally, the compiler will ensure that +// anything the owner borrows are valid until the pair's drop. Therefore, the +// dependent cannot contain any references which will be invalidated before the +// drop of the Pair. As far as I know, this is the only concern surrounding +// dropck not understanding the semantics of Pair, and cannot cause unsoundness +// for the reasons described above. +impl Drop for Pair { + fn drop(&mut self) { + // Drop the dependent `Box>` + + // SAFETY: `self.dependent` was originally created from a Box, and never + // invalidated since then. Because we are in drop, we know there are no + // outstanding borrows to the dependent. Therefore, reconstructing the + // original Box> is okay. + let dependent = + unsafe { Box::from_raw(self.dependent.cast::>().as_ptr()) }; + + // We're about to drop the dependent - if it panics, we want to be able + // to drop the boxed owner before unwinding the rest of the stack to + // avoid unnecessarily leaking memory (and potentially other resources). + let panic_drop_guard = DropGuard(|| { + // If this code is executed, it means the dependent's drop panicked + // and we never `mem::forget(..)`'d this drop guard. Recover and + // drop the boxed owner. + + // SAFETY: `self.owner` was originally created from a Box, and never + // invalidated since then. Because we are in drop, and we just + // dropped the dependent (well, the drop panicked - but its borrow + // of the owner has certainly expired), we know there are no + // outstanding borrows to owner. Therefore, reconstructing the + // original Box is okay. + let owner: Box = unsafe { Box::from_raw(self.owner.as_ptr()) }; + + // If the owner's drop *also* panics, that will be a double-panic. + // This will cause an abort, which is fine - drops generally + // shouldn't panic, and if the user *really* wants to handle this, + // they can check if the thread is panicking within owner's drop + // before performing any operations which could panic. + drop(owner); + }); + + // Drop the dependent + drop(dependent); + + // The dependent's drop didn't panic - disarm our drop guard + core::mem::forget(panic_drop_guard); + + // Drop the owner `Box` + + // SAFETY: `self.owner` was originally created from a Box, and never + // invalidated since then. Because we are in drop, and we just dropped + // the dependent, we know there are no outstanding borrows to owner. + // Therefore, reconstructing the original Box is okay. + let owner = unsafe { Box::from_raw(self.owner.as_ptr()) }; + + drop(owner); + } +} + +// SAFETY: `Pair` has no special thread-related invariants or requirements, so +// sending a `Pair` to another thread could only cause problems if sending +// either the owner or the dependent to another thread could cause problems +// (since both are semantically moved with and made accessible through the +// `Pair`). +unsafe impl Send for Pair +where + O: Send, + for<'any> Dependent<'any, O>: Send, +{ +} + +// SAFETY: `Pair` has no special thread-related invariants or requirements, so +// sharing a reference to a `Pair` across multiple threads could only cause +// problems if sharing a reference to either the owner or the dependent across +// multiple threads could cause problems (since references to both are made +// accessible through references to the `Pair`). +unsafe impl Sync for Pair +where + O: Sync, + for<'any> Dependent<'any, O>: Sync, +{ +} + +impl Debug for Pair +where + for<'any> Dependent<'any, O>: Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.with_dependent(|dependent| { + f.debug_struct("Pair") + .field("owner", &self.owner()) + .field("dependent", dependent) + .finish() + }) + } +} + +impl Owner = (), Error = Infallible> + Default> Default for Pair { + fn default() -> Self { + Self::new(O::default()) + } +} diff --git a/tests/alternative_ctors.rs b/tests/alternative_ctors.rs new file mode 100644 index 0000000..2b299c4 --- /dev/null +++ b/tests/alternative_ctors.rs @@ -0,0 +1,148 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::convert::Infallible; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +#[derive(Debug)] +struct BuffFallible(String); + +impl<'owner> HasDependent<'owner> for BuffFallible { + type Dependent = Vec<&'owner str>; +} + +impl Owner for BuffFallible { + type Context<'a> = (); + type Error = String; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + let parts: Vec<_> = self.0.split_whitespace().collect(); + + if parts.is_empty() { + Err(String::from("Conversion failed")) + } else { + Ok(parts) + } + } +} + +#[test] +fn fallible() { + let pair = Pair::try_new(BuffFallible(String::from("This is a test of pair."))).unwrap(); + assert_eq!(pair.owner().0, "This is a test of pair."); + + let (buff, err) = Pair::try_new(BuffFallible(String::from(" "))).unwrap_err(); + assert_eq!(buff.0, " "); + assert_eq!(err, "Conversion failed"); + + let pair = Pair::try_new_from_box(Box::new(BuffFallible(String::from( + "This is a test of pair.", + )))) + .unwrap(); + assert_eq!(pair.owner().0, "This is a test of pair."); + + let (buff, err) = + Pair::try_new_from_box(Box::new(BuffFallible(String::from(" ")))).unwrap_err(); + assert_eq!(buff.0, " "); + assert_eq!(err, "Conversion failed"); +} + +#[derive(Debug)] +struct BuffWithContext(String); + +impl<'owner> HasDependent<'owner> for BuffWithContext { + type Dependent = Vec<&'owner str>; +} + +impl Owner for BuffWithContext { + type Context<'a> = &'a str; + type Error = Infallible; + + fn make_dependent( + &self, + context: Self::Context<'_>, + ) -> Result, Self::Error> { + Ok(self.0.split(context).collect()) + } +} + +#[test] +fn with_context() { + let pair = Pair::new_with_context(BuffWithContext(String::from("foo, bar, bat, baz")), ", "); + assert_eq!(pair.owner().0, "foo, bar, bat, baz"); + + let pair = Pair::new_from_box_with_context( + Box::new(BuffWithContext(String::from("foo, bar, bat, baz"))), + ", ", + ); + assert_eq!(pair.owner().0, "foo, bar, bat, baz"); +} + +#[derive(Debug)] +struct BuffFallibleWithContext(String); + +impl<'owner> HasDependent<'owner> for BuffFallibleWithContext { + type Dependent = Vec<&'owner str>; +} + +impl Owner for BuffFallibleWithContext { + type Context<'a> = &'a str; + type Error = String; + + fn make_dependent( + &self, + context: Self::Context<'_>, + ) -> Result, Self::Error> { + let parts: Vec<_> = self.0.split(context).collect(); + + if parts.len() > 1 { + Ok(parts) + } else { + Err(format!( + "Conversion of string '{}' with context '{context}' failed.", + self.0 + )) + } + } +} + +#[test] +fn fallible_with_context() { + let pair = Pair::try_new_with_context( + BuffFallibleWithContext(String::from("foo, bar, bat, baz")), + ", ", + ) + .unwrap(); + assert_eq!(pair.owner().0, "foo, bar, bat, baz"); + + let (buff, err) = Pair::try_new_with_context( + BuffFallibleWithContext(String::from("This is a test of pair.")), + ", ", + ) + .unwrap_err(); + assert_eq!(buff.0, "This is a test of pair."); + assert_eq!( + err, + "Conversion of string 'This is a test of pair.' with context ', ' failed." + ); + + let pair = Pair::try_new_from_box_with_context( + Box::new(BuffFallibleWithContext(String::from("foo, bar, bat, baz"))), + ", ", + ) + .unwrap(); + assert_eq!(pair.owner().0, "foo, bar, bat, baz"); + + let (buff, err) = Pair::try_new_from_box_with_context( + Box::new(BuffFallibleWithContext(String::from( + "This is a test of pair.", + ))), + ", ", + ) + .unwrap_err(); + assert_eq!(buff.0, "This is a test of pair."); + assert_eq!( + err, + "Conversion of string 'This is a test of pair.' with context ', ' failed." + ); +} diff --git a/tests/basic_usage.rs b/tests/basic_usage.rs new file mode 100644 index 0000000..bf69a0d --- /dev/null +++ b/tests/basic_usage.rs @@ -0,0 +1,105 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::convert::Infallible; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +#[derive(Debug, PartialEq)] +struct Buff(String); + +impl<'owner> HasDependent<'owner> for Buff { + type Dependent = Vec<&'owner str>; +} + +impl Owner for Buff { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(self.0.split_whitespace().collect()) + } +} + +#[test] +fn basic_usage() { + let mut pair = Pair::new(Buff(String::from("This is a test of pair."))); + let owner: &Buff = pair.owner(); + let dep: &Vec<&str> = pair.with_dependent(|dep| dep); + + assert_eq!(owner.0, "This is a test of pair."); + assert_eq!(dep, &["This", "is", "a", "test", "of", "pair."]); + + pair.with_dependent_mut(|dep| dep.push("hi")); + pair.with_dependent_mut(|dep| dep.push("hey")); + assert_eq!( + pair.with_both(|owner, dep| (owner, dep)), + ( + &Buff(String::from("This is a test of pair.")), + &vec!["This", "is", "a", "test", "of", "pair.", "hi", "hey"] + ) + ); + pair.with_dependent_mut(|dep| dep.sort_unstable()); + assert_eq!( + pair.with_dependent(|d| d), + &["This", "a", "hey", "hi", "is", "of", "pair.", "test"] + ); + + let last_word = pair.with_both_mut(|owner, dep| { + assert_eq!(owner, &Buff(String::from("This is a test of pair."))); + dep.pop() + }); + assert_eq!(last_word, Some("test")); + assert_eq!( + pair.with_dependent(|d| d), + &["This", "a", "hey", "hi", "is", "of", "pair."] + ); + + let owner: Buff = pair.into_owner(); + assert_eq!(owner.0, "This is a test of pair."); +} + +#[test] +fn basic_api_stress_test() { + // Let's just do a bunch of the basic API functions interlaced together and + // see what MIRI thinks + let mut pair = Pair::new(Buff(String::from("This is a test of pair."))); + let owner1 = pair.owner(); + let owner2 = pair.owner(); + let owner3 = pair.owner(); + let dep1 = pair.with_dependent(|dep| dep); + let owner4 = pair.owner(); + let (owner5, dep2) = pair.with_both(|owner, dep| (owner, dep)); + println!("{owner1:?}{owner2:?}{owner3:?}{owner4:?}{owner5:?}{dep1:?}{dep2:?}"); + pair.with_dependent_mut(|dep| dep.push("hey")); + let owner1 = pair.owner(); + let dep1 = pair.with_dependent(|dep| dep); + let owner2 = pair.owner(); + let dep2 = pair.with_dependent(|dep| dep); + println!("{owner1:?}{owner2:?}{dep1:?}{dep2:?}"); + pair.with_both_mut(|owner, dep| { + if owner.0.contains("hey") { + dep.push("what's up"); + } + }); + pair.with_dependent_mut(|dep| dep.push("hello")); + let owner1 = pair.owner(); + let owner2 = pair.owner(); + println!("{owner1:?}{owner2:?}"); + #[expect( + clippy::redundant_closure_call, + reason = "I'm just doing weird stuff for funsies and testing" + )] + let new_pair = (|x| x)(std::convert::identity(pair)); + let owner1 = new_pair.owner(); + let owner2 = new_pair.owner(); + let owner3 = new_pair.owner(); + let dep1 = new_pair.with_dependent(|dep| dep); + let owner4 = new_pair.owner(); + let (owner5, dep2) = new_pair.with_both(|owner, dep| (owner, dep)); + println!("{owner1:?}{owner2:?}{owner3:?}{owner4:?}{owner5:?}{dep1:?}{dep2:?}"); + let owner1 = new_pair.owner(); + let dep1 = new_pair.with_dependent(|dep| dep); + let owner2 = new_pair.owner(); + let dep2 = new_pair.with_dependent(|dep| dep); + println!("{owner1:?}{owner2:?}{dep1:?}{dep2:?}"); +} diff --git a/tests/compile_fails.rs b/tests/compile_fails.rs new file mode 100644 index 0000000..1250069 --- /dev/null +++ b/tests/compile_fails.rs @@ -0,0 +1,112 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::{ + ffi::OsString, + path::{Path, PathBuf}, + process::Command, +}; + +/// Ensures `pair`s build artifacts are available in the target directory, and +/// returns a path to that target directory (relative to the current working +/// directory). +fn ensure_pair_available() -> Option { + Command::new("cargo") + .arg("build") + .status() + .ok()? + .success() + .then_some(())?; + + Some( + std::env::var("CARGO_TARGET_DIR") + .map_or_else(|_| PathBuf::from("target"), PathBuf::from) + .join("debug"), + ) +} + +/// Returns the stderr from rustc attempting to compile the given file. +/// +/// Makes quite a few assumptions about the environment, namely that +/// `ensure_pair_available` has been called. +/// +/// # Panics +/// In quite a few situations, read the code lol +fn get_compiler_err(target_dir: &Path, test_file_path: &Path) -> String { + let mut pair_path_arg = OsString::from("pair="); + pair_path_arg.push(target_dir.join("libpair.rlib")); + let mut dependency_arg = OsString::from("dependency="); + dependency_arg.push(target_dir.join("deps")); + + let output = Command::new("rustc") + .arg(test_file_path) + .arg("--extern") + .arg(pair_path_arg) + .arg("-L") + .arg(dependency_arg) + .output() + .expect("failed to get output of rustc command"); + + assert!( + !output.status.success(), + "test compiled, but was expected not to: {test_file_path:?}" + ); + + String::from_utf8(output.stderr).expect("rustc output was not UTF-8") +} + +#[test] +fn compile_fail_tests_nomiri() { + // I would have preferred to use trybuild or compiletest_rs, but both seem + // to require an exact .stderr match, which is not desirable for me. I don't + // care about the *exact* error message, which may change in small ways + // between different versions of the compiler. In fact, I was using trybuild + // until I discovered that my tests fail on beta (which was 1.86.0) due to a + // tiny change to what part of the source code gets highlighted. All I + // really care about is that the code doesn't compile, and is generally for + // the reason I expect. I wasn't able to find a better way to do this than a + // custom little test framework. If you have a better idea, I'd welcome an + // issue with the suggestion :) + + // Get a list of all test files in tests/compile_fails/ + // Some majorly sauced up functional magic + let test_file_paths: Vec<_> = std::fs::read_dir("tests/compile_fails") + .and_then(|dir_iter| { + dir_iter + .filter_map(|entry| { + entry + .and_then(|entry| { + Ok((entry.file_type()?.is_file() + && entry + .path() + .extension() + .is_some_and(|extension| extension == "rs")) + .then_some(entry.path())) + }) + .transpose() + }) + .collect() + }) + .expect("failed to read `tests/compile_fails` directory"); + + // Ensure `pair`'s build artifacts are available, and get the target dir + let target_dir = ensure_pair_available().expect("failed to compile `pair`"); + + // For each file, ensure it fails to compile with the expected error message + for test_file_path in test_file_paths { + let expected_path = test_file_path.with_extension("expected"); + let expected_substrings: Vec<_> = std::fs::read_to_string(&expected_path) + .unwrap_or_else(|_| panic!("failed to read file: {expected_path:?}")) + .lines() + .map(str::to_owned) + .collect(); + + let compiler_output = get_compiler_err(&target_dir, &test_file_path); + + for expected_substring in expected_substrings { + assert!( + compiler_output.contains(&expected_substring), + "compiler error did not contain expected substring: {expected_substring}" + ); + } + } +} diff --git a/tests/compile_fails/extract_with_dep_mut.expected b/tests/compile_fails/extract_with_dep_mut.expected new file mode 100644 index 0000000..599e01b --- /dev/null +++ b/tests/compile_fails/extract_with_dep_mut.expected @@ -0,0 +1,5 @@ +`pair` does not live long enough +tests/compile_fails/extract_with_dep_mut.rs +returning this value requires that `pair` is borrowed for `'static` +borrowed value does not live long enough +`pair` dropped here while still borrowed diff --git a/tests/compile_fails/extract_with_dep_mut.rs b/tests/compile_fails/extract_with_dep_mut.rs new file mode 100644 index 0000000..b8fe553 --- /dev/null +++ b/tests/compile_fails/extract_with_dep_mut.rs @@ -0,0 +1,29 @@ +extern crate pair; + +use std::convert::Infallible; + +use pair::{HasDependent, Owner, Pair}; + +#[derive(Debug)] +struct Buff(String); + +impl<'owner> HasDependent<'owner> for Buff { + type Dependent = Vec<&'owner str>; +} + +impl Owner for Buff { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent( + &self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + Ok(self.0.split_whitespace().collect()) + } +} + +fn main() { + let mut pair = Pair::new(Buff(String::from("This is a test of pair."))); + let _: &mut Vec<&str> = pair.with_dependent_mut(|dep| dep); +} diff --git a/tests/compile_fails/invariant_dep_extraction.expected b/tests/compile_fails/invariant_dep_extraction.expected new file mode 100644 index 0000000..a447738 --- /dev/null +++ b/tests/compile_fails/invariant_dep_extraction.expected @@ -0,0 +1,5 @@ +`pair` does not live long enough +tests/compile_fails/invariant_dep_extraction.rs +returning this value requires that `pair` is borrowed for `'static` +borrowed value does not live long enough +`pair` dropped here while still borrowed diff --git a/tests/compile_fails/invariant_dep_extraction.rs b/tests/compile_fails/invariant_dep_extraction.rs new file mode 100644 index 0000000..c0eba60 --- /dev/null +++ b/tests/compile_fails/invariant_dep_extraction.rs @@ -0,0 +1,30 @@ +extern crate pair; + +use std::{cell::Cell, convert::Infallible, marker::PhantomData}; + +use pair::{HasDependent, Owner, Pair}; + +struct InvarOwner; + +impl<'owner> HasDependent<'owner> for InvarOwner { + type Dependent = PhantomData>; +} + +impl Owner for InvarOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent( + &self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + Ok(PhantomData) + } +} + +fn main() { + let pair: Pair = Pair::new(InvarOwner); + + // This should fail to compile + pair.with_dependent(|dep| dep); +} diff --git a/tests/compile_fails/keep_dep_after_into_owner.expected b/tests/compile_fails/keep_dep_after_into_owner.expected new file mode 100644 index 0000000..f26e072 --- /dev/null +++ b/tests/compile_fails/keep_dep_after_into_owner.expected @@ -0,0 +1,6 @@ +cannot move out of `pair` because it is borrowed +tests/compile_fails/keep_dep_after_into_owner.rs +binding `pair` declared here +borrow of `pair` occurs here +move out of `pair` occurs here +borrow later used here diff --git a/tests/compile_fails/keep_dep_after_into_owner.rs b/tests/compile_fails/keep_dep_after_into_owner.rs new file mode 100644 index 0000000..93cccc3 --- /dev/null +++ b/tests/compile_fails/keep_dep_after_into_owner.rs @@ -0,0 +1,35 @@ +extern crate pair; + +use std::convert::Infallible; + +use pair::{HasDependent, Owner, Pair}; + +#[derive(Debug)] +struct Buff(String); + +impl<'owner> HasDependent<'owner> for Buff { + type Dependent = Vec<&'owner str>; +} + +impl Owner for Buff { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent( + &self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + Ok(self.0.split_whitespace().collect()) + } +} + +fn main() { + let pair = Pair::new(Buff(String::from("This is a test of pair."))); + let dep: &Vec<&str> = pair.with_dependent(|dep| dep); + + let owner = pair.into_owner(); + + let _ = dep; + + drop(owner); +} diff --git a/tests/compile_fails/keep_dep_after_pair_drop.expected b/tests/compile_fails/keep_dep_after_pair_drop.expected new file mode 100644 index 0000000..0823c0b --- /dev/null +++ b/tests/compile_fails/keep_dep_after_pair_drop.expected @@ -0,0 +1,6 @@ +cannot move out of `pair` because it is borrowed +tests/compile_fails/keep_dep_after_pair_drop.rs +binding `pair` declared here +borrow of `pair` occurs here +move out of `pair` occurs here +borrow later used here diff --git a/tests/compile_fails/keep_dep_after_pair_drop.rs b/tests/compile_fails/keep_dep_after_pair_drop.rs new file mode 100644 index 0000000..0bb3a9a --- /dev/null +++ b/tests/compile_fails/keep_dep_after_pair_drop.rs @@ -0,0 +1,33 @@ +extern crate pair; + +use std::convert::Infallible; + +use pair::{HasDependent, Owner, Pair}; + +#[derive(Debug)] +struct Buff(String); + +impl<'owner> HasDependent<'owner> for Buff { + type Dependent = Vec<&'owner str>; +} + +impl Owner for Buff { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent( + &self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + Ok(self.0.split_whitespace().collect()) + } +} + +fn main() { + let pair = Pair::new(Buff(String::from("This is a test of pair."))); + let dep: &Vec<&str> = pair.with_dependent(|dep| dep); + + drop(pair); + + let _ = dep; +} diff --git a/tests/compile_fails/not_send_dep.expected b/tests/compile_fails/not_send_dep.expected new file mode 100644 index 0000000..f7219f5 --- /dev/null +++ b/tests/compile_fails/not_send_dep.expected @@ -0,0 +1,3 @@ +cannot be sent between threads safely +tests/compile_fails/not_send_dep.rs +required for `Pair` to implement `Send` diff --git a/tests/compile_fails/not_send_dep.rs b/tests/compile_fails/not_send_dep.rs new file mode 100644 index 0000000..072c8a7 --- /dev/null +++ b/tests/compile_fails/not_send_dep.rs @@ -0,0 +1,35 @@ +extern crate pair; + +use std::{convert::Infallible, sync::MutexGuard}; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +struct MyOwner; +struct NotSend(MutexGuard<'static, ()>); + +impl<'owner> HasDependent<'owner> for MyOwner { + type Dependent = NotSend; +} + +impl Owner for MyOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent<'owner>( + &'owner self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + unimplemented!() + } +} + +fn check_send() {} +fn check_sync() {} + +fn main() { + check_send::(); + check_sync::<(MyOwner, Dependent<'_, MyOwner>)>(); + + check_send::>(); + check_sync::>(); +} diff --git a/tests/compile_fails/not_send_owner.expected b/tests/compile_fails/not_send_owner.expected new file mode 100644 index 0000000..f37905f --- /dev/null +++ b/tests/compile_fails/not_send_owner.expected @@ -0,0 +1,4 @@ +cannot be sent between threads safely +tests/compile_fails/not_send_owner.rs +note: required because it appears within the type `NotSend` +required for `Pair` to implement `Send` diff --git a/tests/compile_fails/not_send_owner.rs b/tests/compile_fails/not_send_owner.rs new file mode 100644 index 0000000..a605712 --- /dev/null +++ b/tests/compile_fails/not_send_owner.rs @@ -0,0 +1,34 @@ +extern crate pair; + +use std::{convert::Infallible, sync::MutexGuard}; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +struct NotSend(MutexGuard<'static, ()>); + +impl<'owner> HasDependent<'owner> for NotSend { + type Dependent = (); +} + +impl Owner for NotSend { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent<'owner>( + &'owner self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + unimplemented!() + } +} + +fn check_send() {} +fn check_sync() {} + +fn main() { + check_send::>(); + check_sync::<(NotSend, Dependent<'_, NotSend>)>(); + + check_send::>(); + check_sync::>(); +} diff --git a/tests/compile_fails/not_sync_dep.expected b/tests/compile_fails/not_sync_dep.expected new file mode 100644 index 0000000..7de0b37 --- /dev/null +++ b/tests/compile_fails/not_sync_dep.expected @@ -0,0 +1,3 @@ +cannot be shared between threads safely +tests/compile_fails/not_sync_dep.rs +required for `Pair` to implement `Sync` diff --git a/tests/compile_fails/not_sync_dep.rs b/tests/compile_fails/not_sync_dep.rs new file mode 100644 index 0000000..ec01b53 --- /dev/null +++ b/tests/compile_fails/not_sync_dep.rs @@ -0,0 +1,35 @@ +extern crate pair; + +use std::{cell::UnsafeCell, convert::Infallible}; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +struct MyOwner; +struct NotSync(UnsafeCell<()>); + +impl<'owner> HasDependent<'owner> for MyOwner { + type Dependent = NotSync; +} + +impl Owner for MyOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent<'owner>( + &'owner self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + unimplemented!() + } +} + +fn check_send() {} +fn check_sync() {} + +fn main() { + check_send::<(MyOwner, Dependent<'_, MyOwner>)>(); + check_sync::(); + + check_send::>(); + check_sync::>(); +} diff --git a/tests/compile_fails/not_sync_owner.expected b/tests/compile_fails/not_sync_owner.expected new file mode 100644 index 0000000..3bedcc8 --- /dev/null +++ b/tests/compile_fails/not_sync_owner.expected @@ -0,0 +1,3 @@ +cannot be shared between threads safely +tests/compile_fails/not_sync_owner.rs +required for `Pair` to implement `Sync` diff --git a/tests/compile_fails/not_sync_owner.rs b/tests/compile_fails/not_sync_owner.rs new file mode 100644 index 0000000..75b0ef9 --- /dev/null +++ b/tests/compile_fails/not_sync_owner.rs @@ -0,0 +1,34 @@ +extern crate pair; + +use std::{cell::UnsafeCell, convert::Infallible}; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +struct NotSync(UnsafeCell<()>); + +impl<'owner> HasDependent<'owner> for NotSync { + type Dependent = (); +} + +impl Owner for NotSync { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent<'owner>( + &'owner self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + unimplemented!() + } +} + +fn check_send() {} +fn check_sync() {} + +fn main() { + check_send::<(NotSync, Dependent<'_, NotSync>)>(); + check_sync::>(); + + check_send::>(); + check_sync::>(); +} diff --git a/tests/compile_fails/pair_not_covariant.expected b/tests/compile_fails/pair_not_covariant.expected new file mode 100644 index 0000000..65547b0 --- /dev/null +++ b/tests/compile_fails/pair_not_covariant.expected @@ -0,0 +1,7 @@ +lifetime may not live long enough +tests/compile_fails/pair_not_covariant.rs +lifetime `'longer` defined here +lifetime `'shorter` defined here +type annotation requires that `'shorter` must outlive `'longer` +requirement occurs because of the type `Pair>`, which makes the generic argument `Foo<'_>` invariant +the struct `Pair` is invariant over the parameter `O` diff --git a/tests/compile_fails/pair_not_covariant.rs b/tests/compile_fails/pair_not_covariant.rs new file mode 100644 index 0000000..19c5369 --- /dev/null +++ b/tests/compile_fails/pair_not_covariant.rs @@ -0,0 +1,48 @@ +#![allow(unused)] + +extern crate pair; + +use std::{convert::Infallible, marker::PhantomData}; + +use pair::{HasDependent, Owner, Pair}; + +fn main() {} + +struct Foo<'a>(PhantomData<&'a ()>); + +struct Covariant(PhantomData); + +impl<'owner> HasDependent<'owner> for Foo<'_> { + type Dependent = (); +} +impl Owner for Foo<'_> { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent( + &self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + unimplemented!() + } +} + +fn uh_oh<'shorter, 'longer>() +where + 'longer: 'shorter, +{ + // Foo<'a> should be covariant over 'a, so this should be okay + let mut foo_shorter: Foo<'shorter> = Foo(PhantomData); + let foo_longer: Foo<'longer> = Foo(PhantomData); + foo_shorter = foo_longer; + + // Covariant should be covariant over T, so this should be okay + let mut cov_shorter: Covariant> = Covariant(PhantomData); + let cov_longer: Covariant> = Covariant(PhantomData); + cov_shorter = cov_longer; + + // Pair should *not* be covariant over O, so this should *not* be okay + let mut pair_shorter: Pair> = Pair::new(Foo(PhantomData)); + let pair_longer: Pair> = Pair::new(Foo(PhantomData)); + pair_shorter = pair_longer; +} diff --git a/tests/compile_fails/send_sync_miserable_failure.expected b/tests/compile_fails/send_sync_miserable_failure.expected new file mode 100644 index 0000000..122d540 --- /dev/null +++ b/tests/compile_fails/send_sync_miserable_failure.expected @@ -0,0 +1,5 @@ +cannot be sent between threads safely +tests/compile_fails/send_sync_miserable_failure.rs +required for `Pair` to implement `Send` +cannot be shared between threads safely +required for `Pair` to implement `Sync` diff --git a/tests/compile_fails/send_sync_miserable_failure.rs b/tests/compile_fails/send_sync_miserable_failure.rs new file mode 100644 index 0000000..3f85080 --- /dev/null +++ b/tests/compile_fails/send_sync_miserable_failure.rs @@ -0,0 +1,31 @@ +extern crate pair; + +use std::convert::Infallible; + +use pair::{HasDependent, Owner, Pair}; + +struct NotSendOrSync(*mut ()); + +impl<'owner> HasDependent<'owner> for NotSendOrSync { + type Dependent = NotSendOrSync; +} + +impl Owner for NotSendOrSync { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent<'owner>( + &'owner self, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + unimplemented!() + } +} + +fn check_send() {} +fn check_sync() {} + +fn main() { + check_send::>(); + check_sync::>(); +} diff --git a/tests/concurrent_loom.rs b/tests/concurrent_loom.rs new file mode 100644 index 0000000..58827cf --- /dev/null +++ b/tests/concurrent_loom.rs @@ -0,0 +1,202 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::sync::{Arc, Mutex, RwLock, mpsc}; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +// A simple owner type for testing thread safety +#[derive(Debug)] +struct Buff(String); + +// A dependent type that borrows from Buff +#[derive(Debug)] +struct Parsed<'a> { + tokens: Vec<&'a str>, +} + +fn parse(buffer: &Buff) -> Parsed<'_> { + let tokens = buffer.0.split_whitespace().collect(); + Parsed { tokens } +} + +impl<'owner> HasDependent<'owner> for Buff { + type Dependent = Parsed<'owner>; +} + +impl Owner for Buff { + type Context<'a> = (); + type Error = std::convert::Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(parse(self)) + } +} + +// Test sending ownership of a Pair between threads through channels +#[test] +fn pair_ownership_transfer_loom_nomiri() { + loom::model(|| { + let (tx, rx) = mpsc::channel(); + + let pair = Pair::new(Buff(String::from("this is a test"))); + + let t1 = loom::thread::spawn(move || { + assert_eq!(pair.owner().0, "this is a test"); + assert_eq!( + pair.with_dependent(|parsed| parsed).tokens, + ["this", "is", "a", "test"] + ); + tx.send(pair).unwrap(); + }); + + let t2 = loom::thread::spawn(move || { + let received_pair = rx.recv().unwrap(); + + assert_eq!(received_pair.owner().0, "this is a test"); + assert_eq!( + received_pair.with_dependent(|parsed| parsed).tokens, + ["this", "is", "a", "test"] + ); + + received_pair + }); + + t1.join().unwrap(); + let received_pair = t2.join().unwrap(); + + assert_eq!(received_pair.owner().0, "this is a test"); + assert_eq!( + received_pair.with_dependent(|parsed| parsed).tokens, + ["this", "is", "a", "test"] + ); + }); +} + +// Test sending and sharing a Pair via Arc> across multiple threads +#[test] +fn pair_arc_sharing_loom_nomiri() { + loom::model(|| { + let pair = Arc::new(Pair::new(Buff(String::from("arc sharing test")))); + + let pair1 = Arc::clone(&pair); + let t1 = loom::thread::spawn(move || { + assert_eq!(pair1.owner().0, "arc sharing test"); + assert_eq!( + pair1.with_dependent(|parsed| parsed).tokens, + ["arc", "sharing", "test"] + ); + }); + + let pair2 = Arc::clone(&pair); + let t2 = loom::thread::spawn(move || { + assert_eq!(pair2.owner().0, "arc sharing test"); + assert_eq!( + pair2.with_dependent(|parsed| parsed).tokens, + ["arc", "sharing", "test"] + ); + }); + + t1.join().unwrap(); + t2.join().unwrap(); + }); +} + +// Test concurrently accessing an Arc> from multiple threads +#[test] +fn pair_mutex_concurrent_access_loom_nomiri() { + loom::model(|| { + let pair = Arc::new(Mutex::new(Pair::new(Buff(String::from( + "mutex concurrent test", + ))))); + + let join_handles: Vec<_> = (0..4) + .map(|_| { + let pair_clone = Arc::clone(&pair); + loom::thread::spawn(move || { + let mut pair = pair_clone.lock().unwrap(); + assert_eq!( + pair.with_dependent(|parsed| parsed).tokens[..3], + ["mutex", "concurrent", "test"] + ); + pair.with_dependent_mut(|parsed| parsed.tokens.push("modified")); + }) + }) + .collect(); + + for join_handle in join_handles { + join_handle.join().unwrap(); + } + + let pair = Arc::into_inner(pair).unwrap().into_inner().unwrap(); + pair.with_dependent(|parsed| { + assert_eq!( + parsed.tokens, + [ + "mutex", + "concurrent", + "test", + "modified", + "modified", + "modified", + "modified", + ] + ); + }); + }); +} + +// Test concurrently accessing an Arc> from multiple threads +#[test] +fn pair_rwlock_concurrent_access_loom_nomiri() { + loom::model(|| { + let pair = Arc::new(RwLock::new(Pair::new(Buff(String::from( + "rwlock concurrent test", + ))))); + + let mut join_handles: Vec<_> = Vec::new(); + + // Reader threads + for _ in 0..2 { + let pair_clone = Arc::clone(&pair); + let join_handle = loom::thread::spawn(move || { + let pair = pair_clone.read().unwrap(); + assert_eq!( + pair.with_dependent(|parsed| parsed).tokens[..3], + ["rwlock", "concurrent", "test"] + ); + }); + + join_handles.push(join_handle); + } + + // Writer threads + for _ in 0..2 { + let pair_clone = Arc::clone(&pair); + let join_handle = loom::thread::spawn(move || { + let pair = pair_clone.read().unwrap(); + assert_eq!( + pair.with_dependent(|parsed| parsed).tokens[..3], + ["rwlock", "concurrent", "test"] + ); + drop(pair); + + let mut pair = pair_clone.write().unwrap(); + pair.with_dependent_mut(|parsed| parsed.tokens.push("modified")); + }); + + join_handles.push(join_handle); + } + + for join_handle in join_handles { + join_handle.join().unwrap(); + } + + let pair = Arc::into_inner(pair).unwrap().into_inner().unwrap(); + pair.with_dependent(|parsed| { + assert_eq!( + parsed.tokens, + ["rwlock", "concurrent", "test", "modified", "modified"] + ); + }); + }); +} diff --git a/tests/concurrent_std.rs b/tests/concurrent_std.rs new file mode 100644 index 0000000..9a6019c --- /dev/null +++ b/tests/concurrent_std.rs @@ -0,0 +1,194 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::sync::{Arc, Mutex, RwLock, mpsc}; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +// A simple owner type for testing thread safety +#[derive(Debug)] +struct Buff(String); + +// A dependent type that borrows from Buff +#[derive(Debug)] +struct Parsed<'a> { + tokens: Vec<&'a str>, +} + +fn parse(buffer: &Buff) -> Parsed<'_> { + let tokens = buffer.0.split_whitespace().collect(); + Parsed { tokens } +} + +impl<'owner> HasDependent<'owner> for Buff { + type Dependent = Parsed<'owner>; +} + +impl Owner for Buff { + type Context<'a> = (); + type Error = std::convert::Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(parse(self)) + } +} + +// Test sending ownership of a Pair between threads through channels +#[test] +fn pair_ownership_transfer() { + let (tx, rx) = mpsc::channel(); + + let pair = Pair::new(Buff(String::from("this is a test"))); + + let t1 = std::thread::spawn(move || { + assert_eq!(pair.owner().0, "this is a test"); + assert_eq!( + pair.with_dependent(|parsed| parsed).tokens, + ["this", "is", "a", "test"] + ); + tx.send(pair).unwrap(); + }); + + let t2 = std::thread::spawn(move || { + let received_pair = rx.recv().unwrap(); + + assert_eq!(received_pair.owner().0, "this is a test"); + assert_eq!( + received_pair.with_dependent(|parsed| parsed).tokens, + ["this", "is", "a", "test"] + ); + + received_pair + }); + + t1.join().unwrap(); + let received_pair = t2.join().unwrap(); + + assert_eq!(received_pair.owner().0, "this is a test"); + assert_eq!( + received_pair.with_dependent(|parsed| parsed).tokens, + ["this", "is", "a", "test"] + ); +} + +// Test sending and sharing a Pair via Arc> across multiple threads +#[test] +fn pair_arc_sharing() { + let pair = Arc::new(Pair::new(Buff(String::from("arc sharing test")))); + + let pair1 = Arc::clone(&pair); + let t1 = std::thread::spawn(move || { + assert_eq!(pair1.owner().0, "arc sharing test"); + assert_eq!( + pair1.with_dependent(|parsed| parsed).tokens, + ["arc", "sharing", "test"] + ); + }); + + let pair2 = Arc::clone(&pair); + let t2 = std::thread::spawn(move || { + assert_eq!(pair2.owner().0, "arc sharing test"); + assert_eq!( + pair2.with_dependent(|parsed| parsed).tokens, + ["arc", "sharing", "test"] + ); + }); + + t1.join().unwrap(); + t2.join().unwrap(); +} + +// Test concurrently accessing an Arc> from multiple threads +#[test] +fn pair_mutex_concurrent_access() { + let pair = Arc::new(Mutex::new(Pair::new(Buff(String::from( + "mutex concurrent test", + ))))); + + let join_handles: Vec<_> = (0..4) + .map(|_| { + let pair_clone = Arc::clone(&pair); + std::thread::spawn(move || { + let mut pair = pair_clone.lock().unwrap(); + assert_eq!( + pair.with_dependent(|parsed| parsed).tokens[..3], + ["mutex", "concurrent", "test"] + ); + pair.with_dependent_mut(|parsed| parsed.tokens.push("modified")); + }) + }) + .collect(); + + for join_handle in join_handles { + join_handle.join().unwrap(); + } + + let pair = Arc::into_inner(pair).unwrap().into_inner().unwrap(); + pair.with_dependent(|parsed| { + assert_eq!( + parsed.tokens, + [ + "mutex", + "concurrent", + "test", + "modified", + "modified", + "modified", + "modified", + ] + ); + }); +} + +// Test concurrently accessing an Arc> from multiple threads +#[test] +fn pair_rwlock_concurrent_access() { + let pair = Arc::new(RwLock::new(Pair::new(Buff(String::from( + "rwlock concurrent test", + ))))); + + let mut join_handles: Vec<_> = Vec::new(); + + // Reader threads + for _ in 0..2 { + let pair_clone = Arc::clone(&pair); + let join_handle = std::thread::spawn(move || { + let pair = pair_clone.read().unwrap(); + assert_eq!( + pair.with_dependent(|parsed| parsed).tokens[..3], + ["rwlock", "concurrent", "test"] + ); + }); + + join_handles.push(join_handle); + } + + // Writer threads + for _ in 0..2 { + let pair_clone = Arc::clone(&pair); + let join_handle = std::thread::spawn(move || { + let pair = pair_clone.read().unwrap(); + assert_eq!( + pair.with_dependent(|parsed| parsed).tokens[..3], + ["rwlock", "concurrent", "test"] + ); + drop(pair); + + let mut pair = pair_clone.write().unwrap(); + pair.with_dependent_mut(|parsed| parsed.tokens.push("modified")); + }); + + join_handles.push(join_handle); + } + + for join_handle in join_handles { + join_handle.join().unwrap(); + } + + let pair = Arc::into_inner(pair).unwrap().into_inner().unwrap(); + pair.with_dependent(|parsed| { + assert_eq!( + parsed.tokens, + ["rwlock", "concurrent", "test", "modified", "modified"] + ); + }); +} diff --git a/tests/contravariance.rs b/tests/contravariance.rs new file mode 100644 index 0000000..e3a7771 --- /dev/null +++ b/tests/contravariance.rs @@ -0,0 +1,41 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::convert::Infallible; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +struct ContraOwner; + +impl<'owner> HasDependent<'owner> for ContraOwner { + type Dependent = fn(&'owner u32); +} +impl Owner for ContraOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(|_| {}) + } +} + +fn contravariant_example<'self_borrow>(pair: &'self_borrow Pair) { + // The with_dependent function requires that the provided closure works for + // *any* dependent lifetime. Because of that, our closure cannot assume that + // the function pointer it's given can be called with any specific short + // lifetime. In fact, the *only* lifetime we can assume the function pointer + // requires is 'static - this is okay since we can give a 'static reference + // to a function pointer that expects any shorter lifetime (since &'a u32 is + // covariant in 'a). But any lifetime potentially less than 'static might + // not live as long as the body of the function pointer was allowed to + // assume - therefore, this cannot compile: + // let f: &'self_borrow for<'a> fn(&'a u32) = pair.with_dependent(|f| f); + // But this can: + let f: &'self_borrow fn(&'static u32) = pair.with_dependent(|f| f); + + f(&42); +} + +#[test] +fn examples() { + contravariant_example(&Pair::new(ContraOwner)); +} diff --git a/tests/debug.rs b/tests/debug.rs new file mode 100644 index 0000000..6721a30 --- /dev/null +++ b/tests/debug.rs @@ -0,0 +1,180 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::{borrow::Cow, convert::Infallible, fmt::Debug}; + +use pair::{Dependent, HasDependent, Owner}; + +mod real { + pub use pair::Pair; +} + +mod fake { + use std::fmt::Debug; + + use pair::{Dependent, Owner}; + + #[derive(Debug)] + pub struct Pair<'a, O: Owner> + where + Dependent<'a, O>: Debug, + { + #[expect(dead_code, reason = "we actually care about the derived Debug")] + pub owner: O, + #[expect(dead_code, reason = "we actually care about the derived Debug")] + pub dependent: Dependent<'a, O>, + } +} + +#[expect( + clippy::needless_pass_by_value, + reason = "more ergonomic in the a later macro invocation" +)] +fn debugs_match Owner = (), Error = Infallible> + Clone + Debug>( + owner: O, +) where + for<'any> Dependent<'any, O>: Debug, +{ + let Ok(dependent) = owner.make_dependent(()); + let pair_real = real::Pair::new(owner.clone()); + let pair_fake = fake::Pair { + owner: owner.clone(), + dependent, + }; + + // Normal debug (and pretty-print) + assert_eq!(format!("{pair_real:?}"), format!("{pair_fake:?}")); + assert_eq!(format!("{pair_real:#?}"), format!("{pair_fake:#?}")); + + // Hex integers (lowercase and uppercase) + // See: https://doc.rust-lang.org/std/fmt/index.html#formatting-traits + assert_eq!(format!("{pair_real:x?}"), format!("{pair_fake:x?}")); + assert_eq!(format!("{pair_real:X?}"), format!("{pair_fake:X?}")); + assert_eq!(format!("{pair_real:#x?}"), format!("{pair_fake:#x?}")); + assert_eq!(format!("{pair_real:#X?}"), format!("{pair_fake:#X?}")); + + // Getting crazy with it (I'm not gonna test every combination, but I'm down + // to just throw a bunch of random stuff at it and make sure that works out) + // + // The "🦀^+#12.5?" means: ferris fill, center aligned, with sign, pretty + // printed, no "0" option integer formatting (would override fill/align), + // width 12, 5 digits of precision, debug formatted. + // + // See: https://doc.rust-lang.org/std/fmt/index.html#formatting-parameters + assert_eq!( + format!("{pair_real:🦀^+#12.5?}"), + format!("{pair_fake:🦀^+#12.5?}") + ); + assert_eq!( + format!("{pair_real:🦀^+#12.5?}"), + format!("{pair_fake:🦀^+#12.5?}") + ); +} + +macro_rules! debug_tests { + ( + $struct_or_enum:ident $name:ident $decl_body:tt $(; $semicolon_exists:vis)? + & $lt:lifetime $self_kw:ident => $dep_ty:ty : $dep_expr:expr ; + + $($owner:expr),+$(,)? + ) => { + #[derive(Debug, Clone)] + $struct_or_enum $name $decl_body $(; $semicolon_exists)? + + impl<$lt> HasDependent<$lt> for $name { + #![allow(single_use_lifetimes, reason = "macros lol")] + type Dependent = $dep_ty; + } + impl Owner for $name { + type Context<'a> = (); + type Error = Infallible; + fn make_dependent( + &$self_kw, + (): Self::Context<'_>, + ) -> Result, Self::Error> { + Ok($dep_expr) + } + } + $( + debugs_match($owner); + )+ + }; +} + +#[test] +fn debug_impls_match_derive_nomiri() { + debug_tests! { + struct O1(String); + &'owner self => &'owner str: &self.0; + + O1(String::new()), + O1(String::from("Hello, world!")), + } + + debug_tests! { + struct O2(i32); + &'owner self => &'owner i32: &self.0; + + O2(0), + O2(i32::MAX), + } + + debug_tests! { + struct O3(char); + &'owner self => Option>: + Some(std::iter::repeat_n((), self.0 as usize % 20).collect()); + + O3('🦀'), O3(' '), O3('!'), O3('a'), O3('A'), O3('*'), O3('-'), O3('~'), + O3('\\'), O3('"'), O3('\x00'), O3('\n'), O3('\t'), O3('\''), O3('&'), + } + + debug_tests! { + struct O4(f64); + &'owner self => u8: + self.0 + .to_be_bytes() + .iter() + .copied() + .reduce(u8::wrapping_add) + .unwrap(); + + O4(0.0), O4(-0.0), O4(1.0), O4(std::f64::consts::PI), O4(f64::NAN), + O4(f64::INFINITY), O4(f64::NEG_INFINITY), O4(f64::EPSILON), + } + + debug_tests! { + struct O5([isize; 42]); + &'owner self => &'owner [isize]: &self.0[5..24]; + + O5([0; 42]), O5([69; 42]), + } + + debug_tests! { + struct O6(Vec, [char; 6], Option<()>); + &'owner self => (&'owner char, Cow<'owner, str>, Option<&'owner ()>): + (&self.1[1], String::from_utf8_lossy(&self.0), self.2.as_ref()); + + O6(b"Foo bar bat baz".to_vec(), ['f', 'o', 'o', 'b', 'a', 'r'], Some(())), + O6(b"My friend, I wish that I could say that I agree".to_vec(), ['g', 't', 'w', 'w', 'o', 'a'], None), + } + + debug_tests! { + enum O7 { + Foo, + Bar(u8), + Bat(String), + Baz { + name: String, + age: u8, + }, + } + &'owner self => Option>: match self { + O7::Foo => None, + O7::Bar(n) => Some(Cow::Owned(format!("{n}"))), + O7::Bat(s) => Some(Cow::Borrowed(s)), + O7::Baz { name, age } => Some(Cow::Owned(format!("{name} is {age}"))), + }; + + O7::Foo, O7::Bar(0), O7::Bar(1), O7::Bar(42), O7::Bar(69), O7::Bar(u8::MAX), + O7::Bat(String::from("testing")), O7::Baz { name: String::from("Hermes"), age: u8::MAX }, + } +} diff --git a/tests/default.rs b/tests/default.rs new file mode 100644 index 0000000..5cf423f --- /dev/null +++ b/tests/default.rs @@ -0,0 +1,58 @@ +#![allow(missing_docs, reason = "integration test")] + +use pair::{Dependent, HasDependent, Owner, Pair}; +use std::{convert::Infallible, fmt::Debug}; + +#[derive(Default, Debug, PartialEq)] +struct DefaultOwner(T); + +impl<'owner, T> HasDependent<'owner> for DefaultOwner { + type Dependent = &'owner T; +} + +impl Owner for DefaultOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(&self.0) + } +} + +fn test_default() { + let pair: Pair> = Pair::default(); + + assert_eq!(pair.owner().0, T::default()); + pair.with_dependent(|dep| { + assert_eq!(dep, &&T::default()); + }); + + assert_eq!(pair.into_owner().0, T::default()); +} + +#[test] +fn test_default_implementation() { + test_default::<()>(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::(); + test_default::<&str>(); + test_default::<[u8; 10]>(); + test_default::>(); + test_default::>(); + test_default::(); + test_default::>(); +} diff --git a/tests/drop.rs b/tests/drop.rs new file mode 100644 index 0000000..158e10f --- /dev/null +++ b/tests/drop.rs @@ -0,0 +1,89 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::{cell::RefCell, convert::Infallible, rc::Rc}; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +#[derive(Debug)] +struct OnDrop { + value: Rc>, + f: fn(&mut T), +} +impl Drop for OnDrop { + fn drop(&mut self) { + (self.f)(&mut self.value.borrow_mut()); + } +} + +struct OnDropDep { + value: Rc>, + f: fn(&mut T), +} +impl Drop for OnDropDep { + fn drop(&mut self) { + (self.f)(&mut self.value.borrow_mut()); + } +} +impl HasDependent<'_> for OnDrop { + type Dependent = OnDropDep; +} + +impl Owner for OnDrop { + type Context<'a> = (Rc>, fn(&mut T)); + type Error = Infallible; + + fn make_dependent( + &self, + context: Self::Context<'_>, + ) -> Result, Self::Error> { + Ok(OnDropDep { + value: context.0, + f: context.1, + }) + } +} + +#[test] +fn both_drops_called() { + // Using a common Vec instead of two bools, since we care about drop order + let drops_called = Rc::new(RefCell::new(Vec::new())); + let pair = Pair::new_with_context( + OnDrop { + value: Rc::clone(&drops_called), + f: |v| v.push("owner"), + }, + (Rc::clone(&drops_called), |v| v.push("dep")), + ); + + assert_eq!(*drops_called.borrow(), [] as [&str; 0]); + + drop(pair); + + assert_eq!(*drops_called.borrow(), ["dep", "owner"]); +} + +#[test] +fn dep_drop_called_on_into_owner() { + let owner_drop_called = Rc::new(RefCell::new(false)); + let dep_drop_called = Rc::new(RefCell::new(false)); + let pair = Pair::new_with_context( + OnDrop { + value: Rc::clone(&owner_drop_called), + f: |d| *d = true, + }, + (Rc::clone(&dep_drop_called), |d| *d = true), + ); + + assert!(!*owner_drop_called.borrow()); + assert!(!*dep_drop_called.borrow()); + + let owner = pair.into_owner(); + + assert!(!*owner_drop_called.borrow()); + assert!(*dep_drop_called.borrow()); + + drop(owner); + + assert!(*owner_drop_called.borrow()); + assert!(*dep_drop_called.borrow()); +} diff --git a/tests/dyn_trait_owner.rs b/tests/dyn_trait_owner.rs new file mode 100644 index 0000000..e96af12 --- /dev/null +++ b/tests/dyn_trait_owner.rs @@ -0,0 +1,42 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::convert::Infallible; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +trait MyTrait { + fn get(&self) -> &i32; +} + +struct MyConcrete(i32); + +impl MyTrait for MyConcrete { + fn get(&self) -> &i32 { + &self.0 + } +} + +impl<'owner> HasDependent<'owner> for dyn MyTrait { + type Dependent = &'owner i32; +} +impl Owner for dyn MyTrait { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(self.get()) + } +} + +#[test] +fn dyn_trait_owner() { + let pair = Pair::new_from_box(Box::new(MyConcrete(69)) as Box); + let owner: &dyn MyTrait = pair.owner(); + let dep: &i32 = pair.with_dependent(|dep| dep); + + assert_eq!(owner.get(), &69); + assert_eq!(dep, &69); + + let owner: Box = pair.into_boxed_owner(); + assert_eq!(owner.get(), &69); +} diff --git a/tests/interior_mutability.rs b/tests/interior_mutability.rs new file mode 100644 index 0000000..22a9d60 --- /dev/null +++ b/tests/interior_mutability.rs @@ -0,0 +1,121 @@ +#![allow(missing_docs, reason = "integration test")] + +use pair::{Dependent, HasDependent, Owner, Pair}; +use std::cell::{Cell, RefCell}; +use std::convert::Infallible; + +// Test with interior-mutable owner +struct InteriorMutableOwner { + value: RefCell, +} + +impl<'owner> HasDependent<'owner> for InteriorMutableOwner { + type Dependent = &'owner RefCell; +} + +impl Owner for InteriorMutableOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(&self.value) + } +} + +#[test] +fn test_interior_mutable_owner() { + let pair = Pair::new(InteriorMutableOwner { + value: RefCell::new(42), + }); + + // Mutate the owner's value through the RefCell + *pair.owner().value.borrow_mut() = 100; + + // Verify the change is visible to the dependent + assert_eq!(*pair.with_dependent(|dep| dep).borrow(), 100); + + // Mutate the owner's value through the dependent + *pair.with_dependent(|dep| dep).borrow_mut() = 210; + + // Verify the change is visible to the owner + assert_eq!(*pair.owner().value.borrow(), 210); +} + +// Test with interior-mutable dependent +struct RegularOwner { + value: i32, +} + +struct InteriorMutableDependent<'a> { + value_cell: Cell, + original_ref: &'a i32, +} + +impl<'owner> HasDependent<'owner> for RegularOwner { + type Dependent = InteriorMutableDependent<'owner>; +} + +impl Owner for RegularOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(InteriorMutableDependent { + value_cell: Cell::new(self.value), + original_ref: &self.value, + }) + } +} + +#[test] +fn test_interior_mutable_dependent() { + let pair = Pair::new(RegularOwner { value: 42 }); + + // Mutate the dependent's Cell value + pair.with_dependent(|dep| dep.value_cell.set(100)); + assert_eq!(pair.with_dependent(|dep| dep).value_cell.get(), 100); + // The RegularOwner's original value should be unchanged + assert_eq!(pair.with_dependent(|dep| dep).original_ref, &42); +} + +// Test with interior-mutable owner and dependent +struct BothInteriorMutableOwner { + value: RefCell, +} + +struct BothInteriorMutableDependent<'a> { + owner_ref: &'a RefCell, + local_value: Cell, +} + +impl<'owner> HasDependent<'owner> for BothInteriorMutableOwner { + type Dependent = BothInteriorMutableDependent<'owner>; +} + +impl Owner for BothInteriorMutableOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(BothInteriorMutableDependent { + owner_ref: &self.value, + local_value: Cell::new(*self.value.borrow()), + }) + } +} + +#[test] +fn test_both_interior_mutable() { + let pair = Pair::new(BothInteriorMutableOwner { + value: RefCell::new(42), + }); + + // Mutate the owner + *pair.owner().value.borrow_mut() = 100; + assert_eq!(*pair.with_dependent(|dep| dep).owner_ref.borrow(), 100); + assert_eq!(pair.with_dependent(|dep| dep).local_value.get(), 42); + + // Mutate the dependent + pair.with_dependent(|dep| dep).local_value.set(200); + assert_eq!(pair.with_dependent(|dep| dep).local_value.get(), 200); +} diff --git a/tests/multiborrow.rs b/tests/multiborrow.rs new file mode 100644 index 0000000..8a56999 --- /dev/null +++ b/tests/multiborrow.rs @@ -0,0 +1,60 @@ +#![allow(missing_docs, reason = "integration test")] + +use pair::{Dependent, HasDependent, Owner, Pair}; +use std::convert::Infallible; + +#[derive(PartialEq)] +struct MultiPartOwner { + field1: String, + field2: Vec, + field3: bool, + field4: Box, +} + +struct MultiPartDependent<'a> { + string: &'a str, + int: &'a i32, + boolean: &'a bool, +} + +impl<'owner> HasDependent<'owner> for MultiPartOwner { + type Dependent = MultiPartDependent<'owner>; +} + +impl Owner for MultiPartOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(MultiPartDependent { + string: &self.field1, + int: &self.field2[0], + boolean: &self.field3, + }) + } +} + +#[test] +fn test_multiple_borrows() { + #![expect(clippy::bool_assert_comparison, reason = "for clarity and consistency")] + + let owner = MultiPartOwner { + field1: "Hello, world!".to_string(), + field2: vec![3, 1, 4, 1, 5], + field3: true, + field4: Box::default(), + }; + + let pair = Pair::new(owner); + + pair.with_dependent(|dep| { + assert_eq!(dep.string, "Hello, world!"); + assert_eq!(*dep.int, 3); + assert_eq!(*dep.boolean, true); + }); + + let owner = pair.into_owner(); + assert_eq!(owner.field1, "Hello, world!"); + assert_eq!(owner.field2, [3, 1, 4, 1, 5]); + assert_eq!(owner.field3, true); +} diff --git a/tests/nested_pair.rs b/tests/nested_pair.rs new file mode 100644 index 0000000..3f3d533 --- /dev/null +++ b/tests/nested_pair.rs @@ -0,0 +1,74 @@ +#![allow(missing_docs, reason = "integration test")] + +use pair::{Dependent, HasDependent, Owner, Pair}; +use std::convert::Infallible; + +#[derive(Debug)] +struct SimpleOwner(i32); + +impl<'owner> HasDependent<'owner> for SimpleOwner { + type Dependent = &'owner i32; +} + +impl Owner for SimpleOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(&self.0) + } +} + +// Pair that owns another Pair +#[derive(Debug)] +struct PairOwner { + value: i32, + inner_pair: Pair, +} + +#[derive(Debug)] +struct PairOwnerDependent<'owner> { + value_ref: &'owner i32, + inner_pair_ref: &'owner Pair, +} + +impl<'owner> HasDependent<'owner> for PairOwner { + type Dependent = PairOwnerDependent<'owner>; +} + +impl Owner for PairOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(PairOwnerDependent { + value_ref: &self.value, + inner_pair_ref: &self.inner_pair, + }) + } +} + +#[test] +fn test_pair_owning_pair() { + let inner_pair = Pair::new(SimpleOwner(42)); + + let pair_owner = PairOwner { + value: 100, + inner_pair, + }; + + let outer_pair = Pair::new(pair_owner); + + outer_pair.with_dependent(|outer_dep| { + assert_eq!(*outer_dep.value_ref, 100); + + // Access the inner pair's owner and dependent + assert_eq!(outer_dep.inner_pair_ref.owner().0, 42); + assert_eq!(outer_dep.inner_pair_ref.with_dependent(|dep| dep), &&42); + }); + + let PairOwner { value, inner_pair } = outer_pair.into_owner(); + assert_eq!(value, 100); + assert_eq!(inner_pair.owner().0, 42); + assert_eq!(inner_pair.with_dependent(|dep| dep), &&42); +} diff --git a/tests/panic_safety.rs b/tests/panic_safety.rs new file mode 100644 index 0000000..7a1aa40 --- /dev/null +++ b/tests/panic_safety.rs @@ -0,0 +1,131 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::{ + cell::RefCell, + convert::Infallible, + panic::{AssertUnwindSafe, catch_unwind, panic_any}, + rc::Rc, +}; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +#[derive(Debug, PartialEq, Eq)] +struct MyPayload(u8); + +// make_dependent panics +struct PanicOnMakeDependent(Rc>); +impl Drop for PanicOnMakeDependent { + fn drop(&mut self) { + *self.0.borrow_mut() = true; + } +} + +impl HasDependent<'_> for PanicOnMakeDependent { + type Dependent = (); +} + +impl Owner for PanicOnMakeDependent { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + panic_any(MyPayload(7)); + } +} + +#[test] +fn make_dependent_panic_handled() { + let owner_drop_called = Rc::new(RefCell::new(false)); + let owner_drop_called2 = Rc::clone(&owner_drop_called); + let payload: MyPayload = *catch_unwind(AssertUnwindSafe(|| { + Pair::new(PanicOnMakeDependent(owner_drop_called2)); + })) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(payload, MyPayload(7)); + assert!(*owner_drop_called.borrow()); +} + +// dependent drop panics in into_owner +#[derive(Debug)] +struct PanicOnDepDropIntoOwner(Rc>); +impl Drop for PanicOnDepDropIntoOwner { + fn drop(&mut self) { + *self.0.borrow_mut() = true; + } +} + +struct PanicOnDepDropIntoOwnerDep; +impl Drop for PanicOnDepDropIntoOwnerDep { + fn drop(&mut self) { + panic_any(MyPayload(11)); + } +} +impl HasDependent<'_> for PanicOnDepDropIntoOwner { + type Dependent = PanicOnDepDropIntoOwnerDep; +} + +impl Owner for PanicOnDepDropIntoOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(PanicOnDepDropIntoOwnerDep) + } +} + +#[test] +fn dependent_drop_panic_handled_in_into_owner() { + let owner_drop_called = Rc::new(RefCell::new(false)); + let pair = Pair::new(PanicOnDepDropIntoOwner(Rc::clone(&owner_drop_called))); + let payload: MyPayload = *catch_unwind(AssertUnwindSafe(|| pair.into_owner())) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(payload, MyPayload(11)); + assert!(*owner_drop_called.borrow()); +} + +// dependent drop panics in pair drop +#[derive(Debug)] +struct PanicOnDepDropPairDrop(Rc>); +impl Drop for PanicOnDepDropPairDrop { + fn drop(&mut self) { + *self.0.borrow_mut() = true; + } +} + +struct PanicOnDepDropPairDropDep; +impl Drop for PanicOnDepDropPairDropDep { + fn drop(&mut self) { + panic_any(MyPayload(3)); + } +} +impl HasDependent<'_> for PanicOnDepDropPairDrop { + type Dependent = PanicOnDepDropPairDropDep; +} + +impl Owner for PanicOnDepDropPairDrop { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(PanicOnDepDropPairDropDep) + } +} + +#[test] +fn dependent_drop_panic_handled_in_pair_drop() { + let owner_drop_called = Rc::new(RefCell::new(false)); + let pair = Pair::new(PanicOnDepDropPairDrop(Rc::clone(&owner_drop_called))); + let payload: MyPayload = *catch_unwind(AssertUnwindSafe(|| drop(pair))) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(payload, MyPayload(3)); + assert!(*owner_drop_called.borrow()); +} diff --git a/tests/unsized_owner.rs b/tests/unsized_owner.rs new file mode 100644 index 0000000..7b5706f --- /dev/null +++ b/tests/unsized_owner.rs @@ -0,0 +1,64 @@ +#![allow(missing_docs, reason = "integration test")] + +use std::convert::Infallible; + +use pair::{Dependent, HasDependent, Owner, Pair}; + +#[derive(Debug)] +struct Buff(T); + +impl Buff<[u8]> { + pub fn new(data: [u8; N]) -> Box { + Box::new(Buff(data)) + } +} + +impl<'owner> HasDependent<'owner> for Buff<[u8]> { + type Dependent = (&'owner [u8], usize); +} + +impl Owner for Buff<[u8]> { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + let start = usize::min(1, self.0.len()); + let end = self.0.len().saturating_sub(1); + Ok((&self.0[start..end], self.0.len())) + } +} + +#[test] +fn unsized_owner() { + let mut pair = Pair::new_from_box(Buff::new([2, 69, 42, 5, 6, 7, 8])); + let owner: &Buff<[u8]> = pair.owner(); + let dep: &(&[u8], usize) = pair.with_dependent(|dep| dep); + + assert_eq!(owner.0, [2, 69, 42, 5, 6, 7, 8]); + assert_eq!(*dep, ([69, 42, 5, 6, 7].as_slice(), 7)); + + pair.with_dependent_mut(|dep| dep.1 -= 2); + pair.with_dependent_mut(|dep| dep.1 *= 10); + assert_eq!( + *pair.with_dependent(|d| d), + ([69, 42, 5, 6, 7].as_slice(), 50) + ); + pair.with_dependent_mut(|dep| std::ops::AddAssign::add_assign(&mut dep.1, 5)); + assert_eq!( + *pair.with_dependent(|d| d), + ([69, 42, 5, 6, 7].as_slice(), 55) + ); + + let n = pair.with_dependent_mut(|dep| { + dep.1 = 0; + dep.1 + }); + assert_eq!(n, 0); + assert_eq!( + *pair.with_dependent(|d| d), + ([69, 42, 5, 6, 7].as_slice(), 0) + ); + + let owner: Box> = pair.into_boxed_owner(); + assert_eq!(owner.0, [2, 69, 42, 5, 6, 7, 8]); +} diff --git a/tests/zst.rs b/tests/zst.rs new file mode 100644 index 0000000..7d9d031 --- /dev/null +++ b/tests/zst.rs @@ -0,0 +1,167 @@ +#![allow(missing_docs, reason = "integration test")] + +use pair::{Dependent, HasDependent, Owner, Pair}; +use std::convert::Infallible; + +// 1-ZST owner with non-ZST dependent +#[derive(Debug)] +struct OneZstOwner; +const _: () = assert!(align_of::() == 1); + +#[derive(Debug, PartialEq)] +struct NonZstDependent(i32); + +impl HasDependent<'_> for OneZstOwner { + type Dependent = NonZstDependent; +} + +impl Owner for OneZstOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(NonZstDependent(42)) + } +} + +#[test] +fn test_1zst_owner() { + let pair = Pair::new(OneZstOwner); + + assert_eq!(size_of_val(pair.owner()), 0); + assert_eq!(*pair.with_dependent(|dep| dep), NonZstDependent(42)); +} + +// Non-ZST owner with 1-ZST dependent +#[derive(Debug, PartialEq)] +struct Non1ZstOwner(i32); + +#[derive(Debug)] +struct OneZstDependent; +const _: () = assert!(align_of::() == 1); + +impl HasDependent<'_> for Non1ZstOwner { + type Dependent = OneZstDependent; +} + +impl Owner for Non1ZstOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(OneZstDependent) + } +} + +#[test] +fn test_1zst_dependent() { + let pair = Pair::new(Non1ZstOwner(123)); + + assert_eq!(*pair.owner(), Non1ZstOwner(123)); + assert_eq!(size_of_val(pair.with_dependent(|dep| dep)), 0); +} + +// Both owner and dependent are 1-ZSTs +struct Both1ZstOwner; +const _: () = assert!(align_of::() == 1); + +impl HasDependent<'_> for Both1ZstOwner { + type Dependent = OneZstDependent; +} + +impl Owner for Both1ZstOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(OneZstDependent) + } +} + +#[test] +fn test_both_1zst() { + let pair = Pair::new(Both1ZstOwner); + assert_eq!(size_of_val(pair.owner()), 0); + assert_eq!(size_of_val(pair.with_dependent(|dep| dep)), 0); +} + +// ///////////////////////////////////////////////////////////////////////////// + +// Non-1 alignment ZST owner with non-ZST dependent +#[derive(Debug)] +struct BigZstOwner([u64; 0]); +const _: () = assert!(align_of::() > 1); + +impl HasDependent<'_> for BigZstOwner { + type Dependent = NonZstDependent; +} + +impl Owner for BigZstOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(NonZstDependent(23)) + } +} + +#[test] +fn test_bigzst_owner() { + let pair = Pair::new(BigZstOwner([])); + + assert_eq!(size_of_val(pair.owner()), 0); + assert_eq!(*pair.with_dependent(|dep| dep), NonZstDependent(23)); +} + +// Non-ZST owner with Non-1 alignment ZST dependent +#[derive(Debug, PartialEq)] +struct NonBigZstOwner(i32); + +#[derive(Debug)] +struct BigZstDependent([u64; 0]); +const _: () = assert!(align_of::() > 1); + +impl HasDependent<'_> for NonBigZstOwner { + type Dependent = BigZstDependent; +} + +impl Owner for NonBigZstOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(BigZstDependent([])) + } +} + +#[test] +fn test_bigzst_dependent() { + let pair = Pair::new(NonBigZstOwner(789)); + + assert_eq!(*pair.owner(), NonBigZstOwner(789)); + assert_eq!(size_of_val(pair.with_dependent(|dep| dep)), 0); +} + +// Both owner and dependent are Non-1 alignment ZSTs +struct BothBigZstOwner([u64; 0]); +const _: () = assert!(align_of::() > 1); + +impl HasDependent<'_> for BothBigZstOwner { + type Dependent = BigZstDependent; +} + +impl Owner for BothBigZstOwner { + type Context<'a> = (); + type Error = Infallible; + + fn make_dependent(&self, (): Self::Context<'_>) -> Result, Self::Error> { + Ok(BigZstDependent([])) + } +} + +#[test] +fn test_both_bigzst() { + let pair = Pair::new(BothBigZstOwner([])); + assert_eq!(size_of_val(pair.owner()), 0); + assert_eq!(size_of_val(pair.with_dependent(|dep| dep)), 0); +}