For this assignment, you are to extend the SILLY interpreter from HW2 to allow for subroutines and local/global variables. The expanded EBNF for the SILLY language is as follows:
The SubDecl
statement declares a simple subroutine, which may have an arbitrary number of
parameters. There is an optional local variable declaration that can appear at the beginning of
a subroutine, followed by the sequence of statements that make up the subroutine. The SubCall
statement calls a specific subroutine, with a series of expressions specifying the input values
that correspond to subroutine parameters.
When a subroutine is called, the expressions in the call are evaluated and assigned to the subroutine parameters in a new local scope. If any local variables are declared, then these must be created in the local scope as well (local variables are initialized to 0 by default). When executing the statements inside the subroutine, static scoping is used. That is, when looking up or assigning to a variable, the local scope is checked first. If that variable does not appear in the local scope (i.e., it is not a parameter or local variable), then the global scope is used.
For example:
SAMPLE CODE (output in red) >>> sub foo ( x y ) local ( z ) z = ( x + y ) output z output a end >>> a = 0 >>> call foo ( 4 9 ) 13 0 >>> z = 10 >>> a = 20 >>> call foo ( ( z + a ) 5 ) 35 20 >>> output z 10 >>> quit BYE >>> sub countdown ( num ) output num if num num = ( num - 1 ) call countdown ( num ) end end >>> call countdown ( 5 ) 5 4 3 2 1 0 >>> quit BYE
To model the more complex memory structures required in this extension, the MemoryStack
class has been rewritten. Instead of storing a single stack of variable bindings, it now maintains a runtime stack of activation records (each of which contains a stack of variable bindings). The MemoryStack
class also provides methods for creating a new scope (i.e., pushing a new activation record on the runtime stack) and ending the current scope (i.e., popping the current activation record off the runtime stack). Note that all of the previously existing methods still behave as before, so this new MemoryStack
class can be inserted into your project seamlessly.
You must implement the SubDecl
and SubCall
classes to produce a full-featured interpreter. The SubDecl
class models a subroutine declaration. When executed, a copy of the subroutine code is stored in memory so that it can be accessed and invoked by a subroutine call. The SubCall
class models a subroutine call. When executed, it locates the code for that subroutine, calls the createScope
method of MemoryStack
to push an activation record on the runtime stack, executes the statements in the subroutine, and then calls endScope
to pop the activation record. If the subroutine has parameters or local variables, these must be created and initialized at the beginning of the call.
It is strongly suggested that you complete this assignment in stages. It is much better to have code that works on a limited range of options as opposed to code that tries to do everything but succeeds at nothing. I would recommend the following breakdown:
SubDecl
class.
You will need to be able to recognize the optional local declarations and store each identifier token
when you construct the SubDecl
object. When the subroutine is later invoked (by the execute
method of SubCall
), then each of those local variables must be created in the current scope (by calling Interpreter.STACK.declareLocal
).
SubDecl
constructor, you will need to store each parameter
token. When executing a subroutine call, you will need to not only declare each parameter but also assign
each a value.