| Crates.io | unbound |
| lib.rs | unbound |
| version | 0.1.2 |
| created_at | 2025-10-01 16:51:25.878216+00 |
| updated_at | 2025-10-06 13:12:42.615042+00 |
| description | Locally nameless representation with automatic capture-avoiding substitution and alpha equivalence |
| homepage | |
| repository | https://github.com/sdiehl/unbound |
| max_upload_size | |
| id | 1862968 |
| size | 56,076 |
A Rust library for working with locally nameless representations, providing automatic capture-avoiding substitution and alpha equivalence for abstract syntax trees with binding when building functional language typecheckers and compilers.
Provides two derivable macros for automatic implementation of:
Alpha: Automatically derived alpha equivalence checking that correctly
handles binding
Subst: Automatically derived capture-avoiding substitution
use unbound::prelude::*;
#[derive(Clone, Debug, Alpha, Subst)]
enum Expr {
Var(Name<Expr>),
Lam(Bind<Name<Expr>, Box<Expr>>),
App(Box<Expr>, Box<Expr>),
}
The Name<T> type uses phantom type parameters to ensure type safety at the binding level. The phantom parameter T represents the type of AST that can contain this name as a variable:
pub struct Name<T> {
string: String, // Human-readable identifier
index: usize, // Globally unique index via AtomicUsize
_phantom: PhantomData<T>,
}
This prevents mixing names from different AST types - a Name<Expr> cannot be confused with a Name<Type> at compile time, despite having identical runtime representations. The phantom type has zero runtime cost while providing complete type safety.
Names are compared by their globally unique index, not their string representation. This makes alpha-equivalence trivial for free variables (same index = same variable) while the Bind<P, T> type handles the complexity of bound variables:
Alpha checking, bound names are pushed onto a context stack with their corresponding names from the compared termAlphaCtx maintains a bijection between names in the two terms being comparedThe Subst trait's key insight is the is_var method:
trait Subst<V> {
fn is_var(&self) -> Option<SubstName<V>>;
fn subst(&self, var: &Name<V>, value: &V) -> Self;
}
The derive macro generates subst implementations that:
is_var matching the substitution targetBind constructsThe FreshM<T> context threads a FreshState containing a counter and a map of "hints" for generating human-readable names. It's implemented as a closure that takes the state:
pub struct FreshM<T> {
computation: Box<dyn FnOnce(Rc<RefCell<FreshState>>) -> T>,
}
This allows unbind operations to generate fresh names when opening binders, ensuring no accidental capture during substitution operations.
MIT Licensed. Copyright 2025 Stephen Diehl.