This lab contains only electronic parts, to be
submitted using the submit program.
For every exercise you work on,
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.
Your electronic submission should come from a folder called
lab10
. The following files must be included:
features.txt
: Specification for testing.Makefile
: If I type make spl
, this program
should be compiled from source.
Most of your work for this lab will be in value.hpp
value.cpp
, builtin.hpp
, builtin.cpp
,
and spl.ypp
.
The starter code for this week's lab is... the solution to last week's lab. You can stick with your own solution or use mine (or some combination thereof).
We saw in class last week that our SPL interpreter is most definitely not type-safe. This means we can do all kinds of nasty and meaningless things and the compiler won't even notice:
new f := lambda n { write 5; }; # Look ma, no return value! write f(12) * 17; 5 0 write f - 20; 164221140 write f and 16; false new x := true; write x < false; true write (x < false) + 20; 21 write x(5); Segmentation fault
In today's lab we will implement type safety in our SPL interpreter. This will explicitly dis-allow all the terrible things in the example above, with nice informative error messages for the poor SPL programmer.
Actually, most of the machinery for the dynamic type-checking that we're going to do is already in the interpreter. Do you know where?
Then we'll look at the concept of built-in functions and make a few useful ones.
Recall that dynamic type checking requires that type information is stored alongside values in the running program. Every time we execute a statement or evaluate an expression, the types of all values are checked for compatibility, depending on what the statement is.
If you look at the Value
class, you will see that we have
been storing this information all along! Each Value object has a field
type
of type VType
, which is also defined in
the value.hpp
file.
To make your life easier, I wrote a function to print out
VType
s in a nice way. To use it, do the following:
value.hpp
file:
ostream& operator<< (ostream& out, VType vt);
Makefile
to compile this correctly.
You need to add "value.cpp" to the IMPLS variable. (Might as well remove
"value.hpp" from the HEADERS variable as well.)num()
, tf()
, and func()
in the Value
class. So for instance, before returning
the integer value, num()
should confirm that the type of
the object is actually NUM_T
. If not, it should display
a nice error message, like
Type mismatch: expected NUM, got BOOL
A built-in function in a programming language is one that looks like a regular function, but is not written in the language itself but rather hard-coded into the compiler or interpreter.
So how will this work? How can we trigger the execution of arbitrary C++
code within an SPL program? Let's look at an example step by step. We'll write
a built-in function sqr
to compute the square of a number.
Of course this is a really stupid example because we could just
write the function in SPL like this:
new sqr := lambda n { ret := n * n; };
Hopefully the stupidity of this example will make the concepts more clear
for when you have to do more interesting functions.
Now the first thing we will
need is a new kind of Stmt
node whose exec
method
will do whatever it is our built-in function is supposed to do. This new node
type will correspond to the abstract class Builtin
.
I've actually
written this for you already: download the files
builtin.hpp and
builtin.cpp,
copy them to your lab10 directory, and have a look. You will see the
Builtin
abstract class, as well as the Sqr
subclass containing the functionality for squaring numbers.
Important to notice is the getParam()
function in
the Builtin
class. This gets the name of the argument to this
built-in function (which is just "x" by default). The actual code for
squaring is in the exec
method in Sqr
, of course.
Now how do we get this into our interpreter so we can call sqr
from a SPL program and have it execute this node? The first thing to do is
to make a Lambda
whose body contains this new kind of statement.
Specifically, we will make a little AST that looks like this:
The odd thing is that, rather than the scanner and parser generating this
AST, we will create it manually, perhaps in the main
function
in spl.ypp
. The following lines will create the little AST
above:
Id* param = new Id("x"); Sqr* sqr = new Sqr; Block* sqbody = new Block(sqr); Lambda* sqlam = new Lambda(param, sqbody);
So now we have a Lambda
node for our built-in function.
All that remains is to give our baby a name and put it into the global
Frame so it can be accessed anywhere. The following code will do that:
Closure sqc;
sqc.function = sqlam;
sqc.environment = NULL;
global.bind("sqr", sqc); // Note: global is the name of the global frame.
main
so that the "sqr" built-in function works in your SPL interpreter just like
any other function. Important: you probably will want to add
"builtin.cpp" to the "IMPLS" variable in the Makefile.
main
is pretty ugly, and it's going to
get way worse when we start adding more built-in functions. So write a
C++ function getBuiltinClosure
that takes any
Builtin*
pointer and returns a Closure for that function.
This prototype for this function should go in builtin.hpp
,
and its implementation in builtin.cpp
.
Then you should be able to replace the code you added to main
with one or two lines, using your new function.
(Note: you don't need a line in features.txt for this exercise.)
So from the first part, we have type safety implemented in the interpreter. But there is still no way for the SPL programmer to do their own type checks, because the type information is hidden from the programmer.
We'll change this by writing three built-in functions
isnum
, isbool
, and isfunc
, that
take any value and return true or false to indicate if that value has
the stated type. After this, we should be able to write crazy functions like
# Arbirary doubler. Why? Because we can!
new doubler := lambda any {
if (isbool(any)) { ret := true; }
else {
if (isnum(any)) { ret := any + any; }
else { ret := lambda x { any(any(x)); }; }
}
};
isnum
, isbool
, and isfunc
to your interpreter.
cons
, car
, and cdr
.
You can see my solution to this in the file
ex4.spl, where I also define a few more
goodies like nil
for the empty list.
Now that we have user-driven type checking and
built-in functions, much more is possible here!
Try writing a curried function called list
that builds up a list, terminated with nil
, like
list(1)(2)(3)(4)(nil);You should be able to write this function in SPL. Next, try adding a built-in called
display
that
prints out a list all nice, on one line, like
[1 2 3 4]