Interpreter

We don't intend for these assignments to take too long. If you have spent five hours on any part of this assignment please stop and check in with the TAs.


1. Introduction

You will write an interpreter for the Paret language, as described below.

We have provided a function parse, which consumes an expression in the language's concrete syntax and returns the abstract syntax representation of that expression. parse accepts expressions in the grammar of the language described below.

To complete this assignment, you must implement the desugar and interp functions. desugar consumes an abstract syntax tree (i.e., an Expr, as returned by parse), replaces all instances of sugar-and, sugar-or, and sugar-let with desugared equivalents, and returns the result. interp consumes the desugared abstract syntax tree and returns a Paret Value.

fun desugar(expr :: C.Expr) -> C.Expr%(is-desugared)

fun interp(expr :: C.Expr%(is-desugared)) -> C.Value

2. Errors

We have pre-defined all the error cases that you should run into for this assignment.

data InterpError:
    | err-if-got-non-boolean(val :: Value)
    | err-bad-arg-to-op(op :: Operator, val :: Value)
    | err-unbound-id(name :: String)
    | err-not-a-function(val :: Value)
end

You can throw an error by using raise and providing the correct InterpError.

raise(C.err-bad-arg-to-op(C.op-plus, C.v-str("str")))

3. Desugaring

3.1 And/Or

Your desugar function should convert and and or into equivalent expressions using e-if, Your interp function can assume that it's given an expression with no ands or ors in it; it does not have to check. Your interpreter should short-circuit when possible. For example, if the first argument to an and evaluates to false then you should evalute the then branch without evaluating the second arugment.

3.2 Let

let should accept a single id value pair and a body. The let expression should evaluate the value, bind it to id, and then evaluate the body. For example:

(let (x 1) (+ x 2))

should evauate to 3

Your implementation should disallow recursive definitions. That is, in (let (<id> <expr>) <body>), <id> should be bound in <body> but not in <expr>. The desugaring of let may not be obvious, but let us give you a hint: it involves e-lam and e-app.

4. Features to Implement

4.1 Environment

You will use an environment for your interpreter to keep track of the values of identifiers in scope. From the data definitions you can see that an Env is a StringDict<Value>. This means you can use Pyret's built in string-dict functions on your Env (See the documentation at pyret.org/docs/latest).

Your interpreter should allow identifier shadowing, meaning that if you bind an identifier that is already bound, the new binding takes precendence.

4.2 Binary Operators

Paret includes binary addition (+) and number equality testing (num=), as well as string appending (++) and string equality testing (str=). You may define these operations in terms of their counterparts in Pyret.

Evaluation should raise an err-bad-arg-to-op error for non-numeric values passed to + and num= operations, and for non-string values passed to ++ and str= operations. The op part of the error is the operator that was called, and val is the value it was given that had the wrong type.

In place of having separate rules (and syntactic forms) for +, num=, ++, and str=, we will define a single syntactic rule for all binary operators. parse converts these operators into the e-op datatype variant, shown in the data definition below.

We recommend that you define an "operator lookup function" that takes an operator name (of type Operator) and returns the actual function (Pyret procedure) that performs the corresponding operation. Having a single rule like this, accompanied by a mapping, makes it very easy to add new operators to your language: you need only add them to the operator lookup function.

4.3 Conditionals

If-statements in Paret are composed of three parts:

If statements should short circuit and evaluation should raise an err-if-got-non-boolean error for non-boolean "test" values, where val is the "test" value it was given.

4.4 Functions

Functions in Paret should take exactly one argument. Here's are some examples of functions and their applications:

((lam x (+ x 3)) 2)
((lam y 5) 1)

(These should both evaluate to 5.)

There are a couple error cases that may arise when interpreting functions and function applications:

5. Grammar

<expr> ::= | <num>
           | <bool>
           | <string>
           | (+ <expr> <expr>)
           | (++ <expr> <expr>)
           | (num= <expr> <expr>)
           | (str= <expr> <expr>)
           | (if <expr> <expr> <expr>)

           | <id>                    // identifier (a.k.a. variable)
           | (lam <id> <expr>) // anonymous function
           | (<expr> <expr>)            // function application

           | (and <expr> <expr>)
           | (or <expr> <expr>)
           | (let (<id> <expr>) <expr>)

6. Abstract Syntax

type Env = StringDict<Value>

data Value:
  | v-num(value :: Number)
  | v-str(value :: String)
  | v-bool(value :: Boolean)

  | v-fun(param :: String, body :: Expr, env :: Env)
end

data Expr:
  | e-num(value :: Number)
  | e-str(value :: String)
  | e-bool(value :: Boolean)
  | e-op(op :: Operator, left :: Expr, right :: Expr)
  | e-if(cond :: Expr, consq :: Expr, altern :: Expr)

  | e-lam(param :: String, body :: Expr)
  | e-app(func :: Expr, arg :: Expr)
  | e-id(name :: String)

  | sugar-and(left :: Expr, right :: Expr)
  | sugar-or(left :: Expr, right :: Expr)
  | sugar-let(id :: String, value :: Expr, body :: Expr)
end

data Operator:
  | op-plus
  | op-append
  | op-str-eq
  | op-num-eq
end

data InterpError:
  | err-if-got-non-boolean(val :: Value)
  | err-bad-arg-to-op(op :: Operator, val :: Value)

  | err-unbound-id(name :: String)
  | err-not-a-function(val :: Value)
end

7. What to hand in

To get started, you can open the code stencil and the test stencil in code.pyret.org.

7.1 Test cases

To check that a program raises a particular exception, use the syntactic form

eval("(+ 1 \"str\")") raises-satisfies
  lam(err): err == C.err-bad-arg-to-op(C.op-plus, C.v-str("str")) end

You can also be less specific, and not specify the details of the error message:

eval("(+ 1 \"str\")") raises-satisfies C.is-err-bad-arg-to-op

Programs can evaluate to functions. Since your implementation, through desugaring and how you choose to implement environments, affects the representation of functions, in your test submission you should only test whether the program returns a function, not which specific function it returned. Likewise, if you're testing for an exception, make sure not to test that it contains a specific function value.

So do write this:

eval("(lam x 5)") satisfies C.is-v-fun

But don't write this:

eval("(lam x 5)") is C.v-fun("x", C.e-num(5), [list:])

(Although it's fine to write that test in your code file.)

Also, if you test desugar, please put those tests in your code submission, not in your test submission. There's good reason for this: there is more than one correct desugaring, so any tests you write may be implementation-specific. (And, of course, your submitted test cases should indirectly test desugaring, because you'll test that and and or work correctly.)

7.2 Submissions

Please submit two files: "interp-code.arr" and "interp-tests.arr" that contain your code and tests to the VikingWeb classwork page.