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

Calculus of Constructions

The Calculus of Constructions represents the pinnacle of the lambda cube, occupying the most expressive corner where all three dimensions of abstraction converge. This system unifies terms, types, and kinds into a single syntactic framework, eliminating the artificial boundaries that separate computation from logic and enabling types to express arbitrary mathematical propositions with computational content.

Where previous systems in our exploration maintained strict hierarchies between terms, types, and kinds, the Calculus of Constructions dissolves these distinctions. Types become first-class citizens that can be manipulated, passed to functions, and returned as results. This unification enables unprecedented expressiveness while maintaining logical consistency through a carefully constructed universe hierarchy.

Position in the Lambda Cube

The Calculus of Constructions sits at vertex λ2ωP of the lambda cube, combining all three forms of abstraction that define the cube’s dimensions:

Terms depending on Types (\( \uparrow\)-axis): Polymorphic functions like \(\mathsf{id} : \forall A : \mathsf{Type}.; A \to A \) where terms abstract over type parameters, enabling parametric polymorphism across all types in the system.

Types depending on Types (\( \nearrow \)-axis): Type constructors like \( \mathsf{List} : \mathsf{Type} \to \mathsf{Type} \) and \( \Sigma : (A : \mathsf{Type}) \to (A \to \mathsf{Type}) \to \mathsf{Type} \) where types can abstract over other types, enabling higher-kinded polymorphism and type-level computation.

Types depending on Terms (\( \rightarrow \) -axis): Dependent types like \( \mathsf{Vec} : \mathsf{Nat} \to \mathsf{Type} \to \mathsf{Type} \) where the structure of types depends on the values of terms, enabling precise specification of data structure properties and program invariants.

The convergence of these three dimensions creates a system of unprecedented expressiveness. Unlike System F-ω, which provides polymorphism but maintains a clear separation between terms and types, the Calculus of Constructions allows types to depend on arbitrary term-level computations while maintaining decidable type checking through normalization properties.

The Curry-Howard Correspondence

The Calculus of Constructions realizes the profound connection between computation and logic known as the Curry-Howard correspondence. In this correspondence, types represent logical propositions and terms represent constructive proofs of those propositions. This isomorphism enables the same syntactic framework to express both programs and mathematical theorems with their proofs.

Propositions as Types: Every logical statement corresponds to a type. The proposition “for all natural numbers n, n + 0 = n” becomes the type ∀n : Nat. Eq Nat (plus n zero) n, where Eq represents propositional equality.

Proofs as Programs: Every constructive proof corresponds to a program that computes a witness for the proposition. A proof of the above proposition becomes a function that takes a natural number and produces evidence that adding zero preserves the number.

Proof Checking as Type Checking: Verifying the correctness of a mathematical proof reduces to checking that a program has the expected type. The type checker becomes a proof checker, ensuring that purported proofs actually establish their claimed propositions.

This correspondence transforms programming into theorem proving and theorem proving into programming, creating a unified framework where mathematical rigor and computational efficiency coexist naturally.

Dependent Types: The Foundation

The key innovation that enables the Calculus of Constructions’ expressiveness is the dependent product type, or Π-type. This construct generalizes the familiar function arrow to create types whose structure depends on the values they abstract over.

Dependent Products (Π-Types)

The dependent product type \( \Pi x : A.; B \) represents functions where the return type \( B \) can depend on the input value \( x \). When the variable \( x \) does not appear in \( B \), this reduces to the simple function type \( A \to B \). When \( x \) does appear in \( B \), we obtain true dependency where the return type varies based on the input value.

-- Simple function type (non-dependent)
add : Nat → Nat → Nat

-- Dependent function type
vec : (n : Nat) → Type → Type
create_vec : (n : Nat) → (A : Type) → vec n A

-- The return type depends on the input value n
lookup : (n : Nat) → (A : Type) → (v : vec n A) → (i : Fin n) → A

The dependent product enables precise type-level specifications that capture program invariants directly in the type system. A vector lookup function can guarantee at compile time that the index falls within the vector bounds, eliminating runtime bounds checking while maintaining type safety.

Dependent Sums (Σ-Types)

The dependent sum type \( \Sigma x : A.; B \) represents pairs where the type of the second component depends on the value of the first component. This enables existential quantification and the creation of heterogeneous data structures with precise type relationships.

-- Dependent pair: a number and a vector of that length
sized_vec : Type := Σ n : Nat. vec n Int

