Assignment 5, CSC430, Fall 2020
1 Goal
2 Guidelines
2.1 Handling Errors
2.2 Progress Toward Goal comment
3 The Assignment
3.1 Top-Level environment
3.2 New Values
3.3 New Forms
3.4 Order of Evaluation
3.5 Array Values
3.6 Syntax of DXUQ5
4 Suggested Implementation Strategy
4.1 Adding the Store
5 Nice Big Test Cases
5.1 Quicksort (optional)
6 Interface
7.8.0.900

Assignment 5, CSC430, Fall 2020

1 Goal

Extend the interpreter to handle mutable arrays.

2 Guidelines

For this and all remaining assignments, every function you develop must come with the following things:

For this assignment, you must develop your solutions using the typed/racket language. If you haven’t seen them, you might be interested in these Hints on Using Typed Racket in CPE 430.

Your test cases must use the check-equal?, check-=, or check-exn forms.

Your solution should take the form of a single file. Solve each problem separately, and make sure that each solution appears in a separate part of the file, with comments separating each problem’s solution.

Hand in your solution using the handin server. For help with the handin server, please see the course web page.

2.1 Handling Errors

All of your error messages must contain the string "DXUQ". Essentially, this allows my test cases to distinguish errors correctly signaled by your implementation from errors in your implementation. To be more specific: any error message that doesn’t contain the string "DXUQ" will be considered to be an error in your implementation.

2.2 Progress Toward Goal comment

Graders are happier when they know what to expect. Your final submission should start with a short one- or two-line comment indicating how far you got through the project. Ideally, this would just be: “Full project implemented.” But if you only implemented, say, squazz and blotz, and didn’t get to frob or dringo, please indicate this in the comment, so that we don’t spend all our time searching for bits that aren’t there.

3 The Assignment

For this assignment, you must extend Assignment 4 by implementing mutable arrays. These arrays are instead of boxes. Just as the previous assignments asked you to generalize the book’s solution from one argument to many arguments, this assignment generalizes from a single box to an array. Unlike the book, however, we will not require an implementation of Store Passing Style (SPS). Instead you will be permitted one use of Racket mutability to implement a mutable store.

In addition, we’ll be adding a simple substring operation.

3.1 Top-Level environment

We simplified the language significantly in Assignment 4, when we removed binops from the language. Also, we represented true and false as variable references. All of these simplify the language.

Best of all, we implemented let as syntactic sugar; that means that we won’t need to make any changes to it, either. Yay!

3.2 New Values

Add a value that represents the null value. It will be used as the result of mutation operations. The serialize function should produce "null" when called with a null value. The null value should be equal to itself, but not to any other value.

The array value is also a new kind of value, but I’m going to delay discussing it for a few paragraphs.

3.3 New Forms

Implement these new forms as primitive bindings in a top-level environment. For details on this transformation, see the roadmap below.

There’s one more form, and this one can’t be implemented as a primitive function:

In order to allow mutable bindings and arrays, you’ll need to add a store. As a simpler alternative to Store Passing Style, which is the purely functional approach to supporting mutation, you may instead implement your store as a single mutable value, for example a single box or a single Mutable-HashTable.

Note that arrays are not typed; it’s fine to have an array that contains a mixture of numbers and booleans.

In order to allow mutable bindings, you will have to change the type of the environment; rather than mapping names to values, it will map names to locations. That is, "every" binding will be a reference to the store.

3.4 Order of Evaluation

In a language with mutation, programmers can observe the order of evaluation of function call arguments. For this language, all forms must perform left-to-right evaluation.

3.5 Array Values

The equal? function must now accept arrays. It should return true for arrays only when its two arguments evaluate to the same array; that is, two arrays pointing to the same region of memory.

The serialize function must handle arrays. It should simply return the string "#<array>" for an array.

3.6 Syntax of DXUQ5

