1 unstable release
new 0.1.0 | Feb 7, 2025 |
---|
#920 in Rust patterns
164 downloads per month
23KB
374 lines
egg_recursive
- Recursive interface for egg
This crate provides a recursive interface to egg
.
egg
alrady comes with S-expression-based interface, but it has the following drawbacks:
- It uses
FromStr
andDisplay
to parse/format textual representation of ASTs.- These CAN be used for other purposes and this can cause a conflict with other applications.
- Parser favours the first clause for terminal variants with the same parameter.
- This can result in unexpected behaviour under the existence of ambiguity.
- ALL textual representation of ASTs fed to
egg::rewrite
is checked at RUNTIME.- This means we can't see syntax error until compilation;
- This further complicates the debugging process.
- S-expressions get harder and harder to be parsed by human eyes
This crate alleviates these problems by introducing a recursive interface to egg
.
lib.rs
:
This crate provides a recursive interface to [egg
].
[egg
] alrady comes with S-expression-based interface, but it has the following drawbacks:
- It uses
std::str::FromStr
andstd::fmt::Display
to parse/format textual representation of ASTs.- These CAN be used for other purposes and this can cause a conflict with other applications.
- Parser favours the first clause for terminal variants with the same parameter.
- This can result in unexpected behaviour under the existence of ambiguity.
- ALL textual representation of ASTs fed to
egg::rewrite
is checked at RUNTIME.- This means we can't see syntax error until compilation;
- This further complicates the debugging process.
- S-expressions get harder and harder to be parsed by human eyes
This crate alleviates these problems by introducing a recursive interface to [egg
].
Abstraction over Language
You can define a recursive language as a ordinary Self
-referencing enum,
containing [Box
]es, [[Self; N]
], or [Vec
]s or other terminal other types.
Such language AST must implement Recursive
trait and have a Signature
type synonym.
This also provides Pat<L>
for pattern matching, which can be converted to/from Pattern<AsLanguage<L>>
.
We are providing a Language
derive macro for automatically derive such types and impls.
use egg::*;
use egg_recursive::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Language)]
pub enum Arith {
Num(i32),
Neg(Box<Self>),
Add([Box<Self>; 2]),
Mul([Box<Self>; 2]),
}
This generates the following entities:
Recursive
impl forArith
;- You can convert from/to [
RecExpr<Signature<Arith, Id>>
] withFrom
instances orRecursive::into_rec_expr
/Recursive::from_rec_expr
.
- You can convert from/to [
EggArith
type alias forSignature<Arith, Id>
, which implementsegg::Language
;ArithPat
type for recursive pattern ASTs.- It already implements
egg::Searcher
andegg::Applier
. - This is indeed a newtype wrapper of
Pat<AsLanguage<Arith>>
.- We provide this as a newtype to allow (otherwise ophan) impls of utility traits for [
Pat
]s, (e.g.std::ops::Add
,std::ops::Mul
, etc).
- We provide this as a newtype to allow (otherwise ophan) impls of utility traits for [
- This can be converted to/from
egg::Pattern<EggArith>
withFrom
instances.
- It already implements
ArithCons
trait, which provides smart constructors forArith
andArithPat
s.ArithCons
has trait methods likenum(i64)
,add([Self; 2])
,mul([Self; 2])
,neg(Self)
, etc. It has just snake_cased variant of the original enum variants. If the snake_case version is reserved keyword, suffixed with_
(e.g.If
becomesif_()
).
An enum
passed to self::Language
should satisfy the following conditions:
- It MUST NOT have any generic parameters.
- Recursive variants MUST have only one field, which is one of the following (in any case, the type itself MUST be referenced by
Self
, not a enum name itself):Box<Self>
,[Box<Self>; N]
,Vec<Self>
,T<Box<Self>>
for someIntoLanguageChildren
typeT
(described in the next section).
- Arguments to the non-recursive variants MUST implement at least [
Eq
], [Ord
],Hash
, andClone
.
Helper Types for egg::LanguageChildren
Sometimes, the more the LanguageChildren
has a components, the harder to get memorise the order/semantics of components.
To alleviates this situation, we introduce IntoLanguageChildren
trait, which abstracts over view types of egg::LanguageChildren
.
You don't have to write it manually and it can generally be derived by self::LanguageChildren
derive macro.
Suppose we want to add boolean operations and if-expressions:
use egg::*;
use egg_recursive::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, LanguageChildren)]
pub struct IfThenElse<T> {
pub cond: T,
pub then_branch: T,
pub else_branch: T,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Language)]
pub enum Arith {
Num(i32),
Bool(bool),
Neg(Box<Self>),
Add([Box<Self>; 2]),
Mul([Box<Self>; 2]),
IsZero(Box<Self>),
If(IfThenElse<Box<Self>>),
}
This generates RawIfThenElse
and IntoLanguageChildren
instance to convert it to LanguageChildren
.
You can call either IfThenElse::view
or RawIfThenElse::unview
to convert it to/from RawIfThenElse
.
egg::LanguageChildren
macro accepts types with the following conditions:
- It MUST be a struct with exactly one generic parameter.
- Every field must be one of the following (where
T
its only parameter):T
,- [
[T; N]
], or Vec<T>
,
- AT MOST ONE
Vec<T>
is allowed in its field.
Writing Rewrite Rules
egg_recursive
provides a redefined rewrite!
macro to write rewrite rules.
This is almost the same as the original egg::rewrite!
macro, but it allows you to write conditions in a more readable way.
Due to the ambiguity issue, it doesn't support bidirectional rules (<=>
) for the time being.
The following illustrates the usage of the macro:
pub fn make_rules() -> Vec<Rewrite<EggArith, ConstantFold>> {
let v = |s: &str| ArithPat::pat_var(s);
let x = || v("x");
let y = || v("y");
let x_var: Var = "?x".parse().unwrap();
vec![
rewrite!("add-comm"; x() + y() => y() + x()),
rewrite!("mul-comm"; x() * y() => y() * x()),
rewrite!("add-zero"; x() + ArithPat::float(0.0.into()) => x()),
rewrite!("add-zero"; x() => x() + ArithPat::float(0.0.into())),
rewrite!("mul-inv";
x() * x().inv() => ArithPat::float(1.0.into())
; if is_non_zero(x_var.clone())
),
]
}
In the above, +
and *
comes from std::ops::Add
and std::ops::Mul
instances defined manually,
which is possible because ArithPat
is a newtype, but not an alias.
Dependencies
~3.5–6MB
~108K SLoC