The starter code for this assignment is your Assignment 1.
It is recommended that you implement your interpreter using the Visitor interface you created in Assignment 1.
The main function should be modified to be like the one shown below.
int main(int argc, char** argv) {
void* scanner;
yylex_init(&scanner);
if (argc < 2) {
cout << "Expecting file name as argumeent" << endl;
return 1;
}
FILE* infile = fopen(argv[1], "r");
if (infile == NULL) {
cout << "Cannot open file " << argv[1] << endl;
return 1;
}
yyset_in(infile, scanner);
Statement* output;
int rvalue = yyparse(scanner, output);
if (rvalue == 1) {
cout << "Parsing failed" << endl;
return 1;
}
try {
Interpreter interp;
output->accept(interp);
}
catch (InterpreterException& exception) {
cout << exception.message() << endl;
return 1;
}
return 0;
}
Note that the version of main above reads from a file passed as a commandline argument (an earlier version of this
read from stdin, but if you do that, you will not be able to use the input function in your programs).
We will be testing your interpreter by actually executing some programs and checking their output against that of our reference implementation, so you want to make sure your interpreter does not produce any unnecessary output.
The way we are going to deal with this is that we will use instance variables to store both additional
inputs as well as return values.
For example, when visiting expressions, you want your visitor to return the value of the expression, but since the visit methods are void, the solution is to have an instance variable in the interpreter visitor:
Value* rval;
Then, when visiting expressions, you can enforce the property that every visit method for an expression sets the value of rval to the value of evaluating that expression.
So for example, when visiting a binary expression expr, you can have code like:
expr->left->accept(*this);
Value* leftVal = rval;
expr->right->accept(*this);
Value* rightVal = rval;
rval = computeBinaryOp(expr->op, leftVal, rightVal);
return;
In fact, I found it useful to define a method:
Value* eval(Expression* exp){
exp->accept(*this);
return rval;
}
Then, the code above can be rewritten as
Value* leftVal = eval(expr->left);
Value* rightVal = eval(expr->right);
rval = computeBinaryOp(expr->op, leftVal, rightVal);
return;
You can also use an instance variable to keep track of the stack. For example, you can do
std::stack < StackFrame*> frameStack;
In the case of the heap, there is no nead to define your own map, since there is already a map from addresses to values you can use: the heap of the language itself! Thus, for example, any point where you are allocating addresses in the semantics can be implemented by actually allocating memory in the implementation (e.g. by using new).