-
Notifications
You must be signed in to change notification settings - Fork 12
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
[Biplate Macro #2] New Uniplate Derive Macro #290
Conversation
0cd0da5
to
86c6ee0
Compare
@ozgurakgun @gskorokhod I think this is ready for a preliminary review. Known issues:
These errors can be seen by removing the comments in the examples/stmt.rs file. I've got some ideas for how to fix these, and some examples I could writeup to show specifically the underlying problems, but I don't have the time to do this just yet. A general code review would still be helpful though. |
There seems to be a bunch of compile issues, I tried just running
|
The entire project doesn't compile as I haven't refactored conjure oxide to use the new types yet. The uniplate crate specifically should though! |
0025864
to
b711356
Compare
RFC @gskorokhod @ozgurakgun @lixitrixi @ChrisJefferson This is the blocker for this PR; any help would be appreciated! I don't really know how to explain it very well in writing (especially as I last looked at this indepth 2 weeks ago), but hopefully this gets the core of the issue across!. To decide which Biplates to generate, each type found inside the given type is added to a list. A Biplate instance is generated from the given type to each type in this list. #[derive(Uniplate)]
enum A {
B(F,G),
C(H),
D(i32), Deriving Uniplate on Under the hood, Biplates and Uniplates are just recursive invocations of Biplate that walk towards the target type. Currently, for each field in an enum variant, the generated code calls the Biplate that goes from that fields’ type to the target type ( Consider the instance Another issue is that it will try to call The main blocker for this PR is that we need a UI to decide what instances to generate. I’m a bit stuck on what the best way to go about this would be. Also note that traits specialisation does not exist, and generic traits in rust do not yet have mutually exclusive bounds, so what we can do with blanket implementations is very limited. |
picking this up part by part, for
Why not generate that instance in the trivial way? |
we might have to special case primitive types. I see we already have |
I'll give a more concrete example: #[derive(Eq, PartialEq, Clone, Debug, Uniplate)]
enum Stmt {
Assign(String, Expr),
Sequence(Vec<Stmt>),
If(Expr, Box<Stmt>, Box<Stmt>),
While(Expr, Box<Stmt>),
}
#[derive(Eq, PartialEq, Clone, Debug, Uniplate)]
enum Expr {
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Val(i32),
Var(String),
Neg(Box<Expr2>),
}
#[derive(Eq, PartialEq, Clone, Debug, Uniplate)]
enum Expr2 {
Modulo(Box<Expr>)
} When derive runs on
When derive runs on
Here the problematic case is The macro runs on each type in isolation, generating biplates from that type to all detected inner types. If we generate all instances from each type and to each type in We have no way of checking during macro execution if an implementation is missing or is already present, so we must instead allow the user to specify what additional instances to generate. In the Haskell direct derive the user specifies all instances to generate by hand, using the compiler to tell them which ones are missing. I think we can do better here. An example |
I think that I will make another PR moving Uniplate on Conjure Oxide to a manual implementation using the new types. This would help me iterate on the macro implementation without breaking Conjure Oxide as often. |
Thats right - it is just a simple substitution filling in some predefined terminal instances |
* Replace the #[derive(Uniplate)] with a manual implementation. * Use the new uniplate::biplate::Uniplate type instead of uniplate::uniplate::Uniplate. This will be the only Uniplate type in the future. These changes are to help development of the new Uniplate derive macro. In conjure-cp#290, I am in the process of removing the old uniplate type (uniplate::uniplate::Uniplate) and rewriting the macro - the changes made in this commit ensure that during development of this PR Conjure Oxide still compiles and passes the tests.
0ed0a27
to
1eecd51
Compare
Still work in progress, but I have added some syntax thats specifies what instances to generate. It fixes all issues to do with missing trait implementations; however, it requires the user to manually specify which types should be walked into when looking for children. use uniplate::Uniplate;
#[derive(Eq, PartialEq, Clone, Debug, Uniplate)]
#[uniplate()]
#[biplate(to=String,walk_into=(Expr))]
enum Stmt {
Assign(String, Expr),
//Sequence(Vec<Stmt>),
If(Expr, Box<Stmt>, Box<Stmt>),
While(Expr, Box<Stmt>),
}
#[derive(Eq, PartialEq, Clone, Debug, Uniplate)]
#[uniplate()]
#[biplate(to=String)]
enum Expr {
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Val(i32),
Var(String),
Neg(Box<Expr>),
} Forgetting to add a type to walk_into could cause annoying bugs, so I think an opt-out mechanism (don't generate instances for these two types) may be better than this. However, this is significantly simpler to implement. |
5ed75ca
to
0aced1f
Compare
0aced1f
to
27a18fd
Compare
* Use a manual implementation of Uniplate on Expression instead of the derive macro. * Use the new uniplate::biplate::Uniplate type instead of uniplate::uniplate::Uniplate. This will be the only Uniplate type in the future. These changes are to help development of the new Uniplate derive macro. In conjure-cp#290, I am in the process of removing the old uniplate type (uniplate::uniplate::Uniplate) and rewriting the macro - the changes made in this commit ensure that during development of this PR Conjure Oxide still compiles and passes the tests.
* Use a manual implementation of Uniplate on Expression instead of the derive macro. * Use the new uniplate::biplate::Uniplate type instead of uniplate::uniplate::Uniplate. This will be the only Uniplate type in the future. These changes are to help development of the new Uniplate derive macro. In conjure-cp#290, I am in the process of removing the old uniplate type (uniplate::uniplate::Uniplate) and rewriting the macro - the changes made in this commit ensure that during development of this PR Conjure Oxide still compiles and passes the tests.
581ecca
to
cb24ca2
Compare
5bd12a5
to
18727ed
Compare
Reimplement the uniplate-derive macro to allow for Biplates and "multiple type traversals" as discussed in [Niklas Dewally, 2024‐03: Implementing Uniplates and Biplates with Structure Preserving Trees]. (currently at https://github.com/conjure-cp/conjure-oxide/wiki/2024%E2%80%9003-Implementing-Uniplates-and-Biplates-with-Structure-Preserving-Trees). Implementation overview ======================= The library syn supports the creation of custom AST nodes. Using the Parse trait, one can implement parsers for these custom nodes by combining existing Rust AST nodes together as well as parsing Rust tokens by hand. The macro code has two parts: parsing into a custom AST, and using this AST for code-gen. This simplifies code-gen, as only the data needed for Uniplate is encoded in the AST. This also allows for better error reporting, as Most errors are now reported during parsing, allowing errors to be associated with the specific tokens that caused them. The biggest change in Uniplate's AST compared to Rust's is the representation of types. Uniplate has three kinds of type: boxed pointer types, plateable types, and unpalatable types. Plateable types include the current type being derived, or other types explicitly specified through the walk_through attribute in the macro invocation. Boxed pointer types represent smart pointers such as Box and Rc. These use the .borrow() function instead of & to pass the inner value to the Biplate instance. Generating instances for Box<A> and Rc<A> would mean that values are constantly moved from stack to heap - this avoids this. Implementations for standard library iterator types and primitives are provided by Uniplate. These implementations are generated by declarative macro, allowing easy extension to more collection types in the future. Testing ======= Add trybuild, a library for UI testing derive macros. The Rust tester does not run at-all when test code does not compile, but we need to test whether our macro gives a compile error when it should. trybuild also checks that the error messages given by the compiler have not changed. Two test ASTs are used for Uniplate and Biplates: a toy statement-expression found in the original paper, and the current Conjure Oxide AST. Property-based testing in paper.rs shows that children() and with_children() work for random homogenous AST's, including with boxes and iterators. As these two methods essentially act as wrappers for the child tree and context function returned by the uniplate() / biplate() methods, this test is sufficient to show that automatically derived Uniplate implementations act as intended. Signed-off-by: Niklas Dewally <niklas@dewally.com>
18727ed
to
9d11fa6
Compare
Working and ready for review @ozgurakgun 🚀 I think the documentation and UI of the macro still need some work, but I think merging this as is would be best. |
Replace the manual implementation of the Uniplate and Biplate traits for the AST with the derive macro. Adding uniplate_derive to the ast broke is_enum_variant, therefore this commit also removes and replaces all invocations of is_xxx() functions with let..else or pattern matching. Signed-off-by: Niklas Dewally <niklas@dewally.com>
9d11fa6
to
4b6a1a3
Compare
cc: @lixitrixi |
brilliant. I think I'd like a walk through before we merge though. tomorrow afternoon? I can do 3.30. |
This PR adds the new Uniplate derive macro.
The reason for this rewrite was to add Biplates and "multiple-type traversals"; this is discussed further in this wiki article: https://github.com/conjure-cp/conjure-oxide/wiki/2024%E2%80%9003-Implementing-Uniplates-and-Biplates-with-Structure-Preserving-Trees
Implementation
The library
syn
supports the creation of custom AST nodes. Using theParse
trait, one can implement parsers for these custom nodes by combining existing Rust AST nodes together as well as parsing Rust tokens by hand.The macro code has two parts: parsing into a custom AST, and using this AST for code-gen. This simplifies code-gen, as only the data needed for Uniplate is encoded in the AST. This also allows for better error reporting, as Most errors are now reported during parsing, allowing errors to be associated with the specific tokens that caused them.
The biggest change in my AST compared to Rust's is the representation of types. Uniplate has three kinds of type: boxed pointer types, plateable types, and unpalatable types.
Each plateable types contains some wrapper types and a base type. For example, the input type
Vec<Vec<A>
would be split into wrappersVec,Vec
, and a base typeA
. This is useful as we want to generally derive implementations forA
only. Vectors are implicitly unwrapped using default implementations.Boxed pointer types represent smart pointers such as
Box
andRc
. These use the.borrow()
function instead of&
to pass the inner value to theBiplate
instance. Generating instances forBox<A>
andRc<A>
would mean that values are constantly moved from stack to heap - this avoids this.Testing
I added
trybuild
, a library for UI testing derive macros. The Rust tester does not run at-all when test code does not compile, but we need to test whether our macro gives a compile error when it should.trybuild
also checks that the error messages given by the compiler have not changed.Based on #301