Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Type System

Our Calculus of Constructions implementation centers around a type system that unifies terms, types, and kinds into a single syntactic framework. The implementation demonstrates how dependent types, universe polymorphism, and definitional equality work together to create a practical dependently-typed programming language.

AST Design and Term Language

The core of our implementation lies in the unified term language that represents all syntactic categories within a single AST structure. This design reflects the CoC principle that terms, types, and kinds are all inhabitants of the same computational universe.

#![allow(unused)]
fn main() {
use std::fmt;
/// Universe levels for type theory
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Universe {
    Const(u32),                         // Concrete level: 0, 1, 2, ...
    ScopedVar(String, String),          // Scoped universe variable: (scope_name, var_name)
    Meta(u32),                          // Universe metavariable: ?u₀, ?u₁, ...
    Add(Box<Universe>, u32),            // Level + n
    Max(Box<Universe>, Box<Universe>),  // max(u, v)
    IMax(Box<Universe>, Box<Universe>), // imax(u, v)
}
/// Universe level constraints for polymorphism
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UniverseConstraint {
    /// u ≤ v
    LessEq(Universe, Universe),
    /// u = v
    Equal(Universe, Universe),
}
/// Context for universe level variables
#[derive(Debug, Clone, PartialEq)]
pub struct UniverseContext {
    /// Currently bound universe variables
    pub variables: Vec<String>,
    /// Constraints on universe levels
    pub constraints: Vec<UniverseConstraint>,
}
/// Pattern matching arms
#[derive(Debug, Clone, PartialEq)]
pub struct MatchArm {
    pub pattern: Pattern,
    pub body: Term,
}
/// Patterns for match expressions
#[derive(Debug, Clone, PartialEq)]
pub enum Pattern {
    /// Variable pattern: x
    Var(String),

    /// Constructor pattern: Cons x xs
    Constructor(String, Vec<Pattern>),

    /// Wildcard pattern: _
    Wildcard,
}
/// Top-level declarations
#[derive(Debug, Clone, PartialEq)]
pub enum Declaration {
    /// Constant definition: def name {u...} (params) : type := body
    Definition {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        body: Term,
    },

    /// Axiom: axiom name {u...} : type
    Axiom {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        ty: Term,
    },

    /// Inductive type: inductive Name {u...} (params) : Type := constructors
    Inductive {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        constructors: Vec<Constructor>,
    },

    /// Structure (single constructor inductive): structure Name {u...} :=
    /// (fields)
    Structure {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        fields: Vec<Field>,
    },
}
/// Function parameters
#[derive(Debug, Clone, PartialEq)]
pub struct Parameter {
    pub name: String,
    pub ty: Term,
    pub implicit: bool, // for {x : A} vs (x : A)
}
/// Constructor for inductive types
#[derive(Debug, Clone, PartialEq)]
pub struct Constructor {
    pub name: String,
    pub ty: Term,
}
/// Structure fields
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
    pub name: String,
    pub ty: Term,
}
/// Module containing multiple declarations
#[derive(Debug, Clone, PartialEq)]
pub struct Module {
    pub declarations: Vec<Declaration>,
}
impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Universe::Const(n) => write!(f, "{}", n),
            Universe::ScopedVar(scope, var) => write!(f, "{}::{}", scope, var),
            Universe::Meta(id) => write!(f, "?u{}", id),
            Universe::Add(u, n) => write!(f, "{}+{}", u, n),
            Universe::Max(u, v) => write!(f, "max({}, {})", u, v),
            Universe::IMax(u, v) => write!(f, "imax({}, {})", u, v),
        }
    }
}
impl fmt::Display for Term {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Term::Var(x) => write!(f, "{}", x),
            Term::App(t1, t2) => write!(f, "({} {})", t1, t2),
            Term::Abs(x, ty, body) => write!(f, "(fun {} : {} => {})", x, ty, body),
            Term::Pi(x, ty, body, implicit) => {
                if Self::occurs_free(x, body) {
                    if *implicit {
                        write!(f, "{{{}  : {}}} → {}", x, ty, body)
                    } else {
                        write!(f, "({} : {}) → {}", x, ty, body)
                    }
                } else {
                    write!(f, "{} → {}", ty, body)
                }
            }
            Term::Sort(u) => match u {
                Universe::Const(0) => write!(f, "Prop"),
                Universe::Const(n) => {
                    if *n == 1 {
                        write!(f, "Type")
                    } else {
                        write!(f, "Type {}", n - 1)
                    }
                }
                Universe::Add(ref base, n) => {
                    if let Universe::Const(1) = **base {
                        write!(f, "Type {}", n)
                    } else {
                        write!(f, "Sort {}", u)
                    }
                }
                _ => write!(f, "Sort {}", u),
            },
            Term::Let(x, ty, val, body) => write!(f, "(let {} : {} := {} in {})", x, ty, val, body),
            Term::Match(scrutinee, arms) => {
                writeln!(f, "match {} with", scrutinee)?;
                for arm in arms {
                    writeln!(f, "| {} => {}", arm.pattern, arm.body)?;
                }
                Ok(())
            }
            Term::Constructor(name, args) => {
                write!(f, "{}", name)?;
                for arg in args {
                    write!(f, " {}", arg)?;
                }
                Ok(())
            }
            Term::Const(name) => write!(f, "{}", name),
            Term::Meta(name) => write!(f, "?{}", name),
            Term::Proj(term, field) => write!(f, "{}.{}", term, field),
            Term::Sigma(x, domain, codomain) => write!(f, "Σ ({} : {}), {}", x, domain, codomain),
            Term::Pair(fst, snd) => write!(f, "⟨{}, {}⟩", fst, snd),
            Term::Fst(pair) => write!(f, "π₁({})", pair),
            Term::Snd(pair) => write!(f, "π₂({})", pair),
        }
    }
}
impl fmt::Display for Pattern {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Pattern::Var(x) => write!(f, "{}", x),
            Pattern::Constructor(name, args) => {
                write!(f, "{}", name)?;
                for arg in args {
                    write!(f, " {}", arg)?;
                }
                Ok(())
            }
            Pattern::Wildcard => write!(f, "_"),
        }
    }
}
impl Term {
    /// Check if variable occurs free in term
    pub fn occurs_free(var: &str, term: &Term) -> bool {
        match term {
            Term::Var(x) => x == var,
            Term::App(t1, t2) => Self::occurs_free(var, t1) || Self::occurs_free(var, t2),
            Term::Abs(x, ty, body) => {
                Self::occurs_free(var, ty) || (x != var && Self::occurs_free(var, body))
            }
            Term::Pi(x, ty, body, _) => {
                Self::occurs_free(var, ty) || (x != var && Self::occurs_free(var, body))
            }
            Term::Sort(_) => false,
            Term::Let(x, ty, val, body) => {
                Self::occurs_free(var, ty)
                    || Self::occurs_free(var, val)
                    || (x != var && Self::occurs_free(var, body))
            }
            Term::Match(scrutinee, arms) => {
                Self::occurs_free(var, scrutinee)
                    || arms.iter().any(|arm| Self::occurs_free(var, &arm.body))
            }
            Term::Constructor(_, args) => args.iter().any(|arg| Self::occurs_free(var, arg)),
            Term::Const(_) => false,
            Term::Meta(_) => false,
            Term::Proj(term, _) => Self::occurs_free(var, term),
            Term::Sigma(x, domain, codomain) => {
                Self::occurs_free(var, domain) || (x != var && Self::occurs_free(var, codomain))
            }
            Term::Pair(fst, snd) => Self::occurs_free(var, fst) || Self::occurs_free(var, snd),
            Term::Fst(pair) | Term::Snd(pair) => Self::occurs_free(var, pair),
        }
    }
}
impl UniverseContext {
    pub fn new() -> Self {
        UniverseContext {
            variables: Vec::new(),
            constraints: Vec::new(),
        }
    }

    /// Add a universe variable to the context
    pub fn add_var(&mut self, name: String) {
        if !self.variables.contains(&name) {
            self.variables.push(name);
        }
    }

    /// Add a constraint to the context
    pub fn add_constraint(&mut self, constraint: UniverseConstraint) {
        self.constraints.push(constraint);
    }

    /// Check if a universe variable is bound in this context
    pub fn contains(&self, name: &str) -> bool {
        self.variables.contains(&name.to_string())
    }

    /// Extend context with variables from another context
    pub fn extend(&self, other: &UniverseContext) -> Self {
        let mut new_ctx = self.clone();
        for var in &other.variables {
            new_ctx.add_var(var.clone());
        }
        for constraint in &other.constraints {
            new_ctx.add_constraint(constraint.clone());
        }
        new_ctx
    }

    /// Generate a fresh universe variable name
    pub fn fresh_var(&self, base: &str) -> String {
        let mut counter = 0;
        loop {
            let name = if counter == 0 {
                base.to_string()
            } else {
                format!("{}{}", base, counter)
            };
            if !self.contains(&name) {
                return name;
            }
            counter += 1;
        }
    }
}
impl Default for UniverseContext {
    fn default() -> Self {
        Self::new()
    }
}
impl Universe {
    /// Create a scoped universe variable
    pub fn scoped_var(scope: String, name: String) -> Self {
        Universe::ScopedVar(scope, name)
    }
}
/// Core terms in the Calculus of Constructions
#[derive(Debug, Clone, PartialEq)]
pub enum Term {
    /// Variables: x, y, z
    Var(String),

    /// Application: f a
    App(Box<Term>, Box<Term>),

    /// Lambda abstraction: λ x : A, t  (written as fun x : A => t)
    Abs(String, Box<Term>, Box<Term>),

    /// Dependent product: Π x : A, B  (written as (x : A) → B or {x : A} → B)
    Pi(String, Box<Term>, Box<Term>, bool), // bool = implicit

    /// Sort/Type: Sort u, Type, Prop
    Sort(Universe),

    /// Let binding: let x := t in s
    Let(String, Box<Term>, Box<Term>, Box<Term>),

    /// Match expression with patterns
    Match(Box<Term>, Vec<MatchArm>),

    /// Inductive type constructor
    Constructor(String, Vec<Term>),

    /// Local constant (for definitions and axioms)
    Const(String),

    /// Meta-variable for type inference
    Meta(String),

    /// Field projection: term.field
    Proj(Box<Term>, String),

    /// Dependent pair type (Sigma type): Σ (x : A), B
    Sigma(String, Box<Term>, Box<Term>),

    /// Pair constructor: ⟨a, b⟩
    Pair(Box<Term>, Box<Term>),

    /// First projection: π₁
    Fst(Box<Term>),

    /// Second projection: π₂
    Snd(Box<Term>),
}
}

Our Term enum demonstrates the unification principle by using the same constructors to represent functions (Abs), function types (Pi), type constructors, and logical propositions. The same application constructor (App) represents both function application and type application, eliminating the artificial boundaries present in stratified type systems.

Variables (Var) and constants (Const) form the basic building blocks of our term language. Variables represent both term-level bindings and type-level bindings, while constants refer to defined names in the global context. The implementation treats both categories uniformly, enabling the same binding mechanisms to work across all abstraction levels.

Function Types and Lambda Abstractions

The Pi constructor represents dependent product types, generalizing both simple function types and universal quantification. When the bound variable appears in the body type, we obtain dependency; when it does not appear, the Pi-type reduces to a simple function type.

#![allow(unused)]
fn main() {
// Pi(variable_name, domain_type, codomain_type, is_implicit)
Pi("x".to_string(), Box::new(nat_type), Box::new(vec_type), false)
}

The boolean flag indicates whether the parameter should be treated as implicit, enabling our implementation to support both explicit and implicit argument passing. Lambda abstractions (Abs) create inhabitants of Pi-types, with the same constructor serving for both computational functions and proof terms.

Pattern Matching and Inductive Elimination

