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. Theprint
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 themain
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 define
d in earlier entries
in the function body.