You will implement the function calc-locals
twice -- first for a
language in which every with
and lam
is annotated with the type of its
argument, and second where they have no annotations.
A summary of the grammar and abstract syntax for both parts is at the bottom of the page. The definitions files can be found here: Part I, Part II.
(with rec body)
first evaluates rec
to a record value,
and then evaluates body
with all of the record's fields added as bindings to
the current environment.
For example,
(with (record (a 1) (b 2)) (Record (a Num) (b Num)) (+ a b)) # in the typed version
(with (record (a 1) (b 2)) (+ a b)) # in the untyped version
would both evaluate to 3.
In Part I, with
and lam
expressions have type annotations.
Please note that an expression can be a subtype of the given type
annotation. For example, in (with <rec-expr> <rec-type> <expr>)
,
<rec-expr>
can be any subtype of <rec-type>
.
For example, the following program:
parse-n-calc(```(with (record (a (+ 1 2)) (b 3)) (Record (a Num))
(+ a @))```)
should return [set: "a", "b"]
.
You may assume all input to calc-locals
is well-formed. By this we mean that the type annotation on a with
expression
will always be a record and there are no type-errors in the input.
For the second part of the assignment, your task is exactly the same as
before, except that now the language doesn't have types (or type annotations on
with
or lam
).
For example, the following program:
parse-n-calc(```(with (record (a (+ 1 2)) (b 3))
(+ a @))```)
would return [set: "a", "b"]
.
The grammar is almost the same for both parts, so here is a combined presentation:
<expr> ::= <num>
| <bool>
| (+ <expr> <expr>)
| (num= <expr> <expr>)
| (if <expr> <expr> <expr>)
| <id>
| (<expr> <expr>)
| (let (<id> <expr>) <expr>)
| (record (<id> <expr>) ...)
| (lookup <expr> <id>)
| (extend <expr> <id> <expr>)
| @ # a "hole"
# In the "typed" language, for Part I
| (with <expr> <type> <expr>)
| (lam (<id> : <type>) <expr>)
# In the "untyped" language, for Part II
| (with <expr> <expr>)
| (lam <id> <expr>)
<type> ::= Num
| Bool
| (<type> -> <type>) # function type
| (Record (<id> <type>) ...) # record type
Notice that the syntax for with
and lam
is different in each part, and the untyped part doesn't use the type syntax. The abstract syntax is also mostly the same, so we show them together. Again, the only difference between parts is e-with
and e-lam
, and the lack of types in the untyped version.
data Expr:
| e-num(value :: Number)
| e-bool(value :: Boolean)
| e-op(op :: Operator, left :: Expr, right :: Expr)
| e-if(cond :: Expr, consq :: Expr, altern :: Expr)
| e-id(name :: String)
| e-app(func :: Expr, arg :: Expr)
| e-let(name :: String, expr :: Expr, body :: Expr)
| e-rec(fields :: StringDict<Expr>)
| e-lookup(record :: Expr, field-name :: String)
| e-extend(record :: Expr, field-name :: String, new-value :: Expr)
| e-hole
# In the "typed" language, for Part I
| e-with(record :: Expr, rec-type :: Type, body :: Expr)
| e-lam(param :: String, arg-type :: Type, body :: Expr)
# In the "untyped" language, for Part II
| e-with(record :: Expr, body :: Expr)
| e-lam(param :: String, body :: Expr)
end
data Operator:
| op-plus
| op-num-eq
end
data Type:
| t-num
| t-bool
| t-fun(arg-type :: Type, return-type :: Type)
| t-rec(field-types :: StringDict<Type>)
end
To get started on Part I, here is:
Here are the stencils for Part II:
Please read the test guidelines
Do not write test cases that include no holes or more than one hole. For Part I, do
not write test cases where the argument to with
is not a subtype of the type annotation.