Hello, I'm working through Crafting Interpreters and I am reimplementing the interpreter described in the book in Rust.
I got to the point where I need to define the type for an abstract syntax tree and the compiler is (rightfully) telling me that my original attempt was creating a recursive type with infinite size.
The error message has been helpful enough suggesting to use Box to prevent the issues, and in fact the following enum compiler just fine.
That's basically how rustc's own internals look. Depending on your use cases when working with the AST you might want to swap in reference counted types rather than Box, but that's basically it.
Another representation, which is used in the context of IDEs, when you want to have a lossless representation of the source code, as opposed to the abstract syntax tree, is a two-layer one.
On the first layer you have an untyped syntax tree, which basically marks ranges in the source text:
I'm 5 years late, but for posterity, another way is to use Arenas (like bumpalo) or allocate directly on stack frames using a layout like this:
enum Expr<'a>{
Unary(Operator, &'a Expr<'a>),
Binary(&'a Expr<'a>, Operator, &'a Expr<'a>)
Literal(Literal)
}
// Arena Allocation (pass arena around to use these expressions)
let arena = bumpalo::Bump::new();
let literal: &mut Expr = arena.alloc(Literal("true".into()));
let negation_expr = arena.alloc(Expr::Unary(Operation::Negate, literal));
// Stack Allocation (expressions are dropped at end of stack frame, so recursion is needed to use these effectively)
let stack_literal = &Literal("false".into());
let stack_expr = Expr::Unary(Operation::Negate, literal);
Just make sure to pass around the arena to all your functions that need to allocate and keep in mind that nothing is deallocated until you drop the arena.
Using this strategy is really useful if you are doing many, many, operations where you don't need to deallocate a bunch of stuff. It saves so much time in heap allocation because it preallocates the heap. In fact, the rust compiler uses arenas in many cases Memory Management in Rustc - Guide to Rustc Development. The only downside is that you need to annotate everything with lifetime parameters which can be annoying.