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

Assignment 4: Let There Be (Many) Variables🔗

Due: Monday, June 15, 11:59PM

Starter code: fraud-plus.zip

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

Overview🔗

For this assignment, use the starter code in fraud-plus.zip, which is 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 Extort+ to Fraud+🔗

Implement the abs, unary -, and not operations and the cond and case forms from Assignment 3 by modifying interpreter/interp.rkt, interpreter/interp-prim.rkt, compiler/compile.rkt, and compiler/compile-ops.rkt. You can start from your previous code.

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

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))

'#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/define-tests.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/run-interp-tests.rkt will test the interpreter and raco test test/run-compile-tests.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.