I've just started learning Rust (coming from Haskell) and decided to test a toy expression interpreter.
Code:
use std::rc::Rc;
pub type RAST<T> = Rc<AST<T>>;
#[derive(Debug)]
pub enum AST<T> {
ConstInt(isize, Rc<T>),
Add(RAST<T>, RAST<T>, Rc<T>),
}
pub fn eval<T>(x: &RAST<T>) -> RAST<T> {
match &**x {
AST::Add::<T>(l, r, t) => {
let el = eval(&l);
let er = eval(&r);
match (&*el, &*er) {
(AST::ConstInt::<T>(li,_q),
AST::ConstInt::<T>(ri,_r)) =>
Rc::new(AST::ConstInt::<T>(li+ri, Rc::clone(t))),
_ => mk_add(&el, &er, &t),
}
},
_ => Rc::clone(x),
}
}
pub fn mk_int<T>(i: isize, t: &Rc<T>) -> RAST<T> {
Rc::new(AST::ConstInt::<T>(i, Rc::clone(t)))
}
pub fn mk_add<T>(l: &RAST<T>, r: &RAST<T>, t: &Rc<T>) -> RAST<T> {
Rc::new(AST::Add::<T>(Rc::clone(l), Rc::clone(r), Rc::clone(t)))
}
pub fn main() {
let a = Rc::new(());
let x = mk_int(3, &a);
let y = mk_int(6, &a);
let z = mk_add(&x, &y, &a);
let c = mk_add(&x, &z, &a);
let r = eval(&c);
println!("Raw: {:?}", c);
println!("Evaluated: {:?}", r);
}
With this output:
Raw: Add(ConstInt(3, ()), Add(ConstInt(3, ()), ConstInt(6, ()), ()), ())
Evaluated: ConstInt(12, ())
I'd really appreciate any feedback on:
- Am I using the "right" type for the Add subnodes (i.e. Rc)?
- Is it possible to create eval as an AST method instead of a function? I kept fighting the borrow checking if
x
in eval wasself : &AST<T>
(within theimpl
block, of course) - Is there any way to make this program more concise/cleaner? I think I understand why all the additional wrapping and sprinkling of
Rc::clone/new
and lots of reference taking is needed, but was wondering if I was perhaps missing something that might make this code cleaner to read. Any other feedback/helpful pointers would also be appreciated. Thank you!
T
is for? It seems like removing all of the<T>
from your code and remove theRc<T>
fields from your struct doesn't effect the functionality. \$\endgroup\$T
would make the code more concise and readable, but I wanted to have this functionality. \$\endgroup\$