CS 345H: Programming Languages (Honors) Spring 2024

Homework 4: Rust and Type Inference

Due date: April 25, 6pm
Grading: 15% (CS 345H) or 9% (CS 386L) of your course grade

In this homework we'll implement type checking and type inference for the simply typed lambda calculus. Unlike the previous homeworks, this one has no proofs and therefore no Coq. Instead, we'll implement our type inference in Rust, which also gives us a chance to learn more about Rust's type system.

Table of contents

Preparation

Since we'll be writing Rust for this homework, you'll want to make sure you have a development environment set up for it. Start by installing Rust itself; the standard way to do this is by installing rustup, which should give you the right instructions for your platform. There are more detailed instructions in Chapter 1 of the Rust book.

You'll also want an editor. As with the previous homeworks, I suggest using Visual Studio Code, as it has a nice Rust integration called rust-analyzer. To get that Rust support, click the Install button on its homepage or search for rust-analyzer in VSCode's extension pane.

Get the code

We'll be using GitHub Classroom to check out and submit this homework. Follow the GitHub Classroom URL on Ed to create your private copy of the homework repository, and then clone that repository to your machine. For example, the repository it created for me is called hw4-jamesbornholt, so I would do:

git clone git@github.com:utcs345h-24sp/hw4-jamesbornholt.git
cd hw4-jamesbornholt

To check your Rust setup is working correctly, run this command from your homework directory:

cargo test

You should see a bunch of failing tests. You'll know you've completed the homework when all these tests pass!

Complete the homework

This homework is in two parts. The homework directory is a workspace, a collection of two Rust crates called warmup and lambda. There are homework problems in both crates. There are a total of 64 points available on this homework.

Part 1: Warmup (12 points, problems 1-4)

This part is just a few small warmup problems to see Rust ideas that will be useful in Part 2. Complete the four todo!()s in the warmup/src/lib.rs file. You can test your solutions by running:

cargo test -p warmup

Part 2: Lambda calculus

In this part, we'll implement type checking and type inference for the simply typed lambda calculus (STLC). I recommend starting by reading lambda/src/lang.rs, which is where we've defined the syntax of STLC and its type system. You must not modify this file.

We've also included a small REPL to interact with the type checker and type inference engine you'll build. Run it like this:

cargo run

The REPL has three commands: #parse to print the parsed form of a lambda calculus term, #check to run your type checker on a term, and #infer to run your type inference engine on a term. Each command takes a single lambda calculus term as input. You can type \ to mean λ. For example:

λ> #parse \x:bool. x
Abs(Var("x"), Bool, Var(Var("x")))

λ> #check \x:bool. x
(bool -> bool)

λ> #infer \x. x
('0 -> '0)

Here, '0 is the pretty-printed form of a type variable.

Part 2a: Type checking (18 points, problem 5)

In this part we'll implement a type checker for terms in STLC. The type checker takes as input a term and returns a type if the term is well-typed, or an error otherwise. That might not quite conform to your notion of a "type checker", but the trick is that the STLC type system is syntax-directed -- at most one typing judgment rule applies to any given term. That means we can construct the required type just by looking at the term, so we don't need to be given the type as an input.

Complete the todo!() in the file lambda/src/check.rs. There are tests for this part in the file lambda/tests/check.rs. You can run those tests like this:

cargo test --test check

Part 2b: Type inference (34 points, problems 6-11)

In this part we'll implement type inference, which takes a term and returns a type that makes the term well-typed if one exists. The difference between type inference and the type checker from the previous part is that the inference engine needs to be able to invent new types. For example, given this term:

λx:T0. (x true)

a type checker would fail, because the type variable T0 is not a function and so cannot be applied. A type inference engine, on the other hand, would realize that T0 must be a function for this program to be well typed, and that function must take a Bool as input. It would therefore return the type:

Bool -> T1 -> T1

The particular type inference algorithm you're going to implement is known as Hindley-Milner (HM) type inference, named for J. Roger Hindley and Robin Milner. It's a very popular type inference algorithm, and extensions of it are used in almost every language that does type inference, but notably ML (and SML and OCaml) and Haskell. We're going to implement a slightly restricted version of HM that does not support parametric polymorphism.

Hindley-Milner type inference works in two phases. The overall idea is to generate a set of constraints over type variables that must be satisfied in order for the term to be well typed, and then solve those constraints via unification to find a concrete type that works. You'll be implementing both these phases. Hindley-Milner type inference is also sometimes called "constraint-based typing", because of the two-phase approach of first generating constraints and then solving them.

Implement Hindley-Milner type inference by completing the todo!()s in the file lambda/src/infer.rs.

Notes. This is hard! Start by carefully reading the long comment at the top of that file, which describes the type inference algorithm. We've provided the skeleton for you to complete; you shouldn't need to define any more methods, and every method we've defined should likely be used in your solution (they're all used in ours). You should also read Chapter 22 of Types and Programming Languages, which is included in your homework directory as types-and-programming-languages-ch22.pdf. Section 22.3 covers constraint-based typing, and section 22.4 covers unification.

Testing. There are two sets of tests for this part. The first set test the two phases of Hindley-Milner in isolation, so you can test the two parts of your solution independently. These tests are in the same lambda/src/infer.rs file as your solution will be in, and you can run them like this (be careful of the space after the second --):

cargo test --lib -- phase_tests

The second set of tests run your entire type inference implementation. These tests are in the file lambda/tests/infer.rs and can be run like this:

cargo test --test infer

Resources for writing Rust

Rust is a mainstream programming language, which means the documentation is very good and there's ample resources on the internet to help learn it. It's somewhat infamous for having a steep learning curve, although we've tried to write the homework in a way that will keep you away from the sharper edges. The official Rust book is exceptionally good and should be your first port of call for questions about Rust. The standard library documentation is also very good and is a great way to track down methods that can do something helpful for you.

What to submit

Submit your solutions by committing your changes in Git and pushing them to the private repository GitHub Classroom created for you in the Get the code step.

The only files you should edit are:

GitHub Classroom will automatically select your most recent pushed commit before the deadline as your submission. There's no need to manually submit anything else via Canvas or GitHub.

GitHub Classroom also has a simple autograder that just runs all the tests using GitHub Actions. This is only a partial grader, and we're using it only to give you early feedback on your submissions—we will still be reading and grading your code by hand. Just because the autograder passes doesn't mean you'll get full points; just because the autograder fails doesn't mean you won't get full points.