Calculating Record Fields

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.

You will be working with sets. The documentation is on pyret.org/docs/latest.


In this assignment you will write a version of calc-locals to determine which fields of a record can be accessed at a given point in your program. We'll ask you to do this for two variations on the same language -- first where each lam is annotated with the type of its argument, and second where there are no annotations.

Remember, the purpose of `calc-locals` is to statically check what is in scope at a given part of your program, so it should always halt (unlike your interpreter).

1. The Extended Language

You will be working with a language similar to the language for type-checker, with records added. and, or, strings and lists have been removed. There is no desugaring in this language, so you will have to handle e-lets.

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.

1.1 Record Syntax

A record expression represents a record with the given fields. Note that the parser will reject a record expression with duplicate field names.

For example,

(record (a 1) (b 2))

represents a record with two fields, a and b, which contain 1 and 2 respectively.

1.2 Extending a record

(extend rec field-name new-value) extends a record value by adding a new field on to it. The new expression represents a record with all fields of rec plus (field-name new-value).

For example,

(extend (record (a 1) (b 2)) c 3)

is equivalent to (record (a 1) (b 2) (c 3)).

If field-name is already on the given record then it's updated to be new-value as long as the old and new values have the same type.

1.3 Lookups

(lookup rec field-name) is used to inspect the fields of a record. If rec represents a record, with a field called field-name, this expression represents the associated field value.

For example,

(lookup (record (a (record (x 1)) (b 2)) a)

Is equivalent to (record x 1).

For the purposes of this assignment lookup expressions have been extended to include a special case where the field-name is a "hole", similar to the previous `calc-locals` assignment. The concrete syntax to return a list of all the field-names of a record is (lookup rec @).

Part I (Typed)

In this part lambda expressions will be explicitly annotated with the type of their argument using the concrete syntax

(lam (<id> : <type>) <expr>)

You will implement a function that takes an expression containing the expression (lookup <expr> @) and returns the set of field-names accessible at the hole. Concretely, replacing a hole with any of the field-names returned by calc-locals and evaluating that expression should never cause a field not found exception. Additionally, every field-name which isn't returned should cause an error if that lookup expression were evaluated.

For example, the following programs:

parse-n-calc("(lookup (record (a 1) (b 2)) @)")
parse-n-calc("((lam (rec : (Record (a Num) (b Num))) (lookup rec @)) (record (a 1) (b 2)))")

Should both return [set: "a", "b"].

You may assume all input to calc-locals is well-formed. In particular, you can assume that the type annotation on lam will always exactly match the type of the argument it is passed.

Test Cases

Do not write test cases that include no holes or more than one hole. Do not write test cases where the type annotation on lam doesn't match the type of its argument.

Submission of Part I

To get started on Part I, here is:

the code stencil

the tests stencil

Hint: In the code stencil and definitions file, we've included data types and a type signature for a type checker. In our implementation for the typed version, we have a type checker, and we suggest that it may be helpful for you to do the same. However, this is not required-- if you have a solution that does not require a type checker, feel free to ignore the data types in the definitions file and the type-check function in the code stencil.

Upload your completed two files to the appropriate VikingWeb coursework link. Make sure to call them "calc-fields-typed-code.arr" and "calc-fields-typed-tests.arr".

Part II (Untyped)

For the second part of the assignment, your task is exactly the same as before, except that now the language doesn't have type annotations on lam.

Again, your calc-locals function must return precisely the set of field-names accessible at the hole.

For example, the following programs:

parse-n-calc("(lookup (record (a 1) (b 2)) @)")
parse-n-calc("((lam rec (lookup rec @)) (record (a 1) (b 2)))")

Should both return [set: "a", "b"].

Submission of Part II

Here are the stencils for Part II:

code stencil

tests stencil

Submit your files to VikingWeb. Make sure to call them "calc-fields-untyped-code.arr" and "calc-fields-untyped-tests.arr".

2. The Grammar

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> ...)
         | (let (<id> <expr>) <expr>)

         | (record <field> ...)
         | (lookup <expr> <lookup>)
         | (extend <expr> <id> <expr>)

         # In the "typed" language, for Part I
         | (lam (<id> : <type>) <expr>)

         # In the "untyped" language, for Part II
         | (lam <id> <expr>)

<lookup> ::= @                        # a "hole"
           | <id>

<type> ::= Num
         | Bool
         | (<type> -> <type>)         # function type
         | (Record (<id> <type>) ...) # record type

<field> ::= (<id> <expr>)             # field name, and value

Notice that the syntax for 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-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 :: Lookup)
  | e-extend(record :: Expr, field-name :: String, new-value :: Expr)

  # In the "typed" language, for Part I
  | e-lam(id :: String, arg-type :: Type, body :: Expr)

  # In the "untyped" language, for Part II
  | e-lam(param :: String, body :: Expr)
end

data Lookup:
  | l-hole
  | l-name(field-name :: String)
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