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

Attribute Macro support #14

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

Pat-Lafon
Copy link
Contributor

This is a follow up pr on #11(sorry for the delay).

I am interested in using the Rust procedural macro system to automate a lot of the boilerplate that exists in the common use case of this library. To this end, I have identified attribute macros as the proper way forward given that the user can provided additional arguments for the code generation.

Right now, I have a good grasp on what can be done and have implemented a prototype. I'm still missing many important parts like documentation and tests, but I want to nail down the design/scope and get feedback before I do those.

This pr adds the hcons attribute macro. I will use my current use case as an example:

#[hcons(name = "Type", impls = true)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum ActualType {
    Named(String),
    Arrow(Type, Type),
    Tuple(Vec<Type>),
    Mu(String, Type),
    Variant(Vec<(String, Type)>),
}

It takes a required argument name which corresponds to the name of the struct in the following generated code:

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct Type(HConsed<ActualType>);
impl std::ops::Deref for Type {
    type Target = HConsed<ActualType>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

I also optionally take the boolean flag impls which if set will generate the boilerplate around constructing this type via a factory in the following generated code:

consign! { let Type_FACTORY = consign(50) for ActualType ; }
impl Type {
    pub fn Named(args0: String) -> Self {
        Self(Type_FACTORY.mk(ActualType::Named(args0)))
    }
    pub fn Arrow(args0: Type, args1: Type) -> Self {
        Self(Type_FACTORY.mk(ActualType::Arrow(args0, args1)))
    }
    pub fn Tuple(args0: Vec<Type>) -> Self {
        Self(Type_FACTORY.mk(ActualType::Tuple(args0)))
    }
    pub fn Mu(args0: String, args1: Type) -> Self {
        Self(Type_FACTORY.mk(ActualType::Mu(args0, args1)))
    }
    pub fn Variant(args0: Vec<(String, Type)>) -> Self {
        Self(Type_FACTORY.mk(ActualType::Variant(args0)))
    }
}

I've realized I'm based off of #12. I can rebase off of master if that would be easier, but I would also like to make progress on that pr if possible first.

@AdrienChampion
Copy link
Owner

I see that you do require the top-level hashconsed type to be a newtype. That's fine, but I think it's important it Derefs to the hashconsed value so that Type has access to NewType's functions, as HConsed does.

@AdrienChampion
Copy link
Owner

Also, impls really does two things at once: generate a static factory, and generate the constructors.

I would split it in factory and constructors, with factory = false, constructors = true either disallowed, or maybe it could generate the constructors but take the factory as an argument.
Banning this configuration is probably better.

@AdrienChampion
Copy link
Owner

And since we need more than one crate to write the proc-macro, we should probably package the hashconsing crate itself in a hashconsing folder and have a top-level Cargo.toml dealing with the workspace.

@Pat-Lafon
Copy link
Contributor Author

I see that you do require the top-level hashconsed type to be a newtype. That's fine, but I think it's important it Derefs to the hashconsed value so that Type has access to NewType's functions, as HConsed does.

I believe this already does what you want it to? My understanding of automatic dereferencing is that the compiler can actually follow the chain of derefs if the method uses &self. To continue my example:

impl ActualType {
    pub fn is_named(&self) -> bool {
        match self {
            Self::Named(_) => true,
            _ => false,
        }
    }
}

Then I can have a main function as follows:

use program_synthesis_parser::lang::Type;

pub fn main() {
    let exp: Type = Type::Named("Bool".to_string());
    assert!(exp.is_named());
    dbg!(exp.is_named());
    dbg!(exp);
}

Is this the behavior you are looking for? In this case, we are leveraging the fact that HConsed<ActualType> implements Deref.

@Pat-Lafon
Copy link
Contributor Author

And since we need more than one crate to write the proc-macro, we should probably package the hashconsing crate itself in a hashconsing folder and have a top-level Cargo.toml dealing with the workspace.

Sure I've switched over to this structure by imitating what serde does. I can still run the tests so I think this works but I'm not sure whether other changes need to be made for publishing and such.

@Pat-Lafon
Copy link
Contributor Author

Pat-Lafon commented Feb 18, 2023

Also, impls really does two things at once: generate a static factory, and generate the constructors.

I would split it in factory and constructors, with factory = false, constructors = true either disallowed, or maybe it could generate the constructors but take the factory as an argument. Banning this configuration is probably better.

Hmm, this I'm a little less sure on but maybe its just more my person taste.

This is basically talking about what kind of abstraction this macro is trying to provide right? Here one can trade having more complexity for finer grain control of what is being generated. I guess I'm not sure if giving this much control is useful?

I'm currently not exposing the name of the factory, so that would need to be provided. Additionally, what use case do you have for having the factory being generated w/o impls? For that case, I'm wondering if your better off just creating the factory yourself(instead of using the derive macro).

@AdrienChampion
Copy link
Owner

To be clear, I was talking about a flag to (de)activate the static factory. People who use the factory state-monad-style like #13 don't want a static factory.
You would still generate a factory type, can't do anything without it, but without a static factory.

Regarding constructors, you do not want them to be exposed when you want to make sure you preprocess them, typically for simplifications/rewriting/canonicalization.

So it seems mandatory to me to have a flag controlling the static factory (default active) and one controlling the constructors (default active), or at the very least the constructors' visibility so that users can scope-hide them if needed.

@Pat-Lafon Pat-Lafon marked this pull request as ready for review March 3, 2023 19:40
@Pat-Lafon
Copy link
Contributor Author

Hello,

Sorry if I let this slip through. If you find time at some point, I am still interested in addressing concerns and completing this pr.

@AdrienChampion
Copy link
Owner

Thank you for not giving up on this 😺

I can't see any examples, could you add some to facilitate the discussion?

@Pat-Lafon
Copy link
Contributor Author

Pat-Lafon commented Sep 29, 2023

I can't see any examples, could you add some to facilitate the discussion?

Maybe the best way is via writing the doc tests so those get iterated on at the same time? I have pushed an example for this which shows off the standard use case and that Deref is implemented.

@AdrienChampion
Copy link
Owner

Not going to lie it's pretty cool and convenient 😸

@AdrienChampion
Copy link
Owner

AdrienChampion commented Oct 2, 2023

I wrote a few remarks, let me add the following so that we don't forget

  • support struct-like variant syntax, i.e. MyVariant { field_1: Type, field_2: Type, ... }
  • add a test that struct-like variant syntax is supported, potentially by just adding one to the current doc example

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.

2 participants