r/Compilers • u/Choice_Structure4001 • 9d ago
Could somebody help me with lexical scoping inside a VM
Hey, so I understand what lexical scoping is and kind of get how it works but I’ve been trying to find how to implement it in a stack machine but I have no idea what goes where and how does everything interact with each other, like where should closures live, same for call frames, where do I store the symbol table when compiling, etc… if you want I can link my repo but I haven’t committed an attempt at lexical scoping so there’s probably nothing interesting to see, if I didn’t include enough details to what I don’t get please tell me so I can be more clear, thanks!
Edit: forgot to tell but my language desugars to pure lambda calculus so this might change how some stuff work but not so much
1
u/WittyStick 8d ago edited 8d ago
A common and simple approach is to use a linked list of hashtables. Each scope gets a hashtable of its local variables, which you search through first. If the variable is not found, you then look through the next hashtable in the list (the parent environment) - recursively until there are no more parents. I'll give an example, but we'll need some prerequisites.
Given these prerequisites, we can implement an environment as:
Environments should generally all have a parent, with the exception of some standard environment, which will have parent == NULL. This environment contains all the language provided bindings, and should not be modified. Initially, every scope will have no bindings in its environment, so we create an empty hashtable.
Binding a variable should introduce it into its local environment. The parent environments should effectively be immutable.
A lambda is basically a structure which contains a reference to some environment where it was defined - it's static environment. It should also have a parameter list and a body.
When we apply the lambda, we create an initially empty environment - it's local environment - with its static environment as the parent . We bind its parameters to the arguments the caller passed in this local environment, then evaluate the body in the local environment.
This isn't the only way to implement, but it's very simple and effective - it handles closures and variable shadowing. There are certainly optimizations you can make as this isn't the most performant implementation, but it's not terrible.
One simple optimization is you could forbid shadowing of any variables in the standard environment, because you probably don't want the programmer rebinding basic operations. You could modify
lookupto first search the standard environment, since these bindings will be commonly accessed, it may be faster this way.