Universe Polymorphism
Universe polymorphism enables definitions to abstract over universe levels, creating truly generic constructions that work across the entire universe hierarchy. Our implementation includes a dedicated universe constraint solver that handles the complex arithmetic and constraint relationships that arise in polymorphic universe contexts.
Universe Constraint Solver
#![allow(unused)] fn main() { use std::collections::{HashMap, HashSet}; /// Universe level constraint solver for polymorphic universe levels use crate::ast::{Universe, UniverseConstraint}; impl UniverseSolver { pub fn new() -> Self { UniverseSolver { substitutions: HashMap::new(), } } /// Solve universe constraints and return substitutions pub fn solve(&mut self, constraints: &[UniverseConstraint]) -> Result<(), String> { // XXX: Simple constraint solver, in a full system this would be much more // sophisticated for constraint in constraints { self.solve_constraint(constraint)?; } Ok(()) } /// Solve a single constraint fn solve_constraint(&mut self, constraint: &UniverseConstraint) -> Result<(), String> { match constraint { UniverseConstraint::Equal(u1, u2) => self.unify_universes(u1, u2), UniverseConstraint::LessEq(u1, u2) => { // XXX: For now, treat less-equal as equality self.unify_universes(u1, u2) } } } /// Unify two universe levels fn unify_universes(&mut self, u1: &Universe, u2: &Universe) -> Result<(), String> { let u1_subst = self.apply_substitution(u1); let u2_subst = self.apply_substitution(u2); match (&u1_subst, &u2_subst) { (Universe::ScopedVar(_, x), u) | (u, Universe::ScopedVar(_, x)) => { if let Universe::ScopedVar(_, y) = u { if x == y { return Ok(()); } } // Occurs check if self.occurs_check(x, u) { return Err(format!( "Universe variable {} occurs in {}", x, self.universe_to_string(u) )); } self.substitutions.insert(x.clone(), u.clone()); Ok(()) } (Universe::Const(n1), Universe::Const(n2)) if n1 == n2 => Ok(()), (Universe::Const(_), Universe::Const(_)) => { Err("Cannot unify different constants".to_string()) } (Universe::Add(u1, n1), Universe::Add(u2, n2)) if n1 == n2 => { self.unify_universes(u1, u2) } (Universe::Add(_, _), Universe::Add(_, _)) => { Err("Cannot unify different additions".to_string()) } (Universe::Max(u1, v1), Universe::Max(u2, v2)) => { self.unify_universes(u1, u2)?; self.unify_universes(v1, v2) } (Universe::IMax(u1, v1), Universe::IMax(u2, v2)) => { self.unify_universes(u1, u2)?; self.unify_universes(v1, v2) } _ => Err(format!( "Cannot unify universe levels {} and {}", self.universe_to_string(&u1_subst), self.universe_to_string(&u2_subst) )), } } /// Apply substitutions to a universe level fn apply_substitution(&self, u: &Universe) -> Universe { match u { Universe::ScopedVar(_scope, x) => { if let Some(subst) = self.substitutions.get(x) { self.apply_substitution(subst) } else { u.clone() } } Universe::Const(n) => Universe::Const(*n), Universe::Add(u, n) => Universe::Add(Box::new(self.apply_substitution(u)), *n), Universe::Max(u, v) => Universe::Max( Box::new(self.apply_substitution(u)), Box::new(self.apply_substitution(v)), ), Universe::IMax(u, v) => Universe::IMax( Box::new(self.apply_substitution(u)), Box::new(self.apply_substitution(v)), ), _ => u.clone(), } } /// Check if a universe variable occurs in a universe level fn occurs_check(&self, var: &str, u: &Universe) -> bool { match u { Universe::ScopedVar(_, x) => x == var, Universe::Add(u, _) => self.occurs_check(var, u), Universe::Max(u, v) | Universe::IMax(u, v) => { self.occurs_check(var, u) || self.occurs_check(var, v) } Universe::Const(_) => false, Universe::Meta(_) => false, // Meta variables don't contain named variables } } /// Convert universe to string representation fn universe_to_string(&self, u: &Universe) -> String { match u { Universe::Const(n) => n.to_string(), Universe::ScopedVar(scope, var) => format!("{}::{}", scope, var), Universe::Meta(id) => format!("?u{}", id), Universe::Add(u, n) => format!("({} + {})", self.universe_to_string(u), n), Universe::Max(u, v) => format!( "max({}, {})", self.universe_to_string(u), self.universe_to_string(v) ), Universe::IMax(u, v) => format!( "imax({}, {})", self.universe_to_string(u), self.universe_to_string(v) ), } } /// Get the final substitution for a universe variable pub fn get_substitution(&self, var: &str) -> Option<Universe> { self.substitutions .get(var) .map(|u| self.apply_substitution(u)) } /// Apply all substitutions to a universe level pub fn substitute_universe(&self, u: &Universe) -> Universe { self.apply_substitution(u) } /// Get all substitutions pub fn get_all_substitutions(&self) -> &HashMap<String, Universe> { &self.substitutions } /// Check if universe level constraints are satisfiable pub fn is_satisfiable(&self, constraints: &[UniverseConstraint]) -> bool { let mut solver = self.clone(); solver.solve(constraints).is_ok() } /// Generate fresh universe variable pub fn fresh_universe_var(&self, base: &str, avoid: &HashSet<String>) -> String { let mut counter = 0; loop { let name = if counter == 0 { base.to_string() } else { format!("{}{}", base, counter) }; if !avoid.contains(&name) && !self.substitutions.contains_key(&name) { return name; } counter += 1; } } } impl Default for UniverseSolver { fn default() -> Self { Self::new() } } /// Universe level constraint solver #[derive(Debug, Clone)] pub struct UniverseSolver { /// Substitutions for universe variables substitutions: HashMap<String, Universe>, } }
The UniverseSolver
manages universe-level constraints independently from the main constraint solver, enabling specialized algorithms for universe arithmetic and level unification. This separation allows for efficient handling of universe polymorphism without complicating the main type checking algorithms.
Universe Constraint Types
Universe constraints capture the relationships between universe levels that must be maintained for logical consistency:
#![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), } }
The constraint system handles two fundamental relationships:
Equality Constraints (Equal
) require two universe levels to be identical, arising from type equality requirements in dependent contexts where universe levels must match exactly.
Ordering Constraints (LessEq
) ensure that one universe level is less than or equal to another, maintaining the predicativity requirements that prevent logical paradoxes.
Universe Constraint Solving
#![allow(unused)] fn main() { /// Solve universe constraints and return substitutions pub fn solve(&mut self, constraints: &[UniverseConstraint]) -> Result<(), String> { // XXX: Simple constraint solver, in a full system this would be much more // sophisticated for constraint in constraints { self.solve_constraint(constraint)?; } Ok(()) } }
The main solving algorithm processes constraint lists through iterative constraint resolution, ensuring that all universe relationships are satisfied consistently:
#![allow(unused)] fn main() { /// Solve a single constraint fn solve_constraint(&mut self, constraint: &UniverseConstraint) -> Result<(), String> { match constraint { UniverseConstraint::Equal(u1, u2) => self.unify_universes(u1, u2), UniverseConstraint::LessEq(u1, u2) => { // XXX: For now, treat less-equal as equality self.unify_universes(u1, u2) } } } }
Individual constraint solving handles the different constraint types through specialized algorithms. Equality constraints use unification, while ordering constraints require more complex analysis to ensure the universe hierarchy remains consistent.
Universe Unification Algorithm
#![allow(unused)] fn main() { /// Unify two universe levels fn unify_universes(&mut self, u1: &Universe, u2: &Universe) -> Result<(), String> { let u1_subst = self.apply_substitution(u1); let u2_subst = self.apply_substitution(u2); match (&u1_subst, &u2_subst) { (Universe::ScopedVar(_, x), u) | (u, Universe::ScopedVar(_, x)) => { if let Universe::ScopedVar(_, y) = u { if x == y { return Ok(()); } } // Occurs check if self.occurs_check(x, u) { return Err(format!( "Universe variable {} occurs in {}", x, self.universe_to_string(u) )); } self.substitutions.insert(x.clone(), u.clone()); Ok(()) } (Universe::Const(n1), Universe::Const(n2)) if n1 == n2 => Ok(()), (Universe::Const(_), Universe::Const(_)) => { Err("Cannot unify different constants".to_string()) } (Universe::Add(u1, n1), Universe::Add(u2, n2)) if n1 == n2 => { self.unify_universes(u1, u2) } (Universe::Add(_, _), Universe::Add(_, _)) => { Err("Cannot unify different additions".to_string()) } (Universe::Max(u1, v1), Universe::Max(u2, v2)) => { self.unify_universes(u1, u2)?; self.unify_universes(v1, v2) } (Universe::IMax(u1, v1), Universe::IMax(u2, v2)) => { self.unify_universes(u1, u2)?; self.unify_universes(v1, v2) } _ => Err(format!( "Cannot unify universe levels {} and {}", self.universe_to_string(&u1_subst), self.universe_to_string(&u2_subst) )), } } }
Universe unification demonstrates the complexity of working with universe-level arithmetic. The algorithm handles multiple universe expression forms:
-
Variable Unification: When unifying a universe variable with any expression, we create a substitution mapping after performing occurs checking to prevent infinite universe expressions.
-
Constant Unification: Universe constants can only unify with identical constants, ensuring that concrete levels like
Type 0
andType 1
remain distinct. -
Arithmetic Expressions: Universe expressions like
u + 1
andmax(u, v)
require structural decomposition where we recursively unify the component universe expressions.
Substitution and Normalization
#![allow(unused)] fn main() { /// Apply substitutions to a universe level fn apply_substitution(&self, u: &Universe) -> Universe { match u { Universe::ScopedVar(_scope, x) => { if let Some(subst) = self.substitutions.get(x) { self.apply_substitution(subst) } else { u.clone() } } Universe::Const(n) => Universe::Const(*n), Universe::Add(u, n) => Universe::Add(Box::new(self.apply_substitution(u)), *n), Universe::Max(u, v) => Universe::Max( Box::new(self.apply_substitution(u)), Box::new(self.apply_substitution(v)), ), Universe::IMax(u, v) => Universe::IMax( Box::new(self.apply_substitution(u)), Box::new(self.apply_substitution(v)), ), _ => u.clone(), } } }
Universe substitution application demonstrates the recursive nature of universe expressions. When substituting into compound expressions like Add
or Max
, we must recursively apply substitutions to all subcomponents while maintaining the arithmetic structure.
Occurs Check for Universe Variables
#![allow(unused)] fn main() { /// Check if a universe variable occurs in a universe level fn occurs_check(&self, var: &str, u: &Universe) -> bool { match u { Universe::ScopedVar(_, x) => x == var, Universe::Add(u, _) => self.occurs_check(var, u), Universe::Max(u, v) | Universe::IMax(u, v) => { self.occurs_check(var, u) || self.occurs_check(var, v) } Universe::Const(_) => false, Universe::Meta(_) => false, // Meta variables don't contain named variables } } }
The occurs check prevents infinite universe expressions by ensuring that universe variables don’t appear within their own solutions. This check must traverse the entire structure of universe expressions, including arithmetic operations and maximum expressions.
Universe Expression Normalization
The universe solver includes normalization capabilities that simplify universe expressions to canonical forms:
#![allow(unused)] fn main() { 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(), } }
This normalization process:
- Arithmetic Simplification: Combines constants in addition expressions like
Const(2) + 3
becomingConst(5)
- Maximum Computation: Evaluates maximum expressions between constants
- Canonical Forms: Maintains normalized expressions that improve unification success
Universe Polymorphic Definitions
Universe polymorphism enables definitions like:
def id.{u} (A : Sort u) (x : A) : A := x
The .{u}
syntax introduces a universe parameter that can be instantiated at different levels:
-- id instantiated at Type 0
id_nat : Nat → Nat := id.{0} Nat
-- id instantiated at Type 1
id_type : Type → Type := id.{1} Type
Fresh Variable Generation
#![allow(unused)] fn main() { /// Generate fresh universe variable pub fn fresh_universe_var(&self, base: &str, avoid: &HashSet<String>) -> String { let mut counter = 0; loop { let name = if counter == 0 { base.to_string() } else { format!("{}{}", base, counter) }; if !avoid.contains(&name) && !self.substitutions.contains_key(&name) { return name; } counter += 1; } } }
Fresh universe variable generation ensures that each universe abstraction gets unique variable names, preventing conflicts in complex polymorphic definitions. The algorithm:
- Base Name Generation: Starts with a descriptive base name
- Conflict Avoidance: Checks against existing variables and substitutions
- Counter Extension: Adds numeric suffixes when conflicts occur
- Uniqueness Guarantee: Ensures the returned name is globally unique
Substitution Management
#![allow(unused)] fn main() { /// Get the final substitution for a universe variable pub fn get_substitution(&self, var: &str) -> Option<Universe> { self.substitutions .get(var) .map(|u| self.apply_substitution(u)) } }
The solver provides access to resolved universe substitutions, enabling the main type checker to apply universe-level solutions throughout the type checking process.
#![allow(unused)] fn main() { /// Apply all substitutions to a universe level pub fn substitute_universe(&self, u: &Universe) -> Universe { self.apply_substitution(u) } }
Universe substitution application handles the complete resolution of universe expressions, recursively applying all accumulated substitutions to produce fully resolved universe levels.
Integration with Type Checking
The universe solver integrates with the main type checking algorithm at several points:
Type Formation: When checking that types are well-formed, the universe solver ensures that universe level constraints are satisfied.
Polymorphic Instantiation: When instantiating polymorphic definitions, the universe solver generates fresh universe variables and maintains constraints between them.
Definitional Equality: When checking definitional equality between types with universe polymorphism, the universe solver ensures that universe relationships are preserved.
Constraint Satisfaction Checking
#![allow(unused)] fn main() { /// Check if universe level constraints are satisfiable pub fn is_satisfiable(&self, constraints: &[UniverseConstraint]) -> bool { let mut solver = self.clone(); solver.solve(constraints).is_ok() } }
The satisfiability checker enables the type checker to verify that universe constraint sets have solutions before committing to particular type assignments. This early checking prevents backtracking in complex type inference scenarios.
Universe Polymorphism Examples
Our implementation supports several forms of universe polymorphic definitions:
Polymorphic Data Types
structure Pair.{u, v} (A : Sort u) (B : Sort v) : Sort (max u v) :=
(fst : A)
(snd : B)
The Pair
type is polymorphic over two universe levels, with the result type living at the maximum of the argument universe levels.
Polymorphic Functions
def const.{u, v} (A : Sort u) (B : Sort v) (x : A) (y : B) : A := x
The const
function ignores its second argument and returns the first, working at any universe levels for both argument types.
Universe Level Arithmetic
def lift.{u} (A : Sort u) : Sort (u + 1) := A
The lift
operation moves a type from universe u
to universe u + 1
, demonstrating universe level arithmetic in type expressions.
Failure modes
There are a couple of failure cases we handle with custom errors:
#![allow(unused)] fn main() { /// Convert universe to string representation fn universe_to_string(&self, u: &Universe) -> String { match u { Universe::Const(n) => n.to_string(), Universe::ScopedVar(scope, var) => format!("{}::{}", scope, var), Universe::Meta(id) => format!("?u{}", id), Universe::Add(u, n) => format!("({} + {})", self.universe_to_string(u), n), Universe::Max(u, v) => format!( "max({}, {})", self.universe_to_string(u), self.universe_to_string(v) ), Universe::IMax(u, v) => format!( "imax({}, {})", self.universe_to_string(u), self.universe_to_string(v) ), } } }
The failures are categorized into three main types:
- Unification Failures: Show the specific universe levels that couldn’t be unified
- Occurs Check Violations: Identify infinite universe expressions
- Arithmetic Inconsistencies: Point out invalid universe level arithmetic