Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4f0c464
Implement `#[builder(derive(IntoFuture(Box)))]`, with support for `?S…
jakubadamw Aug 8, 2025
62585ae
Add integration tests.
jakubadamw Aug 8, 2025
eb8535b
Add an example to the documentation.
jakubadamw Aug 8, 2025
e740ba3
Update Cargo.lock.
jakubadamw Aug 8, 2025
a0dabdd
Some improvements to the doc test (see commit body below for details)
Veetaha Aug 9, 2025
102238e
Run `cargo fmt`. CI isn't enabled for external contributors by defaul…
Veetaha Aug 9, 2025
1b2453b
Add missing trailing `\` in the error message
Veetaha Aug 9, 2025
75a1f87
Improve the error message for synchronous function handling
Veetaha Aug 9, 2025
79acfc5
Use `crate::prelude` in integration tests
Veetaha Aug 9, 2025
bfafc1e
Remove redundant `#[builder]`
Veetaha Aug 9, 2025
f08916f
Make the output `#[no_std]`- friendly
Veetaha Aug 9, 2025
433e579
Add `IntoFuture` to `ide.rs` for better syntax highlighting and intel…
Veetaha Aug 9, 2025
0e90bf6
Fix various clippy lints
Veetaha Aug 9, 2025
7c9f53a
Add support for non-static lifetimes
Veetaha Aug 9, 2025
d534b14
Add a bit more tests, gate them with 1.64 MSRV
Veetaha Aug 9, 2025
b12ed24
Remove redundant `#[bon::builder]` in ui tests
Veetaha Aug 9, 2025
5217c88
Add lifetimes caveat to the docs
Veetaha Aug 9, 2025
e1a06f3
Split no_std/std-alloc UI tests
Veetaha Aug 9, 2025
051c0e7
Fix MSRV tests - gate the `IntoFuture` import in `ide.rs` by rustc v1.64
Veetaha Aug 9, 2025
129368a
Add a note about 1.64 MSRV for `IntoFuture` to the docs
Veetaha Aug 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions bon-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Expand Down
215 changes: 215 additions & 0 deletions bon-macros/src/builder/builder_gen/builder_derives/into_future.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
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<TokenStream> {
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; \
using it with a synchronous function is likely a mistake"
);
}

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 state_mod = &self.state_mod.ident;
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 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 }
} else {
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 + #state_lifetime
>
::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<Output = Self::Output>
#send_bound
#(+ #builder_lifetime)*
>
>;

fn into_future(self) -> Self::IntoFuture {
#alloc::boxed::#box_::pin(#builder_ident::#finish_fn_ident(self))
}
}
};

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::<Vec<_>>();

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<Cow<'a, syn::WhereClause>>,
builder_lifetime: Option<syn::Lifetime>,
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
}
}
12 changes: 11 additions & 1 deletion bon-macros/src/builder/builder_gen/builder_derives/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod clone;
mod debug;
mod into;
mod into_future;

use super::top_level_config::{DeriveConfig, DerivesConfig};
use super::BuilderGenCtx;
Expand All @@ -9,7 +10,12 @@ use darling::ast::GenericParamExt;

impl BuilderGenCtx {
pub(crate) fn builder_derives(&self) -> Result<TokenStream> {
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();

Expand All @@ -25,6 +31,10 @@ impl BuilderGenCtx {
tokens.extend(self.derive_into()?);
}

if let Some(derive) = into_future {
tokens.extend(self.derive_into_future(derive)?);
}

Ok(tokens)
}

Expand Down
66 changes: 66 additions & 0 deletions bon-macros/src/builder/builder_gen/top_level_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,79 @@ pub(crate) struct DerivesConfig {

#[darling(rename = "Into")]
pub(crate) into: darling::util::Flag,

#[darling(rename = "IntoFuture")]
pub(crate) into_future: Option<IntoFutureConfig>,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct DeriveConfig {
pub(crate) bounds: Option<Punctuated<syn::WherePredicate, syn::Token![,]>>,
}

#[derive(Debug, Clone)]
pub(crate) struct IntoFutureConfig {
pub(crate) box_ident: syn::Ident,
pub(crate) is_send: bool,
}

impl syn::parse::Parse for IntoFutureConfig {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
// 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::<syn::Token![,]>()?;

// Parse "?Send" as a single unit.
if input.peek(syn::Token![?]) {
input.parse::<syn::Token![?]>()?;
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(Self { box_ident, is_send })
}
}

impl FromMeta for IntoFutureConfig {
fn from_meta(meta: &syn::Meta) -> Result<Self> {
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)
}
}

impl FromMeta for DeriveConfig {
fn from_meta(meta: &syn::Meta) -> Result<Self> {
if let syn::Meta::Path(_) = meta {
Expand Down
4 changes: 2 additions & 2 deletions bon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions bon/src/__/ide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ pub mod builder_top_level {

/// See the docs at <https://bon-rs.com/reference/builder/top-level/derive>
pub use core::convert::Into;

/// See the docs at <https://bon-rs.com/reference/builder/top-level/derive>
#[rustversion::since(1.64)]
pub use core::future::IntoFuture;
}

/// The real name of this parameter is `crate` (without the underscore).
Expand Down
Loading
Loading