This is called a persistent array.
If you don't need the Scope
object accessible after finishing processing a scope, you could just use one symbol table, modify the symbol table directly when entering a scope, but record the modifications so that you could undo the modifications in reversed order when exiting a scope. You could use a stack to store information about the current scope and its ancestors. And you only have information about these scopes. All other Scope
objects are destroyed.
Each variable name could also be considered an independent stack. Without the following improvements, you may need a unified stack to record which variable names are modified, but the exact modifications are independent from each other.
Then if you want the Scope
object available after exiting the scope, for each variable name, you could sort the modifications (shadow and unshadow) of the variable in the above process by time, and maintain them in a search tree. Each Scope
is bound to a time (version id). The current state of a variable name is the new value in the latest modification of that variable name before the version id. When you want to create a subscope, create two version ids for entering and exiting the scope just after entering its parent scope, and create the appropriate modifications on the entering version, and undo modifications on the exiting version. The version ids could be created and compared in another shared tree, instead of just using integers, if you may create subscopes in non-sequential order. A parent scope itself should not be modified after a subscope is created, so the the new value on undo could just be a copy of the previous value and don't need to be updated.
The structure is like a simplified Euler tour tree. It is possible to also support modifying a parent scope while there are already subscopes, if used in combination with a full Euler tour tree, to replace undoing.
This is O(logn) time and O(1) space per operation, where n is the number of scopes, assuming variable names are prehashed and don't cost extra time for looking up the tree bound to that variable name.
Some improvements using vEB trees and constant time data structure for maintaining version ids could make it O(loglogn) per operation, though I doubt anyone would really want to implement it.
You could also just use a persistent search tree to store the symbol table, but with O(logn) space overhead per operation.