#![allow(unused)]
fn main() {
use std::fmt;
/// Core terms in the Calculus of Constructions
#[derive(Debug, Clone, PartialEq)]
pub enum Term {
    /// Variables: x, y, z
    Var(String),

    /// Application: f a
    App(Box<Term>, Box<Term>),

    /// Lambda abstraction: λ x : A, t  (written as fun x : A => t)
    Abs(String, Box<Term>, Box<Term>),

    /// Dependent product: Π x : A, B  (written as (x : A) → B or {x : A} → B)
    Pi(String, Box<Term>, Box<Term>, bool), // bool = implicit

    /// Sort/Type: Sort u, Type, Prop
    Sort(Universe),

    /// Let binding: let x := t in s
    Let(String, Box<Term>, Box<Term>, Box<Term>),

    /// Match expression with patterns
    Match(Box<Term>, Vec<MatchArm>),

    /// Inductive type constructor
    Constructor(String, Vec<Term>),

    /// Local constant (for definitions and axioms)
    Const(String),

    /// Meta-variable for type inference
    Meta(String),

    /// Field projection: term.field
    Proj(Box<Term>, String),

    /// Dependent pair type (Sigma type): Σ (x : A), B
    Sigma(String, Box<Term>, Box<Term>),

    /// Pair constructor: ⟨a, b⟩
    Pair(Box<Term>, Box<Term>),

    /// First projection: π₁
    Fst(Box<Term>),

    /// Second projection: π₂
    Snd(Box<Term>),
}
/// Universe levels for type theory
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Universe {
    Const(u32),                         // Concrete level: 0, 1, 2, ...
    ScopedVar(String, String),          // Scoped universe variable: (scope_name, var_name)
    Meta(u32),                          // Universe metavariable: ?u₀, ?u₁, ...
    Add(Box<Universe>, u32),            // Level + n
    Max(Box<Universe>, Box<Universe>),  // max(u, v)
    IMax(Box<Universe>, Box<Universe>), // imax(u, v)
}
/// Universe level constraints for polymorphism
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UniverseConstraint {
    /// u ≤ v
    LessEq(Universe, Universe),
    /// u = v
    Equal(Universe, Universe),
}
/// Context for universe level variables
#[derive(Debug, Clone, PartialEq)]
pub struct UniverseContext {
    /// Currently bound universe variables
    pub variables: Vec<String>,
    /// Constraints on universe levels
    pub constraints: Vec<UniverseConstraint>,
}
/// Patterns for match expressions
#[derive(Debug, Clone, PartialEq)]
pub enum Pattern {
    /// Variable pattern: x
    Var(String),

    /// Constructor pattern: Cons x xs
    Constructor(String, Vec<Pattern>),

    /// Wildcard pattern: _
    Wildcard,
}
/// Top-level declarations
#[derive(Debug, Clone, PartialEq)]
pub enum Declaration {
    /// Constant definition: def name {u...} (params) : type := body
    Definition {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        body: Term,
    },

    /// Axiom: axiom name {u...} : type
    Axiom {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        ty: Term,
    },

    /// Inductive type: inductive Name {u...} (params) : Type := constructors
    Inductive {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        constructors: Vec<Constructor>,
    },

    /// Structure (single constructor inductive): structure Name {u...} :=
    /// (fields)
    Structure {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        fields: Vec<Field>,
    },
}
/// Function parameters
#[derive(Debug, Clone, PartialEq)]
pub struct Parameter {
    pub name: String,
    pub ty: Term,
    pub implicit: bool, // for {x : A} vs (x : A)
}
/// Constructor for inductive types
#[derive(Debug, Clone, PartialEq)]
pub struct Constructor {
    pub name: String,
    pub ty: Term,
}
/// Structure fields
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
    pub name: String,
    pub ty: Term,
}
/// Module containing multiple declarations
#[derive(Debug, Clone, PartialEq)]
pub struct Module {
    pub declarations: Vec<Declaration>,
}
impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Universe::Const(n) => write!(f, "{}", n),
            Universe::ScopedVar(scope, var) => write!(f, "{}::{}", scope, var),
            Universe::Meta(id) => write!(f, "?u{}", id),
            Universe::Add(u, n) => write!(f, "{}+{}", u, n),
            Universe::Max(u, v) => write!(f, "max({}, {})", u, v),
            Universe::IMax(u, v) => write!(f, "imax({}, {})", u, v),
        }
    }
}
impl fmt::Display for Term {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Term::Var(x) => write!(f, "{}", x),
            Term::App(t1, t2) => write!(f, "({} {})", t1, t2),
            Term::Abs(x, ty, body) => write!(f, "(fun {} : {} => {})", x, ty, body),
            Term::Pi(x, ty, body, implicit) => {
                if Self::occurs_free(x, body) {
                    if *implicit {
                        write!(f, "{{{}  : {}}} → {}", x, ty, body)
                    } else {
                        write!(f, "({} : {}) → {}", x, ty, body)
                    }
                } else {
                    write!(f, "{} → {}", ty, body)
                }
            }
            Term::Sort(u) => match u {
                Universe::Const(0) => write!(f, "Prop"),
                Universe::Const(n) => {
                    if *n == 1 {
                        write!(f, "Type")
                    } else {
                        write!(f, "Type {}", n - 1)
                    }
                }
                Universe::Add(ref base, n) => {
                    if let Universe::Const(1) = **base {
                        write!(f, "Type {}", n)
                    } else {
                        write!(f, "Sort {}", u)
                    }
                }
                _ => write!(f, "Sort {}", u),
            },
            Term::Let(x, ty, val, body) => write!(f, "(let {} : {} := {} in {})", x, ty, val, body),
            Term::Match(scrutinee, arms) => {
                writeln!(f, "match {} with", scrutinee)?;
                for arm in arms {
                    writeln!(f, "| {} => {}", arm.pattern, arm.body)?;
                }
                Ok(())
            }
            Term::Constructor(name, args) => {
                write!(f, "{}", name)?;
                for arg in args {
                    write!(f, " {}", arg)?;
                }
                Ok(())
            }
            Term::Const(name) => write!(f, "{}", name),
            Term::Meta(name) => write!(f, "?{}", name),
            Term::Proj(term, field) => write!(f, "{}.{}", term, field),
            Term::Sigma(x, domain, codomain) => write!(f, "Σ ({} : {}), {}", x, domain, codomain),
            Term::Pair(fst, snd) => write!(f, "⟨{}, {}⟩", fst, snd),
            Term::Fst(pair) => write!(f, "π₁({})", pair),
            Term::Snd(pair) => write!(f, "π₂({})", pair),
        }
    }
}
impl fmt::Display for Pattern {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Pattern::Var(x) => write!(f, "{}", x),
            Pattern::Constructor(name, args) => {
                write!(f, "{}", name)?;
                for arg in args {
                    write!(f, " {}", arg)?;
                }
                Ok(())
            }
            Pattern::Wildcard => write!(f, "_"),
        }
    }
}
impl Term {
    /// Check if variable occurs free in term
    pub fn occurs_free(var: &str, term: &Term) -> bool {
        match term {
            Term::Var(x) => x == var,
            Term::App(t1, t2) => Self::occurs_free(var, t1) || Self::occurs_free(var, t2),
            Term::Abs(x, ty, body) => {
                Self::occurs_free(var, ty) || (x != var && Self::occurs_free(var, body))
            }
            Term::Pi(x, ty, body, _) => {
                Self::occurs_free(var, ty) || (x != var && Self::occurs_free(var, body))
            }
            Term::Sort(_) => false,
            Term::Let(x, ty, val, body) => {
                Self::occurs_free(var, ty)
                    || Self::occurs_free(var, val)
                    || (x != var && Self::occurs_free(var, body))
            }
            Term::Match(scrutinee, arms) => {
                Self::occurs_free(var, scrutinee)
                    || arms.iter().any(|arm| Self::occurs_free(var, &arm.body))
            }
            Term::Constructor(_, args) => args.iter().any(|arg| Self::occurs_free(var, arg)),
            Term::Const(_) => false,
            Term::Meta(_) => false,
            Term::Proj(term, _) => Self::occurs_free(var, term),
            Term::Sigma(x, domain, codomain) => {
                Self::occurs_free(var, domain) || (x != var && Self::occurs_free(var, codomain))
            }
            Term::Pair(fst, snd) => Self::occurs_free(var, fst) || Self::occurs_free(var, snd),
            Term::Fst(pair) | Term::Snd(pair) => Self::occurs_free(var, pair),
        }
    }
}
impl UniverseContext {
    pub fn new() -> Self {
        UniverseContext {
            variables: Vec::new(),
            constraints: Vec::new(),
        }
    }

    /// Add a universe variable to the context
    pub fn add_var(&mut self, name: String) {
        if !self.variables.contains(&name) {
            self.variables.push(name);
        }
    }

    /// Add a constraint to the context
    pub fn add_constraint(&mut self, constraint: UniverseConstraint) {
        self.constraints.push(constraint);
    }

    /// Check if a universe variable is bound in this context
    pub fn contains(&self, name: &str) -> bool {
        self.variables.contains(&name.to_string())
    }

    /// Extend context with variables from another context
    pub fn extend(&self, other: &UniverseContext) -> Self {
        let mut new_ctx = self.clone();
        for var in &other.variables {
            new_ctx.add_var(var.clone());
        }
        for constraint in &other.constraints {
            new_ctx.add_constraint(constraint.clone());
        }
        new_ctx
    }

    /// Generate a fresh universe variable name
    pub fn fresh_var(&self, base: &str) -> String {
        let mut counter = 0;
        loop {
            let name = if counter == 0 {
                base.to_string()
            } else {
                format!("{}{}", base, counter)
            };
            if !self.contains(&name) {
                return name;
            }
            counter += 1;
        }
    }
}
impl Default for UniverseContext {
    fn default() -> Self {
        Self::new()
    }
}
impl Universe {
    /// Create a scoped universe variable
    pub fn scoped_var(scope: String, name: String) -> Self {
        Universe::ScopedVar(scope, name)
    }
}
/// Pattern matching arms
#[derive(Debug, Clone, PartialEq)]
pub struct MatchArm {
    pub pattern: Pattern,
    pub body: Term,
}
}

Our implementation includes comprehensive pattern matching through the Match constructor, which enables elimination of inductive types. Each match arm specifies a constructor pattern and the corresponding elimination term, providing the computational content needed for inductive reasoning.

The pattern matching implementation supports nested patterns and variable bindings, enabling destructions of complex inductive data structures while maintaining type safety through exhaustiveness checking.

Universe System Implementation

The implementation includes a comprehensive universe system that prevents logical paradoxes while enabling flexible type-level computation. Our universe design supports both concrete levels and polymorphic universe variables.

#![allow(unused)]
fn main() {
use std::fmt;
/// Core terms in the Calculus of Constructions
#[derive(Debug, Clone, PartialEq)]
pub enum Term {
    /// Variables: x, y, z
    Var(String),

    /// Application: f a
    App(Box<Term>, Box<Term>),

    /// Lambda abstraction: λ x : A, t  (written as fun x : A => t)
    Abs(String, Box<Term>, Box<Term>),

    /// Dependent product: Π x : A, B  (written as (x : A) → B or {x : A} → B)
    Pi(String, Box<Term>, Box<Term>, bool), // bool = implicit

    /// Sort/Type: Sort u, Type, Prop
    Sort(Universe),

    /// Let binding: let x := t in s
    Let(String, Box<Term>, Box<Term>, Box<Term>),

    /// Match expression with patterns
    Match(Box<Term>, Vec<MatchArm>),

    /// Inductive type constructor
    Constructor(String, Vec<Term>),

    /// Local constant (for definitions and axioms)
    Const(String),

    /// Meta-variable for type inference
    Meta(String),

    /// Field projection: term.field
    Proj(Box<Term>, String),

    /// Dependent pair type (Sigma type): Σ (x : A), B
    Sigma(String, Box<Term>, Box<Term>),

    /// Pair constructor: ⟨a, b⟩
    Pair(Box<Term>, Box<Term>),

    /// First projection: π₁
    Fst(Box<Term>),

    /// Second projection: π₂
    Snd(Box<Term>),
}
/// Universe level constraints for polymorphism
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UniverseConstraint {
    /// u ≤ v
    LessEq(Universe, Universe),
    /// u = v
    Equal(Universe, Universe),
}
/// Context for universe level variables
#[derive(Debug, Clone, PartialEq)]
pub struct UniverseContext {
    /// Currently bound universe variables
    pub variables: Vec<String>,
    /// Constraints on universe levels
    pub constraints: Vec<UniverseConstraint>,
}
/// Pattern matching arms
#[derive(Debug, Clone, PartialEq)]
pub struct MatchArm {
    pub pattern: Pattern,
    pub body: Term,
}
/// Patterns for match expressions
#[derive(Debug, Clone, PartialEq)]
pub enum Pattern {
    /// Variable pattern: x
    Var(String),

    /// Constructor pattern: Cons x xs
    Constructor(String, Vec<Pattern>),

    /// Wildcard pattern: _
    Wildcard,
}
/// Top-level declarations
#[derive(Debug, Clone, PartialEq)]
pub enum Declaration {
    /// Constant definition: def name {u...} (params) : type := body
    Definition {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        body: Term,
    },

    /// Axiom: axiom name {u...} : type
    Axiom {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        ty: Term,
    },

    /// Inductive type: inductive Name {u...} (params) : Type := constructors
    Inductive {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        constructors: Vec<Constructor>,
    },

    /// Structure (single constructor inductive): structure Name {u...} :=
    /// (fields)
    Structure {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        fields: Vec<Field>,
    },
}
/// Function parameters
#[derive(Debug, Clone, PartialEq)]
pub struct Parameter {
    pub name: String,
    pub ty: Term,
    pub implicit: bool, // for {x : A} vs (x : A)
}
/// Constructor for inductive types
#[derive(Debug, Clone, PartialEq)]
pub struct Constructor {
    pub name: String,
    pub ty: Term,
}
/// Structure fields
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
    pub name: String,
    pub ty: Term,
}
/// Module containing multiple declarations
#[derive(Debug, Clone, PartialEq)]
pub struct Module {
    pub declarations: Vec<Declaration>,
}
impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Universe::Const(n) => write!(f, "{}", n),
            Universe::ScopedVar(scope, var) => write!(f, "{}::{}", scope, var),
            Universe::Meta(id) => write!(f, "?u{}", id),
            Universe::Add(u, n) => write!(f, "{}+{}", u, n),
            Universe::Max(u, v) => write!(f, "max({}, {})", u, v),
            Universe::IMax(u, v) => write!(f, "imax({}, {})", u, v),
        }
    }
}
impl fmt::Display for Term {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Term::Var(x) => write!(f, "{}", x),
            Term::App(t1, t2) => write!(f, "({} {})", t1, t2),
            Term::Abs(x, ty, body) => write!(f, "(fun {} : {} => {})", x, ty, body),
            Term::Pi(x, ty, body, implicit) => {
                if Self::occurs_free(x, body) {
                    if *implicit {
                        write!(f, "{{{}  : {}}} → {}", x, ty, body)
                    } else {
                        write!(f, "({} : {}) → {}", x, ty, body)
                    }
                } else {
                    write!(f, "{} → {}", ty, body)
                }
            }
            Term::Sort(u) => match u {
                Universe::Const(0) => write!(f, "Prop"),
                Universe::Const(n) => {
                    if *n == 1 {
                        write!(f, "Type")
                    } else {
                        write!(f, "Type {}", n - 1)
                    }
                }
                Universe::Add(ref base, n) => {
                    if let Universe::Const(1) = **base {
                        write!(f, "Type {}", n)
                    } else {
                        write!(f, "Sort {}", u)
                    }
                }
                _ => write!(f, "Sort {}", u),
            },
            Term::Let(x, ty, val, body) => write!(f, "(let {} : {} := {} in {})", x, ty, val, body),
            Term::Match(scrutinee, arms) => {
                writeln!(f, "match {} with", scrutinee)?;
                for arm in arms {
                    writeln!(f, "| {} => {}", arm.pattern, arm.body)?;
                }
                Ok(())
            }
            Term::Constructor(name, args) => {
                write!(f, "{}", name)?;
                for arg in args {
                    write!(f, " {}", arg)?;
                }
                Ok(())
            }
            Term::Const(name) => write!(f, "{}", name),
            Term::Meta(name) => write!(f, "?{}", name),
            Term::Proj(term, field) => write!(f, "{}.{}", term, field),
            Term::Sigma(x, domain, codomain) => write!(f, "Σ ({} : {}), {}", x, domain, codomain),
            Term::Pair(fst, snd) => write!(f, "⟨{}, {}⟩", fst, snd),
            Term::Fst(pair) => write!(f, "π₁({})", pair),
            Term::Snd(pair) => write!(f, "π₂({})", pair),
        }
    }
}
impl fmt::Display for Pattern {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Pattern::Var(x) => write!(f, "{}", x),
            Pattern::Constructor(name, args) => {
                write!(f, "{}", name)?;
                for arg in args {
                    write!(f, " {}", arg)?;
                }
                Ok(())
            }
            Pattern::Wildcard => write!(f, "_"),
        }
    }
}
impl Term {
    /// Check if variable occurs free in term
    pub fn occurs_free(var: &str, term: &Term) -> bool {
        match term {
            Term::Var(x) => x == var,
            Term::App(t1, t2) => Self::occurs_free(var, t1) || Self::occurs_free(var, t2),
            Term::Abs(x, ty, body) => {
                Self::occurs_free(var, ty) || (x != var && Self::occurs_free(var, body))
            }
            Term::Pi(x, ty, body, _) => {
                Self::occurs_free(var, ty) || (x != var && Self::occurs_free(var, body))
            }
            Term::Sort(_) => false,
            Term::Let(x, ty, val, body) => {
                Self::occurs_free(var, ty)
                    || Self::occurs_free(var, val)
                    || (x != var && Self::occurs_free(var, body))
            }
            Term::Match(scrutinee, arms) => {
                Self::occurs_free(var, scrutinee)
                    || arms.iter().any(|arm| Self::occurs_free(var, &arm.body))
            }
            Term::Constructor(_, args) => args.iter().any(|arg| Self::occurs_free(var, arg)),
            Term::Const(_) => false,
            Term::Meta(_) => false,
            Term::Proj(term, _) => Self::occurs_free(var, term),
            Term::Sigma(x, domain, codomain) => {
                Self::occurs_free(var, domain) || (x != var && Self::occurs_free(var, codomain))
            }
            Term::Pair(fst, snd) => Self::occurs_free(var, fst) || Self::occurs_free(var, snd),
            Term::Fst(pair) | Term::Snd(pair) => Self::occurs_free(var, pair),
        }
    }
}
impl UniverseContext {
    pub fn new() -> Self {
        UniverseContext {
            variables: Vec::new(),
            constraints: Vec::new(),
        }
    }

