6 State Interpreter
This assignment has you implement an extension to Paret to support state. At the end of this assignment, you will have more or less implemented Lisp (http://en.wikipedia.org/wiki/Lisp_%28programming_language%29). Paret has immutable lists instead of mutable pairs, a slightly different syntax, and doesn’t implement a few desugarings, but those are mostly minor details.
6.1 Data Definitions
You’ll be implementing an extended Paret with variables.
exp := (<op> <exp> <exp>) |
| (let ((<id> <exp>) ...) <exp>) [where <id>'s are all different] |
| (if <exp> <exp> <exp>) |
| true |
| false |
| <number> |
| (lam (<id> ...) <exp>) [where <id>'s are all different] |
| (<exp> <exp> ...) |
| (<unop> <exp>) |
| (link <exp> <exp>) |
| empty |
|
# for variables |
| (set-var <id> <exp>) |
|
# for convenience |
| (block <exp> <exp> ...) |
|
|
op := + | - | * | / | < | > | == |
unop := first | rest | is-empty |
id := any symbol |
Some examples of Paret programs are:
(let ((x 0)) |
(let ((x2 x)) |
(block |
(set-var x2 5) |
(+ x x2)))) |
|
(let ((counter 0)) |
(let ((next (lam () |
(block |
(set-var counter (+ counter 1)) |
counter)))) |
(link (next) (link (next) empty)))) |
The abstract syntax of Paret is described by a few Pyret data definitions:
data Op: | op-plus | op-minus | op-times | op-divide | op-less | op-greater | op-equal end data UnOp: | op-first | op-rest | op-is-empty end data Expr: | e-num(n :: Number) | e-bool(b :: Boolean) | e-empty | e-link(first :: Expr, rest :: Expr) | e-unop(op :: UnOp, arg :: Expr) | e-op(op :: Op, left :: Expr, right :: Expr) | e-if(cond :: Expr, consq :: Expr, els :: Expr) | e-let(binds :: List<Bind>, body :: Expr) | e-lam(args :: List<String>, body :: Expr) | e-app(fun-expr :: Expr, arg-exprs :: List<Expr>) | e-var(x :: String) | e-set-var(x :: String, new-val :: Expr) | e-block(exps :: List<Expr>%(is-link)) end data Bind: | bind(id :: String, expr :: Expr) end fun is-v-list(v :: Value): is-v-empty(v) or is-v-link(v) end
This is mostly maps one-to-one with the pieces of concrete syntax.
Paret programs can result in values or raise errors, which are described by two Pyret data structures; the only change is to the Env type (noted lower down), and the addition of the equal-funs error:
data Error: | unbound-id | div-0 | not-a-num | not-a-bool | not-a-fun | not-a-list | not-a-link | arity-mismatch | equal-funs end data Value: | v-num(n :: Number) | v-bool(b :: Boolean) | v-empty | v-link(first :: Value, rest :: Value%(is-v-list)) | v-clos(env :: Env, args :: List<String>, body :: Expr) end
Finally, your interpreter will be based on an environment and a store, so there is a definition for environments, and for locations. Note that environments now contain locations rather than directly containing values:
type Location = Number type Env = SD.StringDict<Location>
The store is described more in the next section.
6.2 Paret Semantics
You will define an evaluator for Paret with a single function:
fun interp(env :: Env, store :: Store, expr :: Expr) -> VandS
It is up to you what definitions to use for Store and VandS. You can rename them if you like. You should change the definition of eval to make sure that it returns a Value, to ensure uniform testing. So, for example, you might choose what we did in lecture:
data Store: | mt-sto | sto(l :: Location, v :: Value, rest :: Store) end data VandS: | vs(v :: Value, s :: Store) end
Then you should make sure that eval only returns the v field of the VandS that is returned.
Note that the assignment doesn’t mandate that you implement alloc, update, and deref as we mentioned in class, but that might be a useful place to start.
6.2.1 Testing Multiple Representations
Since you’re free to pick any representation for the store, and any allocation strategy (start locations from 0, from 100, counting backwards, etc.), there are many possible correct behaviors for evaluations that return or v-clos values (which contain Locations in their environments). The Value type is set up so that you cannot directly test equality of closures; you’ll get an error if you do. So, intead of testing that the interpreter returns a v-clos with a particular environment, you could check that the interpreter returned some v-clos:
check: eval("(lam () 5)") satisfies is-v-clos end
This is similar to testing sorts that may or may not be stable. The specification admits many possible input/output behaviors, so it’s relational, rather than functional. If you want to test in more detail, you should make the test apply the function. Of course, you can also test your representations directly in your code file and look for particular locations. Your tests in the external test file need to use the satisfies style to handle all possible correct implementations.
Note that this also helps you use tests from others that you see during review, since if you had different allocation strategies, it would be difficult to re-use or give feedback on tests that depended on the store implementation.
6.2.2 New and Updated Semantics
The semantics of some expressions from original Paret have changed, and new forms added. The new forms are described here:
Environments no longer hold values, but Locations, which are references into the store. Locations are defined to be numbers in this assignment. It is up to you to pick a data definition for the store, and manage allocations, updates, and deferences into it.
e-app expressions evaluate the function position to a value as before, and raise not-a-fun and arity-mismatch as before. When binding arguments to values, new locations should be created for all the values, and the argument names bound to the corresponding locations in the closure’s environment.
e-let should create new locations for all the values in the store, and bind the names to the corresponding locations in the environment.
e-id expressions have been changed to e-var expressions. They evaluate to the value in the store at the location the variable is bound to in the environment. They raise unbound-id if the variable is not in the environment.
e-set-var expressions raise unbound-id if the given variable isn’t in the environment. They then evaluate the new-val sub-expression to a value, and update the store to have that value at the location the variable is bound to in the environment.
e-block expressions evaluate each of their sub-expressions in order (with updates to the store in earlier expressions carried over to later ones), and return the value the last expression evaluated to.
e-op is the same as before, except now it raises equal-funs if given two functions for the op-equal (==) operator.
6.3 Logistics
You have been provided two files:
state-interp-impl.arr, which contains your implementation of Paret.
state-interp-tests.arr, which will hold your external tests for the evaluator.
The files are at:
Code: https://code.pyret.org/editor#share=0B32bNEogmncOSjlnSEtZSzVsQWM&v=v0.5r790
Tests: https://code.pyret.org/editor#share=0B32bNEogmncOZjBucnNUWEpUWXM&v=v0.5r790
Both files import the module fun-interp-defs.arr, which defines the data definitions and the helper function used by eval to parse strings to ASTs:
https://code.pyret.org/editor#share=0B32bNEogmncOYnE4TGs0bHd1Unc&v=v0.5r790
6.3.1 Using Modules
Here is the appropriate binding boilerplate for this assignment, if you prefer to use it:
type Error = I.Error unbound-id = I.unbound-id div-0 = I.div-0 not-a-num = I.not-a-num not-a-bool = I.not-a-bool not-a-fun = I.not-a-fun not-a-link = I.not-a-link not-a-list = I.not-a-list equal-funs = I.equal-funs type Op = I.Op op-plus = I.op-plus op-minus = I.op-minus op-times = I.op-times op-divide = I.op-divide op-less = I.op-less op-greater = I.op-greater op-equal = I.op-equal type UnOp = I.UnOp op-is-empty = I.op-is-empty op-first = I.op-first op-rest = I.op-rest type Expr = I.Expr e-num = I.e-num e-bool = I.e-bool e-op = I.e-op e-if = I.e-if e-let = I.e-let e-var = I.e-var e-lam = I.e-lam e-link = I.e-link e-unop = I.e-unop e-set-var = I.e-set-var type Bind = I.Bind bind = I.bind type Value = I.Value v-num = I.v-num is-v-num = I.is-v-num v-bool = I.v-bool is-v-bool = I.is-v-bool v-clos = I.v-clos is-v-clos = I.is-v-clos v-empty = I.v-empty is-v-empty = I.is-v-empty v-link = I.v-link is-v-link = I.is-v-link is-v-list = I.is-v-list
6.4 Testing and Submission Steps
By midnight Sunday (Feb 22), you should submit 5-10 of what you think are the most interesting tests to Captain Teach at the cs91-state link from:
https://www.captain-teach.org/swarthmore-cs091/assignments/
Submit a single Pyret file with your test cases filled into the check block in the template file. If you’re budgeting your time over the weekend, you shouldn’t need to spend more than about an hour playing with the interpreter to come up with some interesting cases.
By midnight Monday (Feb 23), you should submit any reviews you were assigned. You should feel free to complete the reviews at any time before then, and they will be assigned as submissions come in (so if multiple people submit on Saturday, reviewing can start then).
On Tuesday, a set of different implementations will be released, with instructions for adding an import line to access and test the different implementations.
Update: Bad solutions are available via:
import shared-gdrive("state-interp-different-solutions.arr", "0B32bNEogmncOcWRZQVlZc1ltNU0") as C
The names of the interpreters are:
dynamic-scope-again let-star-again arg-eval-order-again set-returns-old-val store-has-one-location store-has-five-locations store-forgetful-in-args store-forgetful-in-binops store-forgetful-after-app wrong-closure-env-recursion
This was the in-class test that I projected in lab. I recommend getting your state-interp to pass it:
S.eval(``` (let ((sum-to-n (lam (n) (let ((sum-helper (lam (f acc) (if ( == n 0 ) acc (block (set-var n (- n 1)) (f f (+ n acc))))))) (sum-helper sum-helper 0))) ) ) (sum-to-n 5)) ```)
By midnight Thursday (Feb 26), you should submit a completed implementation of the interpreter, and a final test suite. Use the same submission link as above, but submit to the code step, which will unlock after you complete your reviews.
6.5 Grading
Your grade will be in a few parts:
10% for submitting an interesting initial set of tests.
35% for the quality of your tests, based on the number of bad implementations they catch. As before, I reserve the right to grade based on additional bad implementations that I don’t release, so catching the ones that are out there does not guarantee a perfect testing score.
55% for the quality of your implementation, based on its correctness, clarity, and style.
Peer reviews will not factor into your grade, though you are free to copy tests from others that you see during review. Please don’t share tests with one another outside of what’s provided during test review.