On this page:
Overview
Fraud+
From Dupe+  +   to Fraud+
Generalizing Let
Back-Referencing Let
Testing
Submitting
8.18

Assignment 5: Let There Be (Many) Variables🔗

Due: Thursday, October 23, 11:59PM

The goal of this assignment is to extend a compiler with binding forms that can take any number of arguments.

Overview🔗

For this assignment, you are given a fraud-plus.zip file on ELMS with a starter compiler similar to the Fraud language we studied in class.

Fraud+🔗

The Fraud+ language extends the Fraud language we studied in class with some new features:

From Dupe++ to Fraud+🔗

Implement the abs, unary -, and not operations and the cond and case forms from Assignment 4 by modifying interp.rkt, interp-prim.rkt, compile.rkt, and compile-ops.rkt. You can start from your previous code, but you will need to update it to work for the code you are given. What’s essentially left for you to do is to make sure to correctly signal an error ('err) when these constructs are applied to the wrong type of argument.

While you’re at it, implement the predicates integer? and boolean? for checking the type of an argument, modeled by the char? predicate that was covered in the lectures.

Generalizing Let🔗

The Fraud language has a let form that binds a single variable in the scope of some expression. This is a restriction of the more general form of let that binds any number of expressions. So, for example,

(let ((x 1) (y 2) (z 3))
  e)

simultaneously binds x, y, and z in the scope of e.

The syntax of a let expression allows any number of binders to occur, so (let () e) is valid syntax and is equivalent to e.

The binding of each variable is only in-scope within the body, not in the right-hand sides of any of the let. So, for example,

(let ((x 1) (y x)) 0)

is a syntax error because the occurrence of x is not bound.

The given code uses the following abstract representation of let expressions:

; type Expr = ...
; | (Let [Listof Id] [Listof Expr] Expr)
(struct Let (xs es e) #:prefab)

Notice that all of the variable bindings and their RHS expressions have been split into two (equal-length) lists. The third component is the body expression.

The provided parser has been revised to parse these let expressions (as well as the new operation forms):

Examples

> (parse '(let ((x 1)) x))

make[1]: Entering directory '/home/runner/work/cmsc430.github.io/cmsc430.github.io/assignments/fraud-plus'

make[1]: Leaving directory '/home/runner/work/cmsc430.github.io/cmsc430.github.io/assignments/fraud-plus'

'#s(Let (x) (#s(Lit 1)) #s(Var x))

> (parse '(let () 1))

'#s(Let () () #s(Lit 1))

> (parse '(let ((x 1) (y 2)) (+ x y)))

'#s(Let

    (x y)

    (#s(Lit 1) #s(Lit 2))

    #s(Prim2 + #s(Var x) #s(Var y)))

Recall that there are two parsers: parse parses any expression form, while parse-closed only parses closed expressions. The interpreter and compiler may assume that the program is closed (i.e. it is parsed with parse-closed).

Examples

> (parse 'x)

'#s(Var x)

> (parse-closed 'x)

unbound identifiers '(x)

> (parse-closed '(let ((x 1) (y x)) x))

unbound identifiers '(x)

The provided interpreter and compiler work when the let expression happens to bind a single variable, but you must revise the code to work for any number of bindings.

Examples

> (interp (parse '(let ((x 1)) (add1 x))))

2

> (interp (parse '(let ((x 1) (y 2)) (+ x y))))

match: no matching clause for '#s(Let (x y) (#s(Lit 1)

#s(Lit 2)) #s(Prim2 + #s(Var x) #s(Var y)))

> (exec (parse '(let ((x 1)) (add1 x))))

2

> (exec (parse '(let ((x 1) (y 2)) (+ x y))))

match: no matching clause for '#s(Let (x y) (#s(Lit 1)

#s(Lit 2)) #s(Prim2 + #s(Var x) #s(Var y)))

Back-Referencing Let🔗

Similar to let, there is also let* that can also bind any number of expressions. The difference is that previous bindings are available in the right-hand sides of subsequent bindings. For example,

(let* ((x 1) (y 2) (z (add1 y)))
  e)

binds x to 1, y to 2, and z to 3 in the scope of e.

The syntax of a let* expression allows any number of binders to occur, so (let* () e) is valid syntax and is equivalent to e.

Unlike let,

(let* ((x 1) (y x)) 0)

is not a syntax error. However, bindings are only available forward, so

(let* ((x y) (y 1)) 0)

is a syntax error.

The given code uses the following abstract representation of let* expressions:

; type Expr = ...
; | (Let* [Listof Id] [Listof Expr] Expr)
(struct Let* (xs es e) #:prefab)

The provided parser works for let* expressions:

Examples

> (parse '(let* ((x 1)) x))

'#s(Let* (x) (#s(Lit 1)) #s(Var x))

> (parse '(let* () 1))

'#s(Let* () () #s(Lit 1))

> (parse '(let* ((x 1) (y 2)) (+ x y)))

'#s(Let*

    (x y)

    (#s(Lit 1) #s(Lit 2))

    #s(Prim2 + #s(Var x) #s(Var y)))

And the parse-closed parser works appropriately, too:

Examples

> (parse-closed '(let* ((x 1) (y 2) (z (add1 y))) z))

'#s(Let*

    (x y z)

    (#s(Lit 1) #s(Lit 2) #s(Prim1 add1 #s(Var y)))

    #s(Var z))

The provided interpreter and compiler work when the let* expression happens to bind a single variable, but you must revise the code to work for any number of bindings:

Examples

> (interp (parse '(let* ((x 1)) (add1 x))))

2

> (interp (parse '(let* ((x 1) (y 2)) (+ x y))))

match: no matching clause for '#s(Let* (x y) (#s(Lit 1)

#s(Lit 2)) #s(Prim2 + #s(Var x) #s(Var y)))

> (exec (parse '(let* ((x 1)) (add1 x))))

2

> (exec (parse '(let* ((x 1) (y 2)) (+ x y))))

match: no matching clause for '#s(Let* (x y) (#s(Lit 1)

#s(Lit 2)) #s(Prim2 + #s(Var x) #s(Var y)))

Note that when there is only a single binding, let and let* are equivalent.

Testing🔗

A small number of test cases have been provided in test/test-runner.rkt. There is function called test that contains I/O-free test cases and another called test/io that contains I/O tests. To run these tests, raco test test/interp.rkt will test the interpreter and raco test test/compile.rkt will test the compiler. You are encouraged to add your own tests.

Submitting🔗

To submit, use make from within the fraud-plus directory to create a zip file containing your work and submit it to Gradescope.