    /// Add a universe variable to the context
    pub fn add_var(&mut self, name: String) {
        if !self.variables.contains(&name) {
            self.variables.push(name);
        }
    }

    /// Add a constraint to the context
    pub fn add_constraint(&mut self, constraint: UniverseConstraint) {
        self.constraints.push(constraint);
    }

    /// Check if a universe variable is bound in this context
    pub fn contains(&self, name: &str) -> bool {
        self.variables.contains(&name.to_string())
    }

    /// Extend context with variables from another context
    pub fn extend(&self, other: &UniverseContext) -> Self {
        let mut new_ctx = self.clone();
        for var in &other.variables {
            new_ctx.add_var(var.clone());
        }
        for constraint in &other.constraints {
            new_ctx.add_constraint(constraint.clone());
        }
        new_ctx
    }

    /// Generate a fresh universe variable name
    pub fn fresh_var(&self, base: &str) -> String {
        let mut counter = 0;
        loop {
            let name = if counter == 0 {
                base.to_string()
            } else {
                format!("{}{}", base, counter)
            };
            if !self.contains(&name) {
                return name;
            }
            counter += 1;
        }
    }
}
impl Default for UniverseContext {
    fn default() -> Self {
        Self::new()
    }
}
impl Universe {
    /// Create a scoped universe variable
    pub fn scoped_var(scope: String, name: String) -> Self {
        Universe::ScopedVar(scope, name)
    }
}
/// Universe levels for type theory
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Universe {
    Const(u32),                         // Concrete level: 0, 1, 2, ...
    ScopedVar(String, String),          // Scoped universe variable: (scope_name, var_name)
    Meta(u32),                          // Universe metavariable: ?u₀, ?u₁, ...
    Add(Box<Universe>, u32),            // Level + n
    Max(Box<Universe>, Box<Universe>),  // max(u, v)
    IMax(Box<Universe>, Box<Universe>), // imax(u, v)
}
}

Universe Levels and Arithmetic

The universe system supports multiple forms of universe expressions that enable both concrete level specifications and polymorphic universe computations. Concrete levels represented by Const(n) specify particular universe levels like Type 0, Type 1, and so forth, forming the foundation of the universe hierarchy and providing definite homes for specific types. Universe variables through ScopedVar enable universe polymorphism by allowing definitions to be parameterized over universe levels, permitting the same definition to work at multiple universe levels simultaneously. Level arithmetic operations using Add enable universe level computations such as u + 1, supporting common patterns where a type constructor must live at a universe level one higher than its parameter. Maximum operations provided by Max and IMax compute universe level selections that choose the higher of two levels, with IMax implementing the impredicative maximum used in proof-relevant contexts where the universe level computation must respect the logical structure of the proof.

Universe Constraint Solving

#![allow(unused)]
fn main() {
use std::fmt;
/// Core terms in the Calculus of Constructions
#[derive(Debug, Clone, PartialEq)]
pub enum Term {
    /// Variables: x, y, z
    Var(String),

    /// Application: f a
    App(Box<Term>, Box<Term>),

    /// Lambda abstraction: λ x : A, t  (written as fun x : A => t)
    Abs(String, Box<Term>, Box<Term>),

    /// Dependent product: Π x : A, B  (written as (x : A) → B or {x : A} → B)
    Pi(String, Box<Term>, Box<Term>, bool), // bool = implicit

    /// Sort/Type: Sort u, Type, Prop
    Sort(Universe),

    /// Let binding: let x := t in s
    Let(String, Box<Term>, Box<Term>, Box<Term>),

    /// Match expression with patterns
    Match(Box<Term>, Vec<MatchArm>),

    /// Inductive type constructor
    Constructor(String, Vec<Term>),

    /// Local constant (for definitions and axioms)
    Const(String),

    /// Meta-variable for type inference
    Meta(String),

    /// Field projection: term.field
    Proj(Box<Term>, String),

    /// Dependent pair type (Sigma type): Σ (x : A), B
    Sigma(String, Box<Term>, Box<Term>),

    /// Pair constructor: ⟨a, b⟩
    Pair(Box<Term>, Box<Term>),

    /// First projection: π₁
    Fst(Box<Term>),

    /// Second projection: π₂
    Snd(Box<Term>),
}
/// Universe levels for type theory
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Universe {
    Const(u32),                         // Concrete level: 0, 1, 2, ...
    ScopedVar(String, String),          // Scoped universe variable: (scope_name, var_name)
    Meta(u32),                          // Universe metavariable: ?u₀, ?u₁, ...
    Add(Box<Universe>, u32),            // Level + n
    Max(Box<Universe>, Box<Universe>),  // max(u, v)
    IMax(Box<Universe>, Box<Universe>), // imax(u, v)
}
/// Context for universe level variables
#[derive(Debug, Clone, PartialEq)]
pub struct UniverseContext {
    /// Currently bound universe variables
    pub variables: Vec<String>,
    /// Constraints on universe levels
    pub constraints: Vec<UniverseConstraint>,
}
/// Pattern matching arms
#[derive(Debug, Clone, PartialEq)]
pub struct MatchArm {
    pub pattern: Pattern,
    pub body: Term,
}
/// Patterns for match expressions
#[derive(Debug, Clone, PartialEq)]
pub enum Pattern {
    /// Variable pattern: x
    Var(String),

    /// Constructor pattern: Cons x xs
    Constructor(String, Vec<Pattern>),

    /// Wildcard pattern: _
    Wildcard,
}
/// Top-level declarations
#[derive(Debug, Clone, PartialEq)]
pub enum Declaration {
    /// Constant definition: def name {u...} (params) : type := body
    Definition {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        body: Term,
    },

    /// Axiom: axiom name {u...} : type
    Axiom {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        ty: Term,
    },

    /// Inductive type: inductive Name {u...} (params) : Type := constructors
    Inductive {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        constructors: Vec<Constructor>,
    },

    /// Structure (single constructor inductive): structure Name {u...} :=
    /// (fields)
    Structure {
        name: String,
        universe_params: Vec<String>, // Universe level parameters
        params: Vec<Parameter>,
        ty: Term,
        fields: Vec<Field>,
    },
}
/// Function parameters
#[derive(Debug, Clone, PartialEq)]
pub struct Parameter {
    pub name: String,
    pub ty: Term,
    pub implicit: bool, // for {x : A} vs (x : A)
}
/// Constructor for inductive types
#[derive(Debug, Clone, PartialEq)]
pub struct Constructor {
    pub name: String,
    pub ty: Term,
}
/// Structure fields
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
    pub name: String,
    pub ty: Term,
}
/// Module containing multiple declarations
#[derive(Debug, Clone, PartialEq)]
pub struct Module {
    pub declarations: Vec<Declaration>,
}
impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Universe::Const(n) => write!(f, "{}", n),
            Universe::ScopedVar(scope, var) => write!(f, "{}::{}", scope, var),
            Universe::Meta(id) => write!(f, "?u{}", id),
            Universe::Add(u, n) => write!(f, "{}+{}", u, n),
            Universe::Max(u, v) => write!(f, "max({}, {})", u, v),
            Universe::IMax(u, v) => write!(f, "imax({}, {})", u, v),
        }
    }
}
impl fmt::Display for Term {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Term::Var(x) => write!(f, "{}", x),
            Term::App(t1, t2) => write!(f, "({} {})", t1, t2),
            Term::Abs(x, ty, body) => write!(f, "(fun {} : {} => {})", x, ty, body),
            Term::Pi(x, ty, body, implicit) => {
                if Self::occurs_free(x, body) {
                    if *implicit {
                        write!(f, "{{{}  : {}}} → {}", x, ty, body)
                    } else {
                        write!(f, "({} : {}) → {}", x, ty, body)
                    }
                } else {
                    write!(f, "{} → {}", ty, body)
                }
            }
            Term::Sort(u) => match u {
                Universe::Const(0) => write!(f, "Prop"),
                Universe::Const(n) => {
                    if *n == 1 {
                        write!(f, "Type")
                    } else {
                        write!(f, "Type {}", n - 1)
                    }
                }
                Universe::Add(ref base, n) => {
                    if let Universe::Const(1) = **base {
                        write!(f, "Type {}", n)
                    } else {
                        write!(f, "Sort {}", u)
                    }
                }
                _ => write!(f, "Sort {}", u),
            },
            Term::Let(x, ty, val, body) => write!(f, "(let {} : {} := {} in {})", x, ty, val, body),
            Term::Match(scrutinee, arms) => {
                writeln!(f, "match {} with", scrutinee)?;
                for arm in arms {
                    writeln!(f, "| {} => {}", arm.pattern, arm.body)?;
                }
                Ok(())
            }
            Term::Constructor(name, args) => {
                write!(f, "{}", name)?;
                for arg in args {
                    write!(f, " {}", arg)?;
                }
                Ok(())
            }
            Term::Const(name) => write!(f, "{}", name),
            Term::Meta(name) => write!(f, "?{}", name),
            Term::Proj(term, field) => write!(f, "{}.{}", term, field),
            Term::Sigma(x, domain, codomain) => write!(f, "Σ ({} : {}), {}", x, domain, codomain),
            Term::Pair(fst, snd) => write!(f, "⟨{}, {}⟩", fst, snd),
            Term::Fst(pair) => write!(f, "π₁({})", pair),
            Term::Snd(pair) => write!(f, "π₂({})", pair),
        }
    }
}
impl fmt::Display for Pattern {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Pattern::Var(x) => write!(f, "{}", x),
            Pattern::Constructor(name, args) => {
                write!(f, "{}", name)?;
                for arg in args {
                    write!(f, " {}", arg)?;
                }
                Ok(())
            }
            Pattern::Wildcard => write!(f, "_"),
        }
    }
}
impl Term {
    /// Check if variable occurs free in term
    pub fn occurs_free(var: &str, term: &Term) -> bool {
        match term {
            Term::Var(x) => x == var,
            Term::App(t1, t2) => Self::occurs_free(var, t1) || Self::occurs_free(var, t2),
            Term::Abs(x, ty, body) => {
                Self::occurs_free(var, ty) || (x != var && Self::occurs_free(var, body))
            }
            Term::Pi(x, ty, body, _) => {
                Self::occurs_free(var, ty) || (x != var && Self::occurs_free(var, body))
            }
            Term::Sort(_) => false,
            Term::Let(x, ty, val, body) => {
                Self::occurs_free(var, ty)
                    || Self::occurs_free(var, val)
                    || (x != var && Self::occurs_free(var, body))
            }
            Term::Match(scrutinee, arms) => {
                Self::occurs_free(var, scrutinee)
                    || arms.iter().any(|arm| Self::occurs_free(var, &arm.body))
            }
            Term::Constructor(_, args) => args.iter().any(|arg| Self::occurs_free(var, arg)),
            Term::Const(_) => false,
            Term::Meta(_) => false,
            Term::Proj(term, _) => Self::occurs_free(var, term),
            Term::Sigma(x, domain, codomain) => {
                Self::occurs_free(var, domain) || (x != var && Self::occurs_free(var, codomain))
            }
            Term::Pair(fst, snd) => Self::occurs_free(var, fst) || Self::occurs_free(var, snd),
            Term::Fst(pair) | Term::Snd(pair) => Self::occurs_free(var, pair),
        }
    }
}
impl UniverseContext {
    pub fn new() -> Self {
        UniverseContext {
            variables: Vec::new(),
            constraints: Vec::new(),
        }
    }

