Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Rust

on:
- push
- pull_request

permissions: read-all

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}

- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}

- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}

- name: Build
run: cargo build --verbose

- name: Run tests
run: cargo test --verbose

fmt:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt

- name: Check formatting
run: cargo fmt -- --check

clippy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- name: Run Clippy
run: cargo clippy -- -D warnings
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
.direnv
_build


# Added by cargo

/target
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "hm_inference_example"
version = "0.1.0"
edition = "2024"

[dependencies]
65 changes: 60 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,73 @@
# Hindley-Milner Type Inference Example with Algorithm W

This repo contains an interpreter for a toy language based on simply-typed lambda calculus.
It is designed to be a practical illustration of how Algorithm W works in OCaml code.
It is designed to be a practical illustration of how Algorithm W works, now blazingly fast in Rust! 🚀🦀

Most of the code (e.g. parser, type checker) is contained in `/lib`, with `/lib/type_check.ml` being the type inference/checking code, `/lib/parser.mly` containing parser definitions, etc.
The code includes:
- `src/ast.rs`: Abstract syntax tree definitions for expressions
- `src/lexer.rs`: Lexer for tokenizing input
- `src/parser.rs`: Parser for building ASTs from tokens
- `src/type_check.rs`: Type inference/checking code implementing Algorithm W
- `src/eval.rs`: Evaluator for executing expressions
- `src/main.rs`: CLI interface

## Instructions to Build and Run

