Skip to content

Commit

Permalink
Delegation for fmt traits (#322, #321)
Browse files Browse the repository at this point in the history
## Synopsis

See
#321 (comment):
> You’re discarding formatting flags provided by the user in format
string, e.g.:
> 
> ```rust
> #[derive(derive_more::Display)]
> #[display(fmt = "{:?}", _0)]
> struct Num(usize);
> 
> impl std::fmt::Debug for Num {
> fn fmt(&self, fmtr: &mut std::fmt::Formatter) -> std::fmt::Result {
>         self.0.fmt(fmtr)
>     }
> }
> 
> fn main() {
>     let num = Num(7);
>     println!("{num:03?}");  // prints ‘007’ as expected
>     println!("{num:03}");   // prints ‘7’ instead
> }
> ```






## Solution

See
#321 (comment):
> Theoretically, we can support this with the current syntax, because we
can detect so-called _trivial_ cases and transform them into delegation
(we do parse formatting string literal anyway).
> 
> ```rust
> #[derive(derive_more::Display)]
> #[display("{_0:?}")] // <--- it's clear to be a trivial delegation
case
> struct Num(usize);
> ```
> 
> would expand to
> 
> ```rust
> impl std::fmt::Display for Num {
> fn fmt(&self, fmtr: &mut std::fmt::Formatter) -> std::fmt::Result {
>         let _0 = &self.0;
>         std::fmt::Debug::fmt(_0, fmtr)
>     }
> }
> ```
> 
> rather than
> 
> ```rust
> impl std::fmt::Display for Num {
> fn fmt(&self, fmtr: &mut std::fmt::Formatter) -> std::fmt::Result {
>         let _0 = &self.0;
>         write!(fmtr, "{_0:?}")
>     }
> }
> ```
  • Loading branch information
tyranron authored Dec 22, 2023
1 parent 1536f52 commit 732bb12
Show file tree
Hide file tree
Showing 11 changed files with 2,374 additions and 113 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
`#[display(fmt = "...", ("<expr>"),*)]`, and `#[display(bound(<bound>))]`
instead of `#[display(bound = "<bound>")]`. So without the double quotes
around the expressions and bounds.
- The `Debug` and `Display` derives (and other `fmt`-like ones) now transparently
delegate to the inner type when `#[display("...", (<expr>),*)]` attribute is
trivially substitutable with a transparent call.
([#322](https://github.com/JelteF/derive_more/pull/322))
- The `DebugCustom` derive is renamed to just `Debug` (gated now under a separate
`debug` feature), and its semantics were changed to be a superset of `std` variant
of `Debug`.
Expand Down
54 changes: 50 additions & 4 deletions impl/doc/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ The variables available in the arguments is `self` and each member of the struct
structs being named with a leading underscore and their index, i.e. `_0`, `_1`, `_2`, etc.




### Generic data types

When deriving `Debug` for a generic struct/enum, all generic type arguments _used_ during formatting
Expand Down Expand Up @@ -53,8 +51,6 @@ The following where clauses would be generated:
- `&'a T1: Pointer`




### Custom trait bounds

Sometimes you may want to specify additional trait bounds on your generic type parameters, so that they could be used
Expand Down Expand Up @@ -88,6 +84,54 @@ trait MyTrait { fn my_function(&self) -> i32; }
```


### Transparency

If the top-level `#[debug("...", args...)]` attribute (the one for a whole struct or variant) is specified
and can be trivially substituted with a transparent delegation call to the inner type, then all the additional
[formatting parameters][1] do work as expected:
```rust
# use derive_more::Debug;
#
#[derive(Debug)]
#[debug("{_0:o}")] // the same as calling `Octal::fmt()`
struct MyOctalInt(i32);

// so, additional formatting parameters do work transparently
assert_eq!(format!("{:03?}", MyOctalInt(9)), "011");

#[derive(Debug)]
#[debug("{_0:02b}")] // cannot be trivially substituted with `Binary::fmt()`,
struct MyBinaryInt(i32); // because of specified formatting parameters

// so, additional formatting parameters have no effect
assert_eq!(format!("{:07?}", MyBinaryInt(2)), "10");
```

If, for some reason, transparency in trivial cases is not desired, it may be suppressed explicitly
either with the [`format_args!()`] macro usage:
```rust
# use derive_more::Debug;
#
#[derive(Debug)]
#[debug("{}", format_args!("{_0:o}"))] // `format_args!()` obscures the inner type
struct MyOctalInt(i32);

// so, additional formatting parameters have no effect
assert_eq!(format!("{:07?}", MyOctalInt(9)), "11");
```
Or by adding [formatting parameters][1] which cause no visual effects:
```rust
# use derive_more::Debug;
#
#[derive(Debug)]
#[debug("{_0:^o}")] // `^` is centering, but in absence of additional width has no effect
struct MyOctalInt(i32);

// and so, additional formatting parameters have no effect
assert_eq!(format!("{:07?}", MyOctalInt(9)), "11");
```




## Example usage
Expand Down Expand Up @@ -133,3 +177,5 @@ assert_eq!(format!("{:?}", E::EnumFormat(true)), "true");

[`format!()`]: https://doc.rust-lang.org/stable/std/macro.format.html
[`format_args!()`]: https://doc.rust-lang.org/stable/std/macro.format_args.html

[1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-parameters
65 changes: 65 additions & 0 deletions impl/doc/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,64 @@ struct MyStruct<T, U, V> {
```


### Transparency

If the `#[display("...", args...)]` attribute is omitted, the implementation transparently delegates to the format
of the inner type, so all the additional [formatting parameters][1] do work as expected:
```rust
# use derive_more::Display;
#
#[derive(Display)]
struct MyInt(i32);

assert_eq!(format!("{:03}", MyInt(7)), "007");
```

If the `#[display("...", args...)]` attribute is specified and can be trivially substituted with a transparent
delegation call to the inner type, then additional [formatting parameters][1] will work too:
```rust
# use derive_more::Display;
#
#[derive(Display)]
#[display("{_0:o}")] // the same as calling `Octal::fmt()`
struct MyOctalInt(i32);

// so, additional formatting parameters do work transparently
assert_eq!(format!("{:03}", MyOctalInt(9)), "011");

#[derive(Display)]
#[display("{_0:02b}")] // cannot be trivially substituted with `Binary::fmt()`,
struct MyBinaryInt(i32); // because of specified formatting parameters

// so, additional formatting parameters have no effect
assert_eq!(format!("{:07}", MyBinaryInt(2)), "10");
```

If, for some reason, transparency in trivial cases is not desired, it may be suppressed explicitly
either with the [`format_args!()`] macro usage:
```rust
# use derive_more::Display;
#
#[derive(Display)]
#[display("{}", format_args!("{_0:o}"))] // `format_args!()` obscures the inner type
struct MyOctalInt(i32);

// so, additional formatting parameters have no effect
assert_eq!(format!("{:07}", MyOctalInt(9)), "11");
```
Or by adding [formatting parameters][1] which cause no visual effects:
```rust
# use derive_more::Display;
#
#[derive(Display)]
#[display("{_0:^o}")] // `^` is centering, but in absence of additional width has no effect
struct MyOctalInt(i32);

// and so, additional formatting parameters have no effect
assert_eq!(format!("{:07}", MyOctalInt(9)), "11");
```




## Example usage
Expand Down Expand Up @@ -176,3 +234,10 @@ assert_eq!(UnitStruct {}.to_string(), "UnitStruct");
assert_eq!(PositiveOrNegative { x: 1 }.to_string(), "Positive");
assert_eq!(PositiveOrNegative { x: -1 }.to_string(), "Negative");
```




[`format_args!()`]: https://doc.rust-lang.org/stable/std/macro.format_args.html

[1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-parameters
18 changes: 12 additions & 6 deletions impl/src/fmt/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,12 @@ impl<'a> Expansion<'a> {
///
/// [`Debug::fmt()`]: std::fmt::Debug::fmt()
fn generate_body(&self) -> syn::Result<TokenStream> {
if let Some(fmt_attr) = &self.attr.fmt {
return Ok(quote! { ::core::write!(__derive_more_f, #fmt_attr) });
if let Some(fmt) = &self.attr.fmt {
return Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() {
quote! { ::core::fmt::#trait_ident::fmt(&(#expr), __derive_more_f) }
} else {
quote! { ::core::write!(__derive_more_f, #fmt) }
});
};

match self.fields {
Expand Down Expand Up @@ -331,8 +335,9 @@ impl<'a> Expansion<'a> {

if let Some(fmt) = self.attr.fmt.as_ref() {
out.extend(fmt.bounded_types(self.fields).map(|(ty, trait_name)| {
let trait_name = format_ident!("{trait_name}");
parse_quote! { #ty: ::core::fmt::#trait_name }
let trait_ident = format_ident!("{trait_name}");

parse_quote! { #ty: ::core::fmt::#trait_ident }
}));
Ok(out)
} else {
Expand All @@ -344,8 +349,9 @@ impl<'a> Expansion<'a> {
Some(FieldAttribute::Right(fmt_attr)) => {
out.extend(fmt_attr.bounded_types(self.fields).map(
|(ty, trait_name)| {
let trait_name = format_ident!("{trait_name}");
parse_quote! { #ty: ::core::fmt::#trait_name }
let trait_ident = format_ident!("{trait_name}");

parse_quote! { #ty: ::core::fmt::#trait_ident }
},
));
}
Expand Down
19 changes: 15 additions & 4 deletions impl/src/fmt/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,19 @@ impl<'a> Expansion<'a> {
/// [`Display::fmt()`]: fmt::Display::fmt()
fn generate_body(&self) -> syn::Result<TokenStream> {
match &self.attrs.fmt {
Some(fmt) => Ok(quote! { ::core::write!(__derive_more_f, #fmt) }),
Some(fmt) => {
Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() {
quote! { ::core::fmt::#trait_ident::fmt(&(#expr), __derive_more_f) }
} else {
quote! { ::core::write!(__derive_more_f, #fmt) }
})
}
None if self.fields.is_empty() => {
let ident_str = self.ident.to_string();
Ok(quote! { ::core::write!(__derive_more_f, #ident_str) })

Ok(quote! {
::core::write!(__derive_more_f, #ident_str)
})
}
None if self.fields.len() == 1 => {
let field = self
Expand All @@ -235,6 +244,7 @@ impl<'a> Expansion<'a> {
.unwrap_or_else(|| unreachable!("count() == 1"));
let ident = field.ident.clone().unwrap_or_else(|| format_ident!("_0"));
let trait_ident = self.trait_ident;

Ok(quote! {
::core::fmt::#trait_ident::fmt(#ident, __derive_more_f)
})
Expand Down Expand Up @@ -267,8 +277,9 @@ impl<'a> Expansion<'a> {

fmt.bounded_types(self.fields)
.map(|(ty, trait_name)| {
let tr = format_ident!("{}", trait_name);
parse_quote! { #ty: ::core::fmt::#tr }
let trait_ident = format_ident!("{trait_name}");

parse_quote! { #ty: ::core::fmt::#trait_ident }
})
.chain(self.attrs.bounds.0.clone())
.collect()
Expand Down
80 changes: 72 additions & 8 deletions impl/src/fmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ pub(crate) mod display;
mod parsing;

use proc_macro2::TokenStream;
use quote::ToTokens;
use quote::{format_ident, ToTokens};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned as _,
token, Ident,
token,
};

use crate::{
Expand Down Expand Up @@ -133,7 +133,69 @@ impl ToTokens for FmtAttribute {
}

impl FmtAttribute {
/// Returns an [`Iterator`] over bounded [`syn::Type`]s and trait names.
/// Checks whether this [`FmtAttribute`] can be replaced with a transparent delegation (calling
/// a formatting trait directly instead of interpolation syntax).
///
/// If such transparent call is possible, the returns an [`Ident`] of the delegated trait and
/// the [`Expr`] to pass into the call, otherwise [`None`].
///
/// [`Ident`]: struct@syn::Ident
fn transparent_call(&self) -> Option<(Expr, syn::Ident)> {
// `FmtAttribute` is transparent when:

// (1) There is exactly one formatting parameter.
let lit = self.lit.value();
let param =
parsing::format(&lit).and_then(|(more, p)| more.is_empty().then_some(p))?;

// (2) And the formatting parameter doesn't contain any modifiers.
if param
.spec
.map(|s| {
s.align.is_some()
|| s.sign.is_some()
|| s.alternate.is_some()
|| s.zero_padding.is_some()
|| s.width.is_some()
|| s.precision.is_some()
|| !s.ty.is_trivial()
})
.unwrap_or_default()
{
return None;
}

let expr = match param.arg {
// (3) And either exactly one positional argument is specified.
Some(parsing::Argument::Integer(_)) | None => (self.args.len() == 1)
.then(|| self.args.first())
.flatten()
.map(|a| a.expr.clone()),

// (4) Or the formatting parameter's name refers to some outer binding.
Some(parsing::Argument::Identifier(name)) if self.args.is_empty() => {
Some(format_ident!("{name}").into())
}

// (5) Or exactly one named argument is specified for the formatting parameter's name.
Some(parsing::Argument::Identifier(name)) => (self.args.len() == 1)
.then(|| self.args.first())
.flatten()
.filter(|a| a.alias.as_ref().map(|a| a.0 == name).unwrap_or_default())
.map(|a| a.expr.clone()),
}?;

let trait_name = param
.spec
.map(|s| s.ty)
.unwrap_or(parsing::Type::Display)
.trait_name();

Some((expr, format_ident!("{trait_name}")))
}

/// Returns an [`Iterator`] over bounded [`syn::Type`]s (and correspondent trait names) by this
/// [`FmtAttribute`].
fn bounded_types<'a>(
&'a self,
fields: &'a syn::Fields,
Expand Down Expand Up @@ -221,7 +283,9 @@ impl FmtAttribute {
#[derive(Debug)]
struct FmtArgument {
/// `identifier =` [`Ident`].
alias: Option<(Ident, token::Eq)>,
///
/// [`Ident`]: struct@syn::Ident
alias: Option<(syn::Ident, token::Eq)>,

/// `expression` [`Expr`].
expr: Expr,
Expand All @@ -231,15 +295,15 @@ impl FmtArgument {
/// Returns an `identifier` of the [named parameter][1].
///
/// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#named-parameters
fn alias(&self) -> Option<&Ident> {
fn alias(&self) -> Option<&syn::Ident> {
self.alias.as_ref().map(|(ident, _)| ident)
}
}

impl Parse for FmtArgument {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
alias: (input.peek(Ident) && input.peek2(token::Eq))
alias: (input.peek(syn::Ident) && input.peek2(token::Eq))
.then(|| Ok::<_, syn::Error>((input.parse()?, input.parse()?)))
.transpose()?,
expr: input.parse()?,
Expand Down Expand Up @@ -283,7 +347,7 @@ impl<'a> From<parsing::Argument<'a>> for Parameter {
}

/// Representation of a formatting placeholder.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Eq, PartialEq)]
struct Placeholder {
/// Formatting argument (either named or positional) to be used by this placeholder.
arg: Parameter,
Expand Down Expand Up @@ -378,7 +442,7 @@ impl attr::ParseMultiple for ContainerAttributes {
fn merge_attrs(
prev: Spanning<Self>,
new: Spanning<Self>,
name: &Ident,
name: &syn::Ident,
) -> syn::Result<Spanning<Self>> {
let Spanning {
span: prev_span,
Expand Down
Loading

0 comments on commit 732bb12

Please sign in to comment.