    /// Add a universe variable to the context
    pub fn add_var(&mut self, name: String) {
        if !self.variables.contains(&name) {
            self.variables.push(name);
        }
    }

    /// Add a constraint to the context
    pub fn add_constraint(&mut self, constraint: UniverseConstraint) {
        self.constraints.push(constraint);
    }

    /// Check if a universe variable is bound in this context
    pub fn contains(&self, name: &str) -> bool {
        self.variables.contains(&name.to_string())
    }

    /// Extend context with variables from another context
    pub fn extend(&self, other: &UniverseContext) -> Self {
        let mut new_ctx = self.clone();
        for var in &other.variables {
            new_ctx.add_var(var.clone());
        }
        for constraint in &other.constraints {
            new_ctx.add_constraint(constraint.clone());
        }
        new_ctx
    }

    /// Generate a fresh universe variable name
    pub fn fresh_var(&self, base: &str) -> String {
        let mut counter = 0;
        loop {
            let name = if counter == 0 {
                base.to_string()
            } else {
                format!("{}{}", base, counter)
            };
            if !self.contains(&name) {
                return name;
            }
            counter += 1;
        }
    }
}
impl Default for UniverseContext {
    fn default() -> Self {
        Self::new()
    }
}
impl Universe {
    /// Create a scoped universe variable
    pub fn scoped_var(scope: String, name: String) -> Self {
        Universe::ScopedVar(scope, name)
    }
}
/// Universe level constraints for polymorphism
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UniverseConstraint {
    /// u ≤ v
    LessEq(Universe, Universe),
    /// u = v
    Equal(Universe, Universe),
}
}

Our implementation includes a dedicated universe constraint solver that handles the complex relationships between universe levels. The solver maintains a constraint graph and applies algorithms to determine consistent universe level assignments. The constraint solver manages level equality constraints that require two universe levels to be identical, arising from type equality requirements in dependent contexts where definitional equality demands universe level consistency. Level ordering constraints require one universe level to be strictly less than another, arising from the predicativity requirements of the type system that prevent logical paradoxes by maintaining a strict hierarchy of type universes. Arithmetic constraints involving universe level computations enable flexible universe level expressions while maintaining consistency, allowing complex universe polymorphic definitions to specify their universe requirements precisely through level arithmetic expressions that the solver can resolve to concrete level assignments.

Definitional Equality and Normalization

Our type checker implements definitional equality through a comprehensive normalization algorithm that handles β-reduction, η-expansion, and definitional unfolding of constant definitions.

#![allow(unused)]
fn main() {
/// Normalize a term by reducing redexes (beta reduction, eta-conversion,
/// let expansion)
pub fn normalize(&self, term: &Term, ctx: &Context) -> Term {
    match term {
        Term::App(f, arg) => {
            let f_norm = self.normalize(f, ctx);
            let arg_norm = self.normalize(arg, ctx);

            // Beta reduction: (λx.t) s ~> t[s/x]
            if let Term::Abs(x, _ty, body) = &f_norm {
                self.substitute(x, &arg_norm, body)
            } else {
                Term::App(Box::new(f_norm), Box::new(arg_norm))
            }
        }

        Term::Abs(x, ty, body) => {
            let ty_norm = self.normalize(ty, ctx);
            let extended_ctx = ctx.extend(x.clone(), ty_norm.clone());
            let body_norm = self.normalize(body, &extended_ctx);

            // Eta-conversion: λx. f x ≡ f when x is not free in f
            if let Term::App(f, arg) = &body_norm {
                if let Term::Var(arg_var) = arg.as_ref() {
                    if arg_var == x && !Term::occurs_free(x, f) {
                        // Apply eta-conversion: λx. f x ~> f
                        return f.as_ref().clone();
                    }
                }
            }

            Term::Abs(x.clone(), Box::new(ty_norm), Box::new(body_norm))
        }

        Term::Pi(x, ty, body, implicit) => {
            let ty_norm = self.normalize(ty, ctx);
            let extended_ctx = ctx.extend(x.clone(), ty_norm.clone());
            let body_norm = self.normalize(body, &extended_ctx);
            Term::Pi(x.clone(), Box::new(ty_norm), Box::new(body_norm), *implicit)
        }

        Term::Let(x, _ty, val, body) => {
            // Let reduction: let x : T := v in b ~> b[v/x]
            let val_norm = self.normalize(val, ctx);
            self.substitute(x, &val_norm, body)
        }

        Term::Var(x) => {
            // Look up definitions and unfold them
            if let Some(def) = ctx.lookup_axiom(x) {
                def.clone() // Could be further normalized if it's a
                            // definition
            } else {
                term.clone()
            }
        }

        _ => term.clone(), // Other terms are already in normal form
    }
}
}

The normalization algorithm implements β-reduction for function applications, handling both computational reductions and type-level computations. When a lambda abstraction is applied to an argument, the implementation performs substitution while carefully managing variable capture and scope.

\[ (λx : Nat. x + 1) \ 5 ⟹ 5 + 1 ⟹ 6 \]

The implementation extends β-reduction to handle dependent type computations, where type-level functions can be applied to produce new types through computation.

Eta Conversion

Eta conversion ensures that functions are equal to their eta-expanded forms, providing extensional equality for function types. The implementation applies η-expansion during normalization to ensure that definitionally equal terms are recognized as such.

\[ λx : A. f \ x ≡ f \text{ (when } x \notin \text{fv}(f)\text{)} \]

Let-Expansion and Definition Unfolding

The implementation handles let bindings through expansion, replacing let-bound variables with their definitions during normalization. This approach ensures that local definitions do not interfere with definitional equality while providing the computational content needed for type checking.

Definition unfolding enables the type checker to access the computational content of defined constants, allowing definitional equality to work across module boundaries and enabling powerful abstraction mechanisms.

Type Checking Algorithm

Our implementation employs bidirectional type checking that splits the type checking problem into synthesis and checking modes. This approach handles the complexity of dependent types while maintaining decidability and providing informative error messages.

#![allow(unused)]
fn main() {
/// Infer the type of a term
pub fn infer(&mut self, term: &Term, ctx: &Context) -> TypeResult<Term> {
    self.with_context(term, |checker| checker.infer_impl(term, ctx))
}
}

Type Synthesis

The synthesis algorithm determines the type of a term by analyzing its structure and propagating type information through the syntax tree. Variable lookup operates by consulting the typing context, which maintains bindings for both term variables and type variables, ensuring that each variable reference corresponds to a valid binding in the current scope. Function application typing proceeds by synthesizing the function type, ensuring it forms a Pi-type, and checking that the argument type matches the domain specification, with careful handling of dependent types where the codomain may depend on the specific argument value. Pi-type formation verification ensures both the domain and codomain are well-typed, with the codomain checked in a context extended with the bound variable to properly handle the dependency relationship between the domain and codomain types.

#![allow(unused)]
fn main() {
/// Check that a term has a given type
pub fn check(&mut self, term: &Term, ty: &Term, ctx: &Context) -> TypeResult<()> {
    // Special handling for checking against meta-variable types
    if let Term::Meta(_) = ty {
        // When checking against a meta-variable, infer the term's type and unify
        let _inferred = self.infer(term, ctx)?;
        // For now, just check they're compatible - in a full system we'd unify
        // and update the meta-variable
        return Ok(());
    }

    match term {
        Term::Abs(x, param_ty, body) => {
            // Check if ty is a Pi-type
            if let Term::Pi(y, expected_param_ty, expected_body_ty, _impl) = ty {
                // Handle holes in parameter type
                let actual_param_ty = match param_ty.as_ref() {
                    Term::Meta(name) if name == UNDERSCORE => {
                        // Hole in parameter type - use the expected type
                        expected_param_ty.clone()
                    }
                    _ => param_ty.clone(),
                };

                // Check parameter types are definitionally equal (with conversion)
                if !self.definitionally_equal(&actual_param_ty, expected_param_ty, ctx)? {
                    return Err(TypeError::TypeMismatch {
                        expected: expected_param_ty.as_ref().clone(),
                        actual: actual_param_ty.as_ref().clone(),
                    });
                }

                // Check body in extended context
                let extended_ctx = ctx.extend(x.clone(), actual_param_ty.as_ref().clone());

                // Substitute parameter in expected body type - this is the key for dependent
                // types
                let expected_body_ty = if x == y {
                    expected_body_ty.as_ref().clone()
                } else {
                    // Rename bound variable y to x in the body type
                    self.substitute(y, &Term::Var(x.clone()), expected_body_ty)
                };

                self.check(body, &expected_body_ty, &extended_ctx)
            } else {
                Err(TypeError::NotAFunction {
                    term: term.clone(),
                    ty: ty.clone(),
                })
            }
        }

        _ => {
            // General case: infer type and check definitional equivalence
            let inferred_ty = self.infer(term, ctx)?;

            // Handle implicit parameters: if inferred type has implicit params,
            // try to instantiate them to match the expected type
            let elaborated_ty = self.elaborate_implicit_parameters(&inferred_ty, ty, ctx)?;

            // Check definitional equivalence
            if self.definitionally_equal(&elaborated_ty, ty, ctx)? {
                Ok(())
            } else {
                Err(TypeError::TypeMismatch {
                    expected: ty.clone(),
                    actual: elaborated_ty,
                })
            }
        }
    }
}
}

Type Checking Mode

The checking algorithm verifies that a term has an expected type by comparing the term structure with the type structure. This mode often provides better error messages and more efficient checking for terms with complex dependent types. Lambda checking operates by verifying lambda abstractions against Pi-types, ensuring that the body has the correct type in the extended context where the lambda parameter is properly bound. When synthesis and checking produce different results, the algorithm falls back to definitional equality checking, normalizing both the synthesized and expected types to their canonical forms and comparing the results, enabling the type checker to recognize when terms are definitionally equal despite having different syntactic representations.

Context Management

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use crate::ast::{Term, Universe, UniverseConstraint, UniverseContext};
/// Definition with universe parameters
#[derive(Debug, Clone, PartialEq)]
pub struct Definition {
    /// Universe parameters for the definition
    pub universe_params: Vec<String>,
    /// The type of the definition
    pub ty: Term,
    /// Name of the definition (for scoping universe parameters)
    pub name: String,
}
impl Context {
    pub fn new() -> Self {
        Context {
            bindings: HashMap::new(),
            universe_vars: HashMap::new(),
            constructors: HashMap::new(),
            axioms: HashMap::new(),
            definitions: HashMap::new(),
            universe_context: UniverseContext::new(),
        }
    }

    /// Extend context with variable binding
    pub fn extend(&self, var: String, ty: Term) -> Self {
        let mut new_ctx = self.clone();
        new_ctx.bindings.insert(var, ty);
        new_ctx
    }

    /// Extend context with multiple bindings
    pub fn extend_many(&self, bindings: Vec<(String, Term)>) -> Self {
        let mut new_ctx = self.clone();
        for (var, ty) in bindings {
            new_ctx.bindings.insert(var, ty);
        }
        new_ctx
    }

    /// Look up variable type
    pub fn lookup(&self, var: &str) -> Option<&Term> {
        self.bindings.get(var)
    }

    /// Check if context has a binding for the given variable
    pub fn has_binding(&self, var: &str) -> bool {
        self.bindings.contains_key(var)
    }

    /// Get all variable bindings
    pub fn get_all_bindings(&self) -> impl Iterator<Item = (&String, &Term)> {
        self.bindings.iter()
    }

    /// Look up constructor type
    pub fn lookup_constructor(&self, name: &str) -> Option<&Term> {
        self.constructors.get(name)
    }

    /// Look up axiom type
    pub fn lookup_axiom(&self, name: &str) -> Option<&Term> {
        self.axioms.get(name)
    }

    /// Add constructor definition
    pub fn add_constructor(&mut self, name: String, ty: Term) {
        self.constructors.insert(name, ty);
    }

    /// Add axiom definition
    pub fn add_axiom(&mut self, name: String, ty: Term) {
        self.axioms.insert(name, ty);
    }

    /// Add definition with universe parameters
    pub fn add_definition(&mut self, name: String, universe_params: Vec<String>, ty: Term) {
        self.definitions.insert(
            name.clone(),
            Definition {
                universe_params,
                ty,
                name: name.clone(),
            },
        );
    }

    /// Look up definition with universe parameters
    pub fn lookup_definition(&self, name: &str) -> Option<&Definition> {
        self.definitions.get(name)
    }

    /// Add universe variable constraint
    pub fn add_universe_var(&mut self, var: String, constraint: Universe) {
        self.universe_vars.insert(var, constraint);
    }

    /// Check if variable is bound in context
    pub fn contains(&self, var: &str) -> bool {
        self.bindings.contains_key(var)
    }

    /// Get all variable names
    pub fn variables(&self) -> Vec<&String> {
        self.bindings.keys().collect()
    }

    /// Remove variable from context
    pub fn remove(&self, var: &str) -> Self {
        let mut new_ctx = self.clone();
        new_ctx.bindings.remove(var);
        new_ctx
    }

    /// Get fresh variable name not in context
    pub fn fresh_var(&self, base: &str) -> String {
        let mut counter = 0;
        loop {
            let candidate = if counter == 0 {
                base.to_string()
            } else {
                format!("{}{}", base, counter)
            };

            if !self.contains(&candidate) {
                return candidate;
            }
            counter += 1;
        }
    }

    /// Access universe context
    pub fn universe_context(&self) -> &UniverseContext {
        &self.universe_context
    }

    /// Extend context with universe variable
    pub fn extend_universe(&self, name: String) -> Self {
        let mut new_ctx = self.clone();
        new_ctx.universe_context.add_var(name);
        new_ctx
    }

    /// Extend context with multiple universe variables
    pub fn extend_universe_many(&self, names: Vec<String>) -> Self {
        let mut new_ctx = self.clone();
        for name in names {
            new_ctx.universe_context.add_var(name);
        }
        new_ctx
    }

    /// Add universe constraint
    pub fn add_universe_constraint(&mut self, constraint: UniverseConstraint) {
        self.universe_context.add_constraint(constraint);
    }