This project uses [Dune](https://dune.build/) to build and run tests.
This project uses [Cargo](https://doc.rust-lang.org/cargo/) to build and run.

### Prerequisites

Make sure you have Rust and Cargo installed. You can install them from [rustup.rs](https://rustup.rs/).

### Building

```bash
cargo build --release
```

### Running

You can run the interpreter with an expression as a command-line argument:

```bash
cargo run -- "let id = x -> x in id 1"
```

Or run it in REPL mode:

```bash
cargo run
```

### Running Tests

```bash
cargo test
```

## Example Expressions

Here are some example expressions you can try:

```
1 + 2 * 3
let x = 5 in x + 10
x -> x
let id = x -> x in id 1
let twice = f -> x -> f (f x) in twice
if true then 1 else 2
```

## Original OCaml Version

The original OCaml implementation can still be found in the `/lib` and `/bin` directories. To build and run the OCaml version:

This project originally used [Dune](https://dune.build/) to build and run tests.

There are a couple of options to get Dune:
- If you already have [Nix](https://nixos.org), you can run `nix develop` in the root directory of this repo. That will install Dune with required OPAM packages.
- You can install Dune and the required OPAM packages manually, following the normal Dune documentation.

## Instructions to Run the Web Version
To run the web version (OCaml compiled to JavaScript):
Run `python3 -m http.server --bind 127.0.0.1` in the root directory and open `http://localhost:8000` in your browser.

Run `python3 -m http.serever --bind 127.0.0.1` in the root directory and open `http://localhost:8000` in your browser.
54 changes: 54 additions & 0 deletions src/ast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::fmt;

#[derive(Debug, Clone, PartialEq)]
pub enum UnaryOp {
Not,
Neg,
}

#[derive(Debug, Clone, PartialEq)]
pub enum BinaryOp {
Equal,
Add,
Mul,
And,
Or,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Var(String),
Int(i32),
Bool(bool),
OpUnary(UnaryOp, Box<Expr>),
OpBinary(BinaryOp, Box<Expr>, Box<Expr>),
Closure(String, Box<Expr>),
Application(Box<Expr>, Box<Expr>),
Let(String, Box<Expr>, Box<Expr>),
If(Box<Expr>, Box<Expr>, Box<Expr>),
}

impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Expr::Var(x) => write!(f, "{}", x),
Expr::Int(n) => write!(f, "{}", n),
Expr::Bool(b) => write!(f, "{}", b),
Expr::OpUnary(op, e) => match op {
UnaryOp::Not => write!(f, "not {}", e),
UnaryOp::Neg => write!(f, "-{}", e),
},
Expr::OpBinary(op, e1, e2) => match op {
BinaryOp::Equal => write!(f, "{} = {}", e1, e2),
BinaryOp::Add => write!(f, "{} + {}", e1, e2),
BinaryOp::Mul => write!(f, "{} * {}", e1, e2),
BinaryOp::And => write!(f, "{} && {}", e1, e2),
BinaryOp::Or => write!(f, "{} || {}", e1, e2),
},
Expr::Closure(x, e) => write!(f, "{} -> {}", x, e),
Expr::Application(e1, e2) => write!(f, "({}) ({})", e1, e2),
Expr::Let(x, e1, e2) => write!(f, "let {} = {} in {}", x, e1, e2),
Expr::If(e1, e2, e3) => write!(f, "if {} then {} else {}", e1, e2, e3),
}
}
}
99 changes: 99 additions & 0 deletions src/eval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::ast::{BinaryOp, Expr, UnaryOp};

fn subst(e: &Expr, v: &Expr, x: &str) -> Expr {
match e {
Expr::Int(_) | Expr::Bool(_) => e.clone(),
Expr::Var(y) => {
if y == x {
v.clone()
} else {
e.clone()
}
}
Expr::OpUnary(uop, e1) => Expr::OpUnary(uop.clone(), Box::new(subst(e1, v, x))),
Expr::OpBinary(bop, e1, e2) => Expr::OpBinary(
bop.clone(),
Box::new(subst(e1, v, x)),
Box::new(subst(e2, v, x)),
),
Expr::If(cond, e1, e2) => Expr::If(
Box::new(subst(cond, v, x)),
Box::new(subst(e1, v, x)),
Box::new(subst(e2, v, x)),
),
Expr::Closure(y, e1) => {
if y == x {
e.clone()
} else {
Expr::Closure(y.clone(), Box::new(subst(e1, v, x)))
}
}
Expr::Application(e1, e2) => {
Expr::Application(Box::new(subst(e1, v, x)), Box::new(subst(e2, v, x)))
}
Expr::Let(y, e1, e2) => {
let e1_prime = subst(e1, v, x);
if y == x {
Expr::Let(y.clone(), Box::new(e1_prime), e2.clone())
} else {
Expr::Let(y.clone(), Box::new(e1_prime), Box::new(subst(e2, v, x)))
}
}
}
}

pub fn eval(e: &Expr) -> Result<Expr, String> {
match e {
Expr::Int(_) | Expr::Bool(_) | Expr::Closure(_, _) => Ok(e.clone()),
Expr::Var(x) => Err(format!("unbound variable {} while evaluating {}", x, e)),
Expr::OpUnary(uop, e1) => eval_uop(uop, e1),
Expr::OpBinary(bop, e1, e2) => eval_bop(bop, e1, e2),
Expr::Application(e1, e2) => {
let e1_val = eval(e1)?;
match e1_val {
Expr::Closure(x, body) => {
let substituted = subst(&body, e2, &x);
eval(&substituted)
}
_ => Err("application of non-closure".to_string()),
}
}
Expr::Let(x, e1, e2) => {
let e1_val = eval(e1)?;
let substituted = subst(e2, &e1_val, x);
eval(&substituted)
}
Expr::If(cond, e_then, e_else) => {
let cond_val = eval(cond)?;
match cond_val {
Expr::Bool(true) => eval(e_then),
Expr::Bool(false) => eval(e_else),
_ => Err("guard must be bool".to_string()),
}
}
}
}

fn eval_uop(uop: &UnaryOp, e1: &Expr) -> Result<Expr, String> {
let e1_val = eval(e1)?;
match (uop, e1_val) {
(UnaryOp::Not, Expr::Bool(true)) => Ok(Expr::Bool(false)),
(UnaryOp::Not, Expr::Bool(false)) => Ok(Expr::Bool(true)),
(UnaryOp::Neg, Expr::Int(a)) => Ok(Expr::Int(-a)),
_ => Err("unary op with given operand not defined".to_string()),
}
}

fn eval_bop(bop: &BinaryOp, e1: &Expr, e2: &Expr) -> Result<Expr, String> {
let e1_val = eval(e1)?;
let e2_val = eval(e2)?;
match (bop, e1_val, e2_val) {
(BinaryOp::Add, Expr::Int(a), Expr::Int(b)) => Ok(Expr::Int(a + b)),
(BinaryOp::Mul, Expr::Int(a), Expr::Int(b)) => Ok(Expr::Int(a * b)),
(BinaryOp::And, Expr::Bool(a), Expr::Bool(b)) => Ok(Expr::Bool(a && b)),
(BinaryOp::Or, Expr::Bool(a), Expr::Bool(b)) => Ok(Expr::Bool(a || b)),
(BinaryOp::Equal, Expr::Int(a), Expr::Int(b)) => Ok(Expr::Bool(a == b)),
(BinaryOp::Equal, Expr::Bool(a), Expr::Bool(b)) => Ok(Expr::Bool(a == b)),
_ => Err("binary op with given operands not defined".to_string()),
}
}
Loading
Loading