Skip to content

Commit

Permalink
Support clone, copy, deref in #[builder(getter)] (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
Veetaha authored Dec 7, 2024
1 parent 7bd157e commit 7dc46da
Show file tree
Hide file tree
Showing 15 changed files with 921 additions and 193 deletions.
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
306 changes: 245 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 return_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,224 @@ 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) -> #return_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) => {
// Use a `_` type hint with the span of the original type
// to make the compiler point to the original type in case
// if the type doesn't implement `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) => {
// Use a `_` type hint with the span of the original type
// to make the compiler point to the original type in case
// if the type doesn't implement `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)) => {
// Assign the span of the deref target type to the `value` variable
// so that compiler points to that type if there is a type mismatch.
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()
.find(|(name, _)| last_segment_ident_str == *name)
.and_then(|(_, infer)| infer(args))
.ok_or_else(err)?;

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

0 comments on commit 7dc46da

Please sign in to comment.