Lab 10: Type Checking

This is the archived website of SI 413 from the Fall 2012 semester. Feel free to browse around; you may also find more recent offerings at my teaching page.

This lab is due at 0900 next Tuesday, October 30. It should contain all the files in the starter code listed below, as well as a subfolder called tests with your tests. See the submit page for more info.

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). But in either case you should download the updated Makefile, as well as builtin.hpp to keep things running smoothly.

Introduction

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:

spl> new f := lambda n { write 5; }; # Look ma, no return value!

spl> write f@12 * 17;
5
0

spl> write f - 20;
164221140

spl> write f and 16;
false

spl> new x := true;

spl> write x < false;
true

spl> write (x < false) + 20;
21

spl> 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.

Dynamic Type Checking

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.

If you want to do error checking in a consistent way with the rest of SPL, you will have to add the following declarations after the other #include's near the top of the value.hpp file:

#include "colorout.hpp"
extern bool error;
extern colorout errout;

Exercises

  1. Add basic type checking to the "getter" methods 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 UNSET
    
  2. Make sure that type checking works throughout your SPL interpreter. This might require more or less work from you, depending on how far your code has diverged from my solutions. Basically, every one of the nasty things that you see in the examples from the Introduction above should give nice error messages, and never seg faults.

Built-In Functions

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, in the builtin.hpp file in today's starter code. Take a look at it. 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. This is set to "x" by Sqr's constructor. 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.func = sqlam;
sqc.env = NULL;
global.bind("sqr", sqc); // Note: global is the name of the global frame.

Exercises

  1. Add the lines to your main so that the "sqr" built-in function works in your SPL interpreter just like any other function. Important: you will need to do a #include "builtin.hpp" in your spl.ypp file.
  2. Built-in functions are useful when they do something which otherwise would not be possible in the language. Add a built-in function called pause that takes an integer argument and makes the execution pause for that number of seconds. So for example, doing
    pause@3;
    Should just make the interpreter "hang" for 3 seconds and then display the prompt again.
    In C++, you can make this happen by using the sleep command from the unistd.h header file; see this page out of the GNU libc manual for the full details.