Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
901
vendor/ruvector/examples/prime-radiant/tests/hott_tests.rs
vendored
Normal file
901
vendor/ruvector/examples/prime-radiant/tests/hott_tests.rs
vendored
Normal file
@@ -0,0 +1,901 @@
|
||||
//! Comprehensive tests for Homotopy Type Theory (HoTT) Module
|
||||
//!
|
||||
//! This test suite verifies HoTT constructs including:
|
||||
//! - Type checking and inference
|
||||
//! - Path composition and inversion
|
||||
//! - Transport along paths
|
||||
//! - Univalence axiom (equivalence = identity)
|
||||
|
||||
use prime_radiant::hott::{
|
||||
Type, Term, Path, TypeChecker, TypeContext,
|
||||
Equivalence, Transport, Univalence,
|
||||
PathComposition, PathInversion, PathConcatenation,
|
||||
HigherInductiveType, Circle, Sphere, Torus,
|
||||
HomotopyLevel, is_contractible, is_proposition, is_set,
|
||||
FunctionExtensionality, funext,
|
||||
HottError,
|
||||
};
|
||||
use proptest::prelude::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
// =============================================================================
|
||||
// TYPE CHECKING TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod type_checking_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test type checking for base types
|
||||
#[test]
|
||||
fn test_base_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Natural numbers type
|
||||
let nat = Type::Nat;
|
||||
assert!(ctx.is_well_formed(&nat));
|
||||
|
||||
// Boolean type
|
||||
let bool_ty = Type::Bool;
|
||||
assert!(ctx.is_well_formed(&bool_ty));
|
||||
|
||||
// Unit type
|
||||
let unit = Type::Unit;
|
||||
assert!(ctx.is_well_formed(&unit));
|
||||
}
|
||||
|
||||
/// Test type checking for function types
|
||||
#[test]
|
||||
fn test_function_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Nat -> Bool
|
||||
let func_type = Type::Pi {
|
||||
param: Box::new(Type::Nat),
|
||||
body: Box::new(Type::Bool),
|
||||
};
|
||||
|
||||
assert!(ctx.is_well_formed(&func_type));
|
||||
}
|
||||
|
||||
/// Test type checking for dependent types
|
||||
#[test]
|
||||
fn test_dependent_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Dependent product type: (x: A) -> B(x)
|
||||
let dep_prod = Type::Pi {
|
||||
param: Box::new(Type::Nat),
|
||||
body: Box::new(Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|_n| Type::Bool),
|
||||
}),
|
||||
};
|
||||
|
||||
assert!(ctx.is_well_formed(&dep_prod));
|
||||
}
|
||||
|
||||
/// Test type checking for sigma types
|
||||
#[test]
|
||||
fn test_sigma_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Sigma type: (x: A) * B(x)
|
||||
let sigma = Type::Sigma {
|
||||
first: Box::new(Type::Nat),
|
||||
second: Box::new(Type::Bool),
|
||||
};
|
||||
|
||||
assert!(ctx.is_well_formed(&sigma));
|
||||
}
|
||||
|
||||
/// Test type checking for identity types
|
||||
#[test]
|
||||
fn test_identity_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Identity type: a =_A b
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let id_type = Type::Identity {
|
||||
base_type: Box::new(Type::Nat),
|
||||
left: Box::new(a),
|
||||
right: Box::new(b),
|
||||
};
|
||||
|
||||
assert!(ctx.is_well_formed(&id_type));
|
||||
}
|
||||
|
||||
/// Test type inference
|
||||
#[test]
|
||||
fn test_type_inference() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
let zero = Term::zero();
|
||||
let inferred = ctx.infer_type(&zero).unwrap();
|
||||
assert_eq!(inferred, Type::Nat);
|
||||
|
||||
let true_val = Term::true_val();
|
||||
let inferred = ctx.infer_type(&true_val).unwrap();
|
||||
assert_eq!(inferred, Type::Bool);
|
||||
}
|
||||
|
||||
/// Test type checking with variable bindings
|
||||
#[test]
|
||||
fn test_variable_bindings() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Add variable x: Nat to context
|
||||
ctx.add_variable("x", Type::Nat);
|
||||
|
||||
let var_x = Term::variable("x");
|
||||
let inferred = ctx.infer_type(&var_x).unwrap();
|
||||
assert_eq!(inferred, Type::Nat);
|
||||
}
|
||||
|
||||
/// Test lambda type checking
|
||||
#[test]
|
||||
fn test_lambda_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// lambda x: Nat. x + 1
|
||||
let lambda = Term::Lambda {
|
||||
param: "x".to_string(),
|
||||
param_type: Box::new(Type::Nat),
|
||||
body: Box::new(Term::succ(Term::variable("x"))),
|
||||
};
|
||||
|
||||
let inferred = ctx.infer_type(&lambda).unwrap();
|
||||
|
||||
match inferred {
|
||||
Type::Pi { param, body } => {
|
||||
assert_eq!(*param, Type::Nat);
|
||||
assert_eq!(*body, Type::Nat);
|
||||
}
|
||||
_ => panic!("Expected Pi type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PATH COMPOSITION TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod path_composition_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test reflexivity path: refl_a : a = a
|
||||
#[test]
|
||||
fn test_reflexivity_path() {
|
||||
let a = Term::zero();
|
||||
let refl = Path::refl(&a);
|
||||
|
||||
assert_eq!(refl.start(), &a);
|
||||
assert_eq!(refl.end(), &a);
|
||||
assert!(refl.is_reflexivity());
|
||||
}
|
||||
|
||||
/// Test path concatenation: p . q : a = c for p: a = b, q: b = c
|
||||
#[test]
|
||||
fn test_path_concatenation() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
let c = Term::succ(Term::succ(Term::zero()));
|
||||
|
||||
// p: a = b (hypothetical)
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
|
||||
// q: b = c (hypothetical)
|
||||
let q = Path::hypothesis(&b, &c, "q");
|
||||
|
||||
// p . q : a = c
|
||||
let composed = p.concat(&q).unwrap();
|
||||
|
||||
assert_eq!(composed.start(), &a);
|
||||
assert_eq!(composed.end(), &c);
|
||||
}
|
||||
|
||||
/// Test path concatenation fails for non-matching endpoints
|
||||
#[test]
|
||||
fn test_path_concat_mismatch() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
let c = Term::succ(Term::succ(Term::zero()));
|
||||
let d = Term::succ(Term::succ(Term::succ(Term::zero())));
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p"); // a = b
|
||||
let q = Path::hypothesis(&c, &d, "q"); // c = d, not b = something
|
||||
|
||||
let result = p.concat(&q);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test path inversion: p^(-1) : b = a for p : a = b
|
||||
#[test]
|
||||
fn test_path_inversion() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let p_inv = p.inverse();
|
||||
|
||||
assert_eq!(p_inv.start(), &b);
|
||||
assert_eq!(p_inv.end(), &a);
|
||||
}
|
||||
|
||||
/// Test double inversion: (p^(-1))^(-1) = p
|
||||
#[test]
|
||||
fn test_double_inversion() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let p_inv_inv = p.inverse().inverse();
|
||||
|
||||
assert_eq!(p_inv_inv.start(), p.start());
|
||||
assert_eq!(p_inv_inv.end(), p.end());
|
||||
}
|
||||
|
||||
/// Test associativity: (p . q) . r = p . (q . r)
|
||||
#[test]
|
||||
fn test_path_associativity() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
let c = Term::succ(Term::succ(Term::zero()));
|
||||
let d = Term::succ(Term::succ(Term::succ(Term::zero())));
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let q = Path::hypothesis(&b, &c, "q");
|
||||
let r = Path::hypothesis(&c, &d, "r");
|
||||
|
||||
// (p . q) . r
|
||||
let left = p.concat(&q).unwrap().concat(&r).unwrap();
|
||||
|
||||
// p . (q . r)
|
||||
let right = p.concat(&q.concat(&r).unwrap()).unwrap();
|
||||
|
||||
// Both should have same endpoints
|
||||
assert_eq!(left.start(), right.start());
|
||||
assert_eq!(left.end(), right.end());
|
||||
|
||||
// And there should be a path between them (associator)
|
||||
assert!(Path::path_between(&left, &right).is_some());
|
||||
}
|
||||
|
||||
/// Test left unit law: refl_a . p = p
|
||||
#[test]
|
||||
fn test_left_unit_law() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let refl_a = Path::refl(&a);
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
|
||||
let left_unit = refl_a.concat(&p).unwrap();
|
||||
|
||||
// Should be propositionally equal to p
|
||||
assert_eq!(left_unit.start(), p.start());
|
||||
assert_eq!(left_unit.end(), p.end());
|
||||
}
|
||||
|
||||
/// Test right unit law: p . refl_b = p
|
||||
#[test]
|
||||
fn test_right_unit_law() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let refl_b = Path::refl(&b);
|
||||
|
||||
let right_unit = p.concat(&refl_b).unwrap();
|
||||
|
||||
assert_eq!(right_unit.start(), p.start());
|
||||
assert_eq!(right_unit.end(), p.end());
|
||||
}
|
||||
|
||||
/// Test inverse law: p . p^(-1) = refl_a
|
||||
#[test]
|
||||
fn test_inverse_law() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let p_inv = p.inverse();
|
||||
|
||||
let composed = p.concat(&p_inv).unwrap();
|
||||
|
||||
// Should equal refl_a (propositionally)
|
||||
assert_eq!(composed.start(), &a);
|
||||
assert_eq!(composed.end(), &a);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TRANSPORT TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod transport_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test transport along reflexivity path is identity
|
||||
#[test]
|
||||
fn test_transport_refl_is_identity() {
|
||||
let a = Term::zero();
|
||||
let refl = Path::refl(&a);
|
||||
|
||||
// Type family B(x) = Nat for simplicity
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|_| Type::Nat),
|
||||
};
|
||||
|
||||
let b_a = Term::succ(Term::zero()); // Some term in B(a)
|
||||
|
||||
let transported = Transport::transport(&refl, &family, &b_a).unwrap();
|
||||
|
||||
// transport(refl_a, b) = b
|
||||
assert_eq!(transported, b_a);
|
||||
}
|
||||
|
||||
/// Test transport composition: transport(p.q) = transport(q) . transport(p)
|
||||
#[test]
|
||||
fn test_transport_composition() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
let c = Term::succ(Term::succ(Term::zero()));
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let q = Path::hypothesis(&b, &c, "q");
|
||||
let pq = p.concat(&q).unwrap();
|
||||
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|_| Type::Nat),
|
||||
};
|
||||
|
||||
let term_a = Term::succ(Term::succ(Term::succ(Term::zero())));
|
||||
|
||||
// transport(p.q, x)
|
||||
let direct = Transport::transport(&pq, &family, &term_a).unwrap();
|
||||
|
||||
// transport(q, transport(p, x))
|
||||
let p_transported = Transport::transport(&p, &family, &term_a).unwrap();
|
||||
let composed = Transport::transport(&q, &family, &p_transported).unwrap();
|
||||
|
||||
// Should be propositionally equal
|
||||
assert!(Term::propositionally_equal(&direct, &composed));
|
||||
}
|
||||
|
||||
/// Test dependent transport (transport in dependent types)
|
||||
#[test]
|
||||
fn test_dependent_transport() {
|
||||
let ctx = TypeContext::new();
|
||||
|
||||
// Type family indexed by Nat
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|n| Type::Vec {
|
||||
element_type: Box::new(Type::Nat),
|
||||
length: n,
|
||||
}),
|
||||
};
|
||||
|
||||
// Path from 0 to 1
|
||||
let p = Path::hypothesis(&Term::zero(), &Term::succ(Term::zero()), "p");
|
||||
|
||||
// Empty vector at type Vec(Nat, 0)
|
||||
let empty_vec = Term::empty_vec();
|
||||
|
||||
// Transport should fail or produce Vec(Nat, 1)
|
||||
let result = Transport::transport(&p, &family, &empty_vec);
|
||||
|
||||
// May require coercion witness
|
||||
assert!(result.is_ok() || result.is_err());
|
||||
}
|
||||
|
||||
/// Test path lifting (apd)
|
||||
#[test]
|
||||
fn test_apd() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|_| Type::Nat),
|
||||
};
|
||||
|
||||
// Function f: (x: Nat) -> B(x)
|
||||
let f = Term::Lambda {
|
||||
param: "x".to_string(),
|
||||
param_type: Box::new(Type::Nat),
|
||||
body: Box::new(Term::succ(Term::variable("x"))),
|
||||
};
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
|
||||
// apd f p : transport(p, f(a)) = f(b)
|
||||
let apd_path = Transport::apd(&f, &p, &family).unwrap();
|
||||
|
||||
// Check endpoints
|
||||
let f_a = Term::succ(a.clone());
|
||||
let f_b = Term::succ(b.clone());
|
||||
|
||||
let transported_f_a = Transport::transport(&p, &family, &f_a).unwrap();
|
||||
|
||||
assert_eq!(apd_path.start(), &transported_f_a);
|
||||
assert_eq!(apd_path.end(), &f_b);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UNIVALENCE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod univalence_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test that equivalence can be converted to path (ua)
|
||||
#[test]
|
||||
fn test_ua_from_equivalence() {
|
||||
// Equivalence between Bool and Bool (identity)
|
||||
let bool_equiv = Equivalence::identity(Type::Bool);
|
||||
|
||||
// ua should produce a path Bool = Bool
|
||||
let path = Univalence::ua(&bool_equiv).unwrap();
|
||||
|
||||
assert_eq!(path.start_type(), &Type::Bool);
|
||||
assert_eq!(path.end_type(), &Type::Bool);
|
||||
}
|
||||
|
||||
/// Test that path can be converted to equivalence (ua^-1)
|
||||
#[test]
|
||||
fn test_ua_inverse() {
|
||||
// Reflexivity path on type
|
||||
let refl_nat = Path::type_refl(&Type::Nat);
|
||||
|
||||
// ua^-1 should produce equivalence Nat ~ Nat
|
||||
let equiv = Univalence::ua_inverse(&refl_nat).unwrap();
|
||||
|
||||
assert!(equiv.is_valid_equivalence());
|
||||
assert_eq!(equiv.domain(), &Type::Nat);
|
||||
assert_eq!(equiv.codomain(), &Type::Nat);
|
||||
}
|
||||
|
||||
/// Test round-trip: ua(ua^-1(p)) = p
|
||||
#[test]
|
||||
fn test_univalence_round_trip_path() {
|
||||
let p = Path::type_refl(&Type::Bool);
|
||||
|
||||
let equiv = Univalence::ua_inverse(&p).unwrap();
|
||||
let recovered = Univalence::ua(&equiv).unwrap();
|
||||
|
||||
// Should be propositionally equal
|
||||
assert_eq!(recovered.start_type(), p.start_type());
|
||||
assert_eq!(recovered.end_type(), p.end_type());
|
||||
}
|
||||
|
||||
/// Test round-trip: ua^-1(ua(e)) = e
|
||||
#[test]
|
||||
fn test_univalence_round_trip_equiv() {
|
||||
let equiv = Equivalence::identity(Type::Nat);
|
||||
|
||||
let path = Univalence::ua(&equiv).unwrap();
|
||||
let recovered = Univalence::ua_inverse(&path).unwrap();
|
||||
|
||||
// Forward maps should be equal
|
||||
assert!(Equivalence::equal(&recovered, &equiv));
|
||||
}
|
||||
|
||||
/// Test transport along ua(e) is the equivalence
|
||||
#[test]
|
||||
fn test_transport_along_ua() {
|
||||
// Create non-trivial equivalence (e.g., negation on Bool)
|
||||
let neg_equiv = Equivalence::bool_negation();
|
||||
|
||||
let path = Univalence::ua(&neg_equiv).unwrap();
|
||||
|
||||
// Type family that uses the base type directly
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Universe(0)),
|
||||
fiber: Box::new(|ty| ty.clone()),
|
||||
};
|
||||
|
||||
let true_val = Term::true_val();
|
||||
|
||||
// transport(ua(neg), true) should equal neg(true) = false
|
||||
let transported = Transport::transport(&path, &family, &true_val).unwrap();
|
||||
let neg_true = neg_equiv.apply(&true_val).unwrap();
|
||||
|
||||
assert!(Term::propositionally_equal(&transported, &neg_true));
|
||||
}
|
||||
|
||||
/// Test univalence with type isomorphism
|
||||
#[test]
|
||||
fn test_type_isomorphism_gives_equality() {
|
||||
// Unit + Unit is isomorphic to Bool
|
||||
let sum_type = Type::Sum {
|
||||
left: Box::new(Type::Unit),
|
||||
right: Box::new(Type::Unit),
|
||||
};
|
||||
|
||||
// Construct isomorphism
|
||||
let iso = Equivalence::sum_unit_to_bool();
|
||||
|
||||
assert!(iso.is_valid_equivalence());
|
||||
|
||||
// By univalence, types are equal
|
||||
let path = Univalence::ua(&iso).unwrap();
|
||||
|
||||
assert_eq!(*path.start_type(), sum_type);
|
||||
assert_eq!(*path.end_type(), Type::Bool);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HIGHER INDUCTIVE TYPE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod hit_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test circle type S^1
|
||||
#[test]
|
||||
fn test_circle_type() {
|
||||
let circle = Circle::new();
|
||||
|
||||
// Circle has base point
|
||||
let base = circle.base_point();
|
||||
assert!(base.has_type(&Type::Circle));
|
||||
|
||||
// Circle has loop: base = base
|
||||
let loop_path = circle.loop_path();
|
||||
assert_eq!(loop_path.start(), &base);
|
||||
assert_eq!(loop_path.end(), &base);
|
||||
}
|
||||
|
||||
/// Test circle recursion principle
|
||||
#[test]
|
||||
fn test_circle_recursion() {
|
||||
let circle = Circle::new();
|
||||
|
||||
// To map S^1 -> A, need:
|
||||
// - a: A (image of base)
|
||||
// - p: a = a (image of loop)
|
||||
|
||||
let target_type = Type::Nat;
|
||||
let a = Term::zero();
|
||||
let p = Path::refl(&a); // Use refl for simplicity
|
||||
|
||||
let rec = circle.recursion(&target_type, &a, &p).unwrap();
|
||||
|
||||
// rec(base) = a
|
||||
let base_image = rec.apply(&circle.base_point()).unwrap();
|
||||
assert_eq!(base_image, a);
|
||||
}
|
||||
|
||||
/// Test sphere type S^2
|
||||
#[test]
|
||||
fn test_sphere_type() {
|
||||
let sphere = Sphere::new(2);
|
||||
|
||||
let base = sphere.base_point();
|
||||
assert!(base.has_type(&Type::Sphere(2)));
|
||||
|
||||
// S^2 has refl-refl as 2-path
|
||||
let surf = sphere.surface();
|
||||
assert!(surf.is_2_path());
|
||||
}
|
||||
|
||||
/// Test torus type
|
||||
#[test]
|
||||
fn test_torus_type() {
|
||||
let torus = Torus::new();
|
||||
|
||||
let base = torus.base_point();
|
||||
|
||||
// Torus has two loops
|
||||
let p = torus.meridian();
|
||||
let q = torus.longitude();
|
||||
|
||||
// And a square: p . q = q . p
|
||||
let surface = torus.surface();
|
||||
|
||||
// surface : p . q = q . p
|
||||
let pq = p.concat(&q).unwrap();
|
||||
let qp = q.concat(&p).unwrap();
|
||||
|
||||
assert_eq!(surface.start(), &pq);
|
||||
assert_eq!(surface.end(), &qp);
|
||||
}
|
||||
|
||||
/// Test pushout as HIT
|
||||
#[test]
|
||||
fn test_pushout_hit() {
|
||||
// Pushout of A <- C -> B
|
||||
let a_type = Type::Nat;
|
||||
let b_type = Type::Bool;
|
||||
let c_type = Type::Unit;
|
||||
|
||||
let f = Term::Lambda {
|
||||
param: "c".to_string(),
|
||||
param_type: Box::new(c_type.clone()),
|
||||
body: Box::new(Term::zero()),
|
||||
};
|
||||
|
||||
let g = Term::Lambda {
|
||||
param: "c".to_string(),
|
||||
param_type: Box::new(c_type.clone()),
|
||||
body: Box::new(Term::true_val()),
|
||||
};
|
||||
|
||||
let pushout = HigherInductiveType::pushout(&a_type, &b_type, &c_type, &f, &g);
|
||||
|
||||
// Has injections from A and B
|
||||
let inl = pushout.left_injection();
|
||||
let inr = pushout.right_injection();
|
||||
|
||||
// For each c: C, path glue(c): inl(f(c)) = inr(g(c))
|
||||
let unit = Term::unit();
|
||||
let glue_path = pushout.glue(&unit);
|
||||
|
||||
let inl_fc = inl.apply(&f.apply(&unit).unwrap()).unwrap();
|
||||
let inr_gc = inr.apply(&g.apply(&unit).unwrap()).unwrap();
|
||||
|
||||
assert_eq!(glue_path.start(), &inl_fc);
|
||||
assert_eq!(glue_path.end(), &inr_gc);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HOMOTOPY LEVEL TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod homotopy_level_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test contractibility (h-level -2)
|
||||
#[test]
|
||||
fn test_contractible() {
|
||||
// Unit type is contractible
|
||||
assert!(is_contractible(&Type::Unit));
|
||||
|
||||
// Nat is not contractible
|
||||
assert!(!is_contractible(&Type::Nat));
|
||||
}
|
||||
|
||||
/// Test propositions (h-level -1)
|
||||
#[test]
|
||||
fn test_is_proposition() {
|
||||
// Empty type is a proposition (vacuously)
|
||||
assert!(is_proposition(&Type::Empty));
|
||||
|
||||
// Unit type is a proposition (all elements equal)
|
||||
assert!(is_proposition(&Type::Unit));
|
||||
|
||||
// Nat is not a proposition
|
||||
assert!(!is_proposition(&Type::Nat));
|
||||
}
|
||||
|
||||
/// Test sets (h-level 0)
|
||||
#[test]
|
||||
fn test_is_set() {
|
||||
// Nat is a set
|
||||
assert!(is_set(&Type::Nat));
|
||||
|
||||
// Bool is a set
|
||||
assert!(is_set(&Type::Bool));
|
||||
|
||||
// Universe is not a set (by univalence)
|
||||
assert!(!is_set(&Type::Universe(0)));
|
||||
}
|
||||
|
||||
/// Test h-level preservation under products
|
||||
#[test]
|
||||
fn test_hlevel_product() {
|
||||
// Product of sets is a set
|
||||
let nat_nat = Type::Product {
|
||||
left: Box::new(Type::Nat),
|
||||
right: Box::new(Type::Nat),
|
||||
};
|
||||
|
||||
assert!(is_set(&nat_nat));
|
||||
}
|
||||
|
||||
/// Test h-level of identity types
|
||||
#[test]
|
||||
fn test_identity_hlevel() {
|
||||
// For a set A, identity types a =_A b are propositions
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let id_type = Type::Identity {
|
||||
base_type: Box::new(Type::Nat),
|
||||
left: Box::new(a),
|
||||
right: Box::new(b),
|
||||
};
|
||||
|
||||
assert!(is_proposition(&id_type));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FUNCTION EXTENSIONALITY TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod funext_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test function extensionality: (forall x, f(x) = g(x)) -> f = g
|
||||
#[test]
|
||||
fn test_function_extensionality() {
|
||||
let domain = Type::Nat;
|
||||
let codomain = Type::Nat;
|
||||
|
||||
let f = Term::Lambda {
|
||||
param: "x".to_string(),
|
||||
param_type: Box::new(domain.clone()),
|
||||
body: Box::new(Term::succ(Term::variable("x"))),
|
||||
};
|
||||
|
||||
let g = Term::Lambda {
|
||||
param: "y".to_string(),
|
||||
param_type: Box::new(domain.clone()),
|
||||
body: Box::new(Term::succ(Term::variable("y"))),
|
||||
};
|
||||
|
||||
// Pointwise equality witness (hypothetical)
|
||||
let h = Term::Lambda {
|
||||
param: "x".to_string(),
|
||||
param_type: Box::new(domain.clone()),
|
||||
body: Box::new(Path::refl(&Term::succ(Term::variable("x"))).to_term()),
|
||||
};
|
||||
|
||||
// Apply funext
|
||||
let path_f_g = funext(&f, &g, &h).unwrap();
|
||||
|
||||
assert_eq!(path_f_g.start(), &f);
|
||||
assert_eq!(path_f_g.end(), &g);
|
||||
}
|
||||
|
||||
/// Test funext inverse: f = g -> forall x, f(x) = g(x)
|
||||
#[test]
|
||||
fn test_funext_inverse() {
|
||||
let domain = Type::Bool;
|
||||
let codomain = Type::Nat;
|
||||
|
||||
let f = Term::Lambda {
|
||||
param: "b".to_string(),
|
||||
param_type: Box::new(domain.clone()),
|
||||
body: Box::new(Term::if_then_else(
|
||||
Term::variable("b"),
|
||||
Term::zero(),
|
||||
Term::succ(Term::zero()),
|
||||
)),
|
||||
};
|
||||
|
||||
let p = Path::refl(&f);
|
||||
|
||||
// Get pointwise equalities
|
||||
let pointwise = FunctionExtensionality::inverse(&p).unwrap();
|
||||
|
||||
// For each x: Bool, should have f(x) = f(x)
|
||||
let true_val = Term::true_val();
|
||||
let path_at_true = pointwise.at(&true_val).unwrap();
|
||||
|
||||
assert!(path_at_true.is_reflexivity());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROPERTY-BASED TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
/// Property: refl . p = p for all paths
|
||||
#[test]
|
||||
fn prop_left_unit(
|
||||
start in 0..10i32,
|
||||
end in 0..10i32
|
||||
) {
|
||||
let a = Term::from_int(start);
|
||||
let b = Term::from_int(end);
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let refl = Path::refl(&a);
|
||||
|
||||
let composed = refl.concat(&p).unwrap();
|
||||
|
||||
prop_assert_eq!(composed.start(), p.start());
|
||||
prop_assert_eq!(composed.end(), p.end());
|
||||
}
|
||||
|
||||
/// Property: p . refl = p for all paths
|
||||
#[test]
|
||||
fn prop_right_unit(
|
||||
start in 0..10i32,
|
||||
end in 0..10i32
|
||||
) {
|
||||
let a = Term::from_int(start);
|
||||
let b = Term::from_int(end);
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let refl = Path::refl(&b);
|
||||
|
||||
let composed = p.concat(&refl).unwrap();
|
||||
|
||||
prop_assert_eq!(composed.start(), p.start());
|
||||
prop_assert_eq!(composed.end(), p.end());
|
||||
}
|
||||
|
||||
/// Property: (p^-1)^-1 = p
|
||||
#[test]
|
||||
fn prop_double_inverse(
|
||||
start in 0..10i32,
|
||||
end in 0..10i32
|
||||
) {
|
||||
let a = Term::from_int(start);
|
||||
let b = Term::from_int(end);
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let double_inv = p.inverse().inverse();
|
||||
|
||||
prop_assert_eq!(double_inv.start(), p.start());
|
||||
prop_assert_eq!(double_inv.end(), p.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EDGE CASE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod edge_case_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test empty context type checking
|
||||
#[test]
|
||||
fn test_empty_context() {
|
||||
let ctx = TypeContext::new();
|
||||
assert!(ctx.is_empty());
|
||||
}
|
||||
|
||||
/// Test universe levels
|
||||
#[test]
|
||||
fn test_universe_hierarchy() {
|
||||
let ctx = TypeContext::new();
|
||||
|
||||
let type_0 = Type::Universe(0); // Type of small types
|
||||
let type_1 = Type::Universe(1); // Type of large types
|
||||
|
||||
// Type_0 : Type_1
|
||||
assert!(ctx.inhabits(&type_0, &type_1));
|
||||
|
||||
// But not Type_1 : Type_0 (no type-in-type)
|
||||
assert!(!ctx.inhabits(&type_1, &type_0));
|
||||
}
|
||||
|
||||
/// Test type checking with free variables
|
||||
#[test]
|
||||
fn test_free_variable_error() {
|
||||
let ctx = TypeContext::new();
|
||||
|
||||
let free_var = Term::variable("undefined");
|
||||
|
||||
let result = ctx.infer_type(&free_var);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test path between incompatible types
|
||||
#[test]
|
||||
fn test_heterogeneous_path_error() {
|
||||
let nat_term = Term::zero();
|
||||
let bool_term = Term::true_val();
|
||||
|
||||
// Cannot form path between different types directly
|
||||
let result = Path::try_new(&nat_term, &bool_term);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user