Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support clone, copy, deref in #[builder(getter)] #229

Merged
merged 14 commits into from
Dec 7, 2024
2 changes: 2 additions & 0 deletions benchmarks/runtime/benches/criterion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ fn criterion_bench(c: &mut criterion::Criterion) {
let builder_bench_addr: fn() -> u32 = runtime_benchmarks::bench::builder_bench;
let regular_bebch_addr: fn() -> u32 = runtime_benchmarks::bench::regular_bench;

// Comes from nightly 1.85.0
#[allow(unknown_lints, unpredictable_function_pointer_comparisons)]
let equal = if builder_bench_addr == regular_bebch_addr {
"equal"
} else {
Expand Down
10 changes: 5 additions & 5 deletions bon-macros/src/builder/builder_gen/builder_derives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ impl BuilderGenCtx {
let ty = member.underlying_norm_ty();

quote! {
#bon::__::derives::clone_member::<#ty>(
#bon::__::better_errors::clone_member::<#ty>(
&self.__unsafe_private_named.#member_index
)
}
Expand Down Expand Up @@ -174,7 +174,7 @@ impl BuilderGenCtx {
Some(quote! {
output.field(
#member_ident_str,
#bon::__::derives::as_dyn_debug::<#member_ty>(
#bon::__::better_errors::as_dyn_debug::<#member_ty>(
&self.__unsafe_private_start_fn_args.#member_index
)
);
Expand All @@ -187,7 +187,7 @@ impl BuilderGenCtx {
Some(quote! {
output.field(
#member_ident_str,
#bon::__::derives::as_dyn_debug::<#member_ty>(
#bon::__::better_errors::as_dyn_debug::<#member_ty>(
&self.#member_ident
)
);
Expand All @@ -201,7 +201,7 @@ impl BuilderGenCtx {
if let Some(value) = &self.__unsafe_private_named.#member_index {
output.field(
#member_ident_str,
#bon::__::derives::as_dyn_debug::<#member_ty>(value)
#bon::__::better_errors::as_dyn_debug::<#member_ty>(value)
);
}
})
Expand All @@ -219,7 +219,7 @@ impl BuilderGenCtx {
quote! {
output.field(
"self",
#bon::__::derives::as_dyn_debug::<#ty>(
#bon::__::better_errors::as_dyn_debug::<#ty>(
&self.__unsafe_private_receiver
)
);
Expand Down
298 changes: 237 additions & 61 deletions bon-macros/src/builder/builder_gen/getters.rs
Original file line number Diff line number Diff line change
@@ -1,59 +1,59 @@
use super::member::{GetterConfig, GetterKind};
use super::{BuilderGenCtx, NamedMember};
use crate::parsing::SpannedKey;
use crate::util::prelude::*;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;

pub(crate) struct GettersCtx<'a> {
base: &'a BuilderGenCtx,
member: &'a NamedMember,
}

struct GetterItem {
name: syn::Ident,
vis: syn::Visibility,
docs: Vec<syn::Attribute>,
config: &'a GetterConfig,
}

impl<'a> GettersCtx<'a> {
pub(crate) fn new(base: &'a BuilderGenCtx, member: &'a NamedMember) -> Self {
Self { base, member }
pub(crate) fn new(base: &'a BuilderGenCtx, member: &'a NamedMember) -> Option<Self> {
Some(Self {
base,
member,
config: member.config.getter.as_ref()?,
})
}

pub(crate) fn getter_methods(&self) -> TokenStream {
let GetterItem { name, vis, docs } = match GetterItem::new(self) {
Some(item) => item,
None => return quote! {},
};
pub(crate) fn getter_methods(self) -> Result<TokenStream> {
let name = self.config.name.as_deref().cloned().unwrap_or_else(|| {
syn::Ident::new(
&format!("get_{}", self.member.name.snake.raw_name()),
self.member.name.snake.span(),
)
});

let index = &self.member.index;
let ty = self.member.underlying_norm_ty();
let vis = self
.config
.vis
.as_deref()
.unwrap_or(&self.base.builder_type.vis)
.clone();

let (return_type, body) = if self.member.is_required() {
(
quote! { &#ty },
quote! {
unsafe {
// SAFETY: this code is runs in a method that has a where
// bound that ensures the member was set.
::core::option::Option::unwrap_unchecked(
self.__unsafe_private_named.#index.as_ref()
)
}
},
)
} else {
(
// We are not using the fully qualified path to `Option` here
// to make function signature in IDE popus shorter and more
// readable.
quote! { Option<&#ty> },
quote! { self.__unsafe_private_named.#index.as_ref() },
)
};
let docs = self.config.docs.as_deref().cloned().unwrap_or_else(|| {
let header = format!(
"_**Getter.**_ Returns `{}`, which must be set before calling this method.\n\n",
self.member.name.snake,
);

std::iter::once(syn::parse_quote!(#[doc = #header]))
.chain(self.member.docs.iter().cloned())
.collect()
});

let ret_ty = self.return_ty()?;
let body = self.body();

let state_var = &self.base.state_var;
let member_pascal = &self.member.name.pascal;
let state_mod = &self.base.state_mod.ident;

quote! {
Ok(quote! {
#( #docs )*
#[allow(
// This is intentional. We want the builder syntax to compile away
Expand All @@ -62,40 +62,216 @@ impl<'a> GettersCtx<'a> {
)]
#[inline(always)]
#[must_use = "this method has no side effects; it only returns a value"]
#vis fn #name(&self) -> #return_type
#vis fn #name(&self) -> #ret_ty
where
#state_var::#member_pascal: #state_mod::IsSet,
{
#body
}
})
}

fn body(&self) -> TokenStream {
let index = &self.member.index;
let member = quote! {
self.__unsafe_private_named.#index
};

let bon = &self.base.bon;

match self.config.kind.as_deref() {
Some(GetterKind::Copy) => {
let span = self.member.underlying_orig_ty().span();
let ty = quote_spanned!(span=> _);

let copy = quote! {
#bon::__::better_errors::copy_member::<#ty>(&#member)
};

if !self.member.is_required() {
return copy;
}
quote! {
// SAFETY: the method requires S::{Member}: IsSet, so it's Some
unsafe {
::core::option::Option::unwrap_unchecked(#copy)
}
}
}
Some(GetterKind::Clone) => {
let span = self.member.underlying_orig_ty().span();
let ty = quote_spanned!(span=> _);

let clone = quote! {
<#ty as ::core::clone::Clone>::clone
};

if !self.member.is_required() {
return quote! {
#clone(&#member)
};
}
quote! {
match &#member {
Some(value) => #clone(value),

// SAFETY: the method requires S::{Member}: IsSet, so it's Some
None => unsafe {
::core::hint::unreachable_unchecked()
},
}
}
}
Some(GetterKind::Deref(ty)) => {
let span = ty.span();
let value = quote_spanned!(span=> value);

if !self.member.is_required() {
return quote! {
// Explicit match is important to trigger an implicit deref coercion
// that can potentially do multiple derefs to the reach the target type.
match &#member {
Some(#value) => Some(#value),
None => None,
}
};
}
quote! {
// Explicit match is important to trigger an implicit deref coercion
// that can potentially do multiple derefs to the reach the target type.
match &#member {
Some(#value) => #value,

// SAFETY: the method requires S::{Member}: IsSet, so it's Some
None => unsafe {
::core::hint::unreachable_unchecked()
},
}
}
}
None => {
if !self.member.is_required() {
return quote! {
::core::option::Option::as_ref(&#member)
};
}
quote! {
match &#member {
Some(value) => value,

// SAFETY: the method requires S::{Member}: IsSet, so it's Some
None => unsafe {
::core::hint::unreachable_unchecked()
},
}
}
}
}
}
}

impl GetterItem {
fn new(ctx: &GettersCtx<'_>) -> Option<Self> {
let GettersCtx { member, base } = ctx;
fn return_ty(&self) -> Result<TokenStream> {
let underlying_return_ty = self.underlying_return_ty()?;

let config = member.config.getter.as_ref()?;
Ok(if self.member.is_required() {
quote! { #underlying_return_ty }
} else {
// We are not using the fully qualified path to `Option` here
// to make function signature in IDE popus shorter and more
// readable.
quote! { Option<#underlying_return_ty> }
})
}

Some(Self {
name: config.name().cloned().unwrap_or_else(|| {
syn::Ident::new(
&format!("get_{}", member.name.snake.raw_name()),
member.name.snake.span(),
)
fn underlying_return_ty(&self) -> Result<TokenStream> {
let ty = self.member.underlying_norm_ty();

let kind = match &self.config.kind {
Some(kind) => kind,
None => return Ok(quote! { &#ty }),
};

match &kind.value {
GetterKind::Copy | GetterKind::Clone => Ok(quote! { #ty }),
GetterKind::Deref(Some(deref_target)) => Ok(quote! { &#deref_target }),
GetterKind::Deref(None) => Self::infer_deref_target(ty, kind),
}
}

fn infer_deref_target(
underlying_member_ty: &syn::Type,
kind: &SpannedKey<GetterKind>,
) -> Result<TokenStream> {
use quote_spanned as qs;

let span = underlying_member_ty.span();

#[allow(clippy::type_complexity)]
let deref_target_inference_table: &[(_, &dyn Fn(&Punctuated<_, _>) -> _)] = &[
("Vec", &|args| args.first().map(|arg| qs!(span=> [#arg]))),
("Box", &|args| args.first().map(ToTokens::to_token_stream)),
("Rc", &|args| args.first().map(ToTokens::to_token_stream)),
("Arc", &|args| args.first().map(ToTokens::to_token_stream)),
("String", &|args| args.is_empty().then(|| qs!(span=> str))),
("CString", &|args| {
// CStr is available via `core` since 1.64.0:
// https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html#c-compatible-ffi-types-in-core-and-alloc
let module = if rustversion::cfg!(since(1.64.0)) {
format_ident!("core")
} else {
format_ident!("std")
};
args.is_empty().then(|| qs!(span=> ::#module::ffi::CStr))
}),
vis: config.vis().unwrap_or(&base.builder_type.vis).clone(),
docs: config.docs().map(<[_]>::to_vec).unwrap_or_else(|| {
let header = format!(
"_**Getter.**_ Returns `{}`, which must be set before calling this method.\n\n",
member.name.snake,
);

std::iter::once(syn::parse_quote!(#[doc = #header]))
.chain(member.docs.iter().cloned())
.collect()
("OsString", &|args| {
args.is_empty().then(|| qs!(span=> ::std::ffi::OsStr))
}),
})
("PathBuf", &|args| {
args.is_empty().then(|| qs!(span=> ::std::path::Path))
}),
("Cow", &|args| {
args.iter()
.find(|arg| matches!(arg, syn::GenericArgument::Type(_)))
.map(ToTokens::to_token_stream)
}),
];

let err = || {
let inferable_types = deref_target_inference_table
.iter()
.map(|(name, _)| format!("- {name}"))
.join("\n");

err!(
&kind.key,
"can't infer the `Deref::Target` for the getter from the member's type; \
please specify the return type (target of the deref coercion) explicitly \
in parentheses without the leading `&`;\n\
example: `#[builder(getter(deref(TargetTypeHere))]`\n\
\n\
automatic deref target detection is supported only for the following types:\n\
{inferable_types}",
)
};

let path = underlying_member_ty.as_path_no_qself().ok_or_else(err)?;

let last_segment = path.segments.last().ok_or_else(err)?;

let empty_punctuated = Punctuated::new();

let args = match &last_segment.arguments {
syn::PathArguments::AngleBracketed(args) => &args.args,
_ => &empty_punctuated,
};

let last_segment_ident_str = last_segment.ident.to_string();

let inferred = deref_target_inference_table
.iter()
.filter(|(name, _)| last_segment_ident_str == *name)
.find_map(|(_, infer)| infer(args))
.ok_or_else(err)?;

Ok(quote!(&#inferred))
}
}
Loading
Loading