    /// Generate fresh universe variable
    pub fn fresh_universe_var(&self, base: &str) -> String {
        self.universe_context.fresh_var(base)
    }
}
impl Default for Context {
    fn default() -> Self {
        Self::new()
    }
}
/// Type checking errors related to context
#[derive(Debug, Clone, PartialEq)]
pub enum ContextError {
    UnboundVariable { name: String },
    UnboundConstructor { name: String },
    UnboundAxiom { name: String },
    VariableAlreadyBound { name: String },
}
impl std::fmt::Display for ContextError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ContextError::UnboundVariable { name } => {
                write!(f, "Unbound variable: {}", name)
            }
            ContextError::UnboundConstructor { name } => {
                write!(f, "Unbound constructor: {}", name)
            }
            ContextError::UnboundAxiom { name } => {
                write!(f, "Unbound axiom: {}", name)
            }
            ContextError::VariableAlreadyBound { name } => {
                write!(f, "Variable already bound: {}", name)
            }
        }
    }
}
impl std::error::Error for ContextError {}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::ast::Universe;

    #[test]
    fn test_context_operations() {
        let ctx = Context::new();

        let var_type = Term::Sort(Universe::Const(0));
        let ctx2 = ctx.extend("x".to_string(), var_type.clone());

        assert!(ctx2.contains("x"));
        assert_eq!(ctx2.lookup("x"), Some(&var_type));
        assert!(!ctx.contains("x")); // Original context unchanged
    }

    #[test]
    fn test_fresh_var() {
        let mut ctx = Context::new();
        ctx.bindings
            .insert("x".to_string(), Term::Sort(Universe::Const(0)));
        ctx.bindings
            .insert("x1".to_string(), Term::Sort(Universe::Const(0)));

        let fresh = ctx.fresh_var("x");
        assert_eq!(fresh, "x2");

        let fresh2 = ctx.fresh_var("y");
        assert_eq!(fresh2, "y");
    }

    #[test]
    fn test_extend_many() {
        let ctx = Context::new();
        let bindings = vec![
            ("x".to_string(), Term::Sort(Universe::Const(0))),
            ("y".to_string(), Term::Sort(Universe::Const(1))),
        ];

        let ctx2 = ctx.extend_many(bindings);
        assert!(ctx2.contains("x"));
        assert!(ctx2.contains("y"));
    }
}
/// Type checking context containing variable bindings
#[derive(Debug, Clone, PartialEq)]
pub struct Context {
    /// Variable to type bindings
    bindings: HashMap<String, Term>,
    /// Universe variable constraints
    universe_vars: HashMap<String, Universe>,
    /// Constructor definitions
    constructors: HashMap<String, Term>,
    /// Axiom definitions (without universe params - for backwards compat)
    axioms: HashMap<String, Term>,
    /// Definitions with universe parameters
    definitions: HashMap<String, Definition>,
    /// Universe level context for polymorphism
    universe_context: UniverseContext,
}
}

The typing context maintains bindings for variables, definitions, axioms, and constructors. Our implementation uses a context structure that enables efficient lookup while supporting the complex scoping rules required for dependent types. Variable binding operations add new variable bindings with their types, enabling proper scoping in lambda abstractions and Pi-types where the bound variable may appear in the type of subsequent bindings. Definition extension capabilities allow adding new constant definitions with their types and bodies, enabling modular development and abstraction mechanisms that support large-scale program organization. Constructor registration functionality registers inductive type constructors with their types, enabling pattern matching and inductive reasoning that respects the structural properties of the data types and maintains type safety throughout elimination operations.

Implicit Arguments and Elaboration

Our implementation includes comprehensive support for implicit arguments that are automatically inferred by the type checker. This feature bridges the gap between the fully explicit internal representation and the more convenient surface syntax.

#![allow(unused)]
fn main() {
/// Elaborate implicit parameters by trying to instantiate them to match
/// expected type This is a simple version that handles common cases
/// without full constraint solving
fn elaborate_implicit_parameters(
    &mut self,
    inferred_ty: &Term,
    expected_ty: &Term,
    ctx: &Context,
) -> TypeResult<Term> {
    // If inferred type has implicit parameters, try to instantiate them
    match inferred_ty {
        Term::Pi(param_name, param_ty, body_ty, true) => {
            // This is an implicit parameter - try to infer what it should be
            let implicit_arg = self.infer_implicit_argument(param_ty, expected_ty, ctx)?;

            // Substitute the implicit argument and continue elaboration
            let instantiated_ty = self.substitute(param_name, &implicit_arg, body_ty);
            self.elaborate_implicit_parameters(&instantiated_ty, expected_ty, ctx)
        }
        _ => {
            // No more implicit parameters, return the type as-is
            Ok(inferred_ty.clone())
        }
    }
}
}

Implicit Argument Insertion

The type checker automatically inserts implicit arguments when encountering function applications where the function type has implicit parameters. Meta-variable generation creates fresh meta-variables to represent unknown implicit arguments, ensuring that each implicit parameter has a unique placeholder that can be resolved through subsequent constraint solving. Constraint collection gathers relationships that connect meta-variables to known type information, building a system of equations that captures the interdependencies between implicit arguments and the explicitly provided terms. Constraint solving employs the dedicated constraint solver to determine concrete values for meta-variables, using unification algorithms and type-directed search to find solutions that satisfy all accumulated constraints while respecting the type structure of the program.

Meta-variable Resolution

#![allow(unused)]
fn main() {
use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt;
use crate::ast::{Term, Universe};
use crate::context::Context;
use crate::errors::TypeError;
/// Meta-variable identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MetaId(pub u64);
impl fmt::Display for MetaId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "?m{}", self.0)
    }
}
/// Universe meta-variable identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UniverseMetaId(pub u64);
impl fmt::Display for UniverseMetaId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "?u{}", self.0)
    }
}
/// Constraint identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ConstraintId(pub u64);
/// Constraint types
#[derive(Debug, Clone)]
pub enum Constraint {
    /// Unification: t1 ≡ t2
    Unify {
        id: ConstraintId,
        left: Term,
        right: Term,
        strength: ConstraintStrength,
    },
    /// Type constraint: term : type
    HasType {
        id: ConstraintId,
        term: Term,
        expected_type: Term,
    },
    /// Universe unification: u1 ≡ u2
    UnifyUniverse {
        id: ConstraintId,
        left: Universe,
        right: Universe,
        strength: ConstraintStrength,
    },
    /// Universe level constraint: u1 ≤ u2
    UniverseLevel {
        id: ConstraintId,
        left: Universe,
        right: Universe,
    },
    /// Delayed constraint (for complex patterns)
    Delayed {
        id: ConstraintId,
        constraint: Box<Constraint>,
        waiting_on: HashSet<MetaId>,
    },
}
/// Constraint strength for prioritization
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintStrength {
    Required,  // Must be solved
    Preferred, // Should be solved if possible
    Weak,      // Can be postponed
}
/// Substitution mapping meta-variables to terms
#[derive(Debug, Clone, Default)]
pub struct Substitution {
    mapping: HashMap<MetaId, Term>,
    universe_mapping: HashMap<u32, Universe>,
}
impl Substitution {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn insert(&mut self, meta: MetaId, term: Term) {
        self.mapping.insert(meta, term);
    }

    pub fn get(&self, meta: &MetaId) -> Option<&Term> {
        self.mapping.get(meta)
    }

    pub fn insert_universe(&mut self, meta_id: u32, universe: Universe) {
        self.universe_mapping.insert(meta_id, universe);
    }

    pub fn get_universe(&self, meta_id: &u32) -> Option<&Universe> {
        self.universe_mapping.get(meta_id)
    }

    /// Apply substitution to a term
    pub fn apply(&self, term: &Term) -> Term {
        match term {
            Term::Meta(name) => {
                // Try to parse meta ID and look up substitution
                if let Some(meta_id) = self.parse_meta_name(name) {
                    if let Some(subst) = self.mapping.get(&meta_id) {
                        // Recursively apply substitution
                        return self.apply(subst);
                    }
                }
                term.clone()
            }
            Term::App(f, arg) => Term::App(Box::new(self.apply(f)), Box::new(self.apply(arg))),
            Term::Abs(x, ty, body) => Term::Abs(
                x.clone(),
                Box::new(self.apply(ty)),
                Box::new(self.apply(body)),
            ),
            Term::Pi(x, ty, body, implicit) => Term::Pi(
                x.clone(),
                Box::new(self.apply(ty)),
                Box::new(self.apply(body)),
                *implicit,
            ),
            Term::Let(x, ty, val, body) => Term::Let(
                x.clone(),
                Box::new(self.apply(ty)),
                Box::new(self.apply(val)),
                Box::new(self.apply(body)),
            ),
            Term::Sort(u) => Term::Sort(self.apply_universe(u)),
            // Other cases remain unchanged
            _ => term.clone(),
        }
    }

    /// Apply substitution to a universe
    pub fn apply_universe(&self, universe: &Universe) -> Universe {
        let result = match universe {
            Universe::Meta(id) => {
                if let Some(subst) = self.universe_mapping.get(id) {
                    // Recursively apply substitution
                    self.apply_universe(subst)
                } else {
                    universe.clone()
                }
            }
            Universe::Add(base, n) => Universe::Add(Box::new(self.apply_universe(base)), *n),
            Universe::Max(u, v) => Universe::Max(
                Box::new(self.apply_universe(u)),
                Box::new(self.apply_universe(v)),
            ),
            Universe::IMax(u, v) => Universe::IMax(
                Box::new(self.apply_universe(u)),
                Box::new(self.apply_universe(v)),
            ),
            // Constants, variables, and scoped variables remain unchanged during substitution
            Universe::Const(_) | Universe::ScopedVar(_, _) => universe.clone(),
        };
        // Normalize the result to simplify expressions like Const(0) + 1 -> Const(1)
        self.normalize_universe_static(&result)
    }

    /// Static normalization
    fn normalize_universe_static(&self, u: &Universe) -> Universe {
        match u {
            Universe::Add(base, n) => {
                let base_norm = self.normalize_universe_static(base);
                match base_norm {
                    Universe::Const(m) => Universe::Const(m + n),
                    _ => Universe::Add(Box::new(base_norm), *n),
                }
            }
            Universe::Max(u1, u2) => {
                let u1_norm = self.normalize_universe_static(u1);
                let u2_norm = self.normalize_universe_static(u2);
                match (&u1_norm, &u2_norm) {
                    (Universe::Const(n1), Universe::Const(n2)) => Universe::Const((*n1).max(*n2)),
                    _ => Universe::Max(Box::new(u1_norm), Box::new(u2_norm)),
                }
            }
            _ => u.clone(),
        }
    }

    fn parse_meta_name(&self, name: &str) -> Option<MetaId> {
        if let Some(stripped) = name.strip_prefix("?m") {
            stripped.parse::<u64>().ok().map(MetaId)
        } else {
            None
        }
    }
}
/// Advanced constraint solver with dependency tracking
pub struct Solver {
    /// Meta-variables
    metas: HashMap<MetaId, MetaInfo>,
    /// Active constraints
    constraints: HashMap<ConstraintId, Constraint>,
    /// Constraint queue (ordered by priority)
    queue: VecDeque<ConstraintId>,
    /// Solved substitutions
    substitution: Substitution,
    /// Next meta ID
    next_meta_id: u64,
    /// Next universe meta ID
    next_universe_meta_id: u32,
    /// Next constraint ID
    next_constraint_id: u64,
    /// Type checking context
    context: Context,
    /// Dependency graph: meta -> constraints that depend on it
    dependencies: HashMap<MetaId, HashSet<ConstraintId>>,
    /// Delayed constraints that couldn't be solved immediately
    delayed_constraints: HashSet<ConstraintId>,
    /// Recursion depth counter to prevent stack overflow
    recursion_depth: u32,
    /// Flag to enable Miller pattern solving (advanced)
    enable_miller_patterns: bool,
    /// Flag to enable HasType constraint solving (advanced)
    enable_has_type_solving: bool,
}
impl Solver {
    pub fn new(context: Context) -> Self {
        Self {
            metas: HashMap::new(),
            constraints: HashMap::new(),
            queue: VecDeque::new(),
            substitution: Substitution::new(),
            next_meta_id: 0,
            next_universe_meta_id: 0,
            next_constraint_id: 0,
            context,
            dependencies: HashMap::new(),
            delayed_constraints: HashSet::new(),
            recursion_depth: 0,
            enable_miller_patterns: false,  // Advanced feature
            enable_has_type_solving: false, // Advanced feature
        }
    }

    /// Enable Miller pattern solving (advanced)
    pub fn enable_miller_patterns(&mut self) {
        self.enable_miller_patterns = true;
    }

    /// Check if Miller pattern solving is enabled
    pub fn miller_patterns_enabled(&self) -> bool {
        self.enable_miller_patterns
    }

    /// Enable HasType constraint solving (advanced)
    pub fn enable_has_type_solving(&mut self) {
        self.enable_has_type_solving = true;
    }

    /// Check if HasType constraint solving is enabled
    pub fn has_type_solving_enabled(&self) -> bool {
        self.enable_has_type_solving
    }

    /// Create a fresh meta-variable
    pub fn fresh_meta(&mut self, ctx_vars: Vec<String>, expected_type: Option<Term>) -> MetaId {
        let id = MetaId(self.next_meta_id);
        self.next_meta_id += 1;

        let info = MetaInfo {
            id,
            context: ctx_vars,
            expected_type,
            solution: None,
            dependencies: HashSet::new(),
            occurrences: Vec::new(),
        };

        self.metas.insert(id, info);
        id
    }

    /// Create a fresh universe meta-variable
    pub fn fresh_universe_meta(&mut self) -> Universe {
        let id = self.next_universe_meta_id;
        self.next_universe_meta_id += 1;
        Universe::Meta(id)
    }

