Skip to content

Commit

Permalink
uniplate_derive: add new Uniplate derive macro
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
niklasdewally committed Sep 19, 2024
1 parent e9b65e9 commit ce43062
Show file tree
Hide file tree
Showing 31 changed files with 2,033 additions and 1,172 deletions.
798 changes: 434 additions & 364 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/uniplate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wip/
8 changes: 8 additions & 0 deletions crates/uniplate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@ im = {version = "15.1.0", features = ["proptest"]}
proptest = "1.5.0"
proptest-derive = "0.5.0"
thiserror = "1.0.61"
uniplate_derive = {path="../uniplate_derive"}

[dev-dependencies]
trybuild = "1.0.91"

[lints]
workspace = true

[features]
unstable = []

[profile.test.package.proptest]
opt-level = 3

41 changes: 41 additions & 0 deletions crates/uniplate/examples/stmt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#![allow(dead_code)]

use uniplate::{biplate::Biplate, Uniplate};

#[derive(Eq, PartialEq, Clone, Debug, Uniplate)]
#[uniplate()]
#[biplate(to=String,walk_into=[Expr])]
enum Stmt {
Nothing,
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>),
}

pub fn main() {
use Expr::*;
use Stmt::*;

let stmt_1 = Assign("x".into(), Div(Box::new(Val(2)), Box::new(Var("y".into()))));

let strings_in_stmt_1 = <Stmt as Biplate<String>>::universe_bi(&stmt_1);

// Test multi-type traversals
assert_eq!(strings_in_stmt_1.len(), 2);
assert!(strings_in_stmt_1.contains(&"x".into()));
assert!(strings_in_stmt_1.contains(&"y".into()));
}
1 change: 1 addition & 0 deletions crates/uniplate/proptest-regressions/test_common/paper.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cc 733ff88a8edcbb5d1b8a44f18ebff7d61962df8aced5c82ce45c603045bb8d30 # shrinks to ast = If(Val(0), Assign("", Val(0)), Assign("", Val(0))), new_children = [Assign("", Val(0)), Assign("", Val(1))]
2 changes: 1 addition & 1 deletion crates/uniplate/src/biplate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ where
ctx(children.map(op))
}

/// Gest all children of a node, including itself and all children.
/// Gets all children of a node, including itself and all children.
fn universe(&self) -> im::Vector<Self> {
let mut results = vector![self.clone()];
for child in self.children() {
Expand Down
39 changes: 39 additions & 0 deletions crates/uniplate/src/impls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! Implementations of Uniplate and Biplate for common types
//!
//! This includes stdlib types as well as common collections
//!
//! Box types are excluded, and are inlined by the macro.

// NOTE (niklasdewally): my assumption is that we can do all this here, and that llvm will inline
// this and/or devirtualise the Box<dyn Fn()> when necessary to make this fast.
// https://users.rust-lang.org/t/why-box-dyn-fn-is-the-same-fast-as-normal-fn/96392

use im::Vector;
use std::collections::VecDeque;

use crate::biplate::*;
use crate::derive_iter;
use crate::derive_unplateable;
use crate::Tree::*;

derive_unplateable!(i8);
derive_unplateable!(i16);
derive_unplateable!(i32);
derive_unplateable!(i64);
derive_unplateable!(i128);
derive_unplateable!(u8);
derive_unplateable!(u16);
derive_unplateable!(u32);
derive_unplateable!(u64);
derive_unplateable!(u128);
derive_unplateable!(String);

/*****************************/
/* Collections */
/*****************************/

// Implement Biplate for collections by converting them to iterators.

derive_iter!(Vec);
derive_iter!(VecDeque);
derive_iter!(Vector);
Loading

0 comments on commit ce43062

Please sign in to comment.