type-lang 1.0.0

Type representation, unification, and inference scaffolding.
Documentation
# type-lang v0.2.0 — The unification core

**The core the scaffold was built for.** v0.2.0 implements the soundness-critical
layer: a representation for types, inference variables, and first-order unification
with an occurs check. A type checker is written against exactly this — make two
types equal, find out what that forces the unknowns to be, or learn precisely why
they cannot be made equal. Everything a particular language adds on top (its
primitives, its subtyping, its coercions) is expressed in terms of this core, which
stores and compares type constructors but never interprets them.

## What is type-lang?

The type-system substrate of a compiler front-end. It owns the type representation
and the unification/substitution machinery, and nothing else — it does no parsing
and renders no diagnostics. It assigns types to a syntax tree the caller walks, and
its structured errors map cleanly onto a rendered diagnostic in the caller's own
type names. It is one of the semantic crates of the `-lang` language-construction
family.

## What's new in 0.2.0

### `Type` — the type term

A type is one of two shapes: an inference variable, or a constructor applied to
zero or more argument types. Those two shapes describe every first-order type — a
primitive is a constructor with no arguments, `List<int>` is `List` applied to
`int`, and a function is a constructor applied to its parameter and result types.

```rust
use type_lang::{TyCon, Type};

const FUNCTION: TyCon = TyCon::new(0);
const INT: TyCon = TyCon::new(1);
const BOOL: TyCon = TyCon::new(2);

// (int) -> bool
let signature = Type::app(FUNCTION, [Type::con(INT), Type::con(BOOL)]);
assert_eq!(signature.head(), Some(FUNCTION));
assert_eq!(signature.args().len(), 2);
```

Constructors (`TyCon`) are opaque numeric tags the consumer assigns and keeps
stable, so the same core serves any language. Variables (`TyVar`) are opaque handles
minted by the unifier; there is no public constructor, so a variable can only come
from the unifier that can resolve it.

### `Unifier` — variables, substitution, and unification

The `Unifier` mints fresh variables, makes two types equal — recording the variable
bindings that requires — and resolves a type back to its most concrete known form.

```rust
use type_lang::{TyCon, Type, Unifier};

const FUNCTION: TyCon = TyCon::new(0);
const INT: TyCon = TyCon::new(1);

let mut unifier = Unifier::new();
let arg = unifier.fresh();
let ret = unifier.fresh();

// Unify the inferred (?arg) -> ?ret against the known (int) -> int.
let inferred = Type::app(FUNCTION, [Type::var(arg), Type::var(ret)]);
let known = Type::app(FUNCTION, [Type::con(INT), Type::con(INT)]);
unifier.unify(&inferred, &known).expect("same constructor and arity");

assert_eq!(unifier.resolve(&Type::var(arg)), Type::con(INT));
assert_eq!(unifier.resolve(&Type::var(ret)), Type::con(INT));
```

Unification is the textbook first-order algorithm: two constructors match only on
equal head and arity, and their arguments are then unified pairwise; a variable
unifies with any type by being bound to it, guarded by an occurs check so a variable
can never be bound to a type that contains it. The substitution produced is a
most-general unifier. The implementation is iterative — unification and the occurs
check use explicit work stacks rather than recursion — so an arbitrarily deep type
cannot overflow the call stack.

`unify` is not transactional: a call that binds a variable and then hits a conflict
keeps the binding it already made. To try a unification without committing, clone
the unifier first and keep the clone only on success.

### `TypeError` — defined failures, not panics

Unification fails in exactly two ways, each carrying the offending types resolved
under the substitution at the point of failure:

```rust
use type_lang::{TyCon, Type, TypeError, Unifier};

const INT: TyCon = TyCon::new(0);
const LIST: TyCon = TyCon::new(1);

let mut unifier = Unifier::new();

// A constructor clash.
let err = unifier.unify(&Type::con(INT), &Type::con(LIST)).unwrap_err();
assert!(matches!(err, TypeError::Mismatch { .. }));

// An infinite type, caught by the occurs check.
let v = unifier.fresh();
let recursive = Type::app(LIST, [Type::var(v)]);
let err = unifier.unify(&Type::var(v), &recursive).unwrap_err();
assert!(matches!(err, TypeError::Occurs { .. }));
```

`TypeError` is `#[non_exhaustive]`, so a future failure mode is an additive change.

### `serde`, property tests, and benchmarks

Behind the `serde` feature, `Type`, `TyVar`, `TyCon`, and the whole `Unifier`
serialise, so a type term or an entire inference state round-trips through any serde
format. The unification invariants from `dev/DIRECTIVES.md` — soundness, symmetry,
idempotent resolution, reflexivity, and the occurs check — are covered by `proptest`
over randomly generated type terms, and `criterion` benchmarks track variable
minting, unification, and resolution as the terms grow.

## Dependencies: ast, symbol, diag

The roadmap slated `ast-lang`, `symbol-lang`, and `diag-lang` to be wired at this
stage "when first used." They are **not** wired in v0.2.0, and the deferral is
recorded in `dev/ROADMAP.md` with its reason: the unification core is self-contained
and uses none of them. Wiring an unused dependency would violate the project's
dependency policy. Integration with a syntax tree, a symbol table, and a diagnostic
renderer is the consumer's to make — `TypeError` is designed to map onto a
`diag-lang` diagnostic — and these crates will be wired only if and when a
consumer-facing pass inside type-lang needs them.

## Breaking changes

**None against published code.** This is the first release with a public surface;
v0.1.0 had none. The surface is documented in full in `docs/API.md` and settles
across the rest of the 0.x series before it is frozen at `1.0`.

## Verification

```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo +1.85 build --all-features
cargo audit
cargo deny check
```

All green on Linux (WSL2 Ubuntu) and Windows x86_64, Rust stable 1.95 and MSRV 1.85.
Counts at this tag: 29 unit and integration tests plus 19 doctests on default
features; 32 plus 19 with `--all-features` (the serde round-trips). The `criterion`
suite shows unification scaling with term size as expected.

## What's next

- **1.0.0 — API freeze.** Settle and freeze the public surface, mark `docs/API.md`
  stable with a recorded SemVer promise, and run the full property-test and
  benchmark suite green across all three platforms.

## Installation

```toml
[dependencies]
type-lang = "0.2"
```

MSRV: Rust 1.85.

## Documentation

- [README]https://github.com/jamesgober/type-lang/blob/main/README.md
- [API Reference]https://github.com/jamesgober/type-lang/blob/main/docs/API.md
- [CHANGELOG]https://github.com/jamesgober/type-lang/blob/main/CHANGELOG.md

---

**Full diff:** [`v0.1.0...v0.2.0`](https://github.com/jamesgober/type-lang/compare/v0.1.0...v0.2.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/type-lang/blob/main/CHANGELOG.md#020---2026-06-29).