    /// Add a constraint to the solver
    pub fn add_constraint(&mut self, constraint: Constraint) -> ConstraintId {
        let id = match &constraint {
            Constraint::Unify { id, .. }
            | Constraint::HasType { id, .. }
            | Constraint::UnifyUniverse { id, .. }
            | Constraint::UniverseLevel { id, .. }
            | Constraint::Delayed { id, .. } => *id,
        };

        // Track which metas appear in this constraint
        self.track_meta_occurrences(&constraint);

        self.constraints.insert(id, constraint);
        self.queue.push_back(id);
        id
    }

    /// Create a new constraint ID
    pub fn new_constraint_id(&mut self) -> ConstraintId {
        let id = ConstraintId(self.next_constraint_id);
        self.next_constraint_id += 1;
        id
    }

    /// Track meta-variable occurrences in constraints
    fn track_meta_occurrences(&mut self, constraint: &Constraint) {
        let metas = self.collect_metas_in_constraint(constraint);
        let constraint_id = match constraint {
            Constraint::Unify { id, .. }
            | Constraint::HasType { id, .. }
            | Constraint::UnifyUniverse { id, .. }
            | Constraint::UniverseLevel { id, .. }
            | Constraint::Delayed { id, .. } => *id,
        };

        for meta_id in metas {
            if let Some(info) = self.metas.get_mut(&meta_id) {
                info.occurrences.push(constraint_id);
            }
            self.dependencies
                .entry(meta_id)
                .or_default()
                .insert(constraint_id);
        }
    }

    /// Collect all meta-variables in a constraint
    fn collect_metas_in_constraint(&self, constraint: &Constraint) -> HashSet<MetaId> {
        match constraint {
            Constraint::Unify { left, right, .. } => {
                let mut metas = self.collect_metas_in_term(left);
                metas.extend(self.collect_metas_in_term(right));
                metas
            }
            Constraint::HasType {
                term,
                expected_type,
                ..
            } => {
                let mut metas = self.collect_metas_in_term(term);
                metas.extend(self.collect_metas_in_term(expected_type));
                metas
            }
            Constraint::UnifyUniverse { .. } => {
                // Universe constraints don't directly contain term meta-variables
                // but they might contain universe meta-variables (which we track separately)
                HashSet::new()
            }
            Constraint::UniverseLevel { .. } => {
                // Same as above
                HashSet::new()
            }
            Constraint::Delayed {
                constraint,
                waiting_on,
                ..
            } => {
                let mut metas = waiting_on.clone();
                metas.extend(self.collect_metas_in_constraint(constraint));
                metas
            }
        }
    }

    /// Collect all meta-variables in a term
    fn collect_metas_in_term(&self, term: &Term) -> HashSet<MetaId> {
        let mut metas = HashSet::new();
        self.collect_metas_recursive(term, &mut metas);
        metas
    }

    fn collect_metas_recursive(&self, term: &Term, metas: &mut HashSet<MetaId>) {
        match term {
            Term::Meta(name) => {
                if let Some(id) = self.parse_meta_name(name) {
                    metas.insert(id);
                }
            }
            Term::App(f, arg) => {
                self.collect_metas_recursive(f, metas);
                self.collect_metas_recursive(arg, metas);
            }
            Term::Abs(_, ty, body) | Term::Pi(_, ty, body, _) => {
                self.collect_metas_recursive(ty, metas);
                self.collect_metas_recursive(body, metas);
            }
            Term::Let(_, ty, val, body) => {
                self.collect_metas_recursive(ty, metas);
                self.collect_metas_recursive(val, metas);
                self.collect_metas_recursive(body, metas);
            }
            _ => {}
        }
    }

    fn parse_meta_name(&self, name: &str) -> Option<MetaId> {
        if let Some(stripped) = name.strip_prefix("?m") {
            stripped.parse::<u64>().ok().map(MetaId)
        } else {
            None
        }
    }

    /// Main solving loop
    pub fn solve(&mut self) -> Result<Substitution, TypeError> {
        let max_iterations = 1000;
        let mut iterations = 0;

        while !self.queue.is_empty() && iterations < max_iterations {
            iterations += 1;

            // Pick the best constraint to solve
            if let Some(constraint_id) = self.pick_constraint() {
                let constraint =
                    self.constraints
                        .remove(&constraint_id)
                        .ok_or_else(|| TypeError::Internal {
                            message: "Constraint disappeared".to_string(),
                        })?;

                match self.solve_constraint(constraint.clone()) {
                    Ok(progress) => {
                        if progress {
                            // Propagate the solution
                            self.propagate_solution()?;

                            // Wake up delayed constraints that might now be solvable
                            let woken_constraints = self.wake_delayed_constraints()?;

                            // Add woken constraints back to queue with high priority
                            for woken_id in woken_constraints {
                                self.queue.push_front(woken_id);
                            }
                        }
                    }
                    Err(e) => {
                        // Try to delay the constraint if possible
                        if self.can_delay(&constraint_id) {
                            // Determine which meta-variables to wait for
                            let waiting_on = match &constraint {
                                Constraint::Unify { left, right, .. } => {
                                    let mut metas = self.collect_metas_in_term(left);
                                    metas.extend(self.collect_metas_in_term(right));
                                    metas
                                        .into_iter()
                                        .filter(|meta_id| self.is_unsolved_meta(meta_id))
                                        .collect()
                                }
                                Constraint::HasType {
                                    term,
                                    expected_type,
                                    ..
                                } => {
                                    let mut metas = self.collect_metas_in_term(term);
                                    metas.extend(self.collect_metas_in_term(expected_type));
                                    metas
                                        .into_iter()
                                        .filter(|meta_id| self.is_unsolved_meta(meta_id))
                                        .collect()
                                }
                                _ => HashSet::new(),
                            };

                            if !waiting_on.is_empty() {
                                // Put constraint back and delay it
                                self.constraints.insert(constraint_id, constraint);
                                self.delay_constraint(constraint_id, waiting_on)?;
                            } else {
                                // Can't delay, return error
                                return Err(e);
                            }
                        } else {
                            return Err(e);
                        }
                    }
                }
            }
        }

        if iterations >= max_iterations {
            return Err(TypeError::Internal {
                message: "Constraint solving did not converge".to_string(),
            });
        }

        // Check for unsolved required constraints
        for constraint in self.constraints.values() {
            if let Constraint::Unify {
                strength: ConstraintStrength::Required,
                ..
            } = constraint
            {
                return Err(TypeError::Internal {
                    message: "Unsolved required constraints remain".to_string(),
                });
            }
        }

        Ok(self.substitution.clone())
    }

    /// Pick the next constraint to solve based on heuristics
    fn pick_constraint(&mut self) -> Option<ConstraintId> {
        // Priority order:
        // 1. Constraints with no meta-variables
        // 2. Constraints with only solved meta-variables
        // 3. Simple pattern constraints (Miller patterns)
        // 4. Other constraints

        let mut best_constraint = None;
        let mut best_score = i32::MAX;

        for &id in &self.queue {
            if let Some(constraint) = self.constraints.get(&id) {
                let score = self.score_constraint(constraint);
                if score < best_score {
                    best_score = score;
                    best_constraint = Some(id);
                }
            }
        }

        if let Some(id) = best_constraint {
            self.queue.retain(|&x| x != id);
            Some(id)
        } else {
            self.queue.pop_front()
        }
    }

    /// Score a constraint for solving priority (lower is better)
    fn score_constraint(&self, constraint: &Constraint) -> i32 {
        let metas = self.collect_metas_in_constraint(constraint);

        if metas.is_empty() {
            return 0; // No metas, can solve immediately
        }

        let unsolved_count = metas
            .iter()
            .filter(|m| {
                self.metas
                    .get(m)
                    .and_then(|info| info.solution.as_ref())
                    .is_none()
            })
            .count();

        if unsolved_count == 0 {
            return 1; // All metas solved
        }

        // Check for Miller patterns
        if let Constraint::Unify { left, right, .. } = constraint {
            if self.is_miller_pattern_unification(left, right)
                || self.is_miller_pattern_unification(right, left)
            {
                return 10 + unsolved_count as i32;
            }
        }

        100 + unsolved_count as i32
    }

    /// Check if a unification is a Miller pattern (?M x1 ... xn = t)
    fn is_miller_pattern_unification(&self, left: &Term, right: &Term) -> bool {
        self.is_simple_miller_pattern(left, right)
    }

    /// Check if left is a simple Miller pattern and right is safe to solve with
    pub fn is_simple_miller_pattern(&self, left: &Term, right: &Term) -> bool {
        let (head, args) = self.decompose_application(left);

        if let Term::Meta(meta_name) = head {
            // Must have at least one argument to be interesting
            if args.is_empty() {
                return false;
            }

            // Check if all arguments are distinct variables
            let mut seen = HashSet::new();
            for arg in &args {
                if let Term::Var(x) = arg {
                    if !seen.insert(x.clone()) {
                        return false; // Not distinct
                    }
                } else {
                    return false; // Not a variable
                }
            }

            // Check occurs check - right must not contain the meta-variable
            if let Some(meta_id) = self.parse_meta_name(meta_name) {
                if self.collect_metas_in_term(right).contains(&meta_id) {
                    return false; // Occurs check failure
                }
            }

            // For now, only handle simple cases where right is a variable or constant
            match right {
                Term::Var(_) | Term::Const(_) | Term::Sort(_) => true,
                Term::Meta(_) => {
                    // Allow meta-to-meta if they're different
                    if let Term::Meta(right_name) = right {
                        meta_name != right_name
                    } else {
                        false
                    }
                }
                _ => false, // More complex cases need careful handling
            }
        } else {
            false
        }
    }

