use oxilean_kernel::Node;
use oxilean_kernel::{
check_and_derive_family, check_constant_info, AxiomVal, BinderInfo, ConstantInfo, ConstantVal,
DefinitionSafety, DefinitionVal, Environment, Expr, FVarId, InductiveSpec, KernelError, Level,
LocalDecl, Name, ReducibilityHint, TheoremVal, TypeChecker,
};
fn app(f: Expr, a: Expr) -> Expr {
Expr::App(Node::new(f), Node::new(a))
}
fn lam(name: &str, ty: Expr, body: Expr) -> Expr {
Expr::Lam(
BinderInfo::Default,
Name::str(name),
Node::new(ty),
Node::new(body),
)
}
fn pi(name: &str, ty: Expr, body: Expr) -> Expr {
Expr::Pi(
BinderInfo::Default,
Name::str(name),
Node::new(ty),
Node::new(body),
)
}
fn cnst(name: &str) -> Expr {
Expr::Const(Name::str(name), vec![])
}
fn axiom(env: &mut Environment, name: &str, ty: Expr) {
let ci = ConstantInfo::Axiom(AxiomVal {
common: ConstantVal {
name: Name::str(name),
level_params: vec![],
ty,
},
is_unsafe: false,
});
check_constant_info(env, ci).unwrap_or_else(|e| panic!("axiom {name}: {e:?}"));
}
fn definition(env: &mut Environment, name: &str, level_params: &[&str], ty: Expr, value: Expr) {
let ci = ConstantInfo::Definition(DefinitionVal {
common: ConstantVal {
name: Name::str(name),
level_params: level_params.iter().map(|p| Name::str(*p)).collect(),
ty,
},
value,
hints: ReducibilityHint::Regular(1),
safety: DefinitionSafety::Safe,
all: vec![Name::str(name)],
});
check_constant_info(env, ci).unwrap_or_else(|e| panic!("definition {name}: {e:?}"));
}
fn base_env() -> Environment {
let mut env = Environment::new();
let ty = Expr::Sort(Level::succ(Level::zero()));
axiom(&mut env, "N", ty.clone());
axiom(&mut env, "M", ty);
axiom(&mut env, "a", cnst("N"));
axiom(&mut env, "b", cnst("N"));
axiom(
&mut env,
"P",
pi("x", cnst("N"), pi("y", cnst("N"), cnst("N"))),
);
axiom(&mut env, "g", pi("x", cnst("N"), cnst("N")));
axiom(&mut env, "F", Expr::Sort(Level::zero()));
env
}
fn tower(leaf: Expr, height: u32) -> Expr {
let p = Node::new(cnst("P"));
let mut term = leaf;
for _ in 0..height {
let shared = Node::new(term);
term = Expr::App(Node::new(Expr::App(p.clone(), shared.clone())), shared);
}
term
}
fn with_budget<T>(budget: u64, work: impl FnOnce() -> T) -> (T, u64, bool) {
oxilean_kernel::fuel::set_budget(Some(budget));
let result = work();
let used = oxilean_kernel::fuel::used();
let exhausted = oxilean_kernel::fuel::is_exhausted();
oxilean_kernel::fuel::set_budget(None);
(result, used, exhausted)
}
#[test]
fn a_shared_subterm_is_typed_once() {
let env = base_env();
let term = tower(cnst("a"), 50);
let (ty, used, exhausted) = with_budget(1_000_000, || TypeChecker::new(&env).infer_type(&term));
assert_eq!(ty.unwrap_or_else(|e| panic!("{e:?}")), cnst("N"));
assert!(!exhausted);
assert!(used < 5_000, "a 50-level tower charged {used} units");
}
#[test]
fn inference_out_of_budget_is_a_typed_error_and_is_not_remembered() {
let env = base_env();
let term = tower(cnst("a"), 50);
let mut tc = TypeChecker::new(&env);
let (res, _, exhausted) = with_budget(40, || tc.infer_type(&term));
assert!(res.is_err(), "40 units cannot type a 50-level tower");
assert!(exhausted, "the failure must be the budget's");
let (res, _, exhausted) = with_budget(1_000_000, || tc.infer_type(&term));
assert_eq!(res.unwrap_or_else(|e| panic!("{e:?}")), cnst("N"));
assert!(!exhausted);
}
#[test]
fn a_type_is_looked_up_the_second_time() {
let env = base_env();
let term = tower(cnst("a"), 30);
let mut tc = TypeChecker::new(&env);
let (first, first_used, _) = with_budget(1_000_000, || tc.infer_type(&term));
let (second, second_used, _) = with_budget(1_000_000, || tc.infer_type(&term));
assert_eq!(first.ok(), second.ok());
assert!(
first_used > 100,
"the first inference did the work: {first_used}"
);
assert!(
second_used <= 2,
"the second is a lookup: {second_used} units"
);
let again = tower(cnst("a"), 30);
let (third, third_used, _) = with_budget(1_000_000, || tc.infer_type(&again));
assert_eq!(third.ok(), Some(cnst("N")));
assert!(
third_used <= 2,
"an equal term is a lookup: {third_used} units"
);
}
#[test]
fn rebinding_a_variable_drops_what_was_inferred_with_its_old_type() {
let env = base_env();
let mut tc = TypeChecker::new(&env);
let x = FVarId(7);
let g_x = app(cnst("g"), Expr::FVar(x));
let decl = |ty: &str| LocalDecl {
fvar: x,
name: Name::str("x"),
ty: cnst(ty),
val: None,
};
tc.push_local(decl("N"));
assert_eq!(tc.infer_type(&Expr::FVar(x)).ok(), Some(cnst("N")));
assert_eq!(tc.infer_type(&g_x).ok(), Some(cnst("N")));
tc.pop_local();
tc.push_local(decl("M"));
assert_eq!(tc.infer_type(&Expr::FVar(x)).ok(), Some(cnst("M")));
assert!(
tc.infer_type(&g_x).is_err(),
"`g x` with `x : M` must not be typed from a remembered `x : N`"
);
}
#[test]
fn changing_the_transparency_mode_drops_what_was_inferred() {
let env = base_env();
let term = tower(cnst("a"), 30);
let mut tc = TypeChecker::new(&env);
let (_, first_used, _) = with_budget(1_000_000, || tc.infer_type(&term));
tc.set_transparency(oxilean_kernel::reduce::TransparencyMode::Reducible);
let (ty, used, _) = with_budget(1_000_000, || tc.infer_type(&term));
assert_eq!(ty.ok(), Some(cnst("N")));
assert!(
used * 2 > first_used,
"inferred again, not looked up: {used} units against {first_used}"
);
}
#[test]
fn two_equal_towers_in_separate_allocations_are_equal_at_once() {
let env = base_env();
let mut tc = TypeChecker::new(&env);
let start = std::time::Instant::now();
let (equal, used, exhausted) = with_budget(100_000, || {
tc.is_def_eq(&tower(cnst("a"), 34), &tower(cnst("a"), 34))
});
assert!(equal);
assert!(!exhausted);
assert!(used < 1_000, "charged {used} units");
assert!(
start.elapsed().as_secs() < 10,
"comparing two equal 34-level towers took {:?}",
start.elapsed()
);
}
#[test]
fn two_different_towers_are_told_apart_without_walking_their_trees() {
let env = base_env();
let mut tc = TypeChecker::new(&env);
let (equal, used, exhausted) = with_budget(1_000_000, || {
tc.is_def_eq(&tower(cnst("a"), 50), &tower(cnst("b"), 50))
});
assert!(!equal, "`a` and `b` are different axioms");
assert!(!exhausted, "the comparison must finish, not run out");
assert!(used < 50_000, "charged {used} units");
assert!(!tc.is_def_eq(&tower(cnst("a"), 50), &tower(cnst("b"), 50)));
assert!(!tc.is_def_eq(&tower(cnst("b"), 50), &tower(cnst("a"), 50)));
assert!(tc.is_def_eq(&tower(cnst("b"), 50), &tower(cnst("b"), 50)));
}
fn congruence_env(depth: u32) -> Environment {
fn tree(depth: u32) -> Expr {
if depth == 0 {
app(cnst("g"), Expr::BVar(0))
} else {
app(app(cnst("P"), tree(depth - 1)), tree(depth - 1))
}
}
let mut env = base_env();
let n_to_n = pi("x", cnst("N"), cnst("N"));
definition(
&mut env,
"idN",
&[],
n_to_n.clone(),
lam("x", cnst("N"), Expr::BVar(0)),
);
definition(
&mut env,
"big",
&[],
n_to_n.clone(),
lam("x", cnst("N"), tree(depth)),
);
definition(
&mut env,
"konst",
&[],
n_to_n.clone(),
lam("x", cnst("N"), cnst("a")),
);
definition(
&mut env,
"poly",
&["u"],
n_to_n,
lam("x", cnst("N"), Expr::BVar(0)),
);
env
}
#[test]
fn one_definition_applied_to_equal_arguments_is_equal_without_unfolding_it() {
let env = congruence_env(12);
let lhs = app(cnst("big"), app(cnst("idN"), cnst("a")));
let rhs = app(cnst("big"), cnst("a"));
let mut tc = TypeChecker::new(&env);
let (equal, used, exhausted) = with_budget(1_000_000, || tc.is_def_eq(&lhs, &rhs));
assert!(equal);
assert!(!exhausted);
assert!(
used < 500,
"charged {used} units: `big` was unfolded (its value is thousands of nodes)"
);
}
#[test]
fn unequal_arguments_do_not_make_two_applications_equal() {
let env = congruence_env(3);
let mut tc = TypeChecker::new(&env);
assert!(!tc.is_def_eq(&app(cnst("big"), cnst("a")), &app(cnst("big"), cnst("b"))));
assert!(!tc.is_def_eq(&app(cnst("g"), cnst("a")), &app(cnst("g"), cnst("b"))));
let poly = Expr::Const(Name::str("poly"), vec![Level::zero()]);
assert!(tc.is_def_eq(&app(cnst("idN"), cnst("a")), &app(poly, cnst("a"))));
assert!(tc.is_def_eq(&app(cnst("idN"), cnst("a")), &app(cnst("konst"), cnst("b"))));
assert!(!tc.is_def_eq(&app(cnst("idN"), cnst("b")), &app(cnst("konst"), cnst("b"))));
assert!(!tc.is_def_eq(
&app(app(cnst("P"), cnst("a")), cnst("a")),
&app(cnst("g"), cnst("a"))
));
}
#[test]
fn a_failed_comparison_of_the_arguments_decides_nothing() {
let env = congruence_env(3);
let mut tc = TypeChecker::new(&env);
assert!(!tc.is_def_eq(&cnst("a"), &cnst("b")));
assert!(tc.is_def_eq(
&app(cnst("konst"), cnst("a")),
&app(cnst("konst"), cnst("b"))
));
let poly = |level: Level| app(Expr::Const(Name::str("poly"), vec![level]), cnst("a"));
assert!(tc.is_def_eq(&poly(Level::zero()), &poly(Level::succ(Level::zero()))));
let u = Level::param(Name::str("u"));
assert!(tc.is_def_eq(&poly(Level::max(u.clone(), u.clone())), &poly(u)));
}
#[test]
fn proof_irrelevance_on_a_long_telescope_does_not_take_exponential_time() {
let mut env = base_env();
let mut telescope = cnst("F");
for _ in 0..40 {
telescope = pi("x", cnst("N"), telescope);
}
axiom(&mut env, "h1", telescope.clone());
axiom(&mut env, "h2", telescope);
oxilean_kernel::deadline::set_deadline(Some(std::time::Duration::from_secs(20)));
let mut tc = TypeChecker::new(&env);
let equal = tc.is_def_eq(&cnst("h1"), &cnst("h2"));
let expired = oxilean_kernel::deadline::is_expired();
oxilean_kernel::deadline::set_deadline(None);
assert!(!expired, "deciding the telescope's universe took over 20 s");
assert!(equal, "two proofs of one proposition are equal");
let mut env = base_env();
let mut telescope = cnst("N");
for _ in 0..40 {
telescope = pi("x", cnst("N"), telescope);
}
axiom(&mut env, "f1", telescope.clone());
axiom(&mut env, "f2", telescope);
let mut tc = TypeChecker::new(&env);
assert!(!tc.is_def_eq(&cnst("f1"), &cnst("f2")));
}
fn theorem(name: &str, ty: Expr, value: Expr) -> ConstantInfo {
ConstantInfo::Theorem(TheoremVal {
common: ConstantVal {
name: Name::str(name),
level_params: vec![],
ty,
},
value,
all: vec![Name::str(name)],
})
}
fn refused_for(result: Result<(), KernelError>, what: &str) {
match result {
Err(KernelError::Other(message)) => assert!(
message.contains(what),
"expected a refusal for a {what}, got: {message}"
),
other => panic!("expected a refusal for a {what}, got {other:?}"),
}
}
#[test]
fn a_declaration_with_a_free_variable_is_refused() {
let mut env = base_env();
let identity = lam("x", cnst("F"), Expr::BVar(0));
let bad = theorem("bad", cnst("F"), app(identity, Expr::FVar(FVarId(0))));
refused_for(check_constant_info(&mut env, bad), "free variable");
assert!(env.find(&Name::str("bad")).is_none(), "nothing was added");
let alias = theorem(
"alias",
pi("h", cnst("F"), cnst("F")),
lam("x", cnst("F"), Expr::FVar(FVarId(1))),
);
refused_for(check_constant_info(&mut env, alias), "free variable");
assert!(env.find(&Name::str("alias")).is_none(), "nothing was added");
let bad = theorem("bad", Expr::FVar(FVarId(41)), cnst("a"));
refused_for(check_constant_info(&mut env, bad), "free variable");
let bad = ConstantInfo::Definition(DefinitionVal {
common: ConstantVal {
name: Name::str("bad"),
level_params: vec![],
ty: cnst("N"),
},
value: app(cnst("g"), Expr::FVar(FVarId(1 << 62))),
hints: ReducibilityHint::Regular(1),
safety: DefinitionSafety::Safe,
all: vec![Name::str("bad")],
});
refused_for(check_constant_info(&mut env, bad), "free variable");
assert!(env.find(&Name::str("bad")).is_none());
let good = theorem(
"good",
pi("h", cnst("F"), cnst("F")),
lam("h", cnst("F"), Expr::BVar(0)),
);
check_constant_info(&mut env, good).unwrap_or_else(|e| panic!("{e:?}"));
}
#[test]
fn a_declaration_with_a_loose_bound_variable_is_refused() {
let mut env = base_env();
let bad = theorem("bad", cnst("F"), Expr::BVar(0));
refused_for(check_constant_info(&mut env, bad), "loose bound variable");
let bad = theorem(
"bad",
pi("h", cnst("F"), cnst("F")),
lam("h", cnst("F"), Expr::BVar(1)),
);
refused_for(check_constant_info(&mut env, bad), "loose bound variable");
let bad = theorem("bad", app(cnst("g"), Expr::BVar(3)), cnst("a"));
refused_for(check_constant_info(&mut env, bad), "loose bound variable");
assert!(env.find(&Name::str("bad")).is_none());
}
#[test]
fn an_inductive_family_with_a_free_variable_is_refused() {
let env = base_env();
let sort = Expr::Sort(Level::succ(Level::zero()));
let spec = InductiveSpec::new(
Name::str("T"),
sort.clone(),
vec![(Name::str("T.mk"), pi("x", Expr::FVar(FVarId(0)), cnst("T")))],
);
let result = check_and_derive_family(&env, &[], 0, &[spec]).map(|_| ());
refused_for(result, "free variable");
let spec = InductiveSpec::new(Name::str("T"), pi("x", cnst("N"), Expr::BVar(1)), vec![]);
let result = check_and_derive_family(&env, &[], 0, &[spec]).map(|_| ());
refused_for(result, "loose bound variable");
let spec = InductiveSpec::new(
Name::str("T"),
sort,
vec![(Name::str("T.mk"), pi("x", cnst("N"), cnst("T")))],
);
check_and_derive_family(&env, &[], 0, &[spec]).unwrap_or_else(|e| panic!("{e:?}"));
}