I am writing a compiler for a made-up language for fun and education. I have an AST, where each node is a struct like Stmnt
or Block
. Some of these (like Block
) own a symbol table environment where variable declarations for that scope are stored.
Example:
pub struct Block {
pub env: Environment,
pub stmnts: Vec<Stmnt>,
}
I'm writing some generic code for walking this structure; my hope is to implement a visitor pattern where these elements can be visited mutably along with a way of accessing all environments seen on the way down to that element.
It's easy if I only need access to the most-recently-seen environment; I discovered Rust is OK with me cleaving a struct like the above into two mutable references: one for the env
and a separate, disjoint one for the stmnts
. I can then enumerate the stmnts
and visit each one, passing a mutable reference to both it and the environment separately. If the function for walking a Stmnt
in turn walks an expression or something, it can continue to forward on down the most-recently-seen environment.
But I need access to all of the Environment
s seen on the way down, not just the most recent. What is the idiomatic way of handing down a growing list of disjoint mutable references like this?
My failures:
- I tried to collect mutable references to the
Environment
s in aVec
and pass that mutably on down, but that of course doesn't work because I have topop()
before returning from an inner call or else theVec
will have a mutable reference to a child. The compiler caught this and disallowed it. - I tried to create a simple struct
SymTable
that keeps anOptional<&mut SymTable>
along with a&mut Environment
, but I couldn't work out how to express the lifetimes for this (I lost several hours going down this path to no avail...). I figured I could make a new one for eachBlock
encountered on the stack and then populate it with both the passed-inSymTable
and the newly-encounteredEnvironment
.
My half-success: I went back to the Vec
approach, but with a Vec<&mut Environment>
that I create a new copy of each time one needs to be added. This compiles and works because the new Vec
can have the new element added and no pop()
is needed, and thus no risk of a reference from a child outliving the function call. But it seems very wasteful, algorithmically, because it requires I enumerate the list of references when copying them just to add one more. In my particular case, I'm not terribly worried about performance, but there must be a better way?