This lab contains both written and electronic parts, to be
submitted separately. Each exercise is marked according to whether it should
be part of the electronic or written submission. For each electronic part
(only!), add an entry to a features.txt file specifying
what you have working, and anything else the tester needs to know.
You can (and should) work with a partner for this lab, and if so, only one of you should submit each part. And of course include both names on everything you submit, electronically or on paper. Choose partners carefully! This lab is the first in a long series on SPL. You're not obligated to stick with the same partner for the duration, but it will probably be easier if you do.
Your electronic submission should come from a folder called
lab07 and include the following files. You
can download all the starter code at once with the file
lab07.tar.gz.
spl, which displays abstract syntax trees
and (after your modifications) will execute SPL programs.
From today's lab all the way until the end of the semester, we will be working on interpreting and compiling the SPL language that we saw introduced last week. Specifically, the outline is as follows:
javac compiler.
We will be using object-oriented programming in C++ to write the SPL interpreter. Luckily, most of the class structure is already provided by me, and you just have to fill in some of the methods. But here's a summary of how OOP works syntactically in C++. (You should already understand how it works semantically from Java!)
class keyword.
Superclasses are specified using a colon (like the extends
keyword in Java). For example
class Stmt :public AST { ... };
declares a class called Stmt that is a subclass of
AST. The public keyword is necessary and tells
us that the methods in AST can be accessed from
a Stmt object in the same way.
private,
protected, or public. We specify these by grouping
declarations with the same visibility and then writing visibility specifiers
before them, like public:.
virtual keyword, like
virtual void exec() {...}
This allows sub-classes to override that method in the way that we expect
from Java. (In Java, essentially every method is declared
virtual.)
abstract keyword as in Java. Instead, to declare a method
abstract (i.e., it must be overridden by subclasses), we write
= 0 instead of the method body, like
virtual AST* getPart(int i) = 0;
.cpp file. For instance, the following appears in
ast.hpp:
class Atom :public AST {
protected:
void writeLabel(ostream& out);
...
};
Then we specify the definition of this function in the file
ast.cpp Since this is outside the class declaration, we
must specify the name of the class in front of the method name
and use the scope resolution operator ::
as follows:
void Atom::writeLabel(ostream& out) {
/* (function body here) */
}
Stmt should
implement? What are they for a subclass of Exp?
Since we are going to be interpreting SPL code, we better know a thing or two about the language! Here is the grammar, changed ever so slightly from last week (don't expect significant changes from here on, though):
res: stmt | block: LC stmtlist RC stmtlist: stmtlist stmt | stmt: NEW ID ASN exp STOP | ID ASN exp STOP | READ ID STOP | WRITE exp STOP | IF LP exp RP block | IF LP exp RP block ELSE block | WHILE LP exp RP block | block exp: exp BOP exp | NOT exp | exp COMP exp | exp OPA exp | exp OPM exp | OPA exp | LAMBDA ID block | exp LP exp RP | LP exp RP | ID | NUM | BOOL
For the most part, SPL follows a similar syntax to C/C++/Java, albeit with fewer features. However, there are a few oddities.
First, notice that there are no types specified. Variables are declared and given an initial value with a statement like
new x := 10;and afterwards, they can be reassigned by using the
:=
operator without the new keyword.
Function declarations also look a bit funny. They are written using the
lambda keyword, like
new addone := lambda n {ret := n + 1;}
Then this function could be called with code like
addone(5). We'll get into what this means (i.e., the
semantics of SPL functions) in next week's lab.
0
is entered, at which time the sum of all the integers read in is printed
to the screen.spl program in the starter
code. Note: you should enclose your program in a single block with curly
braces so that it prints as a single AST.
Now let's get down to some interpreter writing! Look at the code and
see how an AST is evaluated. Stmt node types have an
exec method that does whatever that node is supposed to do.
Exp node types have an eval method that computes
and returns whatever value is represented by that expression tree. These
methods are currently only implemented in the Num and
Write classes, so we can just run simple one-line programs
like
write 5;
For this part, your task is to extend this to include basic arithmetic
with integer constants. This requires implementing the eval
methods in ArithOp and NegOp. For each of these,
you will have to declare the new method in the class with a line like
Value eval();You can then either define the method right there in the
ast.hpp
file, or externally in the ast.cpp file. Remember, external
definitions require you to specify the class name, like
Value ArithOp::eval() { /* code here */ }
Note that the eval method in ArithOp will have
to have a switch statement depending on which operator is
being used. Look in the class declaration for how the operator is stored,
and see the top of ast.hpp for the enum type Oper
that you will need to use.
Remember that an operation like + must evaluate its arguments
first before using them!
This is accomplished by calling the eval method recursively.
For instance, the first argument to an ArithOp is evaluated
by calling args[0]->eval().
Look at the definition of the Value
class in value.hpp.
This is the type returned by the recursive calls to eval().
You will be interested in using the num() method, which returns
the integer value that is stored in the object.
Also note that we have constructors from the basic
types, so writing something like return 8; will automatically
covert the int 8 to a Value object.
Sweet!
Tip: once you get your interpreter going, it will start to get really annoying having the AST pop up on the screen every time. So just comment out the line
system("evince spl.pdf > /dev/null 2>&1 &");
in spl.ypp. This will ensure that the AST still gets generated
and saved in spl.pdf, but you don't have to look at it unless
you need to.
+, -, *, and / working.
A program like
write 5 * 3 / (-2 + 7);should work and produce
3.
Now incorporate comparisons and boolean arithmetic into the picture.
Start with the Atom type BoolExp, then move on to
CompOp operators, and finally BoolOp and
NotOp.
true and
false as well as the operators <,
>=, !=, etc., and also and,
or, and not.
A program like
write true and 3 > 5 or 1 + 2 != 8;should work and produce
true.
Look at the SymbolTable class in the files
st.hpp and
st.cpp files.
These define a global symbol table object ST that will
be used to implement a single, global scope. (Don't worry! We'll implement
proper scoping in the coming labs.)
Use the methods in SymbolTable
to implement variable declaration, assignment, and reference.
This means implementing eval() in the
Id class, and exec() in the
Asn and NewStmt classes.
new and :=, working in the spl
interpreter program. A program like
new x := 5; x := x + 10; write x;should work and produce
15.
You may have noticed that blocks with curly braces produce valid ASTs,
but still make the interpreter give up. To fix this, implement the
exec() method in the Block class.
(Hint: this should be a one-liner!)
Once blocks are working, start getting the conditional control structures
working by implementing exec() methods in IfStmt,
IfElse, and WhileStmt.
{
new a := 2;
new b := 3;
while (a >= 1) {
if (a = 1) { b := b + 1; }
a := a - 1;
}
if (b < 4) {
write -100;
}
else {
b := 50;
write b;
}
}
should work and print 50 to the screen.
read rightOkay, now just about everything is working except for functions
(which we'll deal with next class) and read. Your first task
is to implement the exec method in the Read class,
which should just read in an integer and store it in the variable that
is named.
But this creates some awkward problems. Your spl interpreter
is now reading the SPL program from standard in, as well as
the input to that program. Sometimes, we might want the program
to come from a file instead so that program and input can be separate. To
facilitate this, change the beginning of the main function
to look like:
extern FILE* yyin;
int main(int argc, char **argv)
{
if (argc > 1)
if (!(yyin = fopen(argv[1],"r"))) {
cerr << "Could not open input file \"" << argv[1] << "\"!" << endl;
exit(1);
}
...
}
This reassigns the yyin stream used by the flex scanner to read
from the named file instead of standard in. With this, running your program
like ./spl with no arguments will still work the same as before,
but running it like
./spl prog1.splwill instead read the program from the file
prog1.spl.
read command working. Typing
new x := 4; read x;into your interpreter should cause it to wait until an integer is typed in. Then writing
write x;should echo that typed-in integer back to the screen.
main as specified above
so that the SPL program can be stored in a separate file, specified on
the command line.
./spl interpreter on your SPL program, stored in a separate
file, and reading in integers until 0 and then printing their sum.
If you've gotten this far, great job! Let's add two more nice features that will make programming more pleasant: short-circuit evaluation, and debugging statements.
Short-circuit evaluation means cutting off a boolean expression
with ands and ors as soon as we know the answer.
For instance, since
false and XXXX must be false no matter what XXXX is,
we can just return false from this without actually evaluating
the second argument.
Debugging statements are little snippets of text that print to the
screen when a certain part of your code is reached. Since SPL doesn't
have any support for strings, we have to add a language feature to support
this. Our approach will be that anything enclosed in double-quotes is
a debugging statement. This will require a new AST Stmt node
class
called Debug, which you will have to create.
false and 1/0(would have given division-by-zero error before), and
true or what(would have given undefined variable error before).
Debug class in ast.hpp, you will also need a new
token type, a new scanner rule, and a new parser rule. But it'll be worth it,
I swear!