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

Initial derive-Error proc-macro implementation #103

Merged

Conversation

ffuugoo
Copy link
Contributor

@ffuugoo ffuugoo commented Nov 4, 2019

First part of #92.

Not based on utils::State and supports only source attribute/method, so far.

Attribute syntax is the one suggested by @tyranron. Lots of examples in tests/error.rs.

@ffuugoo ffuugoo changed the title WIP: Initial derive-Error proc-macro implementation WIP: Initial derive-Error proc-macro implementation (#92) Nov 4, 2019
@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 4, 2019

@JelteF @tyranron

@JelteF
Copy link
Owner

JelteF commented Nov 4, 2019

Thank you for opening the WIP PR. I do still want you to use utils::State.

The main reason I want this to use utils::State is so that it uses the same attribute semantics as all the other derives. For example, these two would be equivalent.

#[derive(Error)]
struct WithoutSource(#[error(not(source))] i32);
#[derive(Error)]
#[error(not(source))] 
struct WithoutSource(i32);

Also, the not semantics will be useful for the attributes of other derives as well. And finally, utils::State also standardizes the parsing of enums. So this code for error could be used as an example to implement #99.

@JelteF
Copy link
Owner

JelteF commented Nov 4, 2019

When using utils::State ignore the fact that other attribute parameters defined in utils (e.g. forward or ref), would be allowed (and ignored) for now. This is a separate issue and is tracked in #104.

Copy link
Collaborator

@tyranron tyranron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ffuugoo ☝️

@JelteF
Copy link
Owner

JelteF commented Nov 6, 2019

Another nice thing that the Error derive would get as a free benefit from State is this (once it's implemented): #102

@JelteF
Copy link
Owner

JelteF commented Nov 7, 2019

@ffuugoo FYI, I've implemented checking of supported attribute parameters for State on master now.

@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 7, 2019

@ffuugoo FYI, I've implemented checking of supported attribute parameters for State on master now.

This is great.

I've worked on moving derive-Error to using State and I have few concerns. I'm writing an elaborate explanation about them RN.

@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 7, 2019

So, I've worked on on moving derive-Error to using State I realized that the way State process (and expose) attributes is not flexible enough for implementing derive-Error attribute-interface that was agreed upon in #92.

So, as far as I understand, the way State currently parses attributes is:

  1. State::new constructs "default" FullMetaInfo structure
  2. Attributes are parsed on everything (enums/structs, enum variants, fields) as MetaInfo
  3. Parsed MetaInfo converted to FullMetaInfo by replacing all unspecified fields with values from "default" FullMetaInfo

The problem is, if we want to implement generic not predicate for attributes, we have to distinguish three cases: explicitly-enabled (e.g., [#error(source)]), explicitly-disabled (e.g., [#error(not(source))]) and not-specified.

So, I see two ways going from here:

  1. Discard the idea of generic not predicate and use ignore instead. It will make possible attribute formats less flexible, but it requires minimal changes to State and won't break existing code.
  2. Implement generic not predicate and expose original MetaInfo in addition to FullMetaInfo in some way.

@JelteF
Copy link
Owner

JelteF commented Nov 7, 2019

@ffuugoo Awesome that you're working so much on this! It's a shame that the current MetaInfo approach is not flexible enough, but I already expected to run into it at some point. I mainly wanted to keep it simple while it was not needed yet. I think option 2 would be most desirable, the not predicate would also be very useful for other derives. One simple way of exposing MetaInfo is by embedding it into FullMetaInfo as a field.

@JelteF
Copy link
Owner

JelteF commented Nov 7, 2019

As far as I understand embedding it should also not break existing code.

@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 7, 2019

I also really don't like the whole MetaInfo/FullMetaInfo idea. Why not just use a HashMap<T, bool>? That's what I did in my derive-Error implementation.

This way we:

  • Can build much more generic attribute parsing code (as we won't have to manually extend MetaInfo/FullMetaInfo with new fields and manually modify map-attribute-to-structure-field code each time new macro is added).

  • If we really want to be type safe, we can get a trade-off (I've used this exact approach in current derive-Error implementation, check it for an example):

    • define an enum with variants representing all the valid attributes
    • define a function, mapping enum variant to attribute name (e.g., derive Display on it)
    • define const array with all enum variants (so we can iterate on all valid attributes)
    • btw, all of the above can be done with a derive-proc-macro; and chances are, it's already implemented somewhere on crates.io (:
    • with such architecture we can ensure (statically-type-safe ensure, I must say), that only a well-known limited set of attributes can be used (I mean, used by clients of State) and have a generic attributes parser implementation that requires absolutely minimal changes to a well scoped piece of code (i.e., enum, enum-to-str function and enum-variants array)
  • Automatically get "explicitly-enabled/disabled or not-specified" distinction (cause HashMap::get returns Option<bool>, where None is obvious "not-specified").

The only real downside I see, is that there is no trivial way to "default-merge" a HashMap as you do with MetaInfo/FullMetaInfo. But I can see two partial solutions that are, probably, good enough:

  • is_explicitly_enabled/is_explicitly_disabled methods:
    fn is_explicitly_enabled<T>(hash_map: &HashMap<T, bool>, key: &T) -> bool {
        match hash_map.get(key) {
            Some(true) => true,
            _ => false,
          }
    }
  • merge it with well-known pre-filled HashMap and use hash_map.get(key).unwrap()

@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 7, 2019

One simple way of exposing MetaInfo is by embedding it into FullMetaInfo as a field.

Didn't think about it. Seems simple/good enough. I'll see how far I can get with this. (:

@JelteF
Copy link
Owner

JelteF commented Nov 7, 2019

I'm definetly not sold on the current design of MetaInfo/FullMetaInfo. The main reason it is like it is now is because it was simple to write/understand and did the job. If embedding FullMetaInfo is good enough for you I think doing that is best for now, since it seems another simple solution.

In the future I definitely think rethinking the MetaInfo/FullMetaInfo approach is a good idea. Things to keep in mind though:

  • A simple Hashmap<T, bool> doesn't work for the value parameter described in Add a default and value attribute parameters #102, since it's not a boolean.
  • Using an enum you would still have to map the strings to a variant, which seems about as much work as mapping them to a field (I might be wrong here though).

@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 7, 2019

Using an enum you would still have to map the strings to a variant, which seems about as much work as mapping them to a field (I might be wrong here though).

#[derive(Copy, Clone, <stuff required for HashMap key>)]
enum ValidAttr {
  Source,
  Backtrace,
};

impl ValidAttr {
  fn str(&self) -> &'static str {
    match self {
      Self::Source => "source",
      Self::Backtrace => "backtrace",
    }
  }
}

const VALID_ATTRS: &[ValidAttr] = [
  ValidAttr::Source,
  ValidAttr::Backtrace,
];

fn test(parsed_attrs: &mut HashMap<ValidAttr, bool>, path: &Path) {
  let attr = VALID_ATTRS.find(|attr| path.is_ident(attr.str()));
  parsed_attrs.insert(attr, true);
}

Kinda the same amount of work, but very trivial and well scoped.

@JelteF
Copy link
Owner

JelteF commented Nov 7, 2019

I see you added some more commits. I'll try to take a look at them soon-ish. One thing that's important, I want to release this with some preliminary backtrace() support. Since I would like not to make a backwards incompatible change on this. The important part for this is the automatic detection of the backtrace field, i.e. check if the type is Backtrace for tuple or if the field is backtrace for struct version. The backtrace attribute doesn't necessarily need to be implemented, since that can be added be without a backward compatibility concern. The default behaviour should be what we want for the final version though.

@tyranron
Copy link
Collaborator

tyranron commented Nov 7, 2019

@JelteF I suggest to implement backtrace() support in a separate PR after this one, so keeping PRs simple and iterative.

@JelteF JelteF changed the base branch from master to error November 7, 2019 16:16
@JelteF
Copy link
Owner

JelteF commented Nov 7, 2019

@tyranron I think that indeed makes sense, I've changed this PR to merge into the error branch of my repo (which is a copy of master). That way we can keep the features in separate PRs, but merge them into master at the same time (by merging error into master once all features are merged in there).

@ffuugoo For clarity, is this one still just a WIP PR? Or is it ready for a full review.
Also, feel free to open the backtrace PR already as well. It's possible you can get that working before I have time to review this one.

@tyranron
Copy link
Collaborator

tyranron commented Nov 7, 2019

@JelteF still WIP. We will remove the WIP: prefix from PR's name once it's ready.

Copy link
Owner

@JelteF JelteF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some preliminary comments, mostly with ideas to better utilize State.

src/error.rs Outdated
where
P: Fn(&syn::Field, usize) -> bool,
{
let fields = state.enabled_fields_data();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use unpacking here to make the rest of the code clearer.

src/error.rs Outdated
info,
)?,

None => process_if_valid_default_field_for_attr(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's split this into two loops. First checking all fields for explicit setting of the source and only if that's not done try to infer it. I think the flow is a bit clearer that way and some of the error cases below disappear.

src/error.rs Outdated
}

impl<'a> ParsedField<'a> {
fn render_as_struct_field_ident(&self) -> TokenStream {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usage of this function can be replaced by multi_field_data.members[self.index]

tests/error.rs Outdated
fn named_implicit_source() {
#[derive(Default, Debug, Display, Error)]
#[display(fmt = "")]
struct S<SS, T> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type parameter is quite confusing since the above error type is also called SS. Let's change it to E (also for the types below).

tests/error.rs Outdated

#[derive(Debug, Display, Error)]
#[display(fmt = "")]
enum E<S, T> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, let's rename this S type parameter such that it doesn't shadow the actual type.

src/error.rs Outdated
}
}

fn render_as_enum_variant_struct_match_arm_tail(&self, len: usize) -> TokenStream {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This an the next function can be replaced by a call to MultiFieldData.initializer.
Something like this:

        let bindings: Vec<_> = (0..len).map(|index| {
            if index == self.index {
                quote!(source)
            } else {
                quote!(_)
            }
        }).collect();
        multi_field_data.initializers(&bindings)

I think this also allows you to remove derive_type from ParsedFields

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added multi_field_data.matcher, now you can do:

multi_field_data.matcher(&[self.index], &[quote!(source)])

@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 8, 2019

@JelteF Should have fixed all of your comments now.

@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 8, 2019

@tyranron Take a look as well, pls.

Copy link
Collaborator

@tyranron tyranron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ffuugoo

Take a look as well, pls.

Well enough as for me. Remove the WIP: prefix, please.

enum_: vec!["ignore"],
struct_: vec!["ignore"],
variant: vec!["ignore"],
field: vec!["ignore", "source"],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no tests at all for the ignore attribute. Let's either add tests or disallow the ignore attribute if it doesn't make sense for error (which I think is the case).

Copy link
Contributor Author

@ffuugoo ffuugoo Nov 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not make much sense, but it does not contradict the design as well. Added tests for possible ignore cases.

src/utils.rs Outdated Show resolved Hide resolved
src/utils.rs Outdated Show resolved Hide resolved
&generics,
quote! {
where
#(#bounds: ::std::fmt::Debug + ::std::fmt::Display + ::std::error::Error + 'static),*
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the Debug and Display bounds necessary? Doesn't Error already require those?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems so. Tests fail to compile for me if I remove these extra bounds.

src/error.rs Show resolved Hide resolved
[[test]]
name = "error"
path = "tests/error.rs"
required-features = ["error"]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have failed in CI, since the test actually requires the display feature as well for the Display derives. I've updated master with a small change to the testing so that this should now fail.

Let's rename this test to error_full and add display to required-features.
Apart from that let's create a small error test, with a some simple Error derives that have a simple Display implementation written out so that we test that the error feature works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed display dependency for tests/error altogether.

src/parsing.rs Outdated
@@ -23,7 +23,7 @@ pub type ParseResult<T> = Result<T, ParseError>;
impl ::std::fmt::Display for ParseError {
fn fmt(
&self,
fmt: &mut ::std::fmt::Formatter<'_>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes to this file seem unnecessary. Let's undo those.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

match meta {
Meta::List(list) if list.path.is_ident("not") => {
if value {
parse_punctuated_nested_meta(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is called recursively it allows #[error(not(not(source))], which is confusing as it's the same as #[error(not(source))]. Let's call another function here, that doesn't allow any Meta::List.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not. if value conditional only allows for a single top-level "not" or fails with an error otherwise.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, can you add a comment explaining that check then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@ffuugoo ffuugoo changed the title WIP: Initial derive-Error proc-macro implementation (#92) Initial derive-Error proc-macro implementation (#92) Nov 11, 2019
@ffuugoo ffuugoo marked this pull request as ready for review November 11, 2019 07:01
Co-Authored-By: Jelte Fennema <github-tech@jeltef.nl>
@ffuugoo ffuugoo requested a review from JelteF November 11, 2019 07:03
@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 15, 2019

@JelteF

Copy link
Owner

@JelteF JelteF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me now, but there are some merge conflicts with the latest master. If you solve those we can merge it into the error branch. FYI I'll probably only have time to review the changes for Backtrace somewhere in December.

@ffuugoo
Copy link
Contributor Author

ffuugoo commented Nov 18, 2019

@JelteF

@JelteF JelteF changed the title Initial derive-Error proc-macro implementation (#92) Initial derive-Error proc-macro implementation Nov 18, 2019
@JelteF JelteF merged commit cde058f into JelteF:error Nov 18, 2019
@ffuugoo ffuugoo deleted the implement-initial-derive-error-macro branch November 18, 2019 12:26
@JelteF
Copy link
Owner

JelteF commented Mar 28, 2020

This is now released in version 0.99.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants