From 4f0c464e25f723783f729e7627652e1433145f30 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Fri, 8 Aug 2025 16:45:40 +0100 Subject: [PATCH 01/20] Implement `#[builder(derive(IntoFuture(Box)))]`, with support for `?Send` as well. --- .../builder_derives/into_future.rs | 77 +++++++++++++++++++ .../builder_gen/builder_derives/mod.rs | 7 +- .../builder_gen/top_level_config/mod.rs | 75 ++++++++++++++++++ 3 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 bon-macros/src/builder/builder_gen/builder_derives/into_future.rs diff --git a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs new file mode 100644 index 00000000..460dae59 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs @@ -0,0 +1,77 @@ +use crate::builder::builder_gen::models::BuilderGenCtx; +use crate::builder::builder_gen::top_level_config::IntoFutureConfig; +use crate::util::prelude::*; + +impl BuilderGenCtx { + pub(super) fn derive_into_future(&self, config: &IntoFutureConfig) -> Result { + if self.finish_fn.asyncness.is_none() { + bail!( + &self.finish_fn.ident, + "`#[builder(derive(IntoFuture(...)))` can only be used with async functions + because `IntoFuture::into_future()` method is an asynchronous method" + ); + } + + if let Some(unsafety) = &self.finish_fn.unsafety { + bail!( + unsafety, + "`#[builder(derive(IntoFuture(...)))` is not supported for unsafe functions \ + because `IntoFuture::into_future()` method is a safe method" + ); + } + + if let Some(arg) = self.finish_fn_args().next() { + bail!( + &arg.config.finish_fn.span(), + "`#[builder(derive(IntoFuture(...)))` is incompatible with `#[builder(finish_fn)]` members \ + because `IntoFuture::into_future()` method accepts zero parameters" + ); + } + + let output_ty = match &self.finish_fn.output { + syn::ReturnType::Default => Box::new(syn::Type::Tuple(syn::TypeTuple { + paren_token: syn::token::Paren::default(), + elems: syn::punctuated::Punctuated::new(), + })), + syn::ReturnType::Type(_, output_ty) => output_ty.clone(), + }; + + let state_mod = &self.state_mod.ident; + let generics_decl = &self.generics.decl_without_defaults; + let generic_args = &self.generics.args; + let builder_ident = &self.builder_type.ident; + let state_var = &self.state_var; + let finish_fn_ident = &self.finish_fn.ident; + + let builder_ty = quote! { + #builder_ident<#(#generic_args,)* #state_var> + }; + + let send_bound = if config.is_send { + quote! { + ::core::marker::Send } + } else { + quote! {} + }; + + let tokens = quote! { + #[automatically_derived] + impl< + #(#generics_decl,)* + #state_var: #state_mod::IsComplete + 'static + > + ::core::future::IntoFuture for #builder_ty + where + #builder_ty: 'static, + { + type Output = #output_ty; + type IntoFuture = ::std::pin::Pin<::std::boxed::Box #send_bound>>; + + fn into_future(self) -> Self::IntoFuture { + ::std::boxed::Box::pin(#builder_ident::#finish_fn_ident(self)) + } + } + }; + + Ok(tokens) + } +} diff --git a/bon-macros/src/builder/builder_gen/builder_derives/mod.rs b/bon-macros/src/builder/builder_gen/builder_derives/mod.rs index 573169ba..99b57163 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives/mod.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives/mod.rs @@ -1,6 +1,7 @@ mod clone; mod debug; mod into; +mod into_future; use super::top_level_config::{DeriveConfig, DerivesConfig}; use super::BuilderGenCtx; @@ -9,7 +10,7 @@ use darling::ast::GenericParamExt; impl BuilderGenCtx { pub(crate) fn builder_derives(&self) -> Result { - let DerivesConfig { clone, debug, into } = &self.builder_type.derives; + let DerivesConfig { clone, debug, into, into_future } = &self.builder_type.derives; let mut tokens = TokenStream::new(); @@ -25,6 +26,10 @@ impl BuilderGenCtx { tokens.extend(self.derive_into()?); } + if let Some(derive) = into_future { + tokens.extend(self.derive_into_future(derive)?); + } + Ok(tokens) } diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index 24230bb3..e68990f4 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -194,6 +194,9 @@ pub(crate) struct DerivesConfig { #[darling(rename = "Into")] pub(crate) into: darling::util::Flag, + + #[darling(rename = "IntoFuture")] + pub(crate) into_future: Option, } #[derive(Debug, Clone, Default)] @@ -201,6 +204,78 @@ pub(crate) struct DeriveConfig { pub(crate) bounds: Option>, } +#[derive(Debug, Clone)] +pub(crate) struct IntoFutureConfig { + pub(crate) is_send: bool, +} + +impl Default for IntoFutureConfig { + fn default() -> Self { + Self { is_send: true } + } +} + +impl FromMeta for IntoFutureConfig { + fn from_meta(meta: &syn::Meta) -> Result { + let meta_list = meta.require_list()?; + meta_list.require_parens_delim()?; + + // Use syn's Parse trait for cleaner parsing. + struct ParsedArguments { + is_send: bool, + } + + impl syn::parse::Parse for ParsedArguments { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + // Parse "Box" as the required first argument. + let box_ident: syn::Ident = input.parse()?; + if box_ident != "Box" { + return Err(syn::Error::new( + box_ident.span(), + "expected `Box` as the first argument, only boxed futures are supported", + )); + } + + // Check for optional ", ?Send" part. + let is_send = if input.peek(syn::Token![,]) { + input.parse::()?; + + // Parse "?Send" as a single unit. + if input.peek(syn::Token![?]) { + input.parse::()?; + let send_ident: syn::Ident = input.parse()?; + if send_ident != "Send" { + return Err(syn::Error::new( + send_ident.span(), + "expected `Send` after ?", + )); + } + false + } else { + return Err(input.error("expected `?Send` as the second argument")); + } + } else { + true + }; + + // Ensure no trailing tokens. + if !input.is_empty() { + return Err(input.error("unexpected tokens after arguments")); + } + + Ok(ParsedArguments { is_send }) + } + } + + let parsed: ParsedArguments = syn::parse2(meta_list.tokens.clone()) + .map_err(|err| Error::from(err).with_span(meta))?; + + Ok(Self { + is_send: parsed.is_send, + }) + } +} + impl FromMeta for DeriveConfig { fn from_meta(meta: &syn::Meta) -> Result { if let syn::Meta::Path(_) = meta { From 62585ae6c1f5c7dab386c542c256fc1f06872750 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Fri, 8 Aug 2025 16:46:04 +0100 Subject: [PATCH 02/20] Add integration tests. --- .../integration/builder/attr_into_future.rs | 86 +++++++++++++++++++ bon/tests/integration/builder/mod.rs | 1 + .../ui/compile_fail/attr_into_future.rs | 54 ++++++++++++ .../ui/compile_fail/attr_into_future.stderr | 45 ++++++++++ 4 files changed, 186 insertions(+) create mode 100644 bon/tests/integration/builder/attr_into_future.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_into_future.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_into_future.stderr diff --git a/bon/tests/integration/builder/attr_into_future.rs b/bon/tests/integration/builder/attr_into_future.rs new file mode 100644 index 00000000..4062e3de --- /dev/null +++ b/bon/tests/integration/builder/attr_into_future.rs @@ -0,0 +1,86 @@ +#[tokio::test] +async fn into_future_basic() { + #[bon::builder] + #[builder(derive(IntoFuture(Box)))] + async fn simple_async_fn(value: u32) -> u32 { + value * 2 + } + + // Test direct call. + let result = simple_async_fn().value(21).call().await; + assert_eq!(result, 42); + + // Test using IntoFuture with await. + let result = simple_async_fn().value(21).await; + assert_eq!(result, 42); +} + +#[tokio::test] +async fn into_future_non_send() { + #[bon::builder] + #[builder(derive(IntoFuture(Box, ?Send)))] + async fn non_send_async_fn(value: u32) -> u32 { + // This future can be !Send. + value * 2 + } + + // Test with non-Send future. + let result = non_send_async_fn().value(21).await; + assert_eq!(result, 42); +} + +#[tokio::test] +async fn into_future_with_result() { + #[bon::builder] + #[builder(derive(IntoFuture(Box)))] + async fn async_with_result(value: u32) -> Result { + if value > 0 { + Ok(value * 2) + } else { + Err("Value must be positive".to_string()) + } + } + + // Test successful case. + let result = async_with_result().value(21).await; + assert_eq!(result.unwrap(), 42); + + // Test error case. + let result = async_with_result().value(0).await; + assert!(result.is_err()); +} + +#[tokio::test] +async fn into_future_with_impl() { + struct Calculator; + + #[bon::bon] + impl Calculator { + #[builder] + #[builder(derive(IntoFuture(Box)))] + async fn multiply(a: u32, b: u32) -> u32 { + a * b + } + } + + // Test using IntoFuture on impl method. + let result = Calculator::multiply().a(6).b(7).await; + assert_eq!(result, 42); +} + +#[tokio::test] +async fn into_future_with_optional() { + #[bon::builder] + #[builder(derive(IntoFuture(Box)))] + async fn optional_param(#[builder(default = 100)] value: u32) -> u32 { + value + } + + // Test with value. + let result = optional_param().value(42).await; + assert_eq!(result, 42); + + // Test without value (using default). + let result = optional_param().await; + assert_eq!(result, 100); +} diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index 3a365a29..1dd46038 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -7,6 +7,7 @@ mod attr_derive; mod attr_field; mod attr_getter; mod attr_into; +mod attr_into_future; mod attr_on; mod attr_overwritable; mod attr_required; diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.rs b/bon/tests/integration/ui/compile_fail/attr_into_future.rs new file mode 100644 index 00000000..d044dbba --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.rs @@ -0,0 +1,54 @@ +// IntoFuture can only be used with async functions +#[bon::builder] +#[builder(derive(IntoFuture(Box)))] +fn sync_function() -> u32 { + 42 +} + +// IntoFuture is not supported for unsafe functions +#[bon::builder] +#[builder(derive(IntoFuture(Box)))] +async unsafe fn unsafe_async_function() -> u32 { + 42 +} + +// IntoFuture is incompatible with finish_fn members +#[bon::builder] +#[builder(derive(IntoFuture(Box)))] +async fn with_finish_fn( + #[builder(finish_fn)] value: u32 +) -> u32 { + value +} + +// IntoFuture requires Box argument +#[bon::builder] +#[builder(derive(IntoFuture))] +async fn missing_box_arg() -> u32 { + 42 +} + +// Only Box is supported as future container +#[bon::builder] +#[builder(derive(IntoFuture(Arc)))] +async fn wrong_container() -> u32 { + 42 +} + +// Wrong syntax for ?Send +#[bon::builder] +#[builder(derive(IntoFuture(Box, Send)))] +async fn wrong_send_syntax() -> u32 { + 42 +} + +use bon::Builder; + +// Cannot be used on structs +#[derive(Builder)] +#[builder(derive(IntoFuture(Box)))] +struct AsyncConfig { + value: u32, +} + +fn main() {} \ No newline at end of file diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr new file mode 100644 index 00000000..502a6c25 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr @@ -0,0 +1,45 @@ +error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions + because `IntoFuture::into_future()` method is an asynchronous method + --> tests/integration/ui/compile_fail/attr_into_future.rs:2:1 + | +2 | #[bon::builder] + | ^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `bon::builder` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `#[builder(derive(IntoFuture(...)))` is not supported for unsafe functions because `IntoFuture::into_future()` method is a safe method + --> tests/integration/ui/compile_fail/attr_into_future.rs:11:7 + | +11 | async unsafe fn unsafe_async_function() -> u32 { + | ^^^^^^ + +error: `#[builder(derive(IntoFuture(...)))` is incompatible with `#[builder(finish_fn)]` members because `IntoFuture::into_future()` method accepts zero parameters + --> tests/integration/ui/compile_fail/attr_into_future.rs:19:15 + | +19 | #[builder(finish_fn)] value: u32 + | ^^^^^^^^^ + +error: expected attribute arguments in parentheses: `IntoFuture(...)` + --> tests/integration/ui/compile_fail/attr_into_future.rs:26:18 + | +26 | #[builder(derive(IntoFuture))] + | ^^^^^^^^^^ + +error: expected `Box` as the first argument, only boxed futures are supported + --> tests/integration/ui/compile_fail/attr_into_future.rs:33:29 + | +33 | #[builder(derive(IntoFuture(Arc)))] + | ^^^ + +error: expected `?Send` as the second argument + --> tests/integration/ui/compile_fail/attr_into_future.rs:40:34 + | +40 | #[builder(derive(IntoFuture(Box, Send)))] + | ^^^^ + +error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions + because `IntoFuture::into_future()` method is an asynchronous method + --> tests/integration/ui/compile_fail/attr_into_future.rs:50:8 + | +50 | struct AsyncConfig { + | ^^^^^^^^^^^ From eb8535b7df34c218ec77cf290b07691bf2994c8e Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Fri, 8 Aug 2025 16:46:14 +0100 Subject: [PATCH 03/20] Add an example to the documentation. --- website/doctests/Cargo.toml | 1 + .../src/reference/builder/top-level/derive.md | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/website/doctests/Cargo.toml b/website/doctests/Cargo.toml index d33474ef..1a584678 100644 --- a/website/doctests/Cargo.toml +++ b/website/doctests/Cargo.toml @@ -16,6 +16,7 @@ anyhow = "1.0" bon = { path = "../../bon", features = ["experimental-overwritable", "implied-bounds"] } buildstructor = "0.6" macro_rules_attribute = "0.2" +tokio-test = "0.4.4" typed-builder = "0.21" [build-dependencies] diff --git a/website/src/reference/builder/top-level/derive.md b/website/src/reference/builder/top-level/derive.md index b9fef91e..938597ea 100644 --- a/website/src/reference/builder/top-level/derive.md +++ b/website/src/reference/builder/top-level/derive.md @@ -6,7 +6,7 @@ _⚠️ Do not confuse this with `#[derive(bon::Builder)]`⚠️_ Generates additional derives for the builder struct itself. The syntax is similar to the regular `#[derive(...)]` attribute, but it must be wrapped in `#[builder(derive(...))]`. Expects one or more of the supported derives separated by a comma. -The following derives are supported: [`Clone`, `Debug`](#clone-and-debug-derives), [`Into`](#into-derive). +The following derives are supported: [`Clone`, `Debug`](#clone-and-debug-derives), [`Into`](#into-derive), [`IntoFuture`](#intofuture-derive). ::: warning The format of the `Debug` output of the builder is not stable, and it may change between patch versions of `bon`. @@ -245,7 +245,7 @@ impl From> for Example { Note that `#[builder(derive(Into))]` is quite limited. Here are some things it doesn't support: -- `async` functions, because `From::from()` is synchronous +- `async` functions, because `From::from()` is synchronous. Refer to [`IntoFuture`](#intofuture-derive) instead. - `unsafe` functions, because `From::from()` is safe - Members marked with [`#[builder(finish_fn)]`](../member/finish_fn) because `From::from()` doesn't accept arguments @@ -270,3 +270,23 @@ take_example( .x1(99) ) ``` + +## `IntoFuture` Derive + +Implements [`IntoFuture`](https://doc.rust-lang.org/std/future/trait.IntoFuture.html) for the builder, allowing it to be +`await`-ed directly. To have the derived implementation produce non-`Send` futures, add `?Send` like so: `#[builder(derive(IntoFuture(Box, ?Send)))]`. + +```rust +use bon::builder; + +#[builder(derive(IntoFuture(Box)))] +async fn fetch_string(url: &str, body: Option>) -> std::io::Result { + // … + Ok("Server response".to_owned()) +} + +tokio_test::block_on(async { + let response = fetch_string().url("https://example.org").await; + assert_eq!(response.ok().as_deref(), Some("Server response")); +}) +``` From e740ba313b543b66ebc0a7a470769097066b791c Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Fri, 8 Aug 2025 16:46:26 +0100 Subject: [PATCH 04/20] Update Cargo.lock. --- Cargo.lock | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b4af2bd7..a8e05f10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,28 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -135,6 +157,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cast" version = "0.3.0" @@ -419,6 +447,7 @@ dependencies = [ "itertools 0.14.0", "lazy-regex", "macro_rules_attribute", + "tokio-test", "typed-builder", "walkdir", ] @@ -451,6 +480,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + [[package]] name = "gimli" version = "0.31.1" @@ -999,6 +1034,30 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "toml" version = "0.9.2" From a0dabdd64bd33de5417bd4e5134029d2824f169f Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 13:02:40 +0000 Subject: [PATCH 05/20] Some improvements to the doc test (see commit body below for details) Use `#[tokio::main]` instead of `tokio_test` in the doc example to make it a more real-world-looking. Also strip the `std::io::Result` for simplicity. Async functions are usually fallible, but for the purposes of an example, it's fine to simplify it. Besides, to my personal mind `std::io::Error` is usually a bad pattern as it returns a non-descriptive error message. I'd promote `anyhow` instead in the docs, but here I think making it infallible is okay. --- Cargo.lock | 64 +------------------ website/doctests/Cargo.toml | 2 +- .../src/reference/builder/top-level/derive.md | 11 ++-- 3 files changed, 10 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8e05f10..1a08929d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,28 +44,6 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -157,12 +135,6 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - [[package]] name = "cast" version = "0.3.0" @@ -447,7 +419,7 @@ dependencies = [ "itertools 0.14.0", "lazy-regex", "macro_rules_attribute", - "tokio-test", + "tokio", "typed-builder", "walkdir", ] @@ -480,12 +452,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - [[package]] name = "gimli" version = "0.31.1" @@ -1010,9 +976,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "io-uring", @@ -1034,30 +1000,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-test" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" -dependencies = [ - "async-stream", - "bytes", - "futures-core", - "tokio", - "tokio-stream", -] - [[package]] name = "toml" version = "0.9.2" diff --git a/website/doctests/Cargo.toml b/website/doctests/Cargo.toml index 1a584678..08569cff 100644 --- a/website/doctests/Cargo.toml +++ b/website/doctests/Cargo.toml @@ -16,7 +16,7 @@ anyhow = "1.0" bon = { path = "../../bon", features = ["experimental-overwritable", "implied-bounds"] } buildstructor = "0.6" macro_rules_attribute = "0.2" -tokio-test = "0.4.4" +tokio = { version = "1.47", features = ["macros", "rt-multi-thread"] } typed-builder = "0.21" [build-dependencies] diff --git a/website/src/reference/builder/top-level/derive.md b/website/src/reference/builder/top-level/derive.md index 938597ea..fd7eb9e8 100644 --- a/website/src/reference/builder/top-level/derive.md +++ b/website/src/reference/builder/top-level/derive.md @@ -280,13 +280,14 @@ Implements [`IntoFuture`](https://doc.rust-lang.org/std/future/trait.IntoFuture. use bon::builder; #[builder(derive(IntoFuture(Box)))] -async fn fetch_string(url: &str, body: Option>) -> std::io::Result { +async fn fetch_string(url: &str, body: Option>) -> String { // … - Ok("Server response".to_owned()) + "Server response".to_owned() } -tokio_test::block_on(async { +#[tokio::main] +async fn main() { let response = fetch_string().url("https://example.org").await; - assert_eq!(response.ok().as_deref(), Some("Server response")); -}) + assert_eq!(response, "Server response"); +} ``` From 102238e8857cbb0d312c1340bcbae0cc79297072 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 13:03:50 +0000 Subject: [PATCH 06/20] Run `cargo fmt`. CI isn't enabled for external contributors by default, so it was missed --- bon-macros/src/builder/builder_gen/builder_derives/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bon-macros/src/builder/builder_gen/builder_derives/mod.rs b/bon-macros/src/builder/builder_gen/builder_derives/mod.rs index 99b57163..df886000 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives/mod.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives/mod.rs @@ -10,7 +10,12 @@ use darling::ast::GenericParamExt; impl BuilderGenCtx { pub(crate) fn builder_derives(&self) -> Result { - let DerivesConfig { clone, debug, into, into_future } = &self.builder_type.derives; + let DerivesConfig { + clone, + debug, + into, + into_future, + } = &self.builder_type.derives; let mut tokens = TokenStream::new(); From 1b2453b6e2a511a6e64e6c90061a4437d1d2b30c Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 13:05:32 +0000 Subject: [PATCH 07/20] Add missing trailing `\` in the error message --- .../src/builder/builder_gen/builder_derives/into_future.rs | 2 +- .../integration/ui/compile_fail/attr_into_future.stderr | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs index 460dae59..c0fe8364 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs @@ -7,7 +7,7 @@ impl BuilderGenCtx { if self.finish_fn.asyncness.is_none() { bail!( &self.finish_fn.ident, - "`#[builder(derive(IntoFuture(...)))` can only be used with async functions + "`#[builder(derive(IntoFuture(...)))` can only be used with async functions \ because `IntoFuture::into_future()` method is an asynchronous method" ); } diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr index 502a6c25..c3141901 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr @@ -1,5 +1,4 @@ -error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions - because `IntoFuture::into_future()` method is an asynchronous method +error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions because `IntoFuture::into_future()` method is an asynchronous method --> tests/integration/ui/compile_fail/attr_into_future.rs:2:1 | 2 | #[bon::builder] @@ -37,8 +36,7 @@ error: expected `?Send` as the second argument 40 | #[builder(derive(IntoFuture(Box, Send)))] | ^^^^ -error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions - because `IntoFuture::into_future()` method is an asynchronous method +error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions because `IntoFuture::into_future()` method is an asynchronous method --> tests/integration/ui/compile_fail/attr_into_future.rs:50:8 | 50 | struct AsyncConfig { From 75a1f87f7a49e837f98637c4fec3853ca508a41d Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 13:10:10 +0000 Subject: [PATCH 08/20] Improve the error message for synchronous function handling --- .../builder/builder_gen/builder_derives/into_future.rs | 9 +++++++-- .../integration/ui/compile_fail/attr_into_future.stderr | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs index c0fe8364..b3d9d7d5 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs @@ -5,10 +5,15 @@ use crate::util::prelude::*; impl BuilderGenCtx { pub(super) fn derive_into_future(&self, config: &IntoFutureConfig) -> Result { if self.finish_fn.asyncness.is_none() { + // While it is technically possible to call a synchronous function + // inside of the `IntoFuture::into_future()`, it's better force the + // user to mark the function as `async` explicitly. Otherwise it may + // indicate of some logic bug where the developer mistakenly marks + // a function that could be sync with `derive(IntoFuture)`. bail!( &self.finish_fn.ident, - "`#[builder(derive(IntoFuture(...)))` can only be used with async functions \ - because `IntoFuture::into_future()` method is an asynchronous method" + "`#[builder(derive(IntoFuture(...)))` can only be used with async functions; \ + using it with a synchronous function is likely a mistake" ); } diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr index c3141901..4e728fc5 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr @@ -1,4 +1,4 @@ -error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions because `IntoFuture::into_future()` method is an asynchronous method +error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake --> tests/integration/ui/compile_fail/attr_into_future.rs:2:1 | 2 | #[bon::builder] @@ -36,7 +36,7 @@ error: expected `?Send` as the second argument 40 | #[builder(derive(IntoFuture(Box, Send)))] | ^^^^ -error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions because `IntoFuture::into_future()` method is an asynchronous method +error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake --> tests/integration/ui/compile_fail/attr_into_future.rs:50:8 | 50 | struct AsyncConfig { From 79acfc58f7dc923963c3f99e0868d932bc6653f8 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 13:31:50 +0000 Subject: [PATCH 09/20] Use `crate::prelude` in integration tests --- bon/tests/integration/builder/attr_into_future.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bon/tests/integration/builder/attr_into_future.rs b/bon/tests/integration/builder/attr_into_future.rs index 4062e3de..2962ead9 100644 --- a/bon/tests/integration/builder/attr_into_future.rs +++ b/bon/tests/integration/builder/attr_into_future.rs @@ -1,6 +1,8 @@ +use crate::prelude::*; + #[tokio::test] async fn into_future_basic() { - #[bon::builder] + #[builder] #[builder(derive(IntoFuture(Box)))] async fn simple_async_fn(value: u32) -> u32 { value * 2 @@ -17,7 +19,7 @@ async fn into_future_basic() { #[tokio::test] async fn into_future_non_send() { - #[bon::builder] + #[builder] #[builder(derive(IntoFuture(Box, ?Send)))] async fn non_send_async_fn(value: u32) -> u32 { // This future can be !Send. @@ -31,7 +33,7 @@ async fn into_future_non_send() { #[tokio::test] async fn into_future_with_result() { - #[bon::builder] + #[builder] #[builder(derive(IntoFuture(Box)))] async fn async_with_result(value: u32) -> Result { if value > 0 { @@ -54,7 +56,7 @@ async fn into_future_with_result() { async fn into_future_with_impl() { struct Calculator; - #[bon::bon] + #[bon] impl Calculator { #[builder] #[builder(derive(IntoFuture(Box)))] @@ -70,7 +72,7 @@ async fn into_future_with_impl() { #[tokio::test] async fn into_future_with_optional() { - #[bon::builder] + #[builder] #[builder(derive(IntoFuture(Box)))] async fn optional_param(#[builder(default = 100)] value: u32) -> u32 { value From bfafc1ee609dee91b88f899561b117d8f10ef3ae Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 13:32:35 +0000 Subject: [PATCH 10/20] Remove redundant `#[builder]` --- bon/tests/integration/builder/attr_into_future.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bon/tests/integration/builder/attr_into_future.rs b/bon/tests/integration/builder/attr_into_future.rs index 2962ead9..90e53e71 100644 --- a/bon/tests/integration/builder/attr_into_future.rs +++ b/bon/tests/integration/builder/attr_into_future.rs @@ -2,7 +2,6 @@ use crate::prelude::*; #[tokio::test] async fn into_future_basic() { - #[builder] #[builder(derive(IntoFuture(Box)))] async fn simple_async_fn(value: u32) -> u32 { value * 2 @@ -19,7 +18,6 @@ async fn into_future_basic() { #[tokio::test] async fn into_future_non_send() { - #[builder] #[builder(derive(IntoFuture(Box, ?Send)))] async fn non_send_async_fn(value: u32) -> u32 { // This future can be !Send. @@ -33,7 +31,6 @@ async fn into_future_non_send() { #[tokio::test] async fn into_future_with_result() { - #[builder] #[builder(derive(IntoFuture(Box)))] async fn async_with_result(value: u32) -> Result { if value > 0 { @@ -72,7 +69,6 @@ async fn into_future_with_impl() { #[tokio::test] async fn into_future_with_optional() { - #[builder] #[builder(derive(IntoFuture(Box)))] async fn optional_param(#[builder(default = 100)] value: u32) -> u32 { value From f08916fa3aced82b7a41e4a48457218e8c80519d Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 14:05:48 +0000 Subject: [PATCH 11/20] Make the output `#[no_std]`- friendly --- bon-macros/Cargo.toml | 3 + .../builder_derives/into_future.rs | 28 +++-- .../builder_gen/top_level_config/mod.rs | 101 ++++++++---------- bon/Cargo.toml | 4 +- .../integration/builder/attr_into_future.rs | 4 +- bon/tests/integration/builder/mod.rs | 5 +- .../ui/compile_fail/attr_into_future.stderr | 2 +- 7 files changed, 80 insertions(+), 67 deletions(-) diff --git a/bon-macros/Cargo.toml b/bon-macros/Cargo.toml index 45df795b..493f944c 100644 --- a/bon-macros/Cargo.toml +++ b/bon-macros/Cargo.toml @@ -63,6 +63,9 @@ rustversion = "1.0" [features] default = [] +alloc = [] +std = [] + # See the docs on this feature in the `bon`'s crate `Cargo.toml` experimental-overwritable = [] diff --git a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs index b3d9d7d5..5ca8aa89 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs @@ -47,6 +47,7 @@ impl BuilderGenCtx { let builder_ident = &self.builder_type.ident; let state_var = &self.state_var; let finish_fn_ident = &self.finish_fn.ident; + let box_ = &config.box_ident; let builder_ty = quote! { #builder_ident<#(#generic_args,)* #state_var> @@ -58,21 +59,36 @@ impl BuilderGenCtx { quote! {} }; + let bon = &self.bon; + + let alloc = if cfg!(feature = "std") { + quote!(::std) + } else if cfg!(feature = "alloc") { + quote!(#bon::__::alloc) + } else { + bail!( + &config.box_ident, + "`#[builder(derive(IntoFuture(Box)))]` requires either `std` or \ + `alloc` feature to be enabled" + ) + }; + let tokens = quote! { #[automatically_derived] impl< #(#generics_decl,)* #state_var: #state_mod::IsComplete + 'static > - ::core::future::IntoFuture for #builder_ty - where - #builder_ty: 'static, - { + ::core::future::IntoFuture for #builder_ty { type Output = #output_ty; - type IntoFuture = ::std::pin::Pin<::std::boxed::Box #send_bound>>; + type IntoFuture = ::core::pin::Pin< + #alloc::boxed::#box_< + dyn ::core::future::Future #send_bound + > + >; fn into_future(self) -> Self::IntoFuture { - ::std::boxed::Box::pin(#builder_ident::#finish_fn_ident(self)) + #alloc::boxed::#box_::pin(#builder_ident::#finish_fn_ident(self)) } } }; diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index e68990f4..c9507cde 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -206,73 +206,64 @@ pub(crate) struct DeriveConfig { #[derive(Debug, Clone)] pub(crate) struct IntoFutureConfig { + pub(crate) box_ident: syn::Ident, pub(crate) is_send: bool, } -impl Default for IntoFutureConfig { - fn default() -> Self { - Self { is_send: true } - } -} - -impl FromMeta for IntoFutureConfig { - fn from_meta(meta: &syn::Meta) -> Result { - let meta_list = meta.require_list()?; - meta_list.require_parens_delim()?; - - // Use syn's Parse trait for cleaner parsing. - struct ParsedArguments { - is_send: bool, +impl syn::parse::Parse for IntoFutureConfig { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + // Parse "Box" as the required first argument. + let box_ident: syn::Ident = input.parse()?; + if box_ident != "Box" { + return Err(syn::Error::new( + box_ident.span(), + "expected `Box` as the first argument, only boxed futures are supported", + )); } - impl syn::parse::Parse for ParsedArguments { - fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { - // Parse "Box" as the required first argument. - let box_ident: syn::Ident = input.parse()?; - if box_ident != "Box" { + // Check for optional ", ?Send" part. + let is_send = if input.peek(syn::Token![,]) { + input.parse::()?; + + // Parse "?Send" as a single unit. + if input.peek(syn::Token![?]) { + input.parse::()?; + let send_ident: syn::Ident = input.parse()?; + if send_ident != "Send" { return Err(syn::Error::new( - box_ident.span(), - "expected `Box` as the first argument, only boxed futures are supported", + send_ident.span(), + "expected `Send` after ?", )); } - - // Check for optional ", ?Send" part. - let is_send = if input.peek(syn::Token![,]) { - input.parse::()?; - - // Parse "?Send" as a single unit. - if input.peek(syn::Token![?]) { - input.parse::()?; - let send_ident: syn::Ident = input.parse()?; - if send_ident != "Send" { - return Err(syn::Error::new( - send_ident.span(), - "expected `Send` after ?", - )); - } - false - } else { - return Err(input.error("expected `?Send` as the second argument")); - } - } else { - true - }; - - // Ensure no trailing tokens. - if !input.is_empty() { - return Err(input.error("unexpected tokens after arguments")); - } - - Ok(ParsedArguments { is_send }) + false + } else { + return Err(input.error("expected `?Send` as the second argument")); } + } else { + true + }; + + // Ensure no trailing tokens. + if !input.is_empty() { + return Err(input.error("unexpected tokens after arguments")); } - let parsed: ParsedArguments = syn::parse2(meta_list.tokens.clone()) - .map_err(|err| Error::from(err).with_span(meta))?; + Ok(Self { box_ident, is_send }) + } +} - Ok(Self { - is_send: parsed.is_send, - }) +impl FromMeta for IntoFutureConfig { + fn from_meta(meta: &syn::Meta) -> Result { + let meta = match meta { + syn::Meta::List(meta) => meta, + _ => bail!(meta, "expected an attribute of form `IntoFuture(Box, ...)`"), + }; + + meta.require_parens_delim()?; + + let me = syn::parse2(meta.tokens.clone())?; + + Ok(me) } } diff --git a/bon/Cargo.toml b/bon/Cargo.toml index dd235ff9..7536eab1 100644 --- a/bon/Cargo.toml +++ b/bon/Cargo.toml @@ -61,9 +61,9 @@ macro_rules_attribute = "0.2" trybuild = "1.0.89" [features] -alloc = [] +alloc = ["bon-macros/alloc"] default = ["std"] -std = ["alloc"] +std = ["bon-macros/std", "alloc"] # See the explanation of what this feature does in the docs here: # https://bon-rs.com/guide/typestate-api/custom-methods#implied-bounds diff --git a/bon/tests/integration/builder/attr_into_future.rs b/bon/tests/integration/builder/attr_into_future.rs index 90e53e71..4e09e807 100644 --- a/bon/tests/integration/builder/attr_into_future.rs +++ b/bon/tests/integration/builder/attr_into_future.rs @@ -32,11 +32,11 @@ async fn into_future_non_send() { #[tokio::test] async fn into_future_with_result() { #[builder(derive(IntoFuture(Box)))] - async fn async_with_result(value: u32) -> Result { + async fn async_with_result(value: u32) -> Result { if value > 0 { Ok(value * 2) } else { - Err("Value must be positive".to_string()) + Err("Value must be positive") } } diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index 1dd46038..b6f93e84 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -7,7 +7,6 @@ mod attr_derive; mod attr_field; mod attr_getter; mod attr_into; -mod attr_into_future; mod attr_on; mod attr_overwritable; mod attr_required; @@ -31,6 +30,10 @@ mod track_caller; use crate::prelude::*; +/// [`core::future::IntoFuture`] relies on [`Box`] +#[cfg(any(feature = "std", feature = "alloc"))] +mod attr_into_future; + #[test] fn leading_underscore_is_stripped() { #[builder] diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr index 4e728fc5..31f6035f 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr @@ -18,7 +18,7 @@ error: `#[builder(derive(IntoFuture(...)))` is incompatible with `#[builder(fini 19 | #[builder(finish_fn)] value: u32 | ^^^^^^^^^ -error: expected attribute arguments in parentheses: `IntoFuture(...)` +error: expected an attribute of form `IntoFuture(Box, ...)` --> tests/integration/ui/compile_fail/attr_into_future.rs:26:18 | 26 | #[builder(derive(IntoFuture))] From 433e57979b381c73a7f641ba1ddadb65336124f3 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 14:06:47 +0000 Subject: [PATCH 12/20] Add `IntoFuture` to `ide.rs` for better syntax highlighting and intellisence --- bon/src/__/ide.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bon/src/__/ide.rs b/bon/src/__/ide.rs index a0809f81..22b24298 100644 --- a/bon/src/__/ide.rs +++ b/bon/src/__/ide.rs @@ -99,6 +99,9 @@ pub mod builder_top_level { /// See the docs at pub use core::convert::Into; + + /// See the docs at + pub use core::future::IntoFuture; } /// The real name of this parameter is `crate` (without the underscore). From 0e90bf6d0938d793f5be5c9455252374b5d9cd8d Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 14:07:58 +0000 Subject: [PATCH 13/20] Fix various clippy lints --- .../integration/builder/attr_into_future.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bon/tests/integration/builder/attr_into_future.rs b/bon/tests/integration/builder/attr_into_future.rs index 4e09e807..f4e76c59 100644 --- a/bon/tests/integration/builder/attr_into_future.rs +++ b/bon/tests/integration/builder/attr_into_future.rs @@ -1,10 +1,11 @@ use crate::prelude::*; +use core::future::ready; #[tokio::test] async fn into_future_basic() { #[builder(derive(IntoFuture(Box)))] async fn simple_async_fn(value: u32) -> u32 { - value * 2 + ready(value * 2).await } // Test direct call. @@ -21,7 +22,7 @@ async fn into_future_non_send() { #[builder(derive(IntoFuture(Box, ?Send)))] async fn non_send_async_fn(value: u32) -> u32 { // This future can be !Send. - value * 2 + ready(value * 2).await } // Test with non-Send future. @@ -33,11 +34,12 @@ async fn into_future_non_send() { async fn into_future_with_result() { #[builder(derive(IntoFuture(Box)))] async fn async_with_result(value: u32) -> Result { - if value > 0 { + ready(if value > 0 { Ok(value * 2) } else { Err("Value must be positive") - } + }) + .await } // Test successful case. @@ -46,7 +48,7 @@ async fn into_future_with_result() { // Test error case. let result = async_with_result().value(0).await; - assert!(result.is_err()); + result.unwrap_err(); } #[tokio::test] @@ -58,7 +60,7 @@ async fn into_future_with_impl() { #[builder] #[builder(derive(IntoFuture(Box)))] async fn multiply(a: u32, b: u32) -> u32 { - a * b + ready(a * b).await } } @@ -71,7 +73,7 @@ async fn into_future_with_impl() { async fn into_future_with_optional() { #[builder(derive(IntoFuture(Box)))] async fn optional_param(#[builder(default = 100)] value: u32) -> u32 { - value + ready(value).await } // Test with value. From 7c9f53a96b226828a8a0d2191fef868e3cd005cf Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 17:05:00 +0000 Subject: [PATCH 14/20] Add support for non-static lifetimes --- .../builder_derives/into_future.rs | 149 ++++++++++++++++-- .../integration/builder/attr_into_future.rs | 46 ++++++ 2 files changed, 179 insertions(+), 16 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs index 5ca8aa89..ecd869ce 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs @@ -1,6 +1,9 @@ use crate::builder::builder_gen::models::BuilderGenCtx; use crate::builder::builder_gen::top_level_config::IntoFutureConfig; use crate::util::prelude::*; +use std::borrow::Cow; +use std::collections::BTreeSet; +use syn::visit_mut::VisitMut; impl BuilderGenCtx { pub(super) fn derive_into_future(&self, config: &IntoFutureConfig) -> Result { @@ -33,25 +36,25 @@ impl BuilderGenCtx { ); } - let output_ty = match &self.finish_fn.output { - syn::ReturnType::Default => Box::new(syn::Type::Tuple(syn::TypeTuple { - paren_token: syn::token::Paren::default(), - elems: syn::punctuated::Punctuated::new(), - })), - syn::ReturnType::Type(_, output_ty) => output_ty.clone(), - }; - let state_mod = &self.state_mod.ident; - let generics_decl = &self.generics.decl_without_defaults; - let generic_args = &self.generics.args; let builder_ident = &self.builder_type.ident; let state_var = &self.state_var; let finish_fn_ident = &self.finish_fn.ident; let box_ = &config.box_ident; - let builder_ty = quote! { - #builder_ident<#(#generic_args,)* #state_var> - }; + let SignatureForIntoFuture { + generics_decl, + generic_args, + where_clause, + builder_lifetime, + output_ty, + } = self.signature_for_into_future(); + + let state_lifetime = builder_lifetime + .clone() + .unwrap_or_else(|| syn::Lifetime::new("'static", Span::call_site())); + + let builder_lifetime = Option::into_iter(builder_lifetime); let send_bound = if config.is_send { quote! { + ::core::marker::Send } @@ -77,13 +80,17 @@ impl BuilderGenCtx { #[automatically_derived] impl< #(#generics_decl,)* - #state_var: #state_mod::IsComplete + 'static + #state_var: #state_mod::IsComplete + #state_lifetime > - ::core::future::IntoFuture for #builder_ty { + ::core::future::IntoFuture for #builder_ident<#(#generic_args,)* #state_var> + #where_clause + { type Output = #output_ty; type IntoFuture = ::core::pin::Pin< #alloc::boxed::#box_< - dyn ::core::future::Future #send_bound + dyn ::core::future::Future + #send_bound + #(+ #builder_lifetime)* > >; @@ -95,4 +102,114 @@ impl BuilderGenCtx { Ok(tokens) } + + /// Handle the special case for a builder that captures lifetimes. + /// + /// Collapse all lifetimes into a single `'builder` lifetime. This is + /// because `dyn Trait` supports only a single `+ 'lifetime` bound. + fn signature_for_into_future(&self) -> SignatureForIntoFuture<'_> { + let generics_decl = &self.generics.decl_without_defaults; + let generic_args = &self.generics.args; + let where_clause = &self.generics.where_clause; + + let output_ty = match &self.finish_fn.output { + syn::ReturnType::Default => Cow::Owned(syn::parse_quote!(())), + syn::ReturnType::Type(_, output_ty) => Cow::Borrowed(output_ty.as_ref()), + }; + + let contains_lifetimes = matches!( + self.generics.args.first(), + Some(syn::GenericArgument::Lifetime(_)) + ); + + if !contains_lifetimes { + return SignatureForIntoFuture { + generics_decl: Cow::Borrowed(generics_decl), + generic_args: Cow::Borrowed(generic_args), + where_clause: where_clause.as_ref().map(Cow::Borrowed), + builder_lifetime: None, + output_ty, + }; + } + + let builder_lifetime = syn::Lifetime::new("'builder", Span::call_site()); + + let new_generic_args = generic_args + .iter() + .map(|arg| match arg { + syn::GenericArgument::Lifetime(_) => { + syn::GenericArgument::Lifetime(builder_lifetime.clone()) + } + _ => arg.clone(), + }) + .collect::>(); + + let mut original_lifetimes = BTreeSet::new(); + let mut new_generics_decl = vec![syn::parse_quote!(#builder_lifetime)]; + + for param in generics_decl { + match param { + syn::GenericParam::Lifetime(lifetime) => { + original_lifetimes.insert(&lifetime.lifetime.ident); + } + _ => { + new_generics_decl.push(param.clone()); + } + } + } + + let mut replace_lifetimes = ReplaceLifetimes { + replacement: &builder_lifetime, + original_lifetimes: &original_lifetimes, + }; + + let mut new_where_clause = where_clause.clone(); + + if let Some(where_clause) = &mut new_where_clause { + replace_lifetimes.visit_where_clause_mut(where_clause); + } + + let mut output_ty = output_ty.into_owned(); + + replace_lifetimes.visit_type_mut(&mut output_ty); + + SignatureForIntoFuture { + generics_decl: Cow::Owned(new_generics_decl), + generic_args: Cow::Owned(new_generic_args), + where_clause: new_where_clause.map(Cow::Owned), + builder_lifetime: Some(builder_lifetime), + output_ty: Cow::Owned(output_ty), + } + } +} + +struct SignatureForIntoFuture<'a> { + generics_decl: Cow<'a, [syn::GenericParam]>, + generic_args: Cow<'a, [syn::GenericArgument]>, + where_clause: Option>, + builder_lifetime: Option, + output_ty: Cow<'a, syn::Type>, +} + +struct ReplaceLifetimes<'a> { + replacement: &'a syn::Lifetime, + original_lifetimes: &'a BTreeSet<&'a syn::Ident>, +} + +impl VisitMut for ReplaceLifetimes<'_> { + fn visit_lifetime_mut(&mut self, lifetime: &mut syn::Lifetime) { + if self.original_lifetimes.contains(&lifetime.ident) { + *lifetime = self.replacement.clone(); + } + } + + fn visit_item_mut(&mut self, _: &mut syn::Item) { + // Don't recurse into child items. They don't inherit the parent item's + // lifetimes. + } + + fn visit_bound_lifetimes_mut(&mut self, _: &mut syn::BoundLifetimes) { + // Don't recurse into bound lifetime declarations. They introduce + // local lifetimes that we should keep as is + } } diff --git a/bon/tests/integration/builder/attr_into_future.rs b/bon/tests/integration/builder/attr_into_future.rs index f4e76c59..f3993096 100644 --- a/bon/tests/integration/builder/attr_into_future.rs +++ b/bon/tests/integration/builder/attr_into_future.rs @@ -84,3 +84,49 @@ async fn into_future_with_optional() { let result = optional_param().await; assert_eq!(result, 100); } + +#[tokio::test] +async fn references_in_params() { + struct Dummy; + + #[builder(derive(IntoFuture(Box)))] + async fn sut<'named1, 'named2>( + _x1: &Dummy, + _x2: &Dummy, + x3: &'named1 Dummy, + x4: &'named2 Dummy, + ) -> &'named2 Dummy { + let _: &'named1 Dummy = x3; + ready(x4).await + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_x1 = Dummy; + let local_x2 = Dummy; + let local_x3 = Dummy; + let local_x4 = Dummy; + + let _: &Dummy = sut() + .x1(&local_x1) + .x2(&local_x2) + .x3(&local_x3) + .x4(&local_x4) + .await; +} + +#[tokio::test] +async fn anon_lifetime_in_return_type() { + struct Dummy; + + #[builder(derive(IntoFuture(Box)))] + async fn sut(x1: &Dummy) -> &Dummy { + ready(x1).await + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_x1 = Dummy; + + let _: &Dummy = sut().x1(&local_x1).await; +} From d534b148a1e585a928eb5057ff5d8ea26fb56dd4 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 18:02:25 +0000 Subject: [PATCH 15/20] Add a bit more tests, gate them with 1.64 MSRV --- .../integration/builder/attr_into_future.rs | 341 ++++++++++++------ bon/tests/integration/builder/mod.rs | 5 +- .../ui/compile_fail/attr_into_future.rs | 34 +- .../ui/compile_fail/attr_into_future.stderr | 70 +++- 4 files changed, 317 insertions(+), 133 deletions(-) diff --git a/bon/tests/integration/builder/attr_into_future.rs b/bon/tests/integration/builder/attr_into_future.rs index f3993096..171c0bc1 100644 --- a/bon/tests/integration/builder/attr_into_future.rs +++ b/bon/tests/integration/builder/attr_into_future.rs @@ -1,132 +1,257 @@ -use crate::prelude::*; -use core::future::ready; - -#[tokio::test] -async fn into_future_basic() { - #[builder(derive(IntoFuture(Box)))] - async fn simple_async_fn(value: u32) -> u32 { - ready(value * 2).await +/// [`core::future::IntoFuture`] relies on [`Box`]. Also this trait was +/// introduced in Rust 1.64, while `bon`'s MSRV is 1.59 at the time of this +/// writing. +#[cfg(any(feature = "std", feature = "alloc"))] +#[rustversion::since(1.64)] +mod tests { + use crate::prelude::*; + use core::future::{ready, IntoFuture}; + use core::marker::PhantomData; + + async fn assert_send(builder: B) -> B::Output + where + B: IntoFuture + Send, + B::IntoFuture: Send, + { + #[expect(clippy::incompatible_msrv)] + let fut = builder.into_future(); + let _: &dyn Send = &fut; + fut.await } - // Test direct call. - let result = simple_async_fn().value(21).call().await; - assert_eq!(result, 42); + #[expect(clippy::future_not_send)] + async fn non_send_future() { + // By keeping `Rc` across an await point, we force the compiler to store it + // as part of the future's state machine struct and thus we make it non-Send + let non_send = PhantomData::>; - // Test using IntoFuture with await. - let result = simple_async_fn().value(21).await; - assert_eq!(result, 42); -} + ready(()).await; -#[tokio::test] -async fn into_future_non_send() { - #[builder(derive(IntoFuture(Box, ?Send)))] - async fn non_send_async_fn(value: u32) -> u32 { - // This future can be !Send. - ready(value * 2).await + let _ = &non_send; } - // Test with non-Send future. - let result = non_send_async_fn().value(21).await; - assert_eq!(result, 42); -} + mod test_fn { + use super::*; -#[tokio::test] -async fn into_future_with_result() { - #[builder(derive(IntoFuture(Box)))] - async fn async_with_result(value: u32) -> Result { - ready(if value > 0 { - Ok(value * 2) - } else { - Err("Value must be positive") - }) - .await - } + #[tokio::test] + async fn basic() { + #[builder(derive(IntoFuture(Box)))] + async fn simple_async_fn(value: u32) -> u32 { + ready(value * 2).await + } - // Test successful case. - let result = async_with_result().value(21).await; - assert_eq!(result.unwrap(), 42); + // Test direct call. + let builder = simple_async_fn().value(21).call(); + assert_eq!(assert_send(builder).await, 42); - // Test error case. - let result = async_with_result().value(0).await; - result.unwrap_err(); -} + // Test using IntoFuture with await. + let builder = simple_async_fn().value(21); + assert_eq!(assert_send(builder).await, 42); + } -#[tokio::test] -async fn into_future_with_impl() { - struct Calculator; + #[tokio::test] + async fn non_send() { + #[builder(derive(IntoFuture(Box, ?Send)))] + #[expect(clippy::future_not_send)] + async fn non_send_async_fn(value: u32) -> u32 { + non_send_future().await; + // This future can be !Send. + ready(value * 2).await + } - #[bon] - impl Calculator { - #[builder] - #[builder(derive(IntoFuture(Box)))] - async fn multiply(a: u32, b: u32) -> u32 { - ready(a * b).await + // Test with non-Send future. + let result = non_send_async_fn().value(21).await; + + assert_eq!(result, 42); } - } - // Test using IntoFuture on impl method. - let result = Calculator::multiply().a(6).b(7).await; - assert_eq!(result, 42); -} + #[tokio::test] + async fn result() { + #[builder(derive(IntoFuture(Box)))] + async fn async_with_result(value: u32) -> Result { + ready(if value > 0 { + Ok(value * 2) + } else { + Err("Value must be positive") + }) + .await + } -#[tokio::test] -async fn into_future_with_optional() { - #[builder(derive(IntoFuture(Box)))] - async fn optional_param(#[builder(default = 100)] value: u32) -> u32 { - ready(value).await - } + // Test successful case. + let builder = async_with_result().value(21); + assert_eq!(assert_send(builder).await.unwrap(), 42); - // Test with value. - let result = optional_param().value(42).await; - assert_eq!(result, 42); + // Test error case. + let builder = async_with_result().value(0); - // Test without value (using default). - let result = optional_param().await; - assert_eq!(result, 100); -} + assert_send(builder).await.unwrap_err(); + } -#[tokio::test] -async fn references_in_params() { - struct Dummy; - - #[builder(derive(IntoFuture(Box)))] - async fn sut<'named1, 'named2>( - _x1: &Dummy, - _x2: &Dummy, - x3: &'named1 Dummy, - x4: &'named2 Dummy, - ) -> &'named2 Dummy { - let _: &'named1 Dummy = x3; - ready(x4).await - } + #[tokio::test] + async fn into_future_with_optional() { + #[builder(derive(IntoFuture(Box)))] + async fn optional_param(#[builder(default = 100)] value: u32) -> u32 { + ready(value).await + } - // Store the dummy struct in local variables to make sure no `'static` - // lifetime promotion happens - let local_x1 = Dummy; - let local_x2 = Dummy; - let local_x3 = Dummy; - let local_x4 = Dummy; - - let _: &Dummy = sut() - .x1(&local_x1) - .x2(&local_x2) - .x3(&local_x3) - .x4(&local_x4) - .await; -} + // Test with value. + let builder = optional_param().value(42); + assert_eq!(assert_send(builder).await, 42); + + // Test without value (using default). + let builder = optional_param(); + assert_eq!(assert_send(builder).await, 100); + } + + #[tokio::test] + async fn references_in_params() { + struct Dummy; + + #[builder(derive(IntoFuture(Box)))] + async fn sut<'named1, 'named2>( + _x1: &Dummy, + _x2: &Dummy, + x3: &'named1 Dummy, + x4: &'named2 Dummy, + ) -> &'named2 Dummy { + let _: &'named1 Dummy = x3; + ready(x4).await + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_x1 = Dummy; + let local_x2 = Dummy; + let local_x3 = Dummy; + let local_x4 = Dummy; + + let builder = sut() + .x1(&local_x1) + .x2(&local_x2) + .x3(&local_x3) + .x4(&local_x4); + + let &Dummy = assert_send(builder).await; + } + + #[tokio::test] + async fn anon_lifetime_in_return_type() { + struct Dummy; + + #[builder(derive(IntoFuture(Box)))] + async fn sut(x1: &Dummy) -> &Dummy { + ready(x1).await + } -#[tokio::test] -async fn anon_lifetime_in_return_type() { - struct Dummy; + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_x1 = Dummy; - #[builder(derive(IntoFuture(Box)))] - async fn sut(x1: &Dummy) -> &Dummy { - ready(x1).await + let builder = sut().x1(&local_x1); + + let &Dummy = assert_send(builder).await; + } } - // Store the dummy struct in local variables to make sure no `'static` - // lifetime promotion happens - let local_x1 = Dummy; + mod test_method { + use super::*; + + #[tokio::test] + async fn basic() { + struct Calculator; + + #[bon] + impl Calculator { + #[builder] + #[builder(derive(IntoFuture(Box)))] + async fn multiply(a: u32, b: u32) -> u32 { + ready(a * b).await + } + } + + // Test using IntoFuture on impl method. + let builder = Calculator::multiply().a(6).b(7); + assert_eq!(assert_send(builder).await, 42); + } + + #[tokio::test] + async fn non_send() { + struct Sut; + + #[bon] + impl Sut { + #[builder(derive(IntoFuture(Box, ?Send)))] + #[expect(clippy::future_not_send)] + async fn sut(self, value: u32) -> u32 { + non_send_future().await; + + // This future can be !Send. + ready(value * 2).await + } + } + + // Test with non-Send future. + let result = Sut.sut().value(21).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn references_in_params() { + struct Dummy; - let _: &Dummy = sut().x1(&local_x1).await; + #[bon] + impl Dummy { + #[builder(derive(IntoFuture(Box)))] + async fn sut<'named1, 'named2>( + &self, + _x1: &Self, + _x2: &Self, + x3: &'named1 Self, + x4: &'named2 Self, + ) -> &'named2 Self { + let _: &'named1 Self = x3; + ready(x4).await + } + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_self = Dummy; + let local_x1 = Dummy; + let local_x2 = Dummy; + let local_x3 = Dummy; + let local_x4 = Dummy; + + let builder = local_self + .sut() + .x1(&local_x1) + .x2(&local_x2) + .x3(&local_x3) + .x4(&local_x4); + + let _: &Dummy = assert_send(builder).await; + } + + #[tokio::test] + async fn anon_lifetime_in_return_type() { + struct Dummy; + + #[bon] + impl Dummy { + #[builder(derive(IntoFuture(Box)))] + async fn sut(&self, _x1: &Self) -> &Self { + ready(self).await + } + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_self = Dummy; + let local_x1 = Dummy; + + let builder = local_self.sut().x1(&local_x1); + + let _: &Dummy = assert_send(builder).await; + } + } } diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index b6f93e84..1dd46038 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -7,6 +7,7 @@ mod attr_derive; mod attr_field; mod attr_getter; mod attr_into; +mod attr_into_future; mod attr_on; mod attr_overwritable; mod attr_required; @@ -30,10 +31,6 @@ mod track_caller; use crate::prelude::*; -/// [`core::future::IntoFuture`] relies on [`Box`] -#[cfg(any(feature = "std", feature = "alloc"))] -mod attr_into_future; - #[test] fn leading_underscore_is_stripped() { #[builder] diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.rs b/bon/tests/integration/ui/compile_fail/attr_into_future.rs index d044dbba..8a4d50f2 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.rs +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.rs @@ -1,3 +1,6 @@ +use bon::{bon, builder, Builder}; +use core::future::IntoFuture; + // IntoFuture can only be used with async functions #[bon::builder] #[builder(derive(IntoFuture(Box)))] @@ -15,9 +18,7 @@ async unsafe fn unsafe_async_function() -> u32 { // IntoFuture is incompatible with finish_fn members #[bon::builder] #[builder(derive(IntoFuture(Box)))] -async fn with_finish_fn( - #[builder(finish_fn)] value: u32 -) -> u32 { +async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { value } @@ -42,8 +43,6 @@ async fn wrong_send_syntax() -> u32 { 42 } -use bon::Builder; - // Cannot be used on structs #[derive(Builder)] #[builder(derive(IntoFuture(Box)))] @@ -51,4 +50,27 @@ struct AsyncConfig { value: u32, } -fn main() {} \ No newline at end of file +fn _non_send() { + struct Sut; + + fn assert_send(_: &dyn Send) {} + + #[bon] + impl Sut { + #[builder(derive(IntoFuture(Box, ?Send)))] + async fn sut(&self, value: u32) -> u32 { + value * 2 + } + } + + assert_send(&Sut.sut().value(21).into_future()); + + #[builder(derive(IntoFuture(Box, ?Send)))] + async fn sut(value: u32) -> u32 { + value * 2 + } + + assert_send(&sut().value(21).into_future()); +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr index 31f6035f..13cc19c2 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr @@ -1,43 +1,83 @@ error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake - --> tests/integration/ui/compile_fail/attr_into_future.rs:2:1 + --> tests/integration/ui/compile_fail/attr_into_future.rs:5:1 | -2 | #[bon::builder] +5 | #[bon::builder] | ^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `bon::builder` (in Nightly builds, run with -Z macro-backtrace for more info) error: `#[builder(derive(IntoFuture(...)))` is not supported for unsafe functions because `IntoFuture::into_future()` method is a safe method - --> tests/integration/ui/compile_fail/attr_into_future.rs:11:7 + --> tests/integration/ui/compile_fail/attr_into_future.rs:14:7 | -11 | async unsafe fn unsafe_async_function() -> u32 { +14 | async unsafe fn unsafe_async_function() -> u32 { | ^^^^^^ error: `#[builder(derive(IntoFuture(...)))` is incompatible with `#[builder(finish_fn)]` members because `IntoFuture::into_future()` method accepts zero parameters - --> tests/integration/ui/compile_fail/attr_into_future.rs:19:15 + --> tests/integration/ui/compile_fail/attr_into_future.rs:21:35 | -19 | #[builder(finish_fn)] value: u32 - | ^^^^^^^^^ +21 | async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { + | ^^^^^^^^^ error: expected an attribute of form `IntoFuture(Box, ...)` - --> tests/integration/ui/compile_fail/attr_into_future.rs:26:18 + --> tests/integration/ui/compile_fail/attr_into_future.rs:27:18 | -26 | #[builder(derive(IntoFuture))] +27 | #[builder(derive(IntoFuture))] | ^^^^^^^^^^ error: expected `Box` as the first argument, only boxed futures are supported - --> tests/integration/ui/compile_fail/attr_into_future.rs:33:29 + --> tests/integration/ui/compile_fail/attr_into_future.rs:34:29 | -33 | #[builder(derive(IntoFuture(Arc)))] +34 | #[builder(derive(IntoFuture(Arc)))] | ^^^ error: expected `?Send` as the second argument - --> tests/integration/ui/compile_fail/attr_into_future.rs:40:34 + --> tests/integration/ui/compile_fail/attr_into_future.rs:41:34 | -40 | #[builder(derive(IntoFuture(Box, Send)))] +41 | #[builder(derive(IntoFuture(Box, Send)))] | ^^^^ error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake - --> tests/integration/ui/compile_fail/attr_into_future.rs:50:8 + --> tests/integration/ui/compile_fail/attr_into_future.rs:49:8 | -50 | struct AsyncConfig { +49 | struct AsyncConfig { | ^^^^^^^^^^^ + +error[E0277]: `dyn Future` cannot be sent between threads safely + --> tests/integration/ui/compile_fail/attr_into_future.rs:66:17 + | +66 | assert_send(&Sut.sut().value(21).into_future()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `dyn Future` cannot be sent between threads safely + | + = help: the trait `Send` is not implemented for `dyn Future` + = note: required for `Unique>` to implement `Send` +note: required because it appears within the type `Box>` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ +note: required because it appears within the type `Pin>>` + --> $RUST/core/src/pin.rs + | + | pub struct Pin { + | ^^^ + = note: required for the cast from `&Pin>>` to `&dyn Send` + +error[E0277]: `(dyn Future + 'static)` cannot be sent between threads safely + --> tests/integration/ui/compile_fail/attr_into_future.rs:73:17 + | +73 | assert_send(&sut().value(21).into_future()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Future + 'static)` cannot be sent between threads safely + | + = help: the trait `Send` is not implemented for `(dyn Future + 'static)` + = note: required for `Unique<(dyn Future + 'static)>` to implement `Send` +note: required because it appears within the type `Box<(dyn Future + 'static)>` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ +note: required because it appears within the type `Pin + 'static)>>` + --> $RUST/core/src/pin.rs + | + | pub struct Pin { + | ^^^ + = note: required for the cast from `&Pin + 'static)>>` to `&dyn Send` From b12ed241d6820320c934181f37d174b3e7dd290c Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 18:03:09 +0000 Subject: [PATCH 16/20] Remove redundant `#[bon::builder]` in ui tests --- .../ui/compile_fail/attr_into_future.rs | 6 --- .../ui/compile_fail/attr_into_future.stderr | 38 +++++++++---------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.rs b/bon/tests/integration/ui/compile_fail/attr_into_future.rs index 8a4d50f2..7c06c0b1 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.rs +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.rs @@ -2,42 +2,36 @@ use bon::{bon, builder, Builder}; use core::future::IntoFuture; // IntoFuture can only be used with async functions -#[bon::builder] #[builder(derive(IntoFuture(Box)))] fn sync_function() -> u32 { 42 } // IntoFuture is not supported for unsafe functions -#[bon::builder] #[builder(derive(IntoFuture(Box)))] async unsafe fn unsafe_async_function() -> u32 { 42 } // IntoFuture is incompatible with finish_fn members -#[bon::builder] #[builder(derive(IntoFuture(Box)))] async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { value } // IntoFuture requires Box argument -#[bon::builder] #[builder(derive(IntoFuture))] async fn missing_box_arg() -> u32 { 42 } // Only Box is supported as future container -#[bon::builder] #[builder(derive(IntoFuture(Arc)))] async fn wrong_container() -> u32 { 42 } // Wrong syntax for ?Send -#[bon::builder] #[builder(derive(IntoFuture(Box, Send)))] async fn wrong_send_syntax() -> u32 { 42 diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr index 13cc19c2..c5f690bc 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr @@ -1,51 +1,51 @@ error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake --> tests/integration/ui/compile_fail/attr_into_future.rs:5:1 | -5 | #[bon::builder] - | ^^^^^^^^^^^^^^^ +5 | #[builder(derive(IntoFuture(Box)))] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: this error originates in the attribute macro `bon::builder` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) error: `#[builder(derive(IntoFuture(...)))` is not supported for unsafe functions because `IntoFuture::into_future()` method is a safe method - --> tests/integration/ui/compile_fail/attr_into_future.rs:14:7 + --> tests/integration/ui/compile_fail/attr_into_future.rs:12:7 | -14 | async unsafe fn unsafe_async_function() -> u32 { +12 | async unsafe fn unsafe_async_function() -> u32 { | ^^^^^^ error: `#[builder(derive(IntoFuture(...)))` is incompatible with `#[builder(finish_fn)]` members because `IntoFuture::into_future()` method accepts zero parameters - --> tests/integration/ui/compile_fail/attr_into_future.rs:21:35 + --> tests/integration/ui/compile_fail/attr_into_future.rs:18:35 | -21 | async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { +18 | async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { | ^^^^^^^^^ error: expected an attribute of form `IntoFuture(Box, ...)` - --> tests/integration/ui/compile_fail/attr_into_future.rs:27:18 + --> tests/integration/ui/compile_fail/attr_into_future.rs:23:18 | -27 | #[builder(derive(IntoFuture))] +23 | #[builder(derive(IntoFuture))] | ^^^^^^^^^^ error: expected `Box` as the first argument, only boxed futures are supported - --> tests/integration/ui/compile_fail/attr_into_future.rs:34:29 + --> tests/integration/ui/compile_fail/attr_into_future.rs:29:29 | -34 | #[builder(derive(IntoFuture(Arc)))] +29 | #[builder(derive(IntoFuture(Arc)))] | ^^^ error: expected `?Send` as the second argument - --> tests/integration/ui/compile_fail/attr_into_future.rs:41:34 + --> tests/integration/ui/compile_fail/attr_into_future.rs:35:34 | -41 | #[builder(derive(IntoFuture(Box, Send)))] +35 | #[builder(derive(IntoFuture(Box, Send)))] | ^^^^ error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake - --> tests/integration/ui/compile_fail/attr_into_future.rs:49:8 + --> tests/integration/ui/compile_fail/attr_into_future.rs:43:8 | -49 | struct AsyncConfig { +43 | struct AsyncConfig { | ^^^^^^^^^^^ error[E0277]: `dyn Future` cannot be sent between threads safely - --> tests/integration/ui/compile_fail/attr_into_future.rs:66:17 + --> tests/integration/ui/compile_fail/attr_into_future.rs:60:17 | -66 | assert_send(&Sut.sut().value(21).into_future()); +60 | assert_send(&Sut.sut().value(21).into_future()); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `dyn Future` cannot be sent between threads safely | = help: the trait `Send` is not implemented for `dyn Future` @@ -63,9 +63,9 @@ note: required because it appears within the type `Pin>>` to `&dyn Send` error[E0277]: `(dyn Future + 'static)` cannot be sent between threads safely - --> tests/integration/ui/compile_fail/attr_into_future.rs:73:17 + --> tests/integration/ui/compile_fail/attr_into_future.rs:67:17 | -73 | assert_send(&sut().value(21).into_future()); +67 | assert_send(&sut().value(21).into_future()); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Future + 'static)` cannot be sent between threads safely | = help: the trait `Send` is not implemented for `(dyn Future + 'static)` From 5217c8887cc543097bd9d3187ccc439e05932096 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 18:08:06 +0000 Subject: [PATCH 17/20] Add lifetimes caveat to the docs --- website/src/reference/builder/top-level/derive.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/src/reference/builder/top-level/derive.md b/website/src/reference/builder/top-level/derive.md index fd7eb9e8..b88ccc09 100644 --- a/website/src/reference/builder/top-level/derive.md +++ b/website/src/reference/builder/top-level/derive.md @@ -291,3 +291,7 @@ async fn main() { assert_eq!(response, "Server response"); } ``` + +### Lifetimes caveat + +There is a caveat that `dyn Trait` objects can only have a single `+ 'lifetime` bound which is the Rust language's fundamental limitation. So the generated `IntoFuture` implementation squashes all lifetimes into a single `'builder` lifetime. This means it's not strictly equivalent the default `finish_fn` in terms of lifetimes. This should generally not be a problem unless the output type of the function's `Future` contains more than one lifetime. From e1a06f39446814b5c89f3b7b62d29af0afb9853e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 18:21:19 +0000 Subject: [PATCH 18/20] Split no_std/std-alloc UI tests --- .../ui/compile_fail/attr_into_future.rs | 26 +------ .../ui/compile_fail/attr_into_future.stderr | 68 ++++--------------- .../std_or_alloc/attr_into_future.rs | 27 ++++++++ .../std_or_alloc/attr_into_future.stderr | 39 +++++++++++ bon/tests/integration/ui/mod.rs | 5 ++ 5 files changed, 86 insertions(+), 79 deletions(-) create mode 100644 bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs create mode 100644 bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.stderr diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.rs b/bon/tests/integration/ui/compile_fail/attr_into_future.rs index 7c06c0b1..192a49c0 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.rs +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.rs @@ -1,5 +1,4 @@ -use bon::{bon, builder, Builder}; -use core::future::IntoFuture; +use bon::{builder, Builder}; // IntoFuture can only be used with async functions #[builder(derive(IntoFuture(Box)))] @@ -44,27 +43,4 @@ struct AsyncConfig { value: u32, } -fn _non_send() { - struct Sut; - - fn assert_send(_: &dyn Send) {} - - #[bon] - impl Sut { - #[builder(derive(IntoFuture(Box, ?Send)))] - async fn sut(&self, value: u32) -> u32 { - value * 2 - } - } - - assert_send(&Sut.sut().value(21).into_future()); - - #[builder(derive(IntoFuture(Box, ?Send)))] - async fn sut(value: u32) -> u32 { - value * 2 - } - - assert_send(&sut().value(21).into_future()); -} - fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr index c5f690bc..a9593e2b 100644 --- a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr @@ -1,83 +1,43 @@ error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake - --> tests/integration/ui/compile_fail/attr_into_future.rs:5:1 + --> tests/integration/ui/compile_fail/attr_into_future.rs:4:1 | -5 | #[builder(derive(IntoFuture(Box)))] +4 | #[builder(derive(IntoFuture(Box)))] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) error: `#[builder(derive(IntoFuture(...)))` is not supported for unsafe functions because `IntoFuture::into_future()` method is a safe method - --> tests/integration/ui/compile_fail/attr_into_future.rs:12:7 + --> tests/integration/ui/compile_fail/attr_into_future.rs:11:7 | -12 | async unsafe fn unsafe_async_function() -> u32 { +11 | async unsafe fn unsafe_async_function() -> u32 { | ^^^^^^ error: `#[builder(derive(IntoFuture(...)))` is incompatible with `#[builder(finish_fn)]` members because `IntoFuture::into_future()` method accepts zero parameters - --> tests/integration/ui/compile_fail/attr_into_future.rs:18:35 + --> tests/integration/ui/compile_fail/attr_into_future.rs:17:35 | -18 | async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { +17 | async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { | ^^^^^^^^^ error: expected an attribute of form `IntoFuture(Box, ...)` - --> tests/integration/ui/compile_fail/attr_into_future.rs:23:18 + --> tests/integration/ui/compile_fail/attr_into_future.rs:22:18 | -23 | #[builder(derive(IntoFuture))] +22 | #[builder(derive(IntoFuture))] | ^^^^^^^^^^ error: expected `Box` as the first argument, only boxed futures are supported - --> tests/integration/ui/compile_fail/attr_into_future.rs:29:29 + --> tests/integration/ui/compile_fail/attr_into_future.rs:28:29 | -29 | #[builder(derive(IntoFuture(Arc)))] +28 | #[builder(derive(IntoFuture(Arc)))] | ^^^ error: expected `?Send` as the second argument - --> tests/integration/ui/compile_fail/attr_into_future.rs:35:34 + --> tests/integration/ui/compile_fail/attr_into_future.rs:34:34 | -35 | #[builder(derive(IntoFuture(Box, Send)))] +34 | #[builder(derive(IntoFuture(Box, Send)))] | ^^^^ error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake - --> tests/integration/ui/compile_fail/attr_into_future.rs:43:8 + --> tests/integration/ui/compile_fail/attr_into_future.rs:42:8 | -43 | struct AsyncConfig { +42 | struct AsyncConfig { | ^^^^^^^^^^^ - -error[E0277]: `dyn Future` cannot be sent between threads safely - --> tests/integration/ui/compile_fail/attr_into_future.rs:60:17 - | -60 | assert_send(&Sut.sut().value(21).into_future()); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `dyn Future` cannot be sent between threads safely - | - = help: the trait `Send` is not implemented for `dyn Future` - = note: required for `Unique>` to implement `Send` -note: required because it appears within the type `Box>` - --> $RUST/alloc/src/boxed.rs - | - | pub struct Box< - | ^^^ -note: required because it appears within the type `Pin>>` - --> $RUST/core/src/pin.rs - | - | pub struct Pin { - | ^^^ - = note: required for the cast from `&Pin>>` to `&dyn Send` - -error[E0277]: `(dyn Future + 'static)` cannot be sent between threads safely - --> tests/integration/ui/compile_fail/attr_into_future.rs:67:17 - | -67 | assert_send(&sut().value(21).into_future()); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Future + 'static)` cannot be sent between threads safely - | - = help: the trait `Send` is not implemented for `(dyn Future + 'static)` - = note: required for `Unique<(dyn Future + 'static)>` to implement `Send` -note: required because it appears within the type `Box<(dyn Future + 'static)>` - --> $RUST/alloc/src/boxed.rs - | - | pub struct Box< - | ^^^ -note: required because it appears within the type `Pin + 'static)>>` - --> $RUST/core/src/pin.rs - | - | pub struct Pin { - | ^^^ - = note: required for the cast from `&Pin + 'static)>>` to `&dyn Send` diff --git a/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs b/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs new file mode 100644 index 00000000..61f4a868 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs @@ -0,0 +1,27 @@ +use bon::{bon, builder}; +use core::future::IntoFuture; + +fn _non_send() { + struct Sut; + + fn assert_send(_: &dyn Send) {} + + #[bon] + impl Sut { + #[builder(derive(IntoFuture(Box, ?Send)))] + async fn sut(&self, value: u32) -> u32 { + value * 2 + } + } + + assert_send(&Sut.sut().value(21).into_future()); + + #[builder(derive(IntoFuture(Box, ?Send)))] + async fn sut(value: u32) -> u32 { + value * 2 + } + + assert_send(&sut().value(21).into_future()); +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.stderr new file mode 100644 index 00000000..b8659383 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.stderr @@ -0,0 +1,39 @@ +error[E0277]: `dyn Future` cannot be sent between threads safely + --> tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs:17:17 + | +17 | assert_send(&Sut.sut().value(21).into_future()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `dyn Future` cannot be sent between threads safely + | + = help: the trait `Send` is not implemented for `dyn Future` + = note: required for `Unique>` to implement `Send` +note: required because it appears within the type `Box>` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ +note: required because it appears within the type `Pin>>` + --> $RUST/core/src/pin.rs + | + | pub struct Pin { + | ^^^ + = note: required for the cast from `&Pin>>` to `&dyn Send` + +error[E0277]: `(dyn Future + 'static)` cannot be sent between threads safely + --> tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs:24:17 + | +24 | assert_send(&sut().value(21).into_future()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Future + 'static)` cannot be sent between threads safely + | + = help: the trait `Send` is not implemented for `(dyn Future + 'static)` + = note: required for `Unique<(dyn Future + 'static)>` to implement `Send` +note: required because it appears within the type `Box<(dyn Future + 'static)>` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ +note: required because it appears within the type `Pin + 'static)>>` + --> $RUST/core/src/pin.rs + | + | pub struct Pin { + | ^^^ + = note: required for the cast from `&Pin + 'static)>>` to `&dyn Send` diff --git a/bon/tests/integration/ui/mod.rs b/bon/tests/integration/ui/mod.rs index e5c3878d..9ecee3fc 100644 --- a/bon/tests/integration/ui/mod.rs +++ b/bon/tests/integration/ui/mod.rs @@ -3,4 +3,9 @@ fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/integration/ui/compile_fail/*.rs"); + + if cfg!(any(feature = "std", feature = "alloc")) { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/integration/ui/compile_fail/std_or_alloc/*.rs"); + } } From 051c0e75e351410dac271f51df7e5faa152c1df4 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 18:24:52 +0000 Subject: [PATCH 19/20] Fix MSRV tests - gate the `IntoFuture` import in `ide.rs` by rustc v1.64 --- bon/src/__/ide.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bon/src/__/ide.rs b/bon/src/__/ide.rs index 22b24298..404b40a7 100644 --- a/bon/src/__/ide.rs +++ b/bon/src/__/ide.rs @@ -101,6 +101,7 @@ pub mod builder_top_level { pub use core::convert::Into; /// See the docs at + #[rustversion::since(1.64)] pub use core::future::IntoFuture; } From 129368a15b1982b3c8c6536e72c374e2efa4cca9 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 9 Aug 2025 18:31:59 +0000 Subject: [PATCH 20/20] Add a note about 1.64 MSRV for `IntoFuture` to the docs --- website/src/reference/builder/top-level/derive.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/src/reference/builder/top-level/derive.md b/website/src/reference/builder/top-level/derive.md index b88ccc09..3bcd25f4 100644 --- a/website/src/reference/builder/top-level/derive.md +++ b/website/src/reference/builder/top-level/derive.md @@ -292,6 +292,8 @@ async fn main() { } ``` +Take into account that `IntoFuture` trait became stable in Rust `1.64`, which is important if you care about your MSRV. + ### Lifetimes caveat There is a caveat that `dyn Trait` objects can only have a single `+ 'lifetime` bound which is the Rust language's fundamental limitation. So the generated `IntoFuture` implementation squashes all lifetimes into a single `'builder` lifetime. This means it's not strictly equivalent the default `finish_fn` in terms of lifetimes. This should generally not be a problem unless the output type of the function's `Future` contains more than one lifetime.