CSC 530 Assignment 2 --
Adding Type Checking to the
Basic Lisp Interpreter of Assignment 1
To the Lisp interpreter of Assignment 1, add interpretation of the following new functions and data:
(deftype name type)where type is(array type [bounds])
- one of the atomic type names sym, int, real, string, or bool
- a composite type specified with one or more of the four type forms below
- the name of a defined type, including a recursive reference to such a name
- a type variable of the form ?X, for any atom X
where the optional bounds is either an integer, or (integer integer) pair, or a type var(record fields)where fields is a list of (name type) pairs in which all names must be unique; fields may be a single type var(union fields)where fields is a list of (name type) pairs in which all names and types must be unique; fields may be a single type var(function args [outs] [suchthat])where args and outs are lists of names or (name type) pairs; names and (name type) pairs can be mixed in a signature; the name in a (name type) pair may be empty; args and/or outs may each be a single type var; suchthat is of the form (suchthat predicate), and the predicate can reference type vars
To provide computation on values of the above types, add interpretation of the following functions on arrays, records, unions, and general types:
where field-type-of is defined as(index ((a (array ?x ?y)) (i int)) ((?y))) (setelem ((a (array ?x ?y)) (i int) (val ?z)) ((array ?x ?y))) (dot ((r (record ?x)) (f sym)) ((?y)) (suchthat (field-type-of ?y (record ?x)))) (setfield ((r (record ?x)) (f sym) (val ?y)) ((record ?x))) (isa ((u (union ?x)) (f sym)) (bool)) (dot ((u (union ?x)) (f sym)) ((?y))) (typeof ((x ?x)) ((t ?t))) (equiv ((t1 ?t1) (t2 ?t2)) (bool))
(defun field-type-of ((field-type ?ft) (record-type ?rt)) ((bool)) (cond ((null record-type) nil) ((equal field-type (cadar record-type)) t) (t field-type-of (cons 'record (cddr record-type)))))
To support definitions of typed functions and variables, update the definition of xdefun, and add a definition for xdefvar of the following forms:
(xdefun name args outs [suchthat] body)
where args, outs, and suchthat are as defined above for the function type
(xdefvar name type [value])
where type is as defined above and the optional value is the initial value assigned to the variable
Literal values for each of the types specified above are denoted as follows:
Type | Literal Denotation |
sym | any quoted atom |
int | any atom for which integerp is true |
real | any atom for which numberp is true and integerp is false |
string | any atom for which stringp is true |
bool | t or nil |
array | a list, the elements of which meet the array's bounds and type specs |
record | a list, the elements of which meet the record's field specs |
union | a value whose type is one of field types |
function | the name of a defun'd function or a lambda expression whose signature matches the function type's args and outs specs |
The functions xdeftype, xdefvar, and xdefun are the three major forms of declarations found in most modern programming languages. They define, respectively, types, variables, and functions. The result of evaluating each of these functions is to create new bindings (on the alist) for the identifier being declared.
The four forms 2 array, record, union, and function are interpreted not as functions but as raw data data. Hence, these forms are only valid in declaration contexts where a type is valid. These construction forms provide the four most commonly available types of data construction found in modern programming languages.
The eight new type-specific functions (starting with index above)
provide functionality to access and manipulate typed data. Here are the specs
for these functions:
Function | Specs |
index | Return the element at position i in the given array a, starting from position 0. Return nil if i is out of bounds. |
setelem | Return an updated version of the given array a, with the value at position i changed to the given value val. Return a unchanged if i is out of bounds. |
dot | Return the value of the field named f in the given record r. Return nil of there is no field of that name in r. |
setfield | Return an updated version of the given record r, with the value of the field named f changed to the given value val. Return r unchanged if there is no field of the given name. |
isa | Return t if a field of the given name f is in the given union u. Return nil otherwise. |
dot | Return the value of the field named f in the given union u. Return nil of there is no field of that name in u or the if the current value of u is not of the declared type of the field named f. |
typeof | Return the type t the given expression x. The type should be returned as a list datum, of the same form used to declare a type using deftype. |
equiv | Return t if the given types t1 and t2 are equivalent (per the rules defined below) nil otherwise. |
Note that the two manipulation functions, setelem and setfield, are non-destructive.
The existing typeless version of xeval from Assignment 1 should be retained, and enhanced to support the above new functions. This version of the interpreter will remain typeless, in that it will execute all the new functions defined above, but it will not perform any type checking.
To perform type checking, define two new interpretation functions named xcheck and xcheck+xeval. The xcheck function performs static type checking of an xLisp program by ensuring that all bindings to typed identifiers are type correct. In our simplified xLisp interpreter, bindings that must be type checked occur in two contexts:
The type checking rule used by xcheck should be structural equivalence. Specifically, two types are equivalent if
The function xcheck+xeval should perform dynamic type checking. That is, it enforces the same type checking rules as does xcheck, except that xcheck+xeval enforces the rules during evaluation. Specifically,
In most programming languages, the type checker reports errors by referring to particular lines of a program on which the errors occur. In general, Lisp is not a line-oriented language, since programs are frequently typed in directly to the interpreter, or read from a large number of small files. Hence, an error reporting convention based on function name and expression position makes more sense for Lisp.
Therefore, type checking errors issued by xcheck should specify the name of the function and the ordinal expression position within that function where the error is detected. For example, given the program
the following error message should be printed(xdefun f ((x int) (y string)) (xsetq x 10) (xsetq y 20) )
Note that within a function, the expression position will only go one level level deep. For example, if an error is detected within an xcond, the expression position will only be reported as the position of the xcond within the function, not at a subexpression position within the xcond. While this form of reporting is not production quality, it will do just fine for us.Error in function f, expression 2, calling function xsetq: type conflict in argument 2 expected type: string given type: int
If errors are detected in top-level expressions that are not within a function, the error reporting should give the message
where E is a pretty print of the entire offending expression.Error in top-level expression E ...
Further examples of error message reporting are given in the sample files in the test-files subdirectory for Assignment 2. This is the directory
~gfisher/classes/530/assignments/2/test-files