The concrete syntax of the DXUQ5 language with these additional features is captured by the following EBNF:

  expr = number
  | string
  | id
  | {id := expr}
  | {if expr expr expr}
  | {let {id = expr} ... in expr}
  | {fn {id ...} expr}
  | {expr expr ...}

  top-level-constants = true
  | false
  | null

  top-level-functions = +
  | -
  | *
  | /
  | equal?
  | <=
  | array
  | new-array
  | aref
  | aset!
  | begin
  | substring

... where an id is not :=, if, let, =, in, or fn.

4 Suggested Implementation Strategy

4.1 Adding the Store

First thing, strip down the language. In the interp function, comment out everything except the evaluation of numbers and applications of primitive functions. Add an other rule that signals an "unimplemented" error for all other forms. Make sure that your test cases for binops still work.

Time to add the store! Formulate the define-types and type aliases that you’ll need. As mentioned earlier, you will be allowed to use a single instance of a Racket mutable value to implement the store, which avoids many of the complexities of the purely functional Store Passing Style. For example, you might use a single Racket box or maybe a single Mutable-HashTable. The store is now passed as an argument to interp. Unlike the SPS implementation in the book, there is no need to change the return type of interp. Change the Env type to map names to locations. Update your test cases so that they pass a store in.

With luck and some effort, you should be able to get those binop programs working again.

Next, I would add identifiers and functions; these are both one-line changes. Now you can re-enable your tests involving these items.

Next, I would add the mutation operations. Note that I’m advising you to add these before adding your other language forms (if, application of closures) back in. First off, I think I’d add an allocate helper function that accepts a store and a list of values to place in the next set of sequential locations, and returns the base location.

Design your store so that the "next allocated" location is derived directly from the store. It could be a separate counter that’s part of a define-type, or it could be a function that just scans the store to find a new address. Don’t use the new-loc defined by the book; it makes testing quite painful.

Next, I would add a new arrayV value to the set of values. This will require a bunch of extra clauses in various places (serialize, for instance). Note that the representation of arrays is up to you, but it had probably better include a location and a length.

Following this, I would add the new-array operation. At this point, you should be able to create arrays, and the result of interpretation should be that the store includes lots of newly allocated locations.

At this point, I would go back and add test cases that create arrays as subexpressions of the equal? binop; check that the allocations happen in the right order. As you go forward, you’ll want to use this technique to check order of evaluation for all of your forms.

At this point, it starts to make less difference what order you add language forms in. I think I would probably wait on applications, just because there will be lots of opportunities for mistakes.

5 Nice Big Test Cases

When you think you have everything working, develop a while function (that is, an DXUQ5 program, using its concrete syntax) that accepts a guard procedure and a body procedure and keeps running the body until the guard returns false. This function will have to be recursive; build a recursive function using mutation. Here’s an example of a factorial function written in this way:

{let {fact = "bogus"}
  in
 {begin {fact := {fn {n} {if {<= n 0} 1 {* n {fact {- n 1}}}}}}
   {fact 12}}}

Then, use your while to develop the in-order function that accepts an array of numbers and its size and returns true if the array is in strictly increasing order.

5.1 Quicksort (optional)

This is not a good early test case, but you might want to see if you can write an in-place quicksort routine. You can model recursion by using the technique that Shriram describes in the next chapter.

Did I mention that this is optional?

6 Interface

Make sure that you include the following functions, and that they match this interface.

procedure

(parse s)  ExprC

  s : s-expression
Parses an expression.

procedure

(interp e env sto)  Value

  e : ExprC
  env : Environment
  sto : Store
Interprets an expression, with a given environment and store.

procedure

(top-interp s)  string

  s : s-expression
Combines parsing and evaluation. You should probably use this definition for this function:

(define (top-interp [s : s-expression]) : string
  (serialize (interp (parse s) initial-env (make-initial-store))))

value

while : s-expression?

an s-expression representing the while function, described above, implemented in DXUQ5 .

value

in-order : s-expression?

an s-expression representing the in-order function, described above, implemented in DXUQ5 . It’s okay for this s-expression to assume that while is already bound.