    /// Decompose an application into head and arguments
    fn decompose_application<'a>(&self, term: &'a Term) -> (&'a Term, Vec<&'a Term>) {
        let mut current = term;
        let mut args = Vec::new();

        while let Term::App(f, arg) = current {
            args.push(arg.as_ref());
            current = f;
        }

        args.reverse();
        (current, args)
    }

    /// Solve a single constraint
    fn solve_constraint(&mut self, constraint: Constraint) -> Result<bool, TypeError> {
        const MAX_RECURSION_DEPTH: u32 = 100;

        if self.recursion_depth >= MAX_RECURSION_DEPTH {
            return Err(TypeError::Internal {
                message: format!(
                    "Maximum recursion depth {} exceeded in solve_constraint",
                    MAX_RECURSION_DEPTH
                ),
            });
        }

        self.recursion_depth += 1;
        let result = self.solve_constraint_impl(constraint);
        self.recursion_depth -= 1;
        result
    }

    fn solve_constraint_impl(&mut self, constraint: Constraint) -> Result<bool, TypeError> {
        match constraint {
            Constraint::Unify {
                left,
                right,
                strength,
                ..
            } => self.unify(&left, &right, strength),
            Constraint::HasType {
                term,
                expected_type,
                ..
            } => {
                if self.enable_has_type_solving {
                    self.solve_has_type_constraint(&term, &expected_type)
                } else {
                    // Default behavior: don't solve HasType constraints
                    Ok(false)
                }
            }
            Constraint::UnifyUniverse {
                left,
                right,
                strength,
                ..
            } => self.unify_universe(&left, &right, strength),
            Constraint::UniverseLevel { left, right, .. } => {
                // For now, treat level constraints as unification constraints
                self.unify_universe(&left, &right, ConstraintStrength::Preferred)
            }
            Constraint::Delayed { constraint, .. } => {
                // Re-queue the delayed constraint
                let inner = *constraint;
                self.add_constraint(inner);
                Ok(false)
            }
        }
    }

    /// Unify two terms
    fn unify(
        &mut self,
        left: &Term,
        right: &Term,
        strength: ConstraintStrength,
    ) -> Result<bool, TypeError> {
        const MAX_RECURSION_DEPTH: u32 = 50;

        if self.recursion_depth >= MAX_RECURSION_DEPTH {
            return Err(TypeError::Internal {
                message: format!(
                    "Maximum recursion depth {} exceeded in unify",
                    MAX_RECURSION_DEPTH
                ),
            });
        }

        self.recursion_depth += 1;
        let result = self.unify_impl(left, right, strength);
        self.recursion_depth -= 1;
        result
    }

    fn unify_impl(
        &mut self,
        left: &Term,
        right: &Term,
        strength: ConstraintStrength,
    ) -> Result<bool, TypeError> {
        // Apply current substitution
        let left = self.substitution.apply(left);
        let right = self.substitution.apply(right);

        // Check if already equal
        if self.alpha_equal(&left, &right) {
            return Ok(false);
        }

        // Miller pattern detection and solving (if enabled)
        if self.enable_miller_patterns {
            if self.is_simple_miller_pattern(&left, &right) {
                if let Some((meta_name, spine)) = self.extract_miller_spine(&left) {
                    // Try to solve the Miller pattern directly
                    return self.solve_miller_pattern(&meta_name, &spine, &right);
                }
            } else if self.is_simple_miller_pattern(&right, &left) {
                if let Some((meta_name, spine)) = self.extract_miller_spine(&right) {
                    // Try to solve the Miller pattern directly
                    return self.solve_miller_pattern(&meta_name, &spine, &left);
                }
            }
        }

        match (&left, &right) {
            // Meta-variable cases
            (Term::Meta(m), t) | (t, Term::Meta(m)) => {
                if let Some(meta_id) = self.parse_meta_name(m) {
                    self.solve_meta(meta_id, t)
                } else {
                    Err(TypeError::Internal {
                        message: format!("Invalid meta-variable: {}", m),
                    })
                }
            }

            // Structural cases
            (Term::App(f1, a1), Term::App(f2, a2)) => {
                let id1 = self.new_constraint_id();
                let id2 = self.new_constraint_id();

                self.add_constraint(Constraint::Unify {
                    id: id1,
                    left: *f1.clone(),
                    right: *f2.clone(),
                    strength,
                });

                self.add_constraint(Constraint::Unify {
                    id: id2,
                    left: *a1.clone(),
                    right: *a2.clone(),
                    strength,
                });

                Ok(true)
            }

            (Term::Pi(x1, t1, b1, i1), Term::Pi(x2, t2, b2, i2)) if i1 == i2 => {
                let id1 = self.new_constraint_id();
                let id2 = self.new_constraint_id();

                self.add_constraint(Constraint::Unify {
                    id: id1,
                    left: *t1.clone(),
                    right: *t2.clone(),
                    strength,
                });

                // Rename bound variables to be consistent
                let fresh_var = format!("x{}", self.next_meta_id);
                let b1_renamed = self.rename_var(x1, &fresh_var, b1);
                let b2_renamed = self.rename_var(x2, &fresh_var, b2);

                self.add_constraint(Constraint::Unify {
                    id: id2,
                    left: b1_renamed,
                    right: b2_renamed,
                    strength,
                });

                Ok(true)
            }

            (Term::Abs(x1, t1, b1), Term::Abs(x2, t2, b2)) => {
                let id1 = self.new_constraint_id();
                let id2 = self.new_constraint_id();

                self.add_constraint(Constraint::Unify {
                    id: id1,
                    left: *t1.clone(),
                    right: *t2.clone(),
                    strength,
                });

                let fresh_var = format!("x{}", self.next_meta_id);
                let b1_renamed = self.rename_var(x1, &fresh_var, b1);
                let b2_renamed = self.rename_var(x2, &fresh_var, b2);

                self.add_constraint(Constraint::Unify {
                    id: id2,
                    left: b1_renamed,
                    right: b2_renamed,
                    strength,
                });

                Ok(true)
            }

            // Universe (Sort) cases
            (Term::Sort(u1), Term::Sort(u2)) => self.unify_universe(u1, u2, strength),

            _ => {
                if strength == ConstraintStrength::Required {
                    Err(TypeError::TypeMismatch {
                        expected: left,
                        actual: right,
                    })
                } else {
                    Ok(false) // Can't solve, but not an error for weak
                              // constraints
                }
            }
        }
    }

    /// Unify two universes
    fn unify_universe(
        &mut self,
        left: &Universe,
        right: &Universe,
        strength: ConstraintStrength,
    ) -> Result<bool, TypeError> {
        // Apply current substitution
        let left = self.substitution.apply_universe(left);
        let right = self.substitution.apply_universe(right);

        // Normalize universes (simplify expressions like 0+1 -> 1)
        let left = self.normalize_universe(&left);
        let right = self.normalize_universe(&right);

        // Check if already equal
        if left == right {
            return Ok(false);
        }

        match (&left, &right) {
            // Same constants
            (Universe::Const(n1), Universe::Const(n2)) if n1 == n2 => Ok(false),

            // Universe variable cases
            (Universe::ScopedVar(s1, v1), Universe::ScopedVar(s2, v2)) if s1 == s2 && v1 == v2 => {
                Ok(false)
            }
            (Universe::ScopedVar(_, _), Universe::ScopedVar(_, _)) => {
                // Different universe variables cannot be unified
                if strength == ConstraintStrength::Required {
                    Err(TypeError::Internal {
                        message: format!(
                            "Cannot unify distinct universe variables {} and {}",
                            left, right
                        ),
                    })
                } else {
                    Ok(false)
                }
            }

            // Meta-variable cases
            (Universe::Meta(id), u) | (u, Universe::Meta(id)) => self.solve_universe_meta(*id, u),

            // Structural cases
            (Universe::Add(base1, n1), Universe::Add(base2, n2)) if n1 == n2 => {
                let constraint_id = self.new_constraint_id();
                self.add_constraint(Constraint::UnifyUniverse {
                    id: constraint_id,
                    left: *base1.clone(),
                    right: *base2.clone(),
                    strength,
                });
                Ok(true)
            }

            // Arithmetic constraints: ?u+n = m means ?u = m-n
            (Universe::Add(base, n), Universe::Const(m))
            | (Universe::Const(m), Universe::Add(base, n)) => {
                if *m >= *n {
                    if let Universe::Meta(id) = base.as_ref() {
                        self.solve_universe_meta(*id, &Universe::Const(m - n))
                    } else {
                        // More complex case - generate constraint for base
                        let constraint_id = self.new_constraint_id();
                        self.add_constraint(Constraint::UnifyUniverse {
                            id: constraint_id,
                            left: *base.clone(),
                            right: Universe::Const(m - n),
                            strength,
                        });
                        Ok(true)
                    }
                } else {
                    Err(TypeError::Internal {
                        message: format!("Cannot solve constraint: {} cannot equal {} (would require negative universe)",
                                       if matches!(&left, Universe::Add(_, _)) { &left } else { &right },
                                       if matches!(&left, Universe::Const(_)) { &left } else { &right }),
                    })
                }
            }

            (Universe::Max(u1, v1), Universe::Max(u2, v2)) => {
                let id1 = self.new_constraint_id();
                let id2 = self.new_constraint_id();

                self.add_constraint(Constraint::UnifyUniverse {
                    id: id1,
                    left: *u1.clone(),
                    right: *u2.clone(),
                    strength,
                });

                self.add_constraint(Constraint::UnifyUniverse {
                    id: id2,
                    left: *v1.clone(),
                    right: *v2.clone(),
                    strength,
                });

                Ok(true)
            }

            (Universe::IMax(u1, v1), Universe::IMax(u2, v2)) => {
                let id1 = self.new_constraint_id();
                let id2 = self.new_constraint_id();

                self.add_constraint(Constraint::UnifyUniverse {
                    id: id1,
                    left: *u1.clone(),
                    right: *u2.clone(),
                    strength,
                });

                self.add_constraint(Constraint::UnifyUniverse {
                    id: id2,
                    left: *v1.clone(),
                    right: *v2.clone(),
                    strength,
                });

                Ok(true)
            }

            _ => {
                if strength == ConstraintStrength::Required {
                    Err(TypeError::Internal {
                        message: format!("Cannot unify universes {} and {}", left, right),
                    })
                } else {
                    Ok(false)
                }
            }
        }
    }

    /// Normalize a universe expression
    fn normalize_universe(&self, u: &Universe) -> Universe {
        match u {
            Universe::Add(base, n) => {
                let base_norm = self.normalize_universe(base);
                match base_norm {
                    Universe::Const(m) => Universe::Const(m + n),
                    _ => Universe::Add(Box::new(base_norm), *n),
                }
            }
            Universe::Max(u1, u2) => {
                let u1_norm = self.normalize_universe(u1);
                let u2_norm = self.normalize_universe(u2);
                match (&u1_norm, &u2_norm) {
                    (Universe::Const(n1), Universe::Const(n2)) => Universe::Const((*n1).max(*n2)),
                    _ => Universe::Max(Box::new(u1_norm), Box::new(u2_norm)),
                }
            }
            _ => u.clone(),
        }
    }

    /// Solve a universe meta-variable
    fn solve_universe_meta(
        &mut self,
        meta_id: u32,
        solution: &Universe,
    ) -> Result<bool, TypeError> {
        // Occurs check for universe meta-variables
        if self.occurs_in_universe(meta_id, solution) {
            return Err(TypeError::Internal {
                message: format!("Universe occurs check failed for ?u{}", meta_id),
            });
        }

        // Record solution
        self.substitution.insert_universe(meta_id, solution.clone());
        Ok(true)
    }

    /// Check if a universe meta-variable occurs in a universe expression
    fn occurs_in_universe(&self, meta_id: u32, universe: &Universe) -> bool {
        match universe {
            Universe::Meta(id) => *id == meta_id,
            Universe::Add(base, _) => self.occurs_in_universe(meta_id, base),
            Universe::Max(u, v) | Universe::IMax(u, v) => {
                self.occurs_in_universe(meta_id, u) || self.occurs_in_universe(meta_id, v)
            }
            _ => false,
        }
    }

    /// Solve a meta-variable
    fn solve_meta(&mut self, meta_id: MetaId, solution: &Term) -> Result<bool, TypeError> {
        // Occurs check
        if self.collect_metas_in_term(solution).contains(&meta_id) {
            return Err(TypeError::Internal {
                message: format!("Occurs check failed for ?m{}", meta_id.0),
            });
        }

        // Check solution is well-scoped
        if let Some(meta_info) = self.metas.get(&meta_id) {
            if !self.is_well_scoped(solution, &meta_info.context) {
                return Err(TypeError::Internal {
                    message: format!("Solution for ?m{} is not well-scoped", meta_id.0),
                });
            }
        }

        // Record solution
        self.substitution.insert(meta_id, solution.clone());

        if let Some(info) = self.metas.get_mut(&meta_id) {
            info.solution = Some(solution.clone());
        }

        Ok(true)
    }

    /// Check if a term is well-scoped in a context
    fn is_well_scoped(&self, term: &Term, context: &[String]) -> bool {
        match term {
            Term::Var(x) => {
                // Check if it's a bound variable or a global constant
                context.contains(x)
                    || self.context.lookup(x).is_some()
                    || self.context.lookup_axiom(x).is_some()
                    || self.context.lookup_constructor(x).is_some()
            }
            Term::App(f, arg) => {
                self.is_well_scoped(f, context) && self.is_well_scoped(arg, context)
            }
            Term::Abs(x, ty, body) => {
                let mut extended_ctx = context.to_vec();
                extended_ctx.push(x.clone());
                self.is_well_scoped(ty, context) && self.is_well_scoped(body, &extended_ctx)
            }
            Term::Pi(x, ty, body, _) => {
                let mut extended_ctx = context.to_vec();
                extended_ctx.push(x.clone());
                self.is_well_scoped(ty, context) && self.is_well_scoped(body, &extended_ctx)
            }
            Term::Const(name) => {
                // Constants should be in the global context
                self.context.lookup_axiom(name).is_some()
                    || self.context.lookup_constructor(name).is_some()
            }
            _ => true, // Sorts, meta-variables, etc.
        }
    }

    /// Alpha equality check
    fn alpha_equal(&self, t1: &Term, t2: &Term) -> bool {
        self.alpha_equal_aux(t1, t2, &mut vec![])
    }

    fn alpha_equal_aux(&self, t1: &Term, t2: &Term, renamings: &mut Vec<(String, String)>) -> bool {
        match (t1, t2) {
            (Term::Var(x), Term::Var(y)) => {
                // Check if this is a bound variable that was renamed
                for (a, b) in renamings.iter() {
                    if a == x && b == y {
                        return true;
                    }
                }
                x == y
            }
            (Term::App(f1, a1), Term::App(f2, a2)) => {
                self.alpha_equal_aux(f1, f2, renamings) && self.alpha_equal_aux(a1, a2, renamings)
            }
            (Term::Abs(x1, t1, b1), Term::Abs(x2, t2, b2)) => {
                if !self.alpha_equal_aux(t1, t2, renamings) {
                    return false;
                }
                renamings.push((x1.clone(), x2.clone()));
                let result = self.alpha_equal_aux(b1, b2, renamings);
                renamings.pop();
                result
            }
            (Term::Pi(x1, t1, b1, i1), Term::Pi(x2, t2, b2, i2)) if i1 == i2 => {
                if !self.alpha_equal_aux(t1, t2, renamings) {
                    return false;
                }
                renamings.push((x1.clone(), x2.clone()));
                let result = self.alpha_equal_aux(b1, b2, renamings);
                renamings.pop();
                result
            }
            (Term::Sort(u1), Term::Sort(u2)) => u1 == u2,
            (Term::Const(c1), Term::Const(c2)) => c1 == c2,
            (Term::Meta(m1), Term::Meta(m2)) => m1 == m2,
            _ => false,
        }
    }

    /// Rename a variable in a term
    fn rename_var(&self, old: &str, new: &str, term: &Term) -> Term {
        match term {
            Term::Var(x) if x == old => Term::Var(new.to_string()),
            Term::Var(x) => Term::Var(x.clone()),
            Term::App(f, arg) => Term::App(
                Box::new(self.rename_var(old, new, f)),
                Box::new(self.rename_var(old, new, arg)),
            ),
            Term::Abs(x, ty, body) if x != old => Term::Abs(
                x.clone(),
                Box::new(self.rename_var(old, new, ty)),
                Box::new(self.rename_var(old, new, body)),
            ),
            Term::Pi(x, ty, body, implicit) if x != old => Term::Pi(
                x.clone(),
                Box::new(self.rename_var(old, new, ty)),
                Box::new(self.rename_var(old, new, body)),
                *implicit,
            ),
            _ => term.clone(),
        }
    }

    /// Propagate solutions to dependent constraints
    fn propagate_solution(&mut self) -> Result<(), TypeError> {
        // Apply substitution to all remaining constraints
        let constraints: Vec<_> = self.constraints.drain().collect();

        for (id, constraint) in constraints {
            let updated = self.apply_subst_to_constraint(constraint);
            self.constraints.insert(id, updated);
        }

        Ok(())
    }

    /// Apply substitution to a constraint
    fn apply_subst_to_constraint(&self, constraint: Constraint) -> Constraint {
        match constraint {
            Constraint::Unify {
                id,
                left,
                right,
                strength,
            } => Constraint::Unify {
                id,
                left: self.substitution.apply(&left),
                right: self.substitution.apply(&right),
                strength,
            },
            Constraint::HasType {
                id,
                term,
                expected_type,
            } => Constraint::HasType {
                id,
                term: self.substitution.apply(&term),
                expected_type: self.substitution.apply(&expected_type),
            },
            Constraint::UnifyUniverse {
                id,
                left,
                right,
                strength,
            } => Constraint::UnifyUniverse {
                id,
                left: self.substitution.apply_universe(&left),
                right: self.substitution.apply_universe(&right),
                strength,
            },
            Constraint::UniverseLevel { id, left, right } => Constraint::UniverseLevel {
                id,
                left: self.substitution.apply_universe(&left),
                right: self.substitution.apply_universe(&right),
            },
            Constraint::Delayed {
                id,
                constraint,
                waiting_on,
            } => Constraint::Delayed {
                id,
                constraint: Box::new(self.apply_subst_to_constraint(*constraint)),
                waiting_on,
            },
        }
    }

    /// Solve a HasType constraint by type inference and unification
    fn solve_has_type_constraint(
        &mut self,
        term: &Term,
        expected_type: &Term,
    ) -> Result<bool, TypeError> {
        match term {
            Term::Meta(meta_name) => {
                // If the term is a meta-variable, we can potentially solve it
                if let Some(meta_id) = self.parse_meta_name(meta_name) {
                    if let Some(meta_info) = self.metas.get(&meta_id) {
                        if meta_info.solution.is_none() {
                            // Try to construct a term that has the expected type
                            if let Some(solution) = self.synthesize_term_of_type(expected_type)? {
                                return self.solve_meta(meta_id, &solution);
                            }
                        }
                    }
                }
                // If we can't solve the meta, try unifying with expected type
                self.unify(term, expected_type, ConstraintStrength::Preferred)
            }

            Term::App(f, arg) => {
                // For applications, we need to ensure the function type is compatible
                // Create a fresh meta for the function type: ?F : ?A -> ?B
                let arg_type_meta_id = self.fresh_meta(vec![], None);
                let result_type_meta_id = self.fresh_meta(vec![], None);
                let arg_type_meta = Term::Meta(format!("?m{}", arg_type_meta_id.0));
                let result_type_meta = Term::Meta(format!("?m{}", result_type_meta_id.0));
                let func_type = Term::Pi(
                    "_".to_string(),
                    Box::new(arg_type_meta.clone()),
                    Box::new(result_type_meta.clone()),
                    false,
                );

                // Add constraints:
                // 1. f : ?A -> ?B
                // 2. arg : ?A
                // 3. ?B ≡ expected_type
                let f_constraint = Constraint::HasType {
                    id: self.new_constraint_id(),
                    term: f.as_ref().clone(),
                    expected_type: func_type,
                };
                let arg_constraint = Constraint::HasType {
                    id: self.new_constraint_id(),
                    term: arg.as_ref().clone(),
                    expected_type: arg_type_meta,
                };
                let result_constraint = Constraint::Unify {
                    id: self.new_constraint_id(),
                    left: result_type_meta,
                    right: expected_type.clone(),
                    strength: ConstraintStrength::Required,
                };

                self.add_constraint(f_constraint);
                self.add_constraint(arg_constraint);
                self.add_constraint(result_constraint);

                Ok(true)
            }

            Term::Abs(param, param_type, body) => {
                // For lambda abstractions, expected type should be a Pi type
                match expected_type {
                    Term::Pi(_, expected_param_type, expected_body_type, _) => {
                        // Unify parameter types and check body type
                        let param_unify = Constraint::Unify {
                            id: self.new_constraint_id(),
                            left: param_type.as_ref().clone(),
                            right: expected_param_type.as_ref().clone(),
                            strength: ConstraintStrength::Required,
                        };

                        let body_check = Constraint::HasType {
                            id: self.new_constraint_id(),
                            term: body.as_ref().clone(),
                            expected_type: expected_body_type.as_ref().clone(),
                        };

                        self.add_constraint(param_unify);
                        self.add_constraint(body_check);

                        Ok(true)
                    }
                    Term::Meta(_) => {
                        // Expected type is a meta-variable, create a Pi type for it
                        let body_type_meta_id = self.fresh_meta(vec![param.clone()], None);
                        let body_type_meta = Term::Meta(format!("?m{}", body_type_meta_id.0));
                        let pi_type = Term::Pi(
                            param.clone(),
                            param_type.clone(),
                            Box::new(body_type_meta.clone()),
                            false,
                        );

                        let type_unify = Constraint::Unify {
                            id: self.new_constraint_id(),
                            left: expected_type.clone(),
                            right: pi_type,
                            strength: ConstraintStrength::Required,
                        };

                        let body_check = Constraint::HasType {
                            id: self.new_constraint_id(),
                            term: body.as_ref().clone(),
                            expected_type: body_type_meta,
                        };

                        self.add_constraint(type_unify);
                        self.add_constraint(body_check);

                        Ok(true)
                    }
                    _ => {
                        // Type mismatch - lambda can't have non-function type
                        Err(TypeError::TypeMismatch {
                            expected: expected_type.clone(),
                            actual: Term::Pi(
                                param.clone(),
                                param_type.clone(),
                                Box::new(Term::Meta("?unknown".to_string())),
                                false,
                            ),
                        })
                    }
                }
            }

            _ => {
                // For other terms, just unify with the expected type
                self.unify(term, expected_type, ConstraintStrength::Preferred)
            }
        }
    }

    /// Attempt to synthesize a term of a given type (basic heuristics)
    fn synthesize_term_of_type(&self, expected_type: &Term) -> Result<Option<Term>, TypeError> {
        match expected_type {
            Term::Sort(_) => {
                // For Sort types, we could return a fresh meta or a simple type
                Ok(Some(Term::Meta(format!("?synth{}", self.next_meta_id))))
            }
            Term::Pi(param, param_type, _body_type, _implicit) => {
                // For Pi types, synthesize a lambda that matches the Pi structure
                // Use a fresh meta-variable for the body to avoid infinite recursion
                let body_term = Term::Meta(format!("?body{}", self.next_meta_id));

                Ok(Some(Term::Abs(
                    param.clone(),
                    param_type.clone(),
                    Box::new(body_term),
                )))
            }
            Term::Meta(_) => {
                // Can't synthesize for unknown types
                Ok(None)
            }
            _ => {
                // For concrete types, return a meta-variable
                Ok(Some(Term::Meta(format!("?synth{}", self.next_meta_id))))
            }
        }
    }

    /// Check if a term is a Miller pattern
    /// Miller patterns are terms of the form: ?M x1 x2 ... xn where xi are
    /// distinct bound variables
    fn is_miller_pattern(&self, term: &Term) -> bool {
        let (head, args) = self.decompose_application(term);

        match head {
            Term::Meta(_) => {
                // Check that all arguments are distinct variables
                let mut seen = HashSet::new();
                for arg in args {
                    if let Term::Var(x) = arg {
                        if !seen.insert(x.clone()) {
                            return false; // Not distinct
                        }
                    } else {
                        return false; // Not a variable
                    }
                }
                true
            }
            _ => false,
        }
    }

    /// Extract the spine of a Miller pattern (the meta-variable and its
    /// arguments)
    fn extract_miller_spine(&self, term: &Term) -> Option<(String, Vec<String>)> {
        // First validate it's actually a Miller pattern
        if !self.is_miller_pattern(term) {
            return None;
        }

        let (head, args) = self.decompose_application(term);

        if let Term::Meta(meta_name) = head {
            // Collect variable names (we already know they're all variables from
            // is_miller_pattern)
            let var_names: Vec<String> = args
                .iter()
                .map(|arg| {
                    if let Term::Var(name) = arg {
                        name.clone()
                    } else {
                        unreachable!()
                    }
                })
                .collect();

            Some((meta_name.clone(), var_names))
        } else {
            None
        }
    }

    /// Solve simple Miller pattern unification
    /// Only handles simple cases
    fn solve_miller_pattern(
        &mut self,
        meta_name: &str,
        spine: &[String],
        other: &Term,
    ) -> Result<bool, TypeError> {
        // Check if we can abstract over the spine variables
        if !Self::can_abstract_over_variables(other, spine) {
            // Can't abstract, fall back to regular unification
            return self.unify(
                &Term::Meta(meta_name.to_string()),
                other,
                ConstraintStrength::Required,
            );
        }

        if let Some(meta_id) = self.parse_meta_name(meta_name) {
            // Only handle simple cases to avoid complexity
            match other {
                // Case 1: ?M x ≡ y (different variable) -> ?M := λx. y
                Term::Var(y) if spine.len() == 1 && spine[0] != *y => {
                    let solution = Term::Abs(
                        spine[0].clone(),
                        Box::new(Term::Meta(format!("?T{}", self.next_meta_id))), /* Type inferred later */
                        Box::new(other.clone()),
                    );
                    return self.solve_meta(meta_id, &solution);
                }

                // Case 2: ?M x ≡ c (constant) -> ?M := λx. c
                Term::Const(_) | Term::Sort(_) if spine.len() == 1 => {
                    let solution = Term::Abs(
                        spine[0].clone(),
                        Box::new(Term::Meta(format!("?T{}", self.next_meta_id))),
                        Box::new(other.clone()),
                    );
                    return self.solve_meta(meta_id, &solution);
                }

                _ => {
                    // For more complex cases, we could build the full lambda
                    // abstraction but for now, fall back to
                    // regular unification
                }
            }
        }

        // Fall back to regular unification
        self.unify(
            &Term::Meta(meta_name.to_string()),
            other,
            ConstraintStrength::Required,
        )
    }

    /// Check if a term can be abstracted over the given variables
    fn can_abstract_over_variables(term: &Term, vars: &[String]) -> bool {
        // For now, implement a simple check
        // In a full implementation, this would check that the term only uses variables
        // that are either in the spine or are bound locally
        match term {
            Term::Var(v) => vars.contains(v), // Variable must be in spine
            Term::App(f, arg) => {
                Self::can_abstract_over_variables(f, vars)
                    && Self::can_abstract_over_variables(arg, vars)
            }
            Term::Abs(param, ty, body) => {
                // Extend the scope with the bound parameter
                let mut extended_vars = vars.to_vec();
                extended_vars.push(param.clone());
                Self::can_abstract_over_variables(ty, vars)
                    && Self::can_abstract_over_variables(body, &extended_vars)
            }
            Term::Meta(_) => true,  // Meta-variables are always abstractable
            Term::Const(_) => true, // Constants are always abstractable
            Term::Sort(_) => true,  // Sorts are always abstractable
            _ => false,             // Conservative for complex constructs
        }
    }

    /// Delay a constraint for later processing
    fn delay_constraint(
        &mut self,
        constraint_id: ConstraintId,
        waiting_on: HashSet<MetaId>,
    ) -> Result<(), TypeError> {
        if let Some(constraint) = self.constraints.remove(&constraint_id) {
            let delayed = Constraint::Delayed {
                id: constraint_id,
                constraint: Box::new(constraint),
                waiting_on,
            };
            self.constraints.insert(constraint_id, delayed);
            self.delayed_constraints.insert(constraint_id);
        }
        Ok(())
    }

    /// Wake up delayed constraints that are no longer blocked
    fn wake_delayed_constraints(&mut self) -> Result<Vec<ConstraintId>, TypeError> {
        let mut woken = Vec::new();
        let mut to_wake = Vec::new();

        // Check which delayed constraints can now be woken up
        for constraint_id in &self.delayed_constraints {
            if let Some(Constraint::Delayed {
                waiting_on,
                constraint,
                ..
            }) = self.constraints.get(constraint_id)
            {
                // Check if all blocking metas are now solved
                let all_solved = waiting_on.iter().all(|meta_id| {
                    self.metas
                        .get(meta_id)
                        .is_some_and(|info| info.solution.is_some())
                });

                if all_solved {
                    to_wake.push((*constraint_id, constraint.as_ref().clone()));
                }
            }
        }

        // Wake up the constraints
        for (constraint_id, original_constraint) in to_wake {
            self.constraints.insert(constraint_id, original_constraint);
            self.delayed_constraints.remove(&constraint_id);
            woken.push(constraint_id);
        }

        Ok(woken)
    }

    /// Check if we can delay a constraint
    fn can_delay(&self, constraint_id: &ConstraintId) -> bool {
        if let Some(constraint) = self.constraints.get(constraint_id) {
            match constraint {
                Constraint::Unify {
                    left,
                    right,
                    strength,
                    ..
                } => {
                    // We can delay weak constraints that involve unsolved metas
                    if *strength == ConstraintStrength::Weak {
                        let left_metas = self.collect_metas_in_term(left);
                        let right_metas = self.collect_metas_in_term(right);

                        // Check if any involved metas are unsolved
                        for meta_id in left_metas.iter().chain(right_metas.iter()) {
                            if self.is_unsolved_meta(meta_id) {
                                return true;
                            }
                        }
                    }
                    false
                }
                Constraint::HasType {
                    term,
                    expected_type,
                    ..
                } => {
                    // Can delay HasType constraints involving unsolved metas
                    let term_metas = self.collect_metas_in_term(term);
                    let type_metas = self.collect_metas_in_term(expected_type);

                    for meta_id in term_metas.iter().chain(type_metas.iter()) {
                        if let Some(meta_info) = self.metas.get(meta_id) {
                            if meta_info.solution.is_none() {
                                return true;
                            }
                        }
                    }
                    false
                }
                Constraint::Delayed { .. } => true, // Already delayed
                _ => false,
            }
        } else {
            false
        }
    }

    /// Check if a meta-variable is unsolved
    fn is_unsolved_meta(&self, meta_id: &MetaId) -> bool {
        self.metas
            .get(meta_id)
            .is_some_and(|info| info.solution.is_none())
    }
}
/// Information about a meta-variable
#[derive(Debug, Clone)]
pub struct MetaInfo {
    pub id: MetaId,
    pub context: Vec<String>, // Variables in scope when meta was created
    pub expected_type: Option<Term>, // Expected type of the meta-variable
    pub solution: Option<Term>, // Solution if solved
    pub dependencies: HashSet<MetaId>, // Meta-variables this depends on
    pub occurrences: Vec<ConstraintId>, // Constraints this meta appears in
}
}

Meta-variables represent unknown terms that get resolved through unification and constraint solving. Our implementation tracks meta-variable dependencies and applies solving algorithms to determine unique solutions when possible.

Takeaways

The Calculus of Constructions represents a unique convergence of computational and logical capabilities that distinguishes it from other type systems. The fundamental characteristic that sets CoC apart is its unification of terms, types, and kinds within a single syntactic framework, eliminating the artificial stratification found in traditional type systems. This unification enables unprecedented expressiveness where types can depend on computational values, creating a system where mathematical specifications and their implementations coexist naturally within the same linguistic framework.

The definitional equality mechanism provides computational content to the type system through β-reduction, η-conversion, and definition unfolding, creating a rich equational theory that recognizes when different syntactic expressions represent the same computational object. This equality extends beyond simple syntactic matching to include semantic equivalence, enabling the type checker to recognize mathematical identities and computational transformations automatically. The normalization algorithm ensures that definitional equality checking remains decidable while providing the computational power needed for type-level computations.