diff --git a/Cargo.lock b/Cargo.lock index 8b63a57..c83ef04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,31 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "future_form" +version = "0.2.0" +dependencies = [ + "future_form_macros", + "futures", + "tokio", + "trybuild", +] + +[[package]] +name = "future_form_macros" +version = "0.2.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures" version = "0.3.31" @@ -64,23 +89,39 @@ dependencies = [ ] [[package]] -name = "futures_kind" -version = "0.1.1" -dependencies = [ - "futures", - "futures_kind_macros", - "tokio", -] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "futures_kind_macros" -version = "0.1.1" +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ - "proc-macro2", - "quote", - "syn", + "equivalent", + "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -111,6 +152,57 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "syn" version = "2.0.114" @@ -122,6 +214,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "tokio" version = "1.49.0" @@ -143,8 +250,98 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "trybuild" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17e807bff86d2a06b52bca4276746584a78375055b6e45843925ce2802b335" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Cargo.toml b/Cargo.toml index dc8ff75..b3a3d85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,24 @@ [workspace] resolver = "3" -members = ["futures_kind", "futures_kind_macros"] +members = ["future_form", "future_form_macros"] [workspace.package] -version = "0.1.1" +version = "0.2.0" edition = "2024" rust-version = "1.90" license = "MIT OR Apache-2.0" -repository = "https://codeberg.org/expede/futures_kind" +repository = "https://codeberg.org/expede/future_form" authors = ["Brooklyn Zelenka "] +[workspace.dependencies] +futures = { version = "0.3.31", default-features = false, features = ["alloc"] } +future_form_macros = { version = "0.2.0", path = "future_form_macros" } +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "visit", "visit-mut"] } +tokio = { version = "1.0", features = ["rt", "macros"] } +trybuild = "1.0" + [workspace.lints.rust] future_incompatible = { level = "warn", priority = -1 } let_underscore = { level = "warn", priority = -1 } @@ -42,11 +51,3 @@ wildcard_enum_match_arm = "warn" all = { level = "deny", priority = -1 } cargo = { level = "deny", priority = -1 } pedantic = { level = "deny", priority = -1 } - -[workspace.dependencies] -futures = { version = "0.3.31", default-features = false, features = ["alloc"] } -futures_kind_macros = { version = "0.1.0", path = "futures_kind_macros" } -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["full"] } -tokio = { version = "1", features = ["rt", "macros"] } diff --git a/DESIGN.md b/DESIGN.md index c376dc0..3127f62 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2,7 +2,7 @@ ## Performance Impact of Boxing Futures -`futures_kind` uses `BoxFuture` and `LocalBoxFuture` under the hood. This section discusses the performance implications of that choice. +`future_form` uses `BoxFuture` and `LocalBoxFuture` under the hood. This section discusses the performance implications of that choice. ### Costs diff --git a/EXTENSIONS.md b/EXTENSIONS.md index a9cc9ed..5a327ef 100644 --- a/EXTENSIONS.md +++ b/EXTENSIONS.md @@ -1,11 +1,11 @@ -# Extending `FutureKind` +# Extending `FutureForm` -The `FutureKind` trait is open for extension. This document sketches out possible implementations beyond the built-in `Sendable` and `Local`. +The `FutureForm` trait is open for extension. This document sketches out possible implementations beyond the built-in `Sendable` and `Local`. ## The Constraint -```rust -pub trait FutureKind { +```rust,ignore +pub trait FutureForm { type Future<'a, T: 'a>: Future + 'a; } ``` @@ -17,62 +17,45 @@ Any implementation must provide a `Future` type that: This rules out concrete async block types (which have fixed output types), but allows any generic future wrapper. -## SmallBoxed Futures +## Traced Futures (Hypothetical) -Using stack allocation with heap fallback via the `smallbox` crate: +A `FutureForm` that logs every poll — useful for debugging or instrumentation: -```rust -use smallbox::{SmallBox, space::S16}; +```rust,ignore use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; +use futures::future::BoxFuture; -/// Stack-allocated futures with heap fallback for large futures. -pub struct SmallBoxed; +pub struct Traced; -/// A future that lives on the stack if small enough, otherwise heap. -pub struct SmallBoxFuture<'a, T>( - Pin + Send + 'a, S16>> -); +pub struct TracedFuture<'a, T> { + inner: BoxFuture<'a, T>, + name: &'static str, +} -impl<'a, T> Future for SmallBoxFuture<'a, T> { +impl<'a, T> Future for TracedFuture<'a, T> { type Output = T; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.0.as_mut().poll(cx) + let this = self.as_mut().get_mut(); + eprintln!("[trace] polling {}", this.name); + Pin::new(&mut this.inner).poll(cx) } } -impl FutureKind for SmallBoxed { - type Future<'a, T: 'a> = SmallBoxFuture<'a, T>; -} - -// Constructor helper -impl<'a, T> SmallBoxFuture<'a, T> { - pub fn new(future: F) -> Self - where - F: Future + Send + 'a, - { - Self(SmallBox::new_pin(future)) - } +impl FutureForm for Traced { + type Future<'a, T: 'a> = TracedFuture<'a, T>; } ``` -Usage: -```rust -#[kinds(SmallBoxed)] -impl MyTrait for MyType { - fn operation(&self) -> K::Future<'_, u32> { - SmallBoxFuture::new(async { 42 }) - } -} -``` +This pattern extends to metrics collection, cancellation tokens, or any wrapper that needs to observe or modify future behavior. ## Result-Wrapped Futures For operations that might fail at construction time: -```rust +```rust,ignore use std::future::{Future, Ready, ready}; use std::pin::Pin; use std::task::{Context, Poll}; @@ -97,7 +80,7 @@ impl<'a, T> Future for FallibleFuture<'a, T> { } } -impl FutureKind for Fallible { +impl FutureForm for Fallible { type Future<'a, T: 'a> = FallibleFuture<'a, T>; } ``` @@ -106,19 +89,19 @@ impl FutureKind for Fallible { True static dispatch without boxing is possible but limited. The future type must be nameable and generic: -```rust +```rust,ignore use std::future::Ready; /// For operations that complete immediately. pub struct Immediate; -impl FutureKind for Immediate { +impl FutureForm for Immediate { type Future<'a, T: 'a> = Ready; } ``` Usage: -```rust +```rust,ignore impl MyTrait for MyType { fn get_value(&self) -> Ready { std::future::ready(42) @@ -131,8 +114,8 @@ This works for `Ready`, `Pending`, or any nameable future type, but *not* for as ## Why Not Fully Unboxed? You might want: -```rust -impl FutureKind for Unboxed { +```rust,ignore +impl FutureForm for Unboxed { type Future<'a, T: 'a> = impl Future; // Not valid } ``` @@ -140,77 +123,55 @@ impl FutureKind for Unboxed { This doesn't work because: 1. Associated types can't use `impl Trait` 2. Async blocks have anonymous, unnameable types -3. Each async block creates a *different* type, but `FutureKind::Future` must be a single type +3. Each async block creates a *different* type, but `FutureForm::Future` must be a single type The boxing in `Sendable`/`Local` is precisely what enables type erasure across different async implementations. -## The `IntoFutureKind` Trait +## The `FromFuture` Trait -The crate provides an `IntoFutureKind` trait that abstracts over lifting a future into a kind's future type: +The crate provides a `FromFuture` trait that abstracts over lifting a future into a kind's future type: -```rust -pub trait IntoFutureKind<'a, T, F> +```rust,ignore +pub trait FromFuture<'a, T, F> where T: 'a, F: Future + 'a, { - fn into_kind(f: F) -> Self; + fn from_future(f: F) -> Self; } ``` -This is implemented for `BoxFuture` (requiring `F: Send`) and `LocalBoxFuture` (no `Send` requirement). Custom future types can implement this trait to enable uniform construction: +This is implemented for `BoxFuture` (requiring `F: Send`) and `LocalBoxFuture` (no `Send` requirement). Custom future types can implement this trait to enable uniform construction. -```rust -impl<'a, T, F> IntoFutureKind<'a, T, F> for SmallBoxFuture<'a, T> -where - T: 'a, - F: Future + Send + 'a, -{ - fn into_kind(f: F) -> Self { - SmallBoxFuture::new(f) - } -} -``` - -Usage is then uniform across all kinds via [`FutureKind::into_kind`]: +Usage is uniform across all forms via `FutureForm::from_future`: -```rust +```rust,ignore impl Counter for Memory { fn next(&self) -> BoxFuture<'_, u32> { - Sendable::into_kind(async { self.val + 1 }) + Sendable::from_future(async { self.val + 1 }) } } impl Counter for Memory { fn next(&self) -> LocalBoxFuture<'_, u32> { - Local::into_kind(async { self.val + 1 }) - } -} - -impl Counter for Memory { - fn next(&self) -> SmallBoxFuture<'_, u32> { - SmallBoxed::into_kind(async { self.val + 1 }) + Local::from_future(async { self.val + 1 }) } } ``` -## Registering Custom Kinds with `#[kinds]` - -The `#[kinds]` macro currently only recognizes `Sendable` and `Local`. To use custom kinds, implement traits manually: +## Using Custom Kinds with `#[future_form]` -```rust -// Won't work (macro doesn't know about SmallBoxed): -// #[kinds(SmallBoxed)] +The `#[future_form]` macro supports custom `FutureForm` types alongside the built-in `Sendable` and `Local`: -// Instead, implement directly: -impl MyTrait for MyType { - fn operation(&self) -> SmallBoxFuture<'_, u32> { - SmallBoxed::into_kind(async { self.value }) +```rust,ignore +#[future_form(Sendable, Local, Traced)] +impl MyTrait for MyType { + fn operation(&self) -> K::Future<'_, u32> { + K::from_future(async { self.value }) } } ``` -Extending the macro to support custom kinds is possible and would involve: -1. Accepting arbitrary type paths in the attribute -2. Generating `::Future<'a, T>` for return types -3. Relying on `IntoFutureKind::into_kind(...)` in method bodies to work for any kind +For custom types, the macro replaces `K::Future<'a, T>` with `CustomType::Future<'a, T>` (the associated type) rather than a concrete type like `BoxFuture`. + +> **Note:** Custom types must implement `FromFuture` for the `from_future` pattern to work. Types like `Immediate` that use non-boxed futures (e.g., `Ready`) require different construction patterns. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..c7f547c --- /dev/null +++ b/HACKING.md @@ -0,0 +1,187 @@ +# Hacking on `future_form` + +> "In nova fert animus mutatas dicere formas corpora." +> +> ("I intend to speak of forms changed into new bodies.") +> +> — Ovid, _Metamorphoses_ + +## Nix Flake + +The project uses a Nix flake for reproducible development. Enter the dev shell: + +```sh +nix develop +``` + +This gives you: +- **Rust 1.90.0** (pinned via `rust-overlay`) +- **Cross-compilation targets**: macOS (aarch64/x86_64), Linux musl (aarch64/x86_64), Wasm, thumbv6m (embedded) +- **Cargo tools**: `cargo-expand`, `cargo-deny`, `cargo-outdated`, `cargo-udeps`, `cargo-sort`, `cargo-component` +- **Formatters**: `nixpkgs-fmt`, `alejandra`, `taplo` + +### Commands + +On shell entry, a `menu` displays available commands: + +| Command | Description | +|----------|------------------------------| +| `test` | Run tests with `cargo-watch` | +| `lint` | Run clippy | +| `fmt` | Format code | +| `build` | Build the project | +| `doc` | Generate docs | +| `watch` | Watch for changes | +| `audit` | Security audit dependencies | +| `semver` | Check semver compatibility | +| `ci` | Run full CI checks | + +### Debugging Macros + +`cargo-expand` is included for inspecting macro output: + +```sh +cargo expand --package future_form --test macro_tests +``` + +## Project Structure + +``` +future_form/ +├── future_form/ # Core library +│ ├── src/lib.rs # FutureForm, Sendable, Local, FromFuture +│ └── tests/ +│ ├── macro_tests.rs # Unit tests for the macro +│ └── ui/ # Compile-fail tests (trybuild) +├── future_form_macros/ # Proc macro crate +│ └── src/lib.rs # #[future_form] attribute macro +└── Cargo.toml # Workspace configuration +``` + +## Building + +```sh +cargo build +``` + +## Testing + +```sh +# Run all tests +cargo test + +# Run only macro unit tests +cargo test --test macro_tests + +# Run only compile-fail UI tests +cargo test --test compile_fail + +# Update UI test expected output after intentional changes +TRYBUILD=overwrite cargo test --test compile_fail +``` + +## How the Macro Works + +``` + ┌─────────────────────────────────┐ + │ #[future_form(Sendable, Local)]│ + │ impl Trait │ + │ for Type { ... } │ + └───────────────┬─────────────────┘ + │ + ┌─────────────────┴─────────────────┐ + │ syn::parse │ + │ Extract K parameter │ + └─────────────────┬─────────────────┘ + │ + ┌─────────────────────────┼─────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────┐ +│ For each variant in │ │ Clone the impl block │ │ VisitMut walks AST │ +│ (Sendable, Local, ...) │ │ and transform: │ │ replacing K only │ +│ │ │ │ │ in type positions │ +└─────────────────────────┘ └─────────────────────────┘ └─────────────────────┘ + │ │ │ + └─────────────────────────┼─────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────┐ + │ │ + │ impl Trait for Type { ... } │ + │ impl Trait for Type { ... } │ + │ impl Trait for Type { ... } │ + │ │ + └─────────────────────────────────────────────────────┘ +``` + +The `#[future_form(Sendable, Local)]` macro transforms a generic impl: + +```rust +#[future_form(Sendable, Local)] +impl Trait for Type { + fn method(&self) -> K::Future<'_, T> { + K::from_future(async { ... }) + } +} +``` + +Into concrete impls for each variant: + +```rust +impl Trait for Type { + fn method(&self) -> BoxFuture<'_, T> { + Sendable::from_future(async { ... }) + } +} + +impl Trait for Type { + fn method(&self) -> LocalBoxFuture<'_, T> { + Local::from_future(async { ... }) + } +} +``` + +Key transformations: +1. Replace `K` type parameter with concrete type (`Sendable`/`Local`) +2. Replace `K::Future<'a, T>` with `BoxFuture<'a, T>`/`LocalBoxFuture<'a, T>` +3. Replace `K::from_future(...)` with `Sendable::from_future(...)`/`Local::from_future(...)` +4. Merge variant-specific `where` clauses into the impl + +The macro uses `syn::visit_mut::VisitMut` to walk the AST and replace identifiers only in type positions, avoiding mangling identifiers that contain the type parameter as a substring (e.g., `WorksOK` when `K` is the parameter). + +## Adding a New Test + +**Unit test** — Add to `future_form/tests/macro_tests.rs`: + +```rust +mod my_feature { + use super::*; + + trait MyTrait { ... } + + #[future_form(Sendable, Local)] + impl MyTrait for MyType { ... } + + #[test] + fn test_my_feature_sendable() { ... } + + #[test] + fn test_my_feature_local() { ... } +} +``` + +**Compile-fail test** — Add two files to `future_form/tests/ui/`: + +1. `my_error.rs` — Code that should fail to compile +2. `my_error.stderr` — Expected compiler output (generate with `TRYBUILD=overwrite`) + +## Release Checklist + +1. Update version in workspace `Cargo.toml` +2. Run `cargo test` +3. Run `cargo publish --dry-run -p future_form_macros` +4. Run `cargo publish --dry-run -p future_form` +5. Commit and tag +6. `cargo publish -p future_form_macros` (first — main crate depends on it) +7. `cargo publish -p future_form` diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..1e7d07a --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..a2804f8 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Brooklyn Zelenka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5487acd..77d0816 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,22 @@ -# `futures_kind` +# `future_form` -Abstractions over `Send` and `!Send` futures in Rust. - -## Motivation - -Async Rust has a fragmentation problem: some runtimes require `Send` futures (like `tokio` and `async-std`), while others work with `!Send` futures (like single-threaded executors or Wasm environments). This forces library authors to either: - -1. Duplicate their async trait implementations for both `Send` and `!Send` variants, including all consumers (e.g. `MyService` and `MyLocalService`) -2. Force all users to use `Send` futures, excluding legitimate `!Send` use cases -3. Create separate crates or feature flags for each variant - -This duplication is verbose, error-prone, and increases maintenance burden. - -## Approach - -`futures_kind` provides an abstraction that allows you to write async code once and support both `Send` and `!Send` futures through generic implementations. - -```rust -use futures_kind::{FutureKind, Sendable, Local}; -use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; - -// Define your trait once, generic over the future kind -pub trait Service { - fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; -} - -struct MyService; - -// Implement for both Send and !Send with minimal boilerplate -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { - async move { x * 2 }.boxed_local() - } -} - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { - async move { x * 2 }.boxed() - } -} -``` - -Now users can choose which variant they need: - -```rust -use futures_kind::{FutureKind, Sendable, Local}; -use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; - -trait Service { - fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; -} - -struct MyService; +> "This isn't even my ~~final~~ _future_ form!" -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { - async move { x * 2 }.boxed_local() - } -} +Abstractions over `Send` and `!Send` futures in Rust. -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { - async move { x * 2 }.boxed() - } -} +## The Problem -// For Send-required runtimes like tokio -async fn use_sendable(service: &impl Service) { - let result = service.handle(42).await; -} +Async Rust has a fragmentation problem: some runtimes require `Send` futures (like `tokio`), while others work with `!Send` futures (like Wasm). This forces library authors to either duplicate their async trait implementations or exclude legitimate use cases. -// For !Send runtimes like Wasm or single-threaded executors -async fn use_local(service: &impl Service) { - let result = service.handle(42).await; -} -``` +## The Solution -Or thread through the `FutureKind` parameter and delay to compile time. -This is typesafe, and will complain if you try to send between threads -at which point you'll know that you need to specialize to `Sendable`. +`future_form` lets you write async code once and support both `Send` and `!Send` futures: ```rust -use futures_kind::{FutureKind, Sendable, Local}; -use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; - -trait Service { - fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; -} - -struct MyService; - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { - async move { x * 2 }.boxed_local() - } -} - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { - async move { x * 2 }.boxed() - } -} - -async fn use_unknown(service: &impl Service) { - let result = service.handle(42).await; -} -``` - -## Core Types - -### `FutureKind` Trait - -The central abstraction that defines an associated type for futures: - -```rust,no_run -pub trait FutureKind { - type Future<'a, T: 'a>: Future + 'a; -} -``` - -### `Sendable` - -Represents `Send` futures, backed by `futures::future::BoxFuture`: - -```rust,no_run -impl FutureKind for Sendable { - type Future<'a, T: 'a> = BoxFuture<'a, T>; -} -``` - -### `Local` - -Represents `!Send` futures, backed by `futures::future::LocalBoxFuture`: - -```rust,no_run -impl FutureKind for Local { - type Future<'a, T: 'a> = LocalBoxFuture<'a, T>; -} -``` - -### `#[kinds]` Macro - -While you can define traits generic over `FutureKind`, Rust cannot verify that a single `async` block satisfies both `Send` and `!Send` bounds for an arbitrary `K: FutureKind`. The `#[kinds]` macro solves this by generating separate implementations for each variant, allowing the compiler to verify each one independently: - -```rust,no_run +use future_form::{FutureForm, Sendable, Local, future_form}; use std::marker::PhantomData; -use futures_kind::{kinds, FutureKind}; -trait Counter { +trait Counter { fn next(&self) -> K::Future<'_, u32>; } @@ -156,150 +25,25 @@ struct Memory { _marker: PhantomData, } -// Generates impl Counter and impl Counter -#[kinds] -impl Counter for Memory { +// Generates impl for both Sendable and Local +#[future_form(Sendable, Local)] +impl Counter for Memory { fn next(&self) -> K::Future<'_, u32> { let val = self.val; - Box::pin(async move { val + 1 }) - } -} -``` - -You can also generate only specific variants: - -```rust,no_run -#[kinds(Sendable)] // Only Sendable -#[kinds(Local)] // Only Local -#[kinds(Sendable, Local)] // Both (default) -``` - -Each variant can have its own additional bounds using `where`: - -```rust,no_run -#[kinds(Sendable where T: Send, Local where T: Debug)] -impl Processor for Container { - fn process(&self) -> K::Future<'_, T> { - let value = self.value.clone(); - Box::pin(async move { value }) + K::from_future(async move { val + 1 }) } } -// Generates: -// impl Processor for Container -// impl Processor for Container ``` -## Threading `FutureKind` Through Your Code - -The simplest pattern is to structure your code so the `FutureKind` type parameter appears naturally in your API. This typically happens when: - -1. **The function returns the future directly** (not the awaited result) -2. **You use a struct to carry the type parameter** - -Here's an example using a struct: - -```rust -use std::marker::PhantomData; -use futures_kind::{FutureKind, Local, Sendable}; -use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; - -trait Service { - fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; -} +## Packages -struct MyService; +| Crate | Description | +|-------|-------------| +| [`future_form`](./future_form) | Core traits and types (`FutureForm`, `Sendable`, `Local`) | +| [`future_form_macros`](./future_form_macros) | The `#[future_form]` attribute macro | -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { - async move { x * 2 }.boxed_local() - } -} - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { - async move { x * 2 }.boxed() - } -} - -pub struct Handler { - service: MyService, - _marker: PhantomData, -} - -impl Handler -where - MyService: Service -{ - pub fn new(service: MyService) -> Self { - Self { - service, - _marker: PhantomData, - } - } - - // K is part of Self, so methods can use it naturally - pub async fn process(&self, x: u8) -> u8 { - Service::::handle(&self.service, x).await - } -} - -# async fn example() { -// Usage is clean and type-safe -let my_service = MyService; -let handler = Handler::::new(my_service); -let result = handler.process(42).await; -# } -``` - -Or when returning futures directly: - -```rust,no_run -// K appears in the return type -pub fn create_task(x: u8) -> K::Future<'static, u8> -where - MyService: Service -{ - // Return the boxed future, don't await it - async move { x * 2 }.boxed() // or .boxed_local() for Local -} -``` - -This pattern allows the `FutureKind` choice to propagate through your entire call stack while maintaining type safety. - -## Use Cases - -| Use Case | Description | -|----------|-------------| -| Cross-platform libraries | Write async traits once, support both native and Wasm targets | -| Runtime flexibility | Allow users to choose their async runtime without forcing `Send` constraints | -| Testing | Use `Local` futures in single-threaded test environments while production uses `Sendable` | -| Gradual migration | Support both variants during migration between runtimes | - -## Design Philosophy - -`futures_kind` embraces the following principles: - -1. **Zero-cost abstraction**: The trait compiles down to direct use of `BoxFuture` or `LocalBoxFuture` -2. **Compile-time dispatch**: All decisions about `Send` vs `!Send` happen at compile time -3. **Minimal API surface**: Just one trait and two implementations keep the crate focused and maintainable -4. **Compatibility**: Works seamlessly with the `futures` crate's existing types -5. **Extensability**: Other boxings or non-boxed types can be implemented directly. - -## Installation - -Add this to your `Cargo.toml`: - -```toml -[dependencies] -futures_kind = "0.1.0" -futures = "0.3.31" -``` +See the [`future_form` README](./future_form/README.md) for full documentation, usage patterns, and comparisons with `async-trait` and `trait-variant`. ## License -Licensed under either of: - -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) - -at your option. +Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option. diff --git a/flake.nix b/flake.nix index 9658bd8..1ad75c2 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "futures_kind"; + description = "future_form"; inputs = { nixpkgs.url = "nixpkgs/nixos-25.11"; @@ -87,7 +87,7 @@ in rec { devShells.default = pkgs.mkShell { - name = "futures_kind shell"; + name = "future_form shell"; nativeBuildInputs = with pkgs; [ diff --git a/futures_kind/Cargo.toml b/future_form/Cargo.toml similarity index 83% rename from futures_kind/Cargo.toml rename to future_form/Cargo.toml index e92312f..d75e53b 100644 --- a/futures_kind/Cargo.toml +++ b/future_form/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "futures_kind" +name = "future_form" description = "Abstractions over Send and !Send futures" readme = "README.md" keywords = ["futures", "async", "send", "boxed-future"] @@ -14,10 +14,11 @@ authors.workspace = true [dependencies] futures = { workspace = true } -futures_kind_macros = { workspace = true } +future_form_macros = { workspace = true } [dev-dependencies] tokio = { workspace = true } +trybuild = { workspace = true } [lints] workspace = true diff --git a/future_form/README.md b/future_form/README.md new file mode 100644 index 0000000..7a8df0a --- /dev/null +++ b/future_form/README.md @@ -0,0 +1,414 @@ +# `future_form` + +> "This isn't even my ~~final~~ _future_ form!" + +Abstractions over `Send` and `!Send` futures in Rust. + +## Motivation + +Async Rust has a fragmentation problem. Some runtimes demand `Send` futures (tokio, async-std), while others are perfectly happy with `!Send` (single-threaded executors, Wasm). Library authors face an unpleasant choice: + +1. Duplicate async trait implementations for both variants — `MyService` _and_ `MyLocalService`, all the way down +2. Force everyone onto `Send` futures, leaving `!Send` use cases out in the cold (or vice versa) +3. Maintain separate crates or feature flags for each variant + +All of these are verbose, error-prone, and a maintenance headache. + +Async Rust developers have long struggled with a question as old as time: _"Should I `Send` or `!Send`?"_ Finally, you can stop asking and simply... embrace your future form and delay to the final concrete call site. + +## Approach + +`future_form` solves this with a simple abstraction: write your async code once, parameterized over the "form" of future you need. The choice between `Send` and `!Send` becomes a type parameter that flows through your code. + +```rust +use future_form::{FutureForm, Sendable, Local}; +use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; + +// Define your trait once, generic over the future kind +pub trait Service { + fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; +} + +struct MyService; + +// Implement for both Send and !Send with minimal boilerplate +impl Service for MyService { + fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { + async move { x * 2 }.boxed_local() + } +} + +impl Service for MyService { + fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { + async move { x * 2 }.boxed() + } +} +``` + +Users pick the variant they need: + +```rust +use future_form::{FutureForm, Sendable, Local}; +use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; + +trait Service { + fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; +} + +struct MyService; + +impl Service for MyService { + fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { + async move { x * 2 }.boxed_local() + } +} + +impl Service for MyService { + fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { + async move { x * 2 }.boxed() + } +} + +// For Send-required runtimes like tokio +async fn run_sendable(service: &impl Service) { + let result = service.handle(42).await; +} + +// For !Send runtimes like Wasm or single-threaded executors +async fn run_local(service: &impl Service) { + let result = service.handle(42).await; +} +``` + +Or thread through the `FutureForm` parameter and delay the choice to compile time. This is typesafe: if you try to send a `Local` future between threads, you'll get a compile error telling you to specialize to `Sendable`. + +```rust +use future_form::{FutureForm, Sendable, Local}; +use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; + +trait Service { + fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; +} + +struct MyService; + +impl Service for MyService { + fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { + async move { x * 2 }.boxed_local() + } +} + +impl Service for MyService { + fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { + async move { x * 2 }.boxed() + } +} + +async fn run(service: &impl Service) { + let result = service.handle(42).await; +} +``` + +## Choose Your Form + +### `FutureForm` Trait + +The core abstraction — a trait with an associated future type: + +```rust,no_run +use std::future::Future; + +pub trait FutureForm { + type Future<'a, T: 'a>: Future + 'a; +} +``` + +This library ships with `Sendable` and `Local`, but you can implement your own forms for other boxing strategies or even unboxed futures. + +### `Sendable` + +> _"Have future, will travel"_ + +Represents `Send` futures, backed by `futures::future::BoxFuture`. For multithreaded [^over9k] contexts: + +```rust,ignore +impl FutureForm for Sendable { + type Future<'a, T: 'a> = BoxFuture<'a, T>; +} +``` + +[^over9k]: Thread level over 9000?! + +### `Local` + +> _What happens on the thread, stays on the thread._ + +Represents `!Send` futures, backed by `futures::future::LocalBoxFuture`: + +```rust,ignore +impl FutureForm for Local { + type Future<'a, T: 'a> = LocalBoxFuture<'a, T>; +} +``` + +### `#[future_form]` Macro + +> _One impl to rule them all._ + +Rust's `async { ... }` blocks have a concrete `Send` or `!Send` type, so you can't write a single generic `impl` that works for both, nor can you extract out the body without incurring those bounds. Without this macro, if you want identical behavior, Rust forces you to write and maintain identical implementations for each variant. This macro lets you write your `impl` once and generates one for each future form you pass in: + +```rust,ignore +use std::marker::PhantomData; +use future_form::{future_form, FutureForm}; + +trait Counter { + fn next(&self) -> K::Future<'_, u32>; +} + +struct Memory { + val: u32, + _marker: PhantomData, +} + +// Generates impl Counter and impl Counter +#[future_form(Sendable, Local)] +impl Counter for Memory { + fn next(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val + 1 }) + } +} +``` + +You can also generate only specific variants: + +```rust,ignore +#[future_form(Sendable)] // Only Sendable +#[future_form(Local)] // Only Local +#[future_form(Sendable, Local)] // Both +``` + +Each variant can have its own additional bounds using `where`: + +```rust,ignore +#[future_form(Sendable where T: Send, Local where T: Debug)] +impl Processor for Container { + fn process(&self) -> K::Future<'_, T> { + let value = self.value.clone(); + K::from_future(async move { value }) + } +} +// Generates: +// impl Processor for Container +// impl Processor for Container +``` + +## Threading `FutureForm` Through Your Code + +The trick is structuring your code so the `FutureForm` parameter flows naturally through your API. Two common patterns: + +1. **Return the future directly** — the `K` appears in the return type +2. **Carry `K` in a struct** — `PhantomData` lets you thread it through methods + +Here's the struct approach: + +```rust +use std::marker::PhantomData; +use future_form::{FutureForm, Local, Sendable}; +use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; + +trait Service { + fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; +} + +struct MyService; + +impl Service for MyService { + fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { + async move { x * 2 }.boxed_local() + } +} + +impl Service for MyService { + fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { + async move { x * 2 }.boxed() + } +} + +pub struct Handler { + service: MyService, + _marker: PhantomData, +} + +impl Handler +where + MyService: Service +{ + pub fn new(service: MyService) -> Self { + Self { + service, + _marker: PhantomData, + } + } + + // K is part of Self, so methods can use it naturally + pub async fn process(&self, x: u8) -> u8 { + Service::::handle(&self.service, x).await + } +} + +# async fn example() { +// Usage is clean and type-safe +let my_service = MyService; +let handler = Handler::::new(my_service); +let result = handler.process(42).await; +# } +``` + +Or when returning futures directly: + +```rust,ignore +// K appears in the return type +pub fn create_task(x: u8) -> K::Future<'static, u8> +where + K::Future<'static, u8>: FromFuture<'static, u8, impl Future> +{ + K::from_future(async move { x * 2 }) +} +``` + +The `FutureForm` choice propagates through your entire call stack — type safety all the way down. + +## Use Cases + +| Use Case | Description | +|----------|-------------| +| Cross-platform libraries | Write async traits once, support both native and Wasm targets | +| Runtime flexibility | Allow users to choose their async runtime without forcing `Send` constraints | +| Testing | Use `Local` futures in single-threaded test environments while production uses `Sendable` | +| Gradual migration | Support both variants during migration between runtimes | + +## Design Philosophy + +`future_form` is deliberately minimal: + +1. **Zero-cost abstraction** — compiles down to direct `BoxFuture` or `LocalBoxFuture` usage +2. **Compile-time dispatch** — `Send` vs `!Send` is resolved statically, no runtime overhead +3. **Small API surface** — one trait, two implementations, one macro. That's it. +4. **Plays well with others** — builds on the `futures` crate's existing types +5. **Extensible** — implement `FutureForm` for your own types if the builtins don't fit + +## Comparison with `async-trait` + +[`async-trait`](https://crates.io/crates/async-trait) and `future_form` are complementary — they solve different problems: + +| Aspect | `async-trait` | `future_form` | +|--------|---------------|----------------| +| **Problem solved** | Async methods in traits (pre-RPITIT) | Abstracting over Send vs !Send | +| **Send/!Send choice** | Per-trait (`#[async_trait(?Send)]`) | Per-usage site (`K: FutureForm`) | +| **When to choose** | At trait definition time | At impl usage time | +| **Post-RPITIT (Rust 1.75+)** | Less necessary for basic cases | Still useful for Send/!Send flexibility | + +### When to use `async-trait` + +- You need async trait methods on older Rust versions (pre-1.75) +- You want the simplest possible syntax for async traits +- All users of your trait will use the same Send/!Send variant + +### When to use `future_form` + +- You want _consumers_ to choose Send vs !Send at usage time +- You're building a library that should work in both multi-threaded and single-threaded contexts +- You need both variants available simultaneously (e.g., for different runtime configurations) + +### Using both together + +Nothing stops you from using both. A common pattern: `async-trait` for internal convenience, `future_form` at your public API boundary: + +```rust,ignore +use async_trait::async_trait; +use future_form::{FutureForm, future_form}; + +// Internal trait using async-trait for convenience +#[async_trait] +trait InternalService { + async fn fetch(&self) -> Vec; +} + +// Public trait using future_form for flexibility +trait PublicService { + fn fetch(&self) -> K::Future<'_, Vec>; +} +``` + +## Comparison with `trait-variant` + +[`trait-variant`](https://crates.io/crates/trait-variant) is the Rust lang team's approach: generate a _second trait_ with `Send` bounds from your base trait. Different philosophy, different tradeoffs: + +| Aspect | `trait-variant` | `future_form` | +|--------|-----------------|----------------| +| **Approach** | Generates two separate traits | Single trait with type parameter | +| **Syntax** | Native `async fn` | Boxed futures via `K::Future` | +| **Trait count** | Two traits (`Local*` and `Send` variant) | One trait generic over `K` | +| **Impl pattern** | Separate impl block per variant | Single impl with `#[future_form]` generates both | +| **Middleware pattern** | Limited (can't conditionally impl Send) | Supported via generic `K` propagation | + +### `trait-variant` example + +```rust,ignore +#[trait_variant::make(HttpService: Send)] +pub trait LocalHttpService { + async fn fetch(&self, url: &str) -> Vec; +} + +// Implementors write separate impl blocks: +impl LocalHttpService for MyClient { ... } +impl HttpService for MyClient { ... } // must duplicate impl body +``` + +### `future_form` example + +```rust,ignore +pub trait HttpService { + fn fetch(&self, url: &str) -> K::Future<'_, Vec>; +} + +// Implementors can support BOTH via #[future_form]: +#[future_form(Sendable, Local)] +impl HttpService for MyClient { + fn fetch(&self, url: &str) -> K::Future<'_, Vec> { + K::from_future(async move { /* ... */ }) + } +} +``` + +### When to use `trait-variant` + +- You want native `async fn` syntax (can avoid boxing in some contexts) +- You're okay with separate impl blocks for each variant +- You prefer the Rust lang team's officially recommended approach + +### When to use `future_form` + +- You want a single trait to support _both_ `Send` and `!Send` usage +- You're building middleware/wrappers that should preserve the caller's Send choice +- You need the `K: FutureForm` parameter to propagate through your type hierarchy + +## Installation + +Add to your `Cargo.toml`: + +```toml +[dependencies] +future_form = "0.2.0" +futures = "0.3.31" +``` + +## License + +Licensed under either of: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. diff --git a/future_form/src/extensions.rs b/future_form/src/extensions.rs new file mode 100644 index 0000000..504140c --- /dev/null +++ b/future_form/src/extensions.rs @@ -0,0 +1,2 @@ +//! Guidance on extending `FutureForm` with custom implementations. +#![doc = include_str!("../../EXTENSIONS.md")] diff --git a/futures_kind/src/lib.rs b/future_form/src/lib.rs similarity index 75% rename from futures_kind/src/lib.rs rename to future_form/src/lib.rs index 5cc989c..52f93b8 100644 --- a/futures_kind/src/lib.rs +++ b/future_form/src/lib.rs @@ -1,10 +1,16 @@ +#![no_std] #![doc = include_str!("../README.md")] +extern crate alloc; + +use alloc::boxed::Box; use core::future::Future; use futures::future::{BoxFuture, LocalBoxFuture}; pub use futures::future::FutureExt; -pub use futures_kind_macros::kinds; +pub use future_form_macros::future_form; + +pub mod extensions; /// An abstraction over [`Future`]s. /// @@ -13,14 +19,14 @@ pub use futures_kind_macros::kinds; /// /// # Example /// -/// Define a database trait generic over [`FutureKind`]: +/// Define a database trait generic over [`FutureForm`]: /// /// ```rust -/// use futures_kind::{FutureKind, Sendable, Local}; +/// use future_form::{FutureForm, Sendable, Local}; /// use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; /// -/// // Database trait that works with any FutureKind -/// trait Database { +/// // Database trait that works with any FutureForm +/// trait Database { /// fn get<'a>(&'a self, key: &'a str) -> K::Future<'a, Option>; /// fn set<'a>(&'a mut self, key: &'a str, value: String) -> K::Future<'a, ()>; /// } @@ -62,10 +68,10 @@ pub use futures_kind_macros::kinds; /// /// ```rust /// use std::marker::PhantomData; -/// use futures_kind::{FutureKind, Sendable}; +/// use future_form::{FutureForm, Sendable}; /// use futures::future::{BoxFuture, FutureExt}; /// -/// trait Database { +/// trait Database { /// fn get<'a>(&'a self, key: &'a str) -> K::Future<'a, Option>; /// } /// @@ -79,13 +85,13 @@ pub use futures_kind_macros::kinds; /// } /// } /// -/// // Repository struct carries the FutureKind parameter -/// struct Repository { +/// // Repository struct carries the FutureForm parameter +/// struct Repository { /// db: InMemoryDb, /// _marker: PhantomData, /// } /// -/// impl Repository +/// impl Repository /// where /// InMemoryDb: Database /// { @@ -101,35 +107,35 @@ pub use futures_kind_macros::kinds; /// } /// } /// ``` -pub trait FutureKind { +pub trait FutureForm { /// An abstraction over future types. /// /// This is especially useful for abstracting over `Send` and `!Send` futures. type Future<'a, T: 'a>: Future + 'a; - /// Lift a future into this kind's future type. + /// Create this form's future type from a raw future. /// /// # Example /// /// ```rust - /// use futures_kind::{FutureKind, Sendable, Local}; + /// use future_form::{FutureForm, Sendable, Local}; /// use futures::future::{BoxFuture, LocalBoxFuture}; /// /// fn sendable_example() -> BoxFuture<'static, u32> { - /// Sendable::into_kind(async { 42 }) + /// Sendable::from_future(async { 42 }) /// } /// /// fn local_example() -> LocalBoxFuture<'static, u32> { - /// Local::into_kind(async { 42 }) + /// Local::from_future(async { 42 }) /// } /// ``` - fn into_kind<'a, T, F>(f: F) -> Self::Future<'a, T> + fn from_future<'a, T, F>(f: F) -> Self::Future<'a, T> where T: 'a, F: Future + 'a, - Self::Future<'a, T>: IntoFutureKind<'a, T, F>, + Self::Future<'a, T>: FromFuture<'a, T, F>, { - IntoFutureKind::into_kind(f) + FromFuture::from_future(f) } } @@ -141,10 +147,10 @@ pub trait FutureKind { /// # Example /// /// ```rust -/// use futures_kind::{FutureKind, Sendable}; +/// use future_form::{FutureForm, Sendable}; /// use futures::future::{BoxFuture, FutureExt}; /// -/// trait Cache { +/// trait Cache { /// fn fetch<'a>(&'a self, key: &'a str) -> K::Future<'a, Option>; /// } /// @@ -162,7 +168,7 @@ pub trait FutureKind { /// ``` #[derive(Debug, Clone, Copy)] pub enum Sendable {} -impl FutureKind for Sendable { +impl FutureForm for Sendable { type Future<'a, T: 'a> = BoxFuture<'a, T>; } @@ -175,10 +181,10 @@ impl FutureKind for Sendable { /// # Example /// /// ```rust -/// use futures_kind::{FutureKind, Local}; +/// use future_form::{FutureForm, Local}; /// use futures::future::{LocalBoxFuture, FutureExt}; /// -/// trait Cache { +/// trait Cache { /// fn fetch<'a>(&'a self, key: &'a str) -> K::Future<'a, Option>; /// } /// @@ -196,24 +202,24 @@ impl FutureKind for Sendable { /// ``` #[derive(Debug, Clone, Copy)] pub enum Local {} -impl FutureKind for Local { +impl FutureForm for Local { type Future<'a, T: 'a> = LocalBoxFuture<'a, T>; } -/// A trait for lifting futures into their [`FutureKind`]-specific type. +/// A trait for constructing [`FutureForm`]-specific types from raw futures. /// /// This abstracts over the wrapping operation, allowing generic code to construct -/// the appropriate future type without knowing the concrete kind. +/// the appropriate future type without knowing the concrete form. /// -/// Typically you'll use [`FutureKind::into_kind`] instead of calling this directly. +/// Typically you'll use [`FutureForm::from_future`] instead of calling this directly. /// /// # Example /// /// ```rust -/// use futures_kind::{FutureKind, Sendable, Local}; +/// use future_form::{FutureForm, Sendable, Local}; /// use futures::future::{BoxFuture, LocalBoxFuture}; /// -/// trait Counter { +/// trait Counter { /// fn next(&self) -> K::Future<'_, u32>; /// } /// @@ -222,42 +228,42 @@ impl FutureKind for Local { /// impl Counter for Memory { /// fn next(&self) -> BoxFuture<'_, u32> { /// let val = self.val; -/// Sendable::into_kind(async move { val + 1 }) +/// Sendable::from_future(async move { val + 1 }) /// } /// } /// /// impl Counter for Memory { /// fn next(&self) -> LocalBoxFuture<'_, u32> { /// let val = self.val; -/// Local::into_kind(async move { val + 1 }) +/// Local::from_future(async move { val + 1 }) /// } /// } /// ``` -pub trait IntoFutureKind<'a, T, F> +pub trait FromFuture<'a, T, F> where T: 'a, F: Future + 'a, { - /// Lift a future into this type. - fn into_kind(f: F) -> Self; + /// Create this type from a future. + fn from_future(f: F) -> Self; } -impl<'a, T, F> IntoFutureKind<'a, T, F> for BoxFuture<'a, T> +impl<'a, T, F> FromFuture<'a, T, F> for BoxFuture<'a, T> where T: 'a, F: Future + Send + 'a, { - fn into_kind(f: F) -> Self { + fn from_future(f: F) -> Self { Box::pin(f) } } -impl<'a, T, F> IntoFutureKind<'a, T, F> for LocalBoxFuture<'a, T> +impl<'a, T, F> FromFuture<'a, T, F> for LocalBoxFuture<'a, T> where T: 'a, F: Future + 'a, { - fn into_kind(f: F) -> Self { + fn from_future(f: F) -> Self { Box::pin(f) } } diff --git a/future_form/tests/compile_fail.rs b/future_form/tests/compile_fail.rs new file mode 100644 index 0000000..4f9fc52 --- /dev/null +++ b/future_form/tests/compile_fail.rs @@ -0,0 +1,5 @@ +#[test] +fn compile_fail_tests() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/future_form/tests/macro_tests.rs b/future_form/tests/macro_tests.rs new file mode 100644 index 0000000..583c397 --- /dev/null +++ b/future_form/tests/macro_tests.rs @@ -0,0 +1,893 @@ +#![allow(missing_docs, clippy::missing_const_for_fn, clippy::future_not_send)] + +use core::marker::PhantomData; +use futures::future::{BoxFuture, LocalBoxFuture}; +use future_form::{FromFuture, FutureForm, Local, Sendable, future_form}; +use std::collections::HashMap; + +trait Counter { + fn next(&self) -> K::Future<'_, u32>; +} + +struct Memory { + val: u32, + _marker: PhantomData, +} + +impl Memory { + fn new(val: u32) -> Self { + Self { + val, + _marker: PhantomData, + } + } +} + +#[future_form(Sendable, Local)] +impl Counter for Memory { + fn next(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val + 1 }) + } +} + +#[tokio::test] +async fn test_macro_impl_with_sendable() { + let memory: Memory = Memory::new(41); + let result = as Counter>::next(&memory).await; + assert_eq!(result, 42); +} + +#[tokio::test] +async fn test_macro_impl_with_local() { + let memory: Memory = Memory::new(41); + let result = as Counter>::next(&memory).await; + assert_eq!(result, 42); +} + +trait Database { + fn get(&self, key: &str) -> K::Future<'_, Option>; + fn count(&self) -> K::Future<'_, usize>; +} + +struct InMemoryDb { + data: std::collections::HashMap, + _marker: PhantomData, +} + +#[future_form(Sendable, Local)] +impl Database for InMemoryDb { + fn get(&self, key: &str) -> K::Future<'_, Option> { + let key = key.to_string(); + K::from_future(async move { self.data.get(&key).cloned() }) + } + + fn count(&self) -> K::Future<'_, usize> { + K::from_future(async move { self.data.len() }) + } +} + +#[tokio::test] +async fn test_database_sendable() { + let mut data = std::collections::HashMap::new(); + data.insert("foo".to_string(), "bar".to_string()); + let db: InMemoryDb = InMemoryDb { + data, + _marker: PhantomData, + }; + + assert_eq!( + as Database>::get(&db, "foo").await, + Some("bar".to_string()) + ); + assert_eq!( + as Database>::get(&db, "missing").await, + None + ); + assert_eq!( + as Database>::count(&db).await, + 1 + ); +} + +#[tokio::test] +async fn test_database_local() { + let mut data = std::collections::HashMap::new(); + data.insert("foo".to_string(), "bar".to_string()); + let db: InMemoryDb = InMemoryDb { + data, + _marker: PhantomData, + }; + + assert_eq!( + as Database>::get(&db, "foo").await, + Some("bar".to_string()) + ); + assert_eq!( + as Database>::get(&db, "missing").await, + None + ); + assert_eq!( as Database>::count(&db).await, 1); +} + +mod selective_generation { + use super::*; + + trait SendableOnly { + fn send_only(&self) -> K::Future<'_, u32>; + } + + struct SendOnlyType { + val: u32, + } + + #[future_form(Sendable)] + impl SendableOnly for SendOnlyType { + fn send_only(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val * 2 }) + } + } + + #[tokio::test] + async fn test_sendable_only() { + let t = SendOnlyType { val: 21 }; + let result = >::send_only(&t).await; + assert_eq!(result, 42); + } + + trait LocalOnly { + fn local_only(&self) -> K::Future<'_, u32>; + } + + struct LocalOnlyType { + val: u32, + } + + #[future_form(Local)] + impl LocalOnly for LocalOnlyType { + fn local_only(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val * 2 }) + } + } + + #[tokio::test] + async fn test_local_only() { + let t = LocalOnlyType { val: 21 }; + let result = >::local_only(&t).await; + assert_eq!(result, 42); + } + + trait ExplicitBoth { + fn explicit(&self) -> K::Future<'_, u32>; + } + + struct ExplicitBothType { + val: u32, + } + + #[future_form(Sendable, Local)] + impl ExplicitBoth for ExplicitBothType { + fn explicit(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val + 1 }) + } + } + + #[tokio::test] + async fn test_explicit_both_sendable() { + let t = ExplicitBothType { val: 41 }; + let result = >::explicit(&t).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_explicit_both_local() { + let t = ExplicitBothType { val: 41 }; + let result = >::explicit(&t).await; + assert_eq!(result, 42); + } +} + +mod generic_consumers { + use super::*; + + async fn use_counter_generic(counter: &impl Counter) -> u32 { + counter.next().await + } + + #[tokio::test] + async fn test_trait_in_trait_sendable() { + let memory: Memory = Memory::new(41); + let result = use_counter_generic::(&memory).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_trait_in_trait_local() { + let memory: Memory = Memory::new(41); + let result = use_counter_generic::(&memory).await; + assert_eq!(result, 42); + } +} + +mod conditional_bounds { + use super::*; + + trait Processor { + fn process(&self) -> K::Future<'_, String>; + } + + struct Container { + value: T, + } + + #[future_form(Sendable where T: Send, Local)] + impl Processor for Container { + fn process(&self) -> K::Future<'_, String> { + let value = self.value.clone(); + K::from_future(async move { value.to_string() }) + } + } + + #[tokio::test] + async fn test_conditional_bounds_sendable() { + let container = Container { + value: "hello".to_string(), + }; + let result = as Processor>::process(&container).await; + assert_eq!(result, "hello"); + } + + #[tokio::test] + async fn test_conditional_bounds_local() { + use std::rc::Rc; + let container = Container { + value: Rc::::from("world"), + }; + let result = > as Processor>::process(&container).await; + assert_eq!(result, "world"); + } + + trait Debugger { + fn debug(&self) -> K::Future<'_, String>; + } + + struct Wrapper(T); + + #[future_form(Sendable where T: Send, Local where T: core::fmt::Debug)] + impl Debugger for Wrapper { + fn debug(&self) -> K::Future<'_, String> { + K::from_future(async move { "debugged".to_string() }) + } + } + + #[tokio::test] + async fn test_local_conditional_bounds() { + let wrapper = Wrapper(42i32); + let result = as Debugger>::debug(&wrapper).await; + assert_eq!(result, "debugged"); + } + + #[tokio::test] + async fn test_sendable_conditional_bounds_separate() { + let wrapper = Wrapper(42i32); + let result = as Debugger>::debug(&wrapper).await; + assert_eq!(result, "debugged"); + } +} + +mod complex_bounds { + use super::*; + + // Test Fn bounds with multiple arguments (commas inside parentheses) + trait CallbackProcessor { + fn process(&self) -> K::Future<'_, u32>; + } + + struct FnHolder(F); + + #[future_form(Sendable where F: Fn(u32, String) -> bool + Send, Local)] + impl bool> CallbackProcessor for FnHolder { + fn process(&self) -> K::Future<'_, u32> { + K::from_future(async { 42 }) + } + } + + #[tokio::test] + async fn test_fn_bounds_with_commas_local() { + let holder: FnHolder bool> = FnHolder(|_, _| true); + let result = bool> as CallbackProcessor>::process(&holder).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_fn_bounds_with_commas_sendable() { + let holder: FnHolder bool> = FnHolder(|_, _| true); + let result = bool> as CallbackProcessor>::process(&holder).await; + assert_eq!(result, 42); + } + + // Test tuple types in bounds + trait TupleHandler { + fn handle(&self) -> K::Future<'_, (u32, String)>; + } + + #[derive(Clone)] + struct TupleStruct; + + #[future_form(Sendable where Self: Clone + Send, Local where Self: Clone)] + impl TupleHandler for TupleStruct { + fn handle(&self) -> K::Future<'_, (u32, String)> { + K::from_future(async { (42, "hello".to_string()) }) + } + } + + #[tokio::test] + async fn test_tuple_return_type() { + let result = >::handle(&TupleStruct).await; + assert_eq!(result, (42, "hello".to_string())); + } +} + +mod from_future_trait { + use super::*; + + #[tokio::test] + async fn test_from_future_sendable() { + let future: BoxFuture<'_, u32> = FromFuture::from_future(async { 42 }); + assert_eq!(future.await, 42); + } + + #[tokio::test] + async fn test_from_future_local() { + let future: LocalBoxFuture<'_, u32> = FromFuture::from_future(async { 42 }); + assert_eq!(future.await, 42); + } + + #[tokio::test] + async fn test_sendable_from_future() { + let future = Sendable::from_future(async { 42 }); + assert_eq!(future.await, 42); + } + + #[tokio::test] + async fn test_local_from_future() { + let future = Local::from_future(async { 42 }); + assert_eq!(future.await, 42); + } + + trait Calculator { + fn add(&self, a: u32, b: u32) -> K::Future<'_, u32>; + } + + struct SimpleCalculator; + + impl Calculator for SimpleCalculator { + fn add(&self, a: u32, b: u32) -> BoxFuture<'_, u32> { + Sendable::from_future(async move { a + b }) + } + } + + impl Calculator for SimpleCalculator { + fn add(&self, a: u32, b: u32) -> LocalBoxFuture<'_, u32> { + Local::from_future(async move { a + b }) + } + } + + #[tokio::test] + async fn test_from_future_in_trait_sendable() { + let calc = SimpleCalculator; + let result = >::add(&calc, 20, 22).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_from_future_in_trait_local() { + let calc = SimpleCalculator; + let result = >::add(&calc, 20, 22).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_sendable_from_future_with_send_data() { + let data = String::from("hello"); + let future = + Sendable::from_future(async move { if data.is_empty() { 0u32 } else { 42u32 } }); + assert_eq!(future.await, 42); + } + + #[tokio::test] + async fn test_local_from_future_with_non_send_data() { + use std::rc::Rc; + let data = Rc::new(42u32); + let future = Local::from_future(async move { *data }); + assert_eq!(future.await, 42); + } +} + +mod alternative_param_names { + use super::*; + + trait AltParamTrait { + fn get(&self) -> Knd::Future<'_, u32>; + } + + struct AltParamType { + val: u32, + } + + #[future_form(Sendable, Local)] + impl AltParamTrait for AltParamType { + fn get(&self) -> Knd::Future<'_, u32> { + let val = self.val; + Knd::from_future(async move { val }) + } + } + + #[tokio::test] + async fn test_alt_param_sendable() { + let t = AltParamType { val: 42 }; + let result = >::get(&t).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_alt_param_local() { + let t = AltParamType { val: 42 }; + let result = >::get(&t).await; + assert_eq!(result, 42); + } + + trait AltParamTrait2 { + fn get(&self) -> FK::Future<'_, u32>; + } + + struct AltParamType2 { + val: u32, + } + + #[future_form(Sendable, Local)] + impl AltParamTrait2 for AltParamType2 { + fn get(&self) -> FK::Future<'_, u32> { + let val = self.val; + FK::from_future(async move { val }) + } + } + + #[tokio::test] + async fn test_alt_param2_sendable() { + let t = AltParamType2 { val: 42 }; + let result = >::get(&t).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_alt_param2_local() { + let t = AltParamType2 { val: 42 }; + let result = >::get(&t).await; + assert_eq!(result, 42); + } +} + +mod multiple_generics { + use super::*; + + trait MultiGeneric { + fn describe(&self) -> K::Future<'_, String>; + } + + struct MultiGenericType { + value: T, + _k: PhantomData, + } + + #[future_form(Sendable where T: Send, Local)] + impl MultiGeneric for MultiGenericType { + fn describe(&self) -> K::Future<'_, String> { + let v = self.value.clone(); + K::from_future(async move { v.to_string() }) + } + } + + #[tokio::test] + async fn test_multi_generic_sendable() { + let t: MultiGenericType = MultiGenericType { + value: 42, + _k: PhantomData, + }; + let result = + as MultiGeneric>::describe(&t).await; + assert_eq!(result, "42"); + } + + #[tokio::test] + async fn test_multi_generic_local() { + let t: MultiGenericType = MultiGenericType { + value: 42, + _k: PhantomData, + }; + let result = + as MultiGeneric>::describe(&t).await; + assert_eq!(result, "42"); + } +} + +mod where_clauses { + use super::*; + + trait WhereClauseTest { + fn process(&self) -> K::Future<'_, String>; + } + + struct WhereClauseType { + value: T, + } + + #[future_form(Sendable where T: Send, Local)] + impl WhereClauseTest for WhereClauseType + where + T: Clone + ToString + 'static, + { + fn process(&self) -> K::Future<'_, String> { + let v = self.value.clone(); + K::from_future(async move { v.to_string() }) + } + } + + #[tokio::test] + async fn test_where_clause_combined_sendable() { + let t = WhereClauseType { + value: "test".to_string(), + }; + let result = as WhereClauseTest>::process(&t).await; + assert_eq!(result, "test"); + } + + #[tokio::test] + async fn test_where_clause_combined_local() { + use std::rc::Rc; + let t = WhereClauseType { + value: Rc::new("test".to_string()), + }; + let result = > as WhereClauseTest>::process(&t).await; + assert_eq!(result, "test"); + } +} + +mod method_generics { + use super::*; + + trait MethodGeneric { + fn convert(&self, u: U) -> K::Future<'_, String>; + } + + struct MethodGenericType { + prefix: String, + _marker: PhantomData, + } + + #[future_form(Sendable, Local)] + impl MethodGeneric for MethodGenericType { + fn convert(&self, u: U) -> K::Future<'_, String> { + let prefix = self.prefix.clone(); + K::from_future(async move { format!("{}{}", prefix, u.to_string()) }) + } + } + + #[tokio::test] + async fn test_method_generic_sendable() { + let t: MethodGenericType = MethodGenericType { + prefix: "value: ".to_string(), + _marker: PhantomData, + }; + let result = + as MethodGeneric>::convert(&t, 42i32).await; + assert_eq!(result, "value: 42"); + } + + #[tokio::test] + async fn test_method_generic_local() { + let t: MethodGenericType = MethodGenericType { + prefix: "value: ".to_string(), + _marker: PhantomData, + }; + let result = + as MethodGeneric>::convert(&t, 42i32).await; + assert_eq!(result, "value: 42"); + } +} + +mod inherent_impls { + use super::*; + + struct InherentType { + val: u32, + _marker: PhantomData, + } + + #[future_form(Sendable, Local)] + impl InherentType { + fn get_value(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val }) + } + + fn add(&self, other: u32) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val + other }) + } + } + + #[tokio::test] + async fn test_inherent_impl_sendable() { + let t: InherentType = InherentType { + val: 40, + _marker: PhantomData, + }; + assert_eq!(t.get_value().await, 40); + assert_eq!(t.add(2).await, 42); + } + + #[tokio::test] + async fn test_inherent_impl_local() { + let t: InherentType = InherentType { + val: 40, + _marker: PhantomData, + }; + assert_eq!(t.get_value().await, 40); + assert_eq!(t.add(2).await, 42); + } +} + +mod lifetime_params { + use super::*; + + trait LifetimeTest<'a, K: FutureForm> { + fn get_ref(&self) -> K::Future<'_, &'a str>; + } + + struct LifetimeType<'a, K> { + data: &'a str, + _marker: PhantomData, + } + + #[future_form(Sendable, Local)] + impl<'a, K: FutureForm> LifetimeTest<'a, K> for LifetimeType<'a, K> { + fn get_ref(&self) -> K::Future<'_, &'a str> { + let data = self.data; + K::from_future(async move { data }) + } + } + + #[tokio::test] + async fn test_lifetime_param_sendable() { + let data = String::from("hello"); + let t: LifetimeType<'_, Sendable> = LifetimeType { + data: &data, + _marker: PhantomData, + }; + let result = + as LifetimeTest<'_, Sendable>>::get_ref(&t).await; + assert_eq!(result, "hello"); + } + + #[tokio::test] + async fn test_lifetime_param_local() { + let data = String::from("hello"); + let t: LifetimeType<'_, Local> = LifetimeType { + data: &data, + _marker: PhantomData, + }; + let result = as LifetimeTest<'_, Local>>::get_ref(&t).await; + assert_eq!(result, "hello"); + } +} + +mod concrete_self_type { + use super::*; + + trait ConcreteImplTrait { + fn compute(&self) -> K::Future<'_, u32>; + } + + struct ConcreteImplType { + val: u32, + } + + #[future_form(Sendable, Local)] + impl ConcreteImplTrait for ConcreteImplType { + fn compute(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val * 2 }) + } + } + + #[tokio::test] + async fn test_concrete_impl_sendable() { + let t = ConcreteImplType { val: 21 }; + let result = >::compute(&t).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_concrete_impl_local() { + let t = ConcreteImplType { val: 21 }; + let result = >::compute(&t).await; + assert_eq!(result, 42); + } +} + +mod method_attributes { + use super::*; + + trait AttributeTest { + fn documented(&self) -> K::Future<'_, u32>; + fn inline_method(&self) -> K::Future<'_, u32>; + } + + struct AttributeType { + val: u32, + _marker: PhantomData, + } + + #[future_form(Sendable, Local)] + impl AttributeTest for AttributeType { + /// This method has documentation + fn documented(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val }) + } + + #[inline] + fn inline_method(&self) -> K::Future<'_, u32> { + let val = self.val; + K::from_future(async move { val + 1 }) + } + } + + #[tokio::test] + async fn test_attributes_preserved_sendable() { + let t: AttributeType = AttributeType { + val: 42, + _marker: PhantomData, + }; + assert_eq!( + as AttributeTest>::documented(&t).await, + 42 + ); + assert_eq!( + as AttributeTest>::inline_method(&t).await, + 43 + ); + } + + #[tokio::test] + async fn test_attributes_preserved_local() { + let t: AttributeType = AttributeType { + val: 42, + _marker: PhantomData, + }; + assert_eq!( + as AttributeTest>::documented(&t).await, + 42 + ); + assert_eq!( + as AttributeTest>::inline_method(&t).await, + 43 + ); + } +} + +mod complex_return_types { + use super::*; + + trait ComplexReturn { + fn get_map(&self) -> K::Future<'_, HashMap>; + fn get_option(&self) -> K::Future<'_, Option>>; + } + + struct ComplexReturnType { + _marker: PhantomData, + } + + #[future_form(Sendable, Local)] + impl ComplexReturn for ComplexReturnType { + fn get_map(&self) -> K::Future<'_, HashMap> { + K::from_future(async move { + let mut map = HashMap::new(); + map.insert("key".to_string(), 42); + map + }) + } + + fn get_option(&self) -> K::Future<'_, Option>> { + K::from_future(async move { Some(vec![1, 2, 3]) }) + } + } + + #[tokio::test] + async fn test_complex_return_sendable() { + let t: ComplexReturnType = ComplexReturnType { + _marker: PhantomData, + }; + let map = as ComplexReturn>::get_map(&t).await; + assert_eq!(map.get("key"), Some(&42)); + + let opt = as ComplexReturn>::get_option(&t).await; + assert_eq!(opt, Some(vec![1, 2, 3])); + } + + #[tokio::test] + async fn test_complex_return_local() { + let t: ComplexReturnType = ComplexReturnType { + _marker: PhantomData, + }; + let map = as ComplexReturn>::get_map(&t).await; + assert_eq!(map.get("key"), Some(&42)); + + let opt = as ComplexReturn>::get_option(&t).await; + assert_eq!(opt, Some(vec![1, 2, 3])); + } +} + +mod identifier_collision { + use super::*; + + /// Trait name contains K as substring - previously caused identifier mangling + trait WorksOK { + fn compute(&self) -> K::Future<'_, u32>; + } + + struct MyType(PhantomData); + + #[future_form(Sendable, Local)] + impl WorksOK for MyType { + fn compute(&self) -> K::Future<'_, u32> { + K::from_future(async { 42 }) + } + } + + #[tokio::test] + async fn test_works_ok_sendable() { + let m: MyType = MyType(PhantomData); + let result = as WorksOK>::compute(&m).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_works_ok_local() { + let m: MyType = MyType(PhantomData); + let result = as WorksOK>::compute(&m).await; + assert_eq!(result, 42); + } + + /// Trait and type both contain K as substring + trait CheckOK { + fn check(&self) -> K::Future<'_, bool>; + } + + struct CheckerOK(PhantomData); + + #[future_form(Sendable, Local)] + impl CheckOK for CheckerOK { + fn check(&self) -> K::Future<'_, bool> { + K::from_future(async { true }) + } + } + + #[tokio::test] + async fn test_checker_ok_sendable() { + let c: CheckerOK = CheckerOK(PhantomData); + let result = as CheckOK>::check(&c).await; + assert!(result); + } + + #[tokio::test] + async fn test_checker_ok_local() { + let c: CheckerOK = CheckerOK(PhantomData); + let result = as CheckOK>::check(&c).await; + assert!(result); + } +} diff --git a/future_form/tests/ui/malformed_where_clause.rs b/future_form/tests/ui/malformed_where_clause.rs new file mode 100644 index 0000000..82e7d70 --- /dev/null +++ b/future_form/tests/ui/malformed_where_clause.rs @@ -0,0 +1,19 @@ +use std::marker::PhantomData; +use future_form::{FutureForm, future_form}; + +trait Counter { + fn next(&self) -> K::Future<'_, u32>; +} + +struct Memory { + _marker: PhantomData, +} + +#[future_form(Sendable where T Send, Local)] +impl Counter for Memory { + fn next(&self) -> K::Future<'_, u32> { + K::from_future(async move { 42 }) + } +} + +fn main() {} diff --git a/future_form/tests/ui/malformed_where_clause.stderr b/future_form/tests/ui/malformed_where_clause.stderr new file mode 100644 index 0000000..e0de332 --- /dev/null +++ b/future_form/tests/ui/malformed_where_clause.stderr @@ -0,0 +1,5 @@ +error: malformed where clause `T Send`: expected `:` + --> tests/ui/malformed_where_clause.rs:12:15 + | +12 | #[future_form(Sendable where T Send, Local)] + | ^^^^^^^^ diff --git a/future_form/tests/ui/missing_future_form_bound.rs b/future_form/tests/ui/missing_future_form_bound.rs new file mode 100644 index 0000000..e633d76 --- /dev/null +++ b/future_form/tests/ui/missing_future_form_bound.rs @@ -0,0 +1,16 @@ +use future_form::future_form; + +trait Counter { + fn next(&self) -> u32; +} + +struct Memory; + +#[future_form(Sendable, Local)] +impl Counter for Memory { + fn next(&self) -> u32 { + 42 + } +} + +fn main() {} diff --git a/future_form/tests/ui/missing_future_form_bound.stderr b/future_form/tests/ui/missing_future_form_bound.stderr new file mode 100644 index 0000000..deb512c --- /dev/null +++ b/future_form/tests/ui/missing_future_form_bound.stderr @@ -0,0 +1,5 @@ +error: Expected a type parameter with FutureForm bound (e.g., `K: FutureForm`) + --> tests/ui/missing_future_form_bound.rs:10:5 + | +10 | impl Counter for Memory { + | ^^^ diff --git a/future_form/tests/ui/missing_variants.rs b/future_form/tests/ui/missing_variants.rs new file mode 100644 index 0000000..1aa7108 --- /dev/null +++ b/future_form/tests/ui/missing_variants.rs @@ -0,0 +1,19 @@ +use std::marker::PhantomData; +use future_form::{FutureForm, future_form}; + +trait Counter { + fn next(&self) -> K::Future<'_, u32>; +} + +struct Memory { + _marker: PhantomData, +} + +#[future_form] +impl Counter for Memory { + fn next(&self) -> K::Future<'_, u32> { + K::from_future(async move { 42 }) + } +} + +fn main() {} diff --git a/future_form/tests/ui/missing_variants.stderr b/future_form/tests/ui/missing_variants.stderr new file mode 100644 index 0000000..b59db18 --- /dev/null +++ b/future_form/tests/ui/missing_variants.stderr @@ -0,0 +1,7 @@ +error: missing FutureForm variants: expected #[future_form(Sendable)], #[future_form(Local)], or #[future_form(Sendable, Local)] + --> tests/ui/missing_variants.rs:12:1 + | +12 | #[future_form] + | ^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `future_form` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/future_form/tests/ui/sendable_with_non_send.rs b/future_form/tests/ui/sendable_with_non_send.rs new file mode 100644 index 0000000..41c94dc --- /dev/null +++ b/future_form/tests/ui/sendable_with_non_send.rs @@ -0,0 +1,20 @@ +use std::rc::Rc; +use future_form::{FutureForm, Sendable, future_form}; + +trait Processor { + fn process(&self) -> K::Future<'_, u32>; +} + +struct Container { + value: Rc, +} + +#[future_form(Sendable)] +impl Processor for Container { + fn process(&self) -> K::Future<'_, u32> { + let value = Rc::clone(&self.value); + K::from_future(async move { *value }) + } +} + +fn main() {} diff --git a/future_form/tests/ui/sendable_with_non_send.stderr b/future_form/tests/ui/sendable_with_non_send.stderr new file mode 100644 index 0000000..1f8ffc9 --- /dev/null +++ b/future_form/tests/ui/sendable_with_non_send.stderr @@ -0,0 +1,28 @@ +error: future cannot be sent between threads safely + --> tests/ui/sendable_with_non_send.rs:12:15 + | +12 | #[future_form(Sendable)] + | _______________^ +13 | | impl Processor for Container { +14 | | fn process(&self) -> K::Future<'_, u32> { +15 | | let value = Rc::clone(&self.value); +16 | | K::from_future(async move { *value }) + | |_____________________________________________^ future created by async block is not `Send` + | + = help: within `{async block@$DIR/tests/ui/sendable_with_non_send.rs:16:24: 16:34}`, the trait `std::marker::Send` is not implemented for `Rc` + = help: the following other types implement trait `FromFuture<'a, T, F>`: + Pin + std::marker::Send>> + Pin>> +note: captured value is not `Send` + --> tests/ui/sendable_with_non_send.rs:16:38 + | +16 | K::from_future(async move { *value }) + | ^^^^^ has type `Rc` which is not `Send` +note: required by a bound in `future_form::FutureForm::from_future` + --> src/lib.rs + | + | fn from_future<'a, T, F>(f: F) -> Self::Future<'a, T> + | ----------- required by a bound in this associated function +... + | Self::Future<'a, T>: FromFuture<'a, T, F>, + | ^^^^^^^^^^^^^^^^^^^^ required by this bound in `FutureForm::from_future` diff --git a/futures_kind_macros/Cargo.toml b/future_form_macros/Cargo.toml similarity index 86% rename from futures_kind_macros/Cargo.toml rename to future_form_macros/Cargo.toml index 5f705dc..f230185 100644 --- a/futures_kind_macros/Cargo.toml +++ b/future_form_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "futures_kind_macros" -description = "Proc macros for futures_kind" +name = "future_form_macros" +description = "Proc macros for future_form" readme = "README.md" keywords = ["futures", "async", "proc-macro"] categories = ["development-tools::procedural-macro-helpers"] diff --git a/future_form_macros/README.md b/future_form_macros/README.md new file mode 100644 index 0000000..0776838 --- /dev/null +++ b/future_form_macros/README.md @@ -0,0 +1,39 @@ +# `future_form_macros` + +Proc macros for [`future_form`](https://crates.io/crates/future_form). + +This crate provides the `#[future_form]` attribute macro for generating concrete trait implementations from generic `FutureForm` impls. + +## Usage + +```rust,ignore +use future_form::{future_form, FutureForm}; + +// Generate both Sendable and Local impls +#[future_form(Sendable, Local)] +impl MyTrait for MyType { ... } + +// Generate only specific variants +#[future_form(Sendable)] // Only Sendable +#[future_form(Local)] // Only Local +#[future_form(Sendable, Local)] // Both + +// Custom FutureForm types are also supported +#[future_form(Sendable, Local, MyCustomForm)] +impl MyTrait for MyType { ... } + +// Add bounds only for specific variants +#[future_form(Sendable where T: Send, Local)] +impl Processor for Container { ... } +``` + +See the [`future_form`](https://crates.io/crates/future_form) crate for full documentation. + +## License + +Licensed under either of: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. diff --git a/future_form_macros/src/lib.rs b/future_form_macros/src/lib.rs new file mode 100644 index 0000000..1f896d0 --- /dev/null +++ b/future_form_macros/src/lib.rs @@ -0,0 +1,614 @@ +//! Proc macros for `future_form`. +//! +//! This crate provides the `#[future_form]` attribute macro. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + GenericParam, Ident, ImplItem, ImplItemFn, ItemImpl, Path, ReturnType, Type, WherePredicate, + parse_macro_input, parse_quote, + visit::Visit, + visit_mut::{self, VisitMut}, +}; + +/// Generate implementations of a trait for `Sendable` and/or `Local` `FutureForm`s. +/// +/// This attribute macro allows you to write a single implementation that works for both +/// `Send` and `!Send` futures, avoiding code duplication. +/// +/// # Usage +/// +/// ```rust,ignore +/// // Generate both Sendable and Local impls +/// #[future_form(Sendable, Local)] +/// impl MyTrait for MyType { ... } +/// +/// // Generate only Sendable impl +/// #[future_form(Sendable)] +/// impl MyTrait for MyType { ... } +/// +/// // Generate only Local impl +/// #[future_form(Local)] +/// impl MyTrait for MyType { ... } +/// +/// // Add bounds only for specific variants +/// #[future_form(Sendable where T: Send, Local)] +/// impl MyTrait for Container { ... } +/// // Generates: impl MyTrait for Container +/// // impl MyTrait for Container +/// ``` +/// +/// # Example +/// +/// ```rust,ignore +/// use std::marker::PhantomData; +/// use future_form::{FutureForm, Sendable, Local, future_form}; +/// +/// trait Counter { +/// fn next(&self) -> F::Future<'_, u32>; +/// } +/// +/// struct Memory { +/// val: u32, +/// _marker: PhantomData, +/// } +/// +/// #[future_form(Sendable, Local)] +/// impl Counter for Memory { +/// fn next(&self) -> F::Future<'_, u32> { +/// let val = self.val; +/// F::from_future(async move { val + 1 }) +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn future_form(attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemImpl); + + let kinds = match parse_kinds(&attr) { + Ok(k) => k, + Err(err) => return err, + }; + + match generate_impls(&input, &kinds) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} + +/// Creates an error `TokenStream` with proper span information. +fn make_error(span: proc_macro2::Span, msg: &str) -> TokenStream { + syn::Error::new(span, msg).to_compile_error().into() +} + +/// Returns the concrete future path for built-in variants, or None for custom types. +fn builtin_future_path(ident: &Ident) -> Option { + if ident == "Sendable" { + Some(parse_quote!(::futures::future::BoxFuture)) + } else if ident == "Local" { + Some(parse_quote!(::futures::future::LocalBoxFuture)) + } else { + None + } +} + +/// Checks if an ident could be a variant name. +/// +/// Variants are type names like `Sendable`, `Local`, or custom `FutureForm` types. +/// Excludes: `where`, single-letter idents (likely type params like T, K, F), +/// and common trait names that appear in bounds. +fn is_likely_variant(ident: &Ident) -> bool { + let s = ident.to_string(); + + // Must start uppercase + if !s.chars().next().is_some_and(char::is_uppercase) { + return false; + } + + // Exclude keywords + if s == "where" || s == "Self" { + return false; + } + + // Exclude single-letter idents (likely type parameters: T, K, F, U, etc.) + if s.len() == 1 { + return false; + } + + // Exclude common trait names that appear in bounds + let common_traits = [ + "Send", "Sync", "Clone", "Copy", "Debug", "Display", "Default", + "Fn", "FnMut", "FnOnce", "Future", "Iterator", "IntoIterator", + "From", "Into", "TryFrom", "TryInto", "AsRef", "AsMut", + "Eq", "PartialEq", "Ord", "PartialOrd", "Hash", + "Sized", "Unpin", "Drop", + ]; + if common_traits.contains(&s.as_str()) { + return false; + } + + true +} + +/// Parses the attribute tokens into a list of `FutureFormVariant`s. +/// +/// Uses token-based parsing to correctly handle all delimiters (parentheses, +/// brackets, braces, angle brackets) in where clause bounds. +#[allow(clippy::expect_used)] // Indexing is bounds-checked by loop conditions +fn parse_kinds(attr: &TokenStream) -> Result, TokenStream> { + use proc_macro2::TokenTree; + + let attr2: TokenStream2 = attr.clone().into(); + let tokens: Vec = attr2.into_iter().collect(); + + if tokens.is_empty() { + return Err(make_error( + proc_macro2::Span::call_site(), + "missing FutureForm variants: expected #[future_form(Sendable)], #[future_form(Local)], or #[future_form(Sendable, Local)]", + )); + } + + let mut kinds = Vec::new(); + let mut i = 0; + + while i < tokens.len() { + // Skip leading commas + while i < tokens.len() { + if let TokenTree::Punct(p) = tokens.get(i).expect("bounds checked") + && p.as_char() == ',' + { + i += 1; + continue; + } + break; + } + + if i >= tokens.len() { + break; + } + + // Expect a variant name (any FutureForm type: Sendable, Local, or custom) + let (kind_path, future_path, variant_span) = match tokens.get(i).expect("bounds checked") { + TokenTree::Ident(ident) => { + if is_likely_variant(ident) { + let future = builtin_future_path(ident); + let path: Path = parse_quote!(#ident); + (path, future, ident.span()) + } else { + return Err(make_error( + ident.span(), + &format!("expected FutureForm variant, found `{ident}`"), + )); + } + } + other @ (TokenTree::Group(_) | TokenTree::Punct(_) | TokenTree::Literal(_)) => { + return Err(make_error( + other.span(), + "expected FutureForm variant (e.g., `Sendable`, `Local`, or custom type)", + )); + } + }; + i += 1; + + // Check for optional `where` clause + let extra_bounds = if i < tokens.len() { + if let TokenTree::Ident(ident) = tokens.get(i).expect("bounds checked") { + if ident == "where" { + i += 1; + // Collect tokens until we hit another variant name or end + let (predicates, new_i) = + collect_where_clause(&tokens, i, variant_span)?; + i = new_i; + predicates + } else { + vec![] + } + } else { + vec![] + } + } else { + vec![] + }; + + kinds.push(FutureFormVariant { + kind_path, + future_path, + extra_bounds, + }); + } + + if kinds.is_empty() { + return Err(make_error( + proc_macro2::Span::call_site(), + "missing FutureForm variants: expected #[future_form(Sendable)], #[future_form(Local)], or #[future_form(Sendable, Local)]", + )); + } + + Ok(kinds) +} + +/// Collects where clause tokens until hitting another variant name or end. +/// +/// Returns the parsed predicates and the new token index. +#[allow(clippy::expect_used)] // Indexing is bounds-checked by loop conditions +fn collect_where_clause( + tokens: &[proc_macro2::TokenTree], + start: usize, + span: proc_macro2::Span, +) -> Result<(Vec, usize), TokenStream> { + use proc_macro2::TokenTree; + + let mut i = start; + let mut current_predicate_tokens: Vec = Vec::new(); + let mut predicates = Vec::new(); + // Track whether we're after a `:` (inside a type bound) - variants only appear after `,` + let mut in_bound = false; + + while i < tokens.len() { + let token = tokens.get(i).expect("bounds checked"); + + // Track if we're inside a bound (after `:`) + if let TokenTree::Punct(p) = token { + if p.as_char() == ':' { + in_bound = true; + } else if p.as_char() == ',' { + in_bound = false; + } + } + + // Check if this is a variant name (end of where clause) + // Only check at the START of a predicate (not inside a bound) + if !in_bound + && current_predicate_tokens.is_empty() + && let TokenTree::Ident(ident) = token + && is_likely_variant(ident) + { + return Ok((predicates, i)); + } + + // Check for comma at top level (predicate separator) + if let TokenTree::Punct(p) = token + && p.as_char() == ',' + { + // Check if next token is a variant name (starts a new variant, not a predicate) + let next_is_variant = tokens.get(i + 1).is_some_and(|t| { + if let TokenTree::Ident(id) = t { + // It's a variant if it looks like one AND is not followed by `:` + is_likely_variant(id) + && !tokens.get(i + 2).is_some_and(|t2| { + matches!(t2, TokenTree::Punct(p2) if p2.as_char() == ':') + }) + } else { + false + } + }); + + if next_is_variant { + // This comma separates variants, not predicates + if !current_predicate_tokens.is_empty() { + predicates.push(parse_predicate_tokens(¤t_predicate_tokens, span)?); + } + i += 1; // Skip the comma + return Ok((predicates, i)); + } + // This comma separates predicates within the where clause + if !current_predicate_tokens.is_empty() { + predicates.push(parse_predicate_tokens(¤t_predicate_tokens, span)?); + current_predicate_tokens.clear(); + } + i += 1; + continue; + } + + // Add token to current predicate + current_predicate_tokens.push(token.clone()); + i += 1; + } + + // End of tokens - parse any remaining predicate + if !current_predicate_tokens.is_empty() { + predicates.push(parse_predicate_tokens(¤t_predicate_tokens, span)?); + } + + Ok((predicates, i)) +} + +/// Parses a sequence of tokens as a where predicate. +fn parse_predicate_tokens( + tokens: &[proc_macro2::TokenTree], + span: proc_macro2::Span, +) -> Result { + let token_stream: TokenStream2 = tokens.iter().cloned().collect(); + let token_str = token_stream.to_string(); + + syn::parse2::(token_stream).map_err(|e| { + make_error(span, &format!("malformed where clause `{token_str}`: {e}")) + }) +} + +fn generate_impls(input: &ItemImpl, kinds: &[FutureFormVariant]) -> syn::Result { + // Find the K type parameter + let k_param = find_k_param(input)?; + + // Generate impl for each requested kind + let impls: Vec = kinds + .iter() + .map(|kind| generate_impl_for_kind(input, &k_param, kind)) + .collect(); + + Ok(quote! { + #(#impls)* + }) +} + +#[derive(Clone)] +struct FutureFormVariant { + /// Path to the `FutureForm` implementor (e.g., `Sendable`, `Local`, `MyCustomForm`) + kind_path: Path, + /// Concrete future path for built-in types (e.g., `BoxFuture`), None for custom types + future_path: Option, + /// Additional where clause bounds for this variant + extra_bounds: Vec, +} + +/// Visitor that replaces standalone `K` type parameter references with concrete paths. +/// +/// Only replaces `Path` nodes where the first segment is exactly the target ident +/// (e.g., `K`, `K::Future`, `K::from_future`), not idents containing it as a substring. +struct KindReplacer { + from_ident: Ident, + to_path: Path, + /// Concrete future path for built-in types (`BoxFuture`, `LocalBoxFuture`). + /// When `None` (custom types), `K::Future` becomes `CustomType::Future`. + future_path: Option, +} + +impl VisitMut for KindReplacer { + fn visit_path_mut(&mut self, path: &mut Path) { + // First, recurse into nested paths (generic arguments, etc.) + visit_mut::visit_path_mut(self, path); + + // Check if first segment is exactly our target ident + if let Some(first) = path.segments.first() + && first.ident == self.from_ident + && first.arguments.is_empty() + { + if path.segments.len() == 1 { + // Standalone K → replace with the target type + *path = self.to_path.clone(); + } else if let Some(second) = path.segments.get(1) { + // K::Future<...> or K::from_future(...) + if second.ident == "Future" { + if let Some(ref concrete_future) = self.future_path { + // Built-in: K::Future<'a, T> → BoxFuture<'a, T> + let args = second.arguments.clone(); + let mut new_path = concrete_future.clone(); + if let Some(last) = new_path.segments.last_mut() { + last.arguments = args; + } + *path = new_path; + } else { + // Custom: K::Future<'a, T> → CustomType::Future<'a, T> + let remaining: Vec<_> = path.segments.iter().skip(1).cloned().collect(); + let mut new_path = self.to_path.clone(); + new_path.segments.extend(remaining); + *path = new_path; + } + } else { + // K::from_future → Type::from_future + let remaining: Vec<_> = path.segments.iter().skip(1).cloned().collect(); + let mut new_path = self.to_path.clone(); + new_path.segments.extend(remaining); + *path = new_path; + } + } + } + } +} + +/// Visitor that checks if a where predicate references a specific ident as a standalone type. +struct IdentFinder { + target: Ident, + found: bool, +} + +impl<'ast> Visit<'ast> for IdentFinder { + fn visit_path(&mut self, path: &'ast Path) { + // Only match standalone K, not as part of larger identifiers + if let Some(first) = path.segments.first() + && path.segments.len() == 1 + && first.ident == self.target + && first.arguments.is_empty() + { + self.found = true; + } + syn::visit::visit_path(self, path); + } +} + +fn find_k_param(input: &ItemImpl) -> syn::Result { + // Look for a type parameter that has FutureForm bound + for param in &input.generics.params { + if let GenericParam::Type(type_param) = param { + for bound in &type_param.bounds { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + let path = &trait_bound.path; + if path + .segments + .last() + .is_some_and(|s| s.ident == "FutureForm") + { + return Ok(type_param.ident.clone()); + } + } + } + } + } + + Err(syn::Error::new_spanned( + &input.generics, + "Expected a type parameter with FutureForm bound (e.g., `K: FutureForm`)", + )) +} + +fn generate_impl_for_kind( + input: &ItemImpl, + k_param: &Ident, + variant: &FutureFormVariant, +) -> TokenStream2 { + let kind_path: Path = variant.kind_path.clone(); + let future_path: Option = variant.future_path.clone(); + + // Clone and modify generics - remove the K parameter + let mut new_generics = input.generics.clone(); + new_generics.params = new_generics + .params + .into_iter() + .filter(|p| { + if let GenericParam::Type(tp) = p { + tp.ident != *k_param + } else { + true + } + }) + .collect(); + + // Remove where clause predicates that reference K, and add extra bounds + if let Some(ref mut where_clause) = new_generics.where_clause { + where_clause.predicates = where_clause + .predicates + .clone() + .into_iter() + .filter(|pred| !predicate_references_ident(pred, k_param)) + .collect(); + // Add variant-specific extra bounds + for bound in &variant.extra_bounds { + where_clause.predicates.push(bound.clone()); + } + } else if !variant.extra_bounds.is_empty() { + // Create a where clause if we have extra bounds but none existed + let mut predicates = syn::punctuated::Punctuated::new(); + for bound in &variant.extra_bounds { + predicates.push(bound.clone()); + } + new_generics.where_clause = Some(syn::WhereClause { + where_token: syn::token::Where::default(), + predicates, + }); + } + + // Replace K in self_ty + let new_self_ty = replace_ident_in_type(&input.self_ty, k_param, &kind_path, &future_path); + + // Replace K in trait path if present + let new_trait = input.trait_.as_ref().map(|(bang, path, for_token)| { + let new_path = replace_ident_in_path(path, k_param, &kind_path, &future_path); + (*bang, new_path, *for_token) + }); + + // Transform methods + let new_items: Vec = input + .items + .iter() + .map(|item| transform_impl_item(item, k_param, &kind_path, &future_path)) + .collect(); + + let (impl_generics, _, where_clause) = new_generics.split_for_impl(); + + let trait_tokens = new_trait.map(|(bang, path, for_token)| { + quote! { #bang #path #for_token } + }); + + quote! { + impl #impl_generics #trait_tokens #new_self_ty #where_clause { + #(#new_items)* + } + } +} + +fn predicate_references_ident(pred: &syn::WherePredicate, ident: &Ident) -> bool { + let mut finder = IdentFinder { + target: ident.clone(), + found: false, + }; + finder.visit_where_predicate(pred); + finder.found +} + +#[allow(clippy::ref_option)] // We clone the Option, so &Option is fine +fn replace_ident_in_type( + ty: &Type, + from: &Ident, + kind_path: &Path, + future_path: &Option, +) -> Type { + let mut ty = ty.clone(); + let mut replacer = KindReplacer { + from_ident: from.clone(), + to_path: kind_path.clone(), + future_path: future_path.clone(), + }; + replacer.visit_type_mut(&mut ty); + ty +} + +#[allow(clippy::ref_option)] +fn replace_ident_in_path( + path: &Path, + from: &Ident, + kind_path: &Path, + future_path: &Option, +) -> Path { + let mut path = path.clone(); + let mut replacer = KindReplacer { + from_ident: from.clone(), + to_path: kind_path.clone(), + future_path: future_path.clone(), + }; + replacer.visit_path_mut(&mut path); + path +} + +#[allow(clippy::wildcard_enum_match_arm)] // We want to pass through unknown variants unchanged +#[allow(clippy::ref_option)] +fn transform_impl_item( + item: &ImplItem, + k_param: &Ident, + kind_path: &Path, + future_path: &Option, +) -> ImplItem { + match item { + ImplItem::Fn(method) => { + ImplItem::Fn(transform_method(method, k_param, kind_path, future_path)) + } + other => other.clone(), + } +} + +#[allow(clippy::ref_option)] +fn transform_method( + method: &ImplItemFn, + k_param: &Ident, + kind_path: &Path, + future_path: &Option, +) -> ImplItemFn { + let mut new_method = method.clone(); + + // Use the visitor to replace K references in the entire method + let mut replacer = KindReplacer { + from_ident: k_param.clone(), + to_path: kind_path.clone(), + future_path: future_path.clone(), + }; + + // Transform return type + if let ReturnType::Type(_, ref mut ty) = new_method.sig.output { + replacer.visit_type_mut(ty); + } + + // Transform method body + replacer.visit_block_mut(&mut new_method.block); + + new_method +} diff --git a/futures_kind/README.md b/futures_kind/README.md deleted file mode 100644 index 547d0e2..0000000 --- a/futures_kind/README.md +++ /dev/null @@ -1,307 +0,0 @@ -# `futures_kind` - -Abstractions over `Send` and `!Send` futures in Rust. - -## Motivation - -Async Rust has a fragmentation problem: some runtimes require `Send` futures (like `tokio` and `async-std`), while others work with `!Send` futures (like single-threaded executors or Wasm environments). This forces library authors to either: - -1. Duplicate their async trait implementations for both `Send` and `!Send` variants, including all consumers (e.g. `MyService` and `MyLocalService`) -2. Force all users to use `Send` futures, excluding legitimate `!Send` use cases -3. Create separate crates or feature flags for each variant - -This duplication is verbose, error-prone, and increases maintenance burden. - -## Approach - -`futures_kind` provides an abstraction that allows you to write async code once and support both `Send` and `!Send` futures through generic implementations. - -```rust -use futures_kind::{FutureKind, Sendable, Local}; -use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; - -// Define your trait once, generic over the future kind -pub trait Service { - fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; -} - -struct MyService; - -// Implement for both Send and !Send with minimal boilerplate -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { - async move { x * 2 }.boxed_local() - } -} - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { - async move { x * 2 }.boxed() - } -} -``` - -Now users can choose which variant they need: - -```rust -use futures_kind::{FutureKind, Sendable, Local}; -use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; - -trait Service { - fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; -} - -struct MyService; - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { - async move { x * 2 }.boxed_local() - } -} - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { - async move { x * 2 }.boxed() - } -} - -// For Send-required runtimes like tokio -async fn use_sendable(service: &impl Service) { - let result = service.handle(42).await; -} - -// For !Send runtimes like Wasm or single-threaded executors -async fn use_local(service: &impl Service) { - let result = service.handle(42).await; -} -``` - -Or thread through the `FutureKind` parameter and delay to compile time. -This is typesafe, and will complain if you try to send between threads -at which point you'll know that you need to specialize to `Sendable`. - -```rust -use futures_kind::{FutureKind, Sendable, Local}; -use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; - -trait Service { - fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; -} - -struct MyService; - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { - async move { x * 2 }.boxed_local() - } -} - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { - async move { x * 2 }.boxed() - } -} - -async fn use_unknown(service: &impl Service) { - let result = service.handle(42).await; -} -``` - -## Core Types - -### `FutureKind` Trait - -The central abstraction that defines an associated type for futures: - -```rust,no_run -use std::future::Future; - -pub trait FutureKind { - type Future<'a, T: 'a>: Future + 'a; -} -``` - -### `Sendable` - -Represents `Send` futures, backed by `futures::future::BoxFuture`: - -```rust,ignore -impl FutureKind for Sendable { - type Future<'a, T: 'a> = BoxFuture<'a, T>; -} -``` - -### `Local` - -Represents `!Send` futures, backed by `futures::future::LocalBoxFuture`: - -```rust,ignore -impl FutureKind for Local { - type Future<'a, T: 'a> = LocalBoxFuture<'a, T>; -} -``` - -### `#[kinds]` Macro - -While you can define traits generic over `FutureKind`, Rust cannot verify that a single `async` block satisfies both `Send` and `!Send` bounds for an arbitrary `K: FutureKind`. The `#[kinds]` macro solves this by generating separate implementations for each variant, allowing the compiler to verify each one independently: - -```rust,no_run -use std::marker::PhantomData; -use futures_kind::{kinds, FutureKind}; - -trait Counter { - fn next(&self) -> K::Future<'_, u32>; -} - -struct Memory { - val: u32, - _marker: PhantomData, -} - -// Generates impl Counter and impl Counter -#[kinds] -impl Counter for Memory { - fn next(&self) -> K::Future<'_, u32> { - let val = self.val; - Box::pin(async move { val + 1 }) - } -} -``` - -You can also generate only specific variants: - -```rust,ignore -#[kinds(Sendable)] // Only Sendable -#[kinds(Local)] // Only Local -#[kinds(Sendable, Local)] // Both (default) -``` - -Each variant can have its own additional bounds using `where`: - -```rust,ignore -#[kinds(Sendable where T: Send, Local where T: Debug)] -impl Processor for Container { - fn process(&self) -> K::Future<'_, T> { - let value = self.value.clone(); - Box::pin(async move { value }) - } -} -// Generates: -// impl Processor for Container -// impl Processor for Container -``` - -## Threading `FutureKind` Through Your Code - -The simplest pattern is to structure your code so the `FutureKind` type parameter appears naturally in your API. This typically happens when: - -1. **The function returns the future directly** (not the awaited result) -2. **You use a struct to carry the type parameter** - -Here's an example using a struct: - -```rust -use std::marker::PhantomData; -use futures_kind::{FutureKind, Local, Sendable}; -use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; - -trait Service { - fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; -} - -struct MyService; - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { - async move { x * 2 }.boxed_local() - } -} - -impl Service for MyService { - fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { - async move { x * 2 }.boxed() - } -} - -pub struct Handler { - service: MyService, - _marker: PhantomData, -} - -impl Handler -where - MyService: Service -{ - pub fn new(service: MyService) -> Self { - Self { - service, - _marker: PhantomData, - } - } - - // K is part of Self, so methods can use it naturally - pub async fn process(&self, x: u8) -> u8 { - Service::::handle(&self.service, x).await - } -} - -# async fn example() { -// Usage is clean and type-safe -let my_service = MyService; -let handler = Handler::::new(my_service); -let result = handler.process(42).await; -# } -``` - -Or when returning futures directly: - -```rust,ignore -// K appears in the return type -pub fn create_task(x: u8) -> K::Future<'static, u8> -where - MyService: Service -{ - // Return the boxed future, don't await it - async move { x * 2 }.boxed() // or .boxed_local() for Local -} -``` - -This pattern allows the `FutureKind` choice to propagate through your entire call stack while maintaining type safety. - -## Use Cases - -| Use Case | Description | -|----------|-------------| -| Cross-platform libraries | Write async traits once, support both native and Wasm targets | -| Runtime flexibility | Allow users to choose their async runtime without forcing `Send` constraints | -| Testing | Use `Local` futures in single-threaded test environments while production uses `Sendable` | -| Gradual migration | Support both variants during migration between runtimes | - -## Design Philosophy - -`futures_kind` embraces the following principles: - -1. **Zero-cost abstraction**: The trait compiles down to direct use of `BoxFuture` or `LocalBoxFuture` -2. **Compile-time dispatch**: All decisions about `Send` vs `!Send` happen at compile time -3. **Minimal API surface**: Just one trait and two implementations keep the crate focused and maintainable -4. **Compatibility**: Works seamlessly with the `futures` crate's existing types -5. **Extensability**: Other boxings or non-boxed types can be implemented directly. - -## Installation - -Add this to your `Cargo.toml`: - -```toml -[dependencies] -futures_kind = "0.1.0" -futures = "0.3.31" -``` - -## License - -Licensed under either of: - -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) - -at your option. diff --git a/futures_kind/tests/macro_tests.rs b/futures_kind/tests/macro_tests.rs deleted file mode 100644 index f5abae7..0000000 --- a/futures_kind/tests/macro_tests.rs +++ /dev/null @@ -1,360 +0,0 @@ -#![allow(missing_docs, clippy::missing_const_for_fn, clippy::future_not_send)] - -use core::marker::PhantomData; -use futures::future::{BoxFuture, LocalBoxFuture}; -use futures_kind::{FutureKind, IntoFutureKind, Local, Sendable, kinds}; - -trait Counter { - fn next(&self) -> K::Future<'_, u32>; -} - -struct Memory { - val: u32, - _marker: PhantomData, -} - -impl Memory { - fn new(val: u32) -> Self { - Self { - val, - _marker: PhantomData, - } - } -} - -// Single proc macro generates both Sendable and Local impls -#[kinds] -impl Counter for Memory { - fn next(&self) -> K::Future<'_, u32> { - let val = self.val; - K::into_kind(async move { val + 1 }) - } -} - -#[tokio::test] -async fn test_macro_impl_with_sendable() { - let memory: Memory = Memory::new(41); - let result = as Counter>::next(&memory).await; - assert_eq!(result, 42); -} - -#[tokio::test] -async fn test_macro_impl_with_local() { - let memory: Memory = Memory::new(41); - let result = as Counter>::next(&memory).await; - assert_eq!(result, 42); -} - -// Test with multiple methods and arguments -trait Database { - fn get(&self, key: &str) -> K::Future<'_, Option>; - fn count(&self) -> K::Future<'_, usize>; -} - -struct InMemoryDb { - data: std::collections::HashMap, - _marker: PhantomData, -} - -#[kinds] -impl Database for InMemoryDb { - fn get(&self, key: &str) -> K::Future<'_, Option> { - let key = key.to_string(); - K::into_kind(async move { self.data.get(&key).cloned() }) - } - - fn count(&self) -> K::Future<'_, usize> { - K::into_kind(async move { self.data.len() }) - } -} - -#[tokio::test] -async fn test_database_sendable() { - let mut data = std::collections::HashMap::new(); - data.insert("foo".to_string(), "bar".to_string()); - let db: InMemoryDb = InMemoryDb { - data, - _marker: PhantomData, - }; - - assert_eq!( - as Database>::get(&db, "foo").await, - Some("bar".to_string()) - ); - assert_eq!( - as Database>::get(&db, "missing").await, - None - ); - assert_eq!( - as Database>::count(&db).await, - 1 - ); -} - -#[tokio::test] -async fn test_database_local() { - let mut data = std::collections::HashMap::new(); - data.insert("foo".to_string(), "bar".to_string()); - let db: InMemoryDb = InMemoryDb { - data, - _marker: PhantomData, - }; - - assert_eq!( - as Database>::get(&db, "foo").await, - Some("bar".to_string()) - ); - assert_eq!( - as Database>::get(&db, "missing").await, - None - ); - assert_eq!( as Database>::count(&db).await, 1); -} - -// Test selective generation - only Sendable -trait SendableOnly { - fn send_only(&self) -> K::Future<'_, u32>; -} - -struct SendOnlyType { - val: u32, -} - -#[kinds(Sendable)] -impl SendableOnly for SendOnlyType { - fn send_only(&self) -> K::Future<'_, u32> { - let val = self.val; - K::into_kind(async move { val * 2 }) - } -} - -#[tokio::test] -async fn test_sendable_only() { - let t = SendOnlyType { val: 21 }; - let result = >::send_only(&t).await; - assert_eq!(result, 42); -} - -// Test selective generation - only Local -trait LocalOnly { - fn local_only(&self) -> K::Future<'_, u32>; -} - -struct LocalOnlyType { - val: u32, -} - -#[kinds(Local)] -impl LocalOnly for LocalOnlyType { - fn local_only(&self) -> K::Future<'_, u32> { - let val = self.val; - K::into_kind(async move { val * 2 }) - } -} - -#[tokio::test] -async fn test_local_only() { - let t = LocalOnlyType { val: 21 }; - let result = >::local_only(&t).await; - assert_eq!(result, 42); -} - -// Test explicit both -trait ExplicitBoth { - fn explicit(&self) -> K::Future<'_, u32>; -} - -struct ExplicitBothType { - val: u32, -} - -#[kinds(Sendable, Local)] -impl ExplicitBoth for ExplicitBothType { - fn explicit(&self) -> K::Future<'_, u32> { - let val = self.val; - K::into_kind(async move { val + 1 }) - } -} - -#[tokio::test] -async fn test_explicit_both_sendable() { - let t = ExplicitBothType { val: 41 }; - let result = >::explicit(&t).await; - assert_eq!(result, 42); -} - -#[tokio::test] -async fn test_explicit_both_local() { - let t = ExplicitBothType { val: 41 }; - let result = >::explicit(&t).await; - assert_eq!(result, 42); -} - -// Test trait-in-trait: consuming a FutureKind-generic trait through another generic context -async fn use_counter_generic(counter: &impl Counter) -> u32 { - counter.next().await -} - -#[tokio::test] -async fn test_trait_in_trait_sendable() { - let memory: Memory = Memory::new(41); - let result = use_counter_generic::(&memory).await; - assert_eq!(result, 42); -} - -#[tokio::test] -async fn test_trait_in_trait_local() { - let memory: Memory = Memory::new(41); - let result = use_counter_generic::(&memory).await; - assert_eq!(result, 42); -} - -// Test conditional bounds: T: Send only required for Sendable -trait Processor { - fn process(&self) -> K::Future<'_, String>; -} - -struct Container { - value: T, -} - -#[kinds(Sendable where T: Send, Local)] -impl Processor for Container { - fn process(&self) -> K::Future<'_, String> { - let value = self.value.clone(); - K::into_kind(async move { value.to_string() }) - } -} - -#[tokio::test] -async fn test_conditional_bounds_sendable() { - // String is Send + Clone + ToString - let container = Container { - value: "hello".to_string(), - }; - let result = as Processor>::process(&container).await; - assert_eq!(result, "hello"); -} - -#[tokio::test] -async fn test_conditional_bounds_local() { - // Rc is !Send but Clone + ToString - only works with Local - use std::rc::Rc; - let container = Container { - value: Rc::::from("world"), - }; - let result = > as Processor>::process(&container).await; - assert_eq!(result, "world"); -} - -// Test conditional bounds on Local variant -trait Debugger { - fn debug(&self) -> K::Future<'_, String>; -} - -struct Wrapper(T); - -// Local requires Debug, Sendable requires Send -#[kinds(Sendable where T: Send, Local where T: core::fmt::Debug)] -impl Debugger for Wrapper { - fn debug(&self) -> K::Future<'_, String> { - K::into_kind(async move { "debugged".to_string() }) - } -} - -#[tokio::test] -async fn test_local_conditional_bounds() { - // i32 is Debug + Clone, works with Local - let wrapper = Wrapper(42i32); - let result = as Debugger>::debug(&wrapper).await; - assert_eq!(result, "debugged"); -} - -#[tokio::test] -async fn test_sendable_conditional_bounds_separate() { - // i32 is Send + Clone, works with Sendable - let wrapper = Wrapper(42i32); - let result = as Debugger>::debug(&wrapper).await; - assert_eq!(result, "debugged"); -} - -// ============================================================ -// IntoFutureKind and FutureKind::into_kind tests -// ============================================================ - -// Test IntoFutureKind trait directly -#[tokio::test] -async fn test_into_future_kind_sendable() { - let future: BoxFuture<'_, u32> = IntoFutureKind::into_kind(async { 42 }); - assert_eq!(future.await, 42); -} - -#[tokio::test] -async fn test_into_future_kind_local() { - let future: LocalBoxFuture<'_, u32> = IntoFutureKind::into_kind(async { 42 }); - assert_eq!(future.await, 42); -} - -// Test FutureKind::into_kind method with concrete types -#[tokio::test] -async fn test_sendable_into_kind() { - let future = Sendable::into_kind(async { 42 }); - assert_eq!(future.await, 42); -} - -#[tokio::test] -async fn test_local_into_kind() { - let future = Local::into_kind(async { 42 }); - assert_eq!(future.await, 42); -} - -// Test K::into_kind in a generic context -trait Calculator { - fn add(&self, a: u32, b: u32) -> K::Future<'_, u32>; -} - -struct SimpleCalculator; - -impl Calculator for SimpleCalculator { - fn add(&self, a: u32, b: u32) -> BoxFuture<'_, u32> { - Sendable::into_kind(async move { a + b }) - } -} - -impl Calculator for SimpleCalculator { - fn add(&self, a: u32, b: u32) -> LocalBoxFuture<'_, u32> { - Local::into_kind(async move { a + b }) - } -} - -#[tokio::test] -async fn test_into_kind_in_trait_sendable() { - let calc = SimpleCalculator; - let result = >::add(&calc, 20, 22).await; - assert_eq!(result, 42); -} - -#[tokio::test] -async fn test_into_kind_in_trait_local() { - let calc = SimpleCalculator; - let result = >::add(&calc, 20, 22).await; - assert_eq!(result, 42); -} - -// Test that Sendable::into_kind enforces Send bound -// (This is a compile-time check - if it compiles, it works) -#[tokio::test] -async fn test_sendable_into_kind_with_send_data() { - let data = String::from("hello"); - let future = Sendable::into_kind(async move { if data.is_empty() { 0u32 } else { 42u32 } }); - assert_eq!(future.await, 42); -} - -// Test that Local::into_kind works with !Send data -#[tokio::test] -async fn test_local_into_kind_with_non_send_data() { - use std::rc::Rc; - let data = Rc::new(42u32); - let future = Local::into_kind(async move { *data }); - assert_eq!(future.await, 42); -} diff --git a/futures_kind_macros/README.md b/futures_kind_macros/README.md deleted file mode 100644 index 2e6756c..0000000 --- a/futures_kind_macros/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# `futures_kind_macros` - -Proc macros for [`futures_kind`](https://crates.io/crates/futures_kind). - -This crate provides the `#[kinds]` attribute macro for generating trait implementations for both `Sendable` and `Local` future kinds. - -## Usage - -```rust,ignore -use futures_kind::{kinds, FutureKind}; - -// Generate both Sendable and Local impls (default) -#[kinds] -impl MyTrait for MyType { ... } - -// Generate only specific variants -#[kinds(Sendable)] // Only Sendable -#[kinds(Local)] // Only Local -#[kinds(Sendable, Local)] // Both (default) - -// Add bounds only for specific variants -#[kinds(Sendable where T: Send, Local)] -impl Processor for Container { ... } -``` - -See the [`futures_kind`](https://crates.io/crates/futures_kind) crate for full documentation. - -## License - -Licensed under either of: - -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) - -at your option. diff --git a/futures_kind_macros/src/lib.rs b/futures_kind_macros/src/lib.rs deleted file mode 100644 index cd452f6..0000000 --- a/futures_kind_macros/src/lib.rs +++ /dev/null @@ -1,478 +0,0 @@ -//! Proc macros for `futures_kind`. -//! -//! This crate provides the `#[kinds]` attribute macro. - -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, quote}; -use syn::{ - GenericParam, Ident, ImplItem, ImplItemFn, ItemImpl, ReturnType, Type, TypePath, - WherePredicate, parse_macro_input, parse_quote, -}; - -/// Generate implementations of a trait for `Sendable` and/or `Local` `FutureKind`s. -/// -/// This attribute macro allows you to write a single implementation that works for both -/// `Send` and `!Send` futures, avoiding code duplication. -/// -/// # Usage -/// -/// ```rust,ignore -/// // Generate both Sendable and Local impls (default) -/// #[kinds] -/// impl MyTrait for MyType { ... } -/// -/// // Explicitly specify which kinds to generate -/// #[kinds(Sendable, Local)] -/// impl MyTrait for MyType { ... } -/// -/// // Generate only Sendable impl -/// #[kinds(Sendable)] -/// impl MyTrait for MyType { ... } -/// -/// // Generate only Local impl -/// #[kinds(Local)] -/// impl MyTrait for MyType { ... } -/// -/// // Add bounds only for specific variants -/// #[kinds(Sendable where T: Send, Local)] -/// impl MyTrait for Container { ... } -/// // Generates: impl MyTrait for Container -/// // impl MyTrait for Container -/// ``` -/// -/// # Example -/// -/// ```rust,ignore -/// use std::marker::PhantomData; -/// use futures_kind::{FutureKind, Sendable, Local, kinds}; -/// -/// trait Counter { -/// fn next(&self) -> K::Future<'_, u32>; -/// } -/// -/// struct Memory { -/// val: u32, -/// _marker: PhantomData, -/// } -/// -/// #[kinds] -/// impl Counter for Memory { -/// fn next(&self) -> K::Future<'_, u32> { -/// let val = self.val; -/// Box::pin(async move { val + 1 }) -/// } -/// } -/// ``` -#[proc_macro_attribute] -pub fn kinds(attr: TokenStream, item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as ItemImpl); - let kinds = parse_kinds(&attr); - - match generate_impls(&input, &kinds) { - Ok(tokens) => tokens.into(), - Err(err) => err.to_compile_error().into(), - } -} - -#[allow(clippy::expect_used)] // expects are guarded by starts_with checks -fn parse_kinds(attr: &TokenStream) -> Vec { - let attr_str = attr.to_string(); - let attr_str = attr_str.trim(); - - if attr_str.is_empty() { - // Default: generate both - return vec![ - FutureKindVariant { - kind: FutureKindType::Sendable, - extra_bounds: vec![], - }, - FutureKindVariant { - kind: FutureKindType::Local, - extra_bounds: vec![], - }, - ]; - } - - let mut kinds = Vec::new(); - - // Split by comma, but be careful not to split inside where clauses - // We'll parse by finding variant names and their optional where clauses - let mut remaining = attr_str; - - while !remaining.is_empty() { - remaining = remaining.trim_start_matches(',').trim(); - if remaining.is_empty() { - break; - } - - // Find the variant name - let (kind_type, rest) = if remaining.starts_with("Sendable") { - ( - Some(FutureKindType::Sendable), - remaining - .strip_prefix("Sendable") - .expect("guarded by starts_with"), - ) - } else if remaining.starts_with("Local") { - ( - Some(FutureKindType::Local), - remaining - .strip_prefix("Local") - .expect("guarded by starts_with"), - ) - } else { - // Skip unknown token until comma or end - let end = remaining.find(',').unwrap_or(remaining.len()); - remaining = &remaining[end..]; - continue; - }; - - let rest = rest.trim(); - - // Check for where clause - let (extra_bounds, after_where) = if rest.starts_with("where") { - let where_content = rest - .strip_prefix("where") - .expect("guarded by starts_with") - .trim(); - // Find where this variant's where clause ends (at next Sendable/Local or end) - let end_idx = find_next_variant(where_content); - let bounds_str = where_content[..end_idx].trim().trim_end_matches(','); - - // Parse the bounds - let bounds = parse_where_predicates(bounds_str); - (bounds, &where_content[end_idx..]) - } else { - (vec![], rest) - }; - - if let Some(kind) = kind_type { - kinds.push(FutureKindVariant { kind, extra_bounds }); - } - - remaining = after_where; - } - - if kinds.is_empty() { - // Fallback to both if parsing failed - vec![ - FutureKindVariant { - kind: FutureKindType::Sendable, - extra_bounds: vec![], - }, - FutureKindVariant { - kind: FutureKindType::Local, - extra_bounds: vec![], - }, - ] - } else { - kinds - } -} - -fn find_next_variant(s: &str) -> usize { - // Find the position of the next "Sendable" or "Local" that starts a new variant - // We need to be careful about these words appearing inside bounds - let mut depth: usize = 0; - - for (i, c) in s.chars().enumerate() { - match c { - '<' => depth += 1, - '>' => depth = depth.saturating_sub(1), - ',' if depth == 0 => { - // Check if after this comma we have a variant name - if let Some(after_comma) = s.get(i + 1..) { - let after_comma = after_comma.trim_start(); - if after_comma.starts_with("Sendable") || after_comma.starts_with("Local") { - return i; - } - } - } - _ => {} - } - } - - s.len() -} - -fn parse_where_predicates(s: &str) -> Vec { - if s.is_empty() { - return vec![]; - } - - // Split by '+' for multiple bounds on same type, or parse as single predicate - // Actually, where predicates are separated by commas, and each can have multiple bounds with + - // For simplicity, we'll try to parse the whole thing as comma-separated predicates - let mut predicates = vec![]; - - // Split carefully by comma, respecting angle brackets - let mut depth: usize = 0; - let mut start = 0; - let chars: Vec = s.chars().collect(); - - for (i, &c) in chars.iter().enumerate() { - match c { - '<' => depth += 1, - '>' => depth = depth.saturating_sub(1), - ',' if depth == 0 => { - let pred_str = s[start..i].trim(); - if let Ok(pred) = syn::parse_str::(pred_str) { - predicates.push(pred); - } - start = i + 1; - } - _ => {} - } - } - - // Don't forget the last predicate - let pred_str = s[start..].trim(); - if !pred_str.is_empty() - && let Ok(pred) = syn::parse_str::(pred_str) - { - predicates.push(pred); - } - - predicates -} - -fn generate_impls(input: &ItemImpl, kinds: &[FutureKindVariant]) -> syn::Result { - // Find the K type parameter - let k_param = find_k_param(input)?; - - // Generate impl for each requested kind - let impls: Vec = kinds - .iter() - .map(|kind| generate_impl_for_kind(input, &k_param, kind)) - .collect(); - - Ok(quote! { - #(#impls)* - }) -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum FutureKindType { - Sendable, - Local, -} - -impl FutureKindType { - fn kind_type(self) -> TokenStream2 { - match self { - Self::Sendable => quote!(::futures_kind::Sendable), - Self::Local => quote!(::futures_kind::Local), - } - } - - fn future_type(self) -> TokenStream2 { - match self { - Self::Sendable => quote!(::futures::future::BoxFuture), - Self::Local => quote!(::futures::future::LocalBoxFuture), - } - } -} - -#[derive(Clone)] -struct FutureKindVariant { - kind: FutureKindType, - extra_bounds: Vec, -} - -fn find_k_param(input: &ItemImpl) -> syn::Result { - // Look for a type parameter that has FutureKind bound - for param in &input.generics.params { - if let GenericParam::Type(type_param) = param { - for bound in &type_param.bounds { - if let syn::TypeParamBound::Trait(trait_bound) = bound { - let path = &trait_bound.path; - if path - .segments - .last() - .is_some_and(|s| s.ident == "FutureKind") - { - return Ok(type_param.ident.clone()); - } - } - } - } - } - - Err(syn::Error::new_spanned( - &input.generics, - "Expected a type parameter with FutureKind bound (e.g., `K: FutureKind`)", - )) -} - -fn generate_impl_for_kind( - input: &ItemImpl, - k_param: &Ident, - variant: &FutureKindVariant, -) -> TokenStream2 { - let kind_type = variant.kind.kind_type(); - let future_type = variant.kind.future_type(); - - // Clone and modify generics - remove the K parameter - let mut new_generics = input.generics.clone(); - new_generics.params = new_generics - .params - .into_iter() - .filter(|p| { - if let GenericParam::Type(tp) = p { - tp.ident != *k_param - } else { - true - } - }) - .collect(); - - // Remove where clause predicates that reference K, and add extra bounds - if let Some(ref mut where_clause) = new_generics.where_clause { - where_clause.predicates = where_clause - .predicates - .clone() - .into_iter() - .filter(|pred| !predicate_references_ident(pred, k_param)) - .collect(); - // Add variant-specific extra bounds - for bound in &variant.extra_bounds { - where_clause.predicates.push(bound.clone()); - } - } else if !variant.extra_bounds.is_empty() { - // Create a where clause if we have extra bounds but none existed - let mut predicates = syn::punctuated::Punctuated::new(); - for bound in &variant.extra_bounds { - predicates.push(bound.clone()); - } - new_generics.where_clause = Some(syn::WhereClause { - where_token: syn::token::Where::default(), - predicates, - }); - } - - // Replace K in self_ty - let new_self_ty = replace_ident_in_type(&input.self_ty, k_param, &kind_type); - - // Replace K in trait path if present - let new_trait = input.trait_.as_ref().map(|(bang, path, for_token)| { - let new_path = replace_ident_in_path(path, k_param, &kind_type); - (*bang, new_path, *for_token) - }); - - // Transform methods - let new_items: Vec = input - .items - .iter() - .map(|item| transform_impl_item(item, k_param, &kind_type, &future_type)) - .collect(); - - let (impl_generics, _, where_clause) = new_generics.split_for_impl(); - - let trait_tokens = new_trait.map(|(bang, path, for_token)| { - quote! { #bang #path #for_token } - }); - - quote! { - impl #impl_generics #trait_tokens #new_self_ty #where_clause { - #(#new_items)* - } - } -} - -fn predicate_references_ident(pred: &syn::WherePredicate, ident: &Ident) -> bool { - let tokens = pred.to_token_stream().to_string(); - tokens.contains(&ident.to_string()) -} - -fn replace_ident_in_type(ty: &Type, from: &Ident, to: &TokenStream2) -> TokenStream2 { - let ty_str = ty.to_token_stream().to_string(); - let from_str = from.to_string(); - - // Simple token replacement - let replaced = ty_str.replace(&from_str, &to.to_string()); - replaced.parse().unwrap_or_else(|_| ty.to_token_stream()) -} - -fn replace_ident_in_path(path: &syn::Path, from: &Ident, to: &TokenStream2) -> syn::Path { - let path_str = path.to_token_stream().to_string(); - let from_str = from.to_string(); - - let replaced = path_str.replace(&from_str, &to.to_string()); - syn::parse_str(&replaced).unwrap_or_else(|_| path.clone()) -} - -#[allow(clippy::wildcard_enum_match_arm)] // We want to pass through unknown variants unchanged -fn transform_impl_item( - item: &ImplItem, - k_param: &Ident, - kind_type: &TokenStream2, - future_type: &TokenStream2, -) -> ImplItem { - match item { - ImplItem::Fn(method) => { - ImplItem::Fn(transform_method(method, k_param, kind_type, future_type)) - } - other => other.clone(), - } -} - -fn transform_method( - method: &ImplItemFn, - k_param: &Ident, - kind_type: &TokenStream2, - future_type: &TokenStream2, -) -> ImplItemFn { - let mut new_method = method.clone(); - - // Transform return type if it's K::Future<...> - if let ReturnType::Type(arrow, ty) = &method.sig.output { - let new_ty = transform_return_type(ty, k_param, future_type); - new_method.sig.output = ReturnType::Type(*arrow, Box::new(new_ty)); - } - - // Transform method body: replace K with concrete kind type - // This handles K::into_kind, K::Future, etc. - let body_str = new_method.block.to_token_stream().to_string(); - let k_str = k_param.to_string(); - let kind_str = kind_type.to_string(); - - // Replace K followed by :: (handles K::into_kind, K::Future, etc.) - // syn may tokenize as "K ::" or "K::" depending on context - let replaced = body_str - .replace(&format!("{k_str} ::"), &format!("{kind_str} ::")) - .replace(&format!("{k_str}::"), &format!("{kind_str}::")); - - if let Ok(new_block) = syn::parse_str(&replaced) { - new_method.block = new_block; - } - - new_method -} - -fn transform_return_type(ty: &Type, k_param: &Ident, future_type: &TokenStream2) -> Type { - // Check if it's K::Future<...> - if let Type::Path(TypePath { qself: None, path }) = ty - && let (Some(first), Some(second)) = (path.segments.get(0), path.segments.get(1)) - && path.segments.len() == 2 - && first.ident == *k_param - && second.ident == "Future" - && let syn::PathArguments::AngleBracketed(args) = &second.arguments - { - // Extract the generic arguments from Future<'a, T> - let args_tokens = &args.args; - return parse_quote!(#future_type<#args_tokens>); - } - - // Fallback: do string replacement - let ty_str = ty.to_token_stream().to_string(); - let k_str = k_param.to_string(); - let pattern = format!("{k_str} :: Future"); - - if ty_str.contains(&pattern) { - let replaced = ty_str.replace(&pattern, &future_type.to_string()); - syn::parse_str(&replaced).unwrap_or_else(|_| ty.clone()) - } else { - ty.clone() - } -}