diamondback

Homework 5: Diamondback, Due Friday, October 4 (Open Collaboration)

For this assignment, you may work in groups of 2.

This assignment is adapted with permission from an assignment by Joe Gibbs Politz

In this assignment you'll implement a compiler for a language called Diamondback, which has top-level function definitions.

Setup

Get the assignment at https://www.cs.cmu.edu/~aldrich/courses/17-363-fa24/hw/diamondback-starter.zip

The Diamondback Language

Concrete Syntax

The concrete syntax of Diamondback has a significant change from past languages. It distinguishes top-level declarations from expressions. The new parts are function definitions, function calls, and the print unary operator.

<prog> := <defn>* <expr> (new!) <defn> := (fun <name> ((<name> <type>)*) <type> <expr>) (new!) <expr> := | <number> | true | false | input | <identifier> | (let (<binding>+) <expr>) | (<op1> <expr>) | (<op2> <expr> <expr>) | (set! <name> <expr>) | (if <expr> <expr> <expr>) | (block <expr>+) | (repeat-until <expr> <expr>) | (<name> <expr>*) (new!) <op1> := add1 | sub1 | print (new!) <op2> := + | - | * | < | > | >= | <= | = <type> := int | bool <binding> := (<identifier> <expr>)

Abstract Syntax

You can choose the abstract syntax you use for Diamondback.

Semantics

A Diamondback program always evaluates to a single integer, a single boolean, or ends with an error. When ending with an error, it should print a message to standard error (eprintln! in Rust works well for this) and a non-zero exit code (std::process::exit(N) for nonzero N in Rust works well for this).

A Diamondback program starts by evaluating the <expr> at the end of the <prog>. The new expressions have the following semantics:

  • (<name> <expr>*) is a function call. It first evaluates the expressions to values. Then it evaluates the body of the corresponding function definition (the one with the given <name>) with the values bound to each of the parameter names in that definition.
  • (print <expr>) evaluates the expression to a value and prints it to standard output followed by a \n character. The print expression itself should evaluate to the given value.

There are several examples further down to make this concrete.

The compiler should stop and report an error if:

  • There is a call to a function name that doesn't exist
  • Multiple functions are defined with the same name
  • A function's parameter list has a duplicate name
  • There is a call to a function with the wrong number or type of arguments
  • input is used in a function definition (rather than in the expression at the end). It's worth thinking of that final expression as the main function or method

If there are multiple errors, the compiler can report any non-empty subset of them.

Here are some examples of Diamondback programs.

Example 1

(fun fact ((n int)) int (let ((i 1) (acc 1)) (repeat-until (if (< i n) (block (set! i (+ i 1)) (set! acc (* acc i)) ) acc ) (>= i n) ) ) ) (fact input)

Example 2

(fun isodd ((n int)) bool (if (< n 0) (isodd (- 0 n)) (if (= n 0) false (iseven (sub1 n)) ) ) ) (fun iseven ((n int)) bool (if (= n 0) true (isodd (sub1 n)) ) ) (block (print input) (print (iseven input)) )

Implementing a Compiler for Diamondback

The main new feature in Diamondback is functions. You should choose and implement a calling convention for these. You're welcome to use the “standard” x86_64 sysv as a convention, or use the purely stack-based approach we discussed in class, or choose something else entirely. Remember that when calling runtime functions in Rust, the generated code needs to respect the x86_64 sysv calling convention.

A compiler for Diamondback does not need guaranteed safe-for-space tail calls, but they are allowed.

Running and Testing

Running and testing are as for Cobra, there is no new infrastructure.

Grading

As with the previous assignment, a lot of the credit you get will be based on us running autograded tests on your submission. You'll be able to see the result of some of these on while the assignment is out, but we may have more that we don't show results for until after assignments are all submitted.

We'll combine that with some amount of manual grading involving looking at your testing and implementation strategy. You should have your own thorough test suite (it's not unreasonable to write many dozens of tests; you probably don't need hundreds), and you need to have recognizably implemented a compiler. For example, you could try to calculate the answer for these programs and generate a single mov instruction: don't do that, it doesn't demonstrate the learning outcomes we care about.

Extension 1: Proper Tail Calls

Implement safe-for-space tail calls for Diamondback. Test with deeply-nested recursion. To make sure you've tested proper tail calls and not just tail recursion, test deeply-nested mutual recursion between functions with different numbers of arguments.

Extension 2: Add Function Definitions to the REPL

Add the ability to define functions to the REPL. Entries should be a definition (which could be define or fun) or an expression.

Functions should be able to use global variables defined in earlier entries in the function body.