For this assignment, you will add state to the Paret language by allowing variable mutation. Please assume left-to-right evaluation order throughout.
The grammar and the abstract syntax have been extended with three new forms:
set
, do
, and rec-lam
.
set
takes an identifier and an expression, evaluates the expression to a value, and assigns that value to the identifier by changing its value in the store. The result of a set
expression is the new value of the identifier. For instance, the Paret program
(let (x 1) (set x 2))
should evaluate to 2
.
If the identifier in a set
expression is not in scope, you should raise an err-unbound-id
exception – it doesn't make sense to change the value of an identifier not bound in the environment!
do
takes a sequence of expressions and evaluates them in order. It returns the value of the last expression in the sequence. do
always has at least one subexpression. (The parser will enforce this.)
rec-lam
is a new piece of syntactic sugar that defines a recursive
function. It's very similar to lam
, except that it also takes a function
name which can be used in its body. More precisely,
(rec-lam f v b)
constructs a function with name f
, arguments v
, and body b
,
where the function name f
is bound inside the body.
To adapt an example from the book, you could define a function S
that
sums the numbers from 0
to n
by writing:
(rec-lam S n (if (num= n 0)
0
(+ n (S (+ n -1)))))
This simply defines the function. To call it, you could write:
((rec-lam S n (if (num= n 0)
0
(+ n (S (+ n -1))))) 3)
which should evaluate to 3+2+1 = 6.
You can also easily write infinite loops using rec-lam
:
((rec-lam forever x (forever x)) "dummy")
Please do not submit tests that run forever, though.
You will define a desugaring for rec-lam
in your desugar
function. It can be desugared to first let-bind the function name to a dummy value
(which will never be seen), then set it to the function it should be, and
finally return the function. So the first example:
(rec-lam S n (if (num= n 0)
0
(+ n (S (+ n -1)))))
could desugar to:
(let (S "dummy")
(set S (lam n (if (num= n 0)
0
(+ n (S (+ n -1)))))))
There's a problem, though: this expression has let
in it, which is
itself syntactic sugar. You can get around this by expanding the let
expression with a recursive call to desugar
. (This kind of recursion,
where instead of just recurring on your arguments you build up a new thing
and then recur on that, is called generative recursion.)
Your task is to add state to Paret. You should follow the store-passing style we studied in class and that is presented in the book. We have added the following definition for the store:
type Store = StringDict<Value>
That is, a Store
is a map from store location to
Value
. With this, we change the environment to be a map
from identifier to store location instead, which is reflected by the
following definition:
type Env = StringDict<String>
You will need to generate store locations, and can use the built-in
gensym
function to do so. For example, you can call gensym("loc")
to obtain a fresh
name that starts with "loc" (e.g., "loc379"
)
As you will use the store-passing style, your interp
should
produce a Result
which is simply a type synonym for a
pair of a Value
and Store
:
type Result = {Value; Store}
Please see below for a guide on how to work with pair values.
To create a pair of a
and b
, write {a; b}
.
For example, {1; "a"}
is a pair of 1
and "a"
, and it has type {Number; String}
.
Given a pair p
, you can destruct it using let-binding:
check:
p = {1; "a"}
{first; second} = p
first is 1
second is "a"
end
Here is the extended grammar:
<expr> ::= | <num>
| true | false
| <string>
| (+ <expr> <expr>)
| (++ <expr> <expr>)
| (num= <expr> <expr>)
| (str= <expr> <expr>)
| (if <expr> <expr> <expr>)
| (and <expr> <expr>)
| (or <expr> <expr>)
| (let (<id> <expr>) <expr>)
| <id>
| (lam <id> <expr>)
| (<expr> <expr>)
| (set <id> <expr>)
| (do <expr> <expr> ...)
| (rec-lam <id> <id> <expr>)
type Env = StringDict<String>
type Store = StringDict<Value>
type Result = {Value; Store}
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)
| e-set(name :: String, val :: Expr)
| e-do(stmts :: List<Expr>)
| sugar-and(left :: Expr, right :: Expr)
| sugar-or(left :: Expr, right :: Expr)
| sugar-let(name :: String, expr :: Expr, body :: Expr)
| sugar-rec-lam(fun-name :: String, param :: String, 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
(For reference, feel free to look at the definitions file.)
To get started, you can open the code stencil and the test stencil in code.pyret.org.
Please read the test guidelines