-- Create a sized vector
example_vec : sized_vec := ⟨3, [1, 2, 3]⟩

-- Pattern match to extract components
process_vec : sized_vec → Int :=
  fun ⟨n, v⟩ => sum_vector v  -- Type checker knows v has length n

Dependent sums enable the expression of existential propositions where we assert the existence of a value with specific properties without revealing the exact value, while maintaining the ability to use that value computationally.

Universe Hierarchy and Logical Consistency

The power of dependent types raises fundamental questions about self-reference and logical consistency. If types can contain arbitrary values and types themselves are values, what prevents the construction of paradoxical types like the set of all sets that do not contain themselves?

The Calculus of Constructions addresses this challenge through a universe hierarchy that stratifies types by their complexity level. Each universe contains types of bounded complexity, and the universe hierarchy prevents the construction of self-referential types that would lead to logical inconsistency.

Universe Levels

-- Universe hierarchy
Prop : Type          -- Propositions with no computational content
Type : Type 1        -- Small types (Nat, Bool, etc.)
Type 1 : Type 2      -- Types of type constructors
Type 2 : Type 3      -- Higher-order type constructors
-- ... infinite hierarchy

Prop: The universe of propositions represents logical statements that, when proven, carry no computational information beyond their truth. Proof irrelevance means that all proofs of the same proposition are considered equal, enabling efficient compilation where proof terms can be erased.

Type n: The universe hierarchy Type 0, Type 1, Type 2, ... represents computational types at increasing levels of abstraction. Types like Nat and Bool inhabit Type 0, while type constructors like List : Type 0 → Type 0 inhabit Type 1.

Universe Polymorphism: Definitions can be polymorphic over universe levels, enabling generic constructions that work across the entire hierarchy. The identity function can be defined once and work for types at any universe level.

The universe hierarchy maintains predicativity for computational types, meaning that a type constructor at level n can only quantify over types at levels strictly less than n. This restriction prevents the construction of large elimination paradoxes while maintaining logical consistency.

However, the proposition universe Prop is impredicative, allowing quantification over arbitrary types including propositions themselves. This impredicativity enables the expression of powerful logical principles while maintaining consistency through proof irrelevance.

Implementation Architecture

Our Calculus of Constructions implementation demonstrates how these theoretical concepts translate into practical type checking algorithms and programming language features.

#![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>),
}
}

The term language unifies all syntactic categories into a single framework where the same constructs serve multiple roles depending on context. Lambda abstractions create both functions and proof terms, applications represent both function calls and modus ponens, and products represent both function types and universal quantification.

Bidirectional Type Checking

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

Synthesis Mode: Given a term, determines its type by analyzing the term’s structure and propagating type information through the syntax tree.

Checking Mode: Given a term and an expected type, verifies that the term inhabits the expected type by checking compatibility modulo definitional equality.

Constraint-Based Inference

The system includes advanced constraint solving for implicit argument inference and unification of dependent types. Meta-variables represent unknown types that get resolved through unification with complex dependency tracking.

#![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)
    }
}
/// 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
}
/// Constraint identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ConstraintId(pub u64);
/// 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 (doesn't require &self)
    #[allow(clippy::only_used_in_recursion)]
    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).unwrap();

                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
    #[allow(clippy::only_used_in_recursion)]
    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
    #[allow(clippy::only_used_in_recursion)]
    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![])
    }

    #[allow(clippy::only_used_in_recursion)]
    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
    #[allow(clippy::only_used_in_recursion)]
    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())
    }
}
/// 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>,
    },
}
}

The constraint solver handles higher-order unification patterns, universe level constraints, and delayed constraint resolution to enable practical programming with dependent types while maintaining theoretical soundness.

Theoretical Significance

The Calculus of Constructions is more than just another extension of System-F; it has a profound theoretical significance. It essentially demonstrates the fundamental unity between computation and mathematics. Which is one of the most profound ideas of theoretical computer science.

By showing that every constructive mathematical proof corresponds to a program and every program type-checks according to logical principles, CoC bridges the gap between formal verification and practical programming. And thats pretty amazing!

The system also provides a foundation for interactive theorem proving, where mathematical proofs are constructed through programming and verified through type checking. Proof assistants like Coq and Lean build upon the theoretical foundations established by the Calculus of Constructions, demonstrating the practical value of this unification.

Although Coq is interesting historically, we’re going to mostly be inspired by the Lean 4 model and adopt a small version of its type system and syntax in our toy implementation.