Incorporating the StackWhat we would like is to be able to replace the data vector we currently have in the Execution class with our new stack:
Doing so will break some other parts of our implementation, unfortunately. The variable manipulation opcode handlers may need rewriting (if they are still desired), the execution's accessors have to be updated, and the function for exposing variable states will have to be changed entirely. Another function that will have to change is SetDataSize(). SetDataSize() and state exposure will need to wait until we finish implementing our custom stack. The accessors are a simple change:
Before we can advance, we will have to begin implementing the scoping part of our stack. First will be a function to initialize what is essentially the outer-most scope. It's a simple resize:
Execution's SetDataSize() can now call this method:
Now we would like to be able to create a group of variables on the stack that can be removed in the same group as well. This functionality will serve us well once we begin implementing a higher-level language with "scope" considerations. To do this, we will have to keep track of the starting positions of these scopes. I refer to these as indices in the implementation. Because we will need to store an additional index for every scope we push onto the stack, we will need to have some mechanism for storing these dynamically. The simple approach I will be taking will include an additional vector to store these indices. The index for the current scope will also be stored, and referred to as the stack base. Implementations for accessing variables from the current scope will then use the current index to find the right place on the actual data vector. It will look something like this:
You might consider storing actual pointers to these stack positions instead of an actual offset into the vector, thinking you could be more efficient with such an approach. However there is the messiness involved in reallocation of the _stack in the event that its capacity is exceeded. The pointers would be invalidated. This is why I chose to store offsets as indices instead of pointers/iterators. These accessors should now be updated accordingly. Note the change in index calculation:
Some new properties could be accessible as well:
New accessors for getting an element indexed from a particular depth should be included. These do the same as the normal Get(), except they don't use the current stack depth as their starting point:
As I said earlier, the function to expose variable state now needs to be rewritten. This function will go through every element on the stack, while keeping track of the current stack depth, and output the value under its appropriate scope heading. It will be a member of the Execution class, and will take an ostream reference as a parameter to allow for flexible output options:
The virtual machine can now relay a call to this new function to show us the variable state through the console:
And with that, the formerly written loading code should now work again with the new implementations. A New Set of InstructionsTo keep things tidy, I will be scrapping the old instructions from the provided source. Many of them would have found themselves redundant anyway. Now, here are some new instructions that will make use of the stack, each with a brief description of how they should work.
The first three of these instructions will require an additional value. For the first instruction, the data value determines the value pushed onto the stack. For the next two, the data determines the index of the variable referenced either for having its value pushed onto the stack in the former case, or being assigned a value as in the latter. A conceptual example of flat representation of an expression: Expression: x = x + 1 Flat Representation (our instructions): push_var x push_const 1 add assign x Note that 'x' is used as a placeholder for a particular variable index, which would correspond to the conceptual 'x' written in the higher-level expression. During a compilation process, the variable 'x' would be assigned a memory index in the stack, and all references to this 'x' would be resolved to that index. Certainly this flat representation could be more optimal if we made use of an instruction that specifically incremented a particular variable, but the more general instruction type for addition still gets the job done. Here are the implementations for some of these instructions:
The rest can be found in the downloadable source.
|