use logicaffeine_kernel::prelude::StandardLibrary;
use logicaffeine_kernel::{
double_check, infer_type, recheck, Context, DoubleCheck, Literal, ReCheckError, Term, Universe,
};
fn ty(n: u32) -> Term {
Term::Sort(Universe::Type(n))
}
fn prop() -> Term {
Term::Sort(Universe::Prop)
}
fn g(name: &str) -> Term {
Term::Global(name.to_string())
}
fn var(name: &str) -> Term {
Term::Var(name.to_string())
}
fn pi(p: &str, a: Term, b: Term) -> Term {
Term::Pi { param: p.to_string(), param_type: Box::new(a), body_type: Box::new(b) }
}
fn lam(p: &str, a: Term, b: Term) -> Term {
Term::Lambda { param: p.to_string(), param_type: Box::new(a), body: Box::new(b) }
}
fn app(f: Term, x: Term) -> Term {
Term::App(Box::new(f), Box::new(x))
}
fn std_ctx() -> Context {
let mut ctx = Context::new();
StandardLibrary::register(&mut ctx);
ctx
}
fn assert_agree(ctx: &Context, term: &Term, what: &str) {
assert!(
infer_type(ctx, term).is_ok(),
"{what}: main kernel rejected a term the test claims is well-typed: {:?}",
infer_type(ctx, term)
);
assert_eq!(
double_check(ctx, term),
DoubleCheck::Agreed,
"{what}: the two kernels must agree, got {:?}",
double_check(ctx, term)
);
}
#[test]
fn sorts_and_cumulativity_agree() {
let ctx = Context::new();
assert_agree(&ctx, &ty(0), "Type 0 : Type 1");
assert_agree(&ctx, &ty(5), "Type 5 : Type 6");
assert_agree(&ctx, &prop(), "Prop : Type 1");
}
#[test]
fn polymorphic_identity_agrees() {
let ctx = Context::new();
let id = lam("A", ty(0), lam("x", var("A"), var("x")));
assert_agree(&ctx, &id, "polymorphic identity");
let t = recheck(&ctx, &id).expect("identity re-checks");
assert!(matches!(t, Term::Pi { .. }), "identity should infer a Π, got {t}");
}
#[test]
fn modus_ponens_proof_term_agrees() {
let ctx = Context::new();
let mp = lam(
"P",
prop(),
lam(
"Q",
prop(),
lam(
"pq",
pi("_", var("P"), var("Q")),
lam("p", var("P"), app(var("pq"), var("p"))),
),
),
);
assert_agree(&ctx, &mp, "modus ponens");
}
#[test]
fn globals_and_application_agree() {
let ctx = std_ctx();
assert_agree(&ctx, &g("Zero"), "Zero : Nat");
assert_agree(&ctx, &g("Succ"), "Succ : Nat → Nat");
assert_agree(&ctx, &app(g("Succ"), g("Zero")), "Succ Zero : Nat");
assert_agree(
&ctx,
&app(g("Succ"), app(g("Succ"), g("Zero"))),
"Succ (Succ Zero) : Nat",
);
}
#[test]
fn impredicative_prop_pi_agrees() {
let ctx = std_ctx();
let universal = pi("x", g("Entity"), prop());
assert_agree(&ctx, &universal, "∀ over Entity into Prop");
assert_agree(&ctx, &pi("x", ty(0), ty(0)), "Type0 → Type0 : Type1");
}
#[test]
fn literals_agree() {
let ctx = std_ctx();
assert_agree(&ctx, &Term::Lit(Literal::Int(42)), "Int literal");
assert_agree(&ctx, &Term::Lit(Literal::Text("hi".into())), "Text literal");
}
#[test]
fn rejects_application_of_a_non_function() {
let ctx = Context::new();
let bad = app(ty(0), prop());
assert!(
matches!(recheck(&ctx, &bad), Err(ReCheckError::Ill(_))),
"applying a non-function must be rejected by the re-checker"
);
assert_eq!(double_check(&ctx, &bad), DoubleCheck::Agreed);
}
#[test]
fn rejects_type_mismatched_application() {
let ctx = std_ctx();
let bad = app(g("Succ"), g("true"));
assert!(
matches!(recheck(&ctx, &bad), Err(ReCheckError::Ill(_))),
"Succ applied to a Bool must be rejected"
);
assert_eq!(double_check(&ctx, &bad), DoubleCheck::Agreed);
}
#[test]
fn rejects_unbound_local_variable() {
let ctx = Context::new();
let bad = var("nope");
assert!(
matches!(recheck(&ctx, &bad), Err(ReCheckError::Ill(_))),
"an unbound local must be rejected"
);
}
#[test]
fn rejects_pi_whose_domain_is_not_a_type() {
let ctx = std_ctx();
let bad = pi("x", app(g("Succ"), g("Zero")), prop());
assert!(
matches!(recheck(&ctx, &bad), Err(ReCheckError::Ill(_))),
"a Π with a non-type domain must be rejected"
);
assert_eq!(double_check(&ctx, &bad), DoubleCheck::Agreed);
}
#[test]
fn rejects_unknown_global() {
let ctx = Context::new();
let bad = g("DoesNotExist");
assert!(matches!(recheck(&ctx, &bad), Err(ReCheckError::Ill(_))));
}
#[test]
fn variable_shadowing_resolves_to_the_inner_binder() {
let ctx = std_ctx();
let shadow = lam("x", ty(0), lam("x", g("Nat"), var("x")));
assert_agree(&ctx, &shadow, "shadowing picks the inner binder");
let t = recheck(&ctx, &shadow).expect("shadowing re-checks");
if let Term::Pi { body_type, .. } = &t {
if let Term::Pi { body_type: inner, .. } = body_type.as_ref() {
assert_eq!(**inner, g("Nat"), "inner x has type Nat, got {inner}");
} else {
panic!("expected nested Π, got {t}");
}
} else {
panic!("expected Π, got {t}");
}
}
#[test]
fn dependent_application_substitutes_a_binder_containing_argument() {
let ctx = std_ctx();
let id = lam("A", ty(0), lam("x", var("A"), var("x")));
let arg = pi("y", g("Nat"), g("Nat"));
let applied = app(id, arg.clone());
assert_agree(&ctx, &applied, "apply id to a Π-typed argument");
let t = recheck(&ctx, &applied).expect("re-checks");
let expected = pi("_", arg.clone(), arg);
assert!(
matches!(&t, Term::Pi { .. }),
"expected an arrow type, got {t}"
);
let _ = expected;
}
#[test]
fn nested_shadowing_under_application_agrees() {
let ctx = std_ctx();
let term = lam(
"x",
pi("_", g("Nat"), g("Nat")),
lam("x", g("Nat"), app(var("x"), var("x"))),
);
assert_eq!(
double_check(&ctx, &term),
DoubleCheck::Agreed,
"both kernels must consistently reject the ill-typed self-application"
);
assert!(recheck(&ctx, &term).is_err());
}
#[test]
fn match_is_fully_double_checked() {
let ctx = std_ctx();
let m = Term::Match {
discriminant: Box::new(g("true")),
motive: Box::new(g("Bool")),
cases: vec![g("true"), g("false")],
};
assert!(recheck(&ctx, &m).is_ok(), "the re-checker must accept a valid match");
assert_eq!(
double_check(&ctx, &m),
DoubleCheck::Agreed,
"a valid Bool match should be fully double-verified, got {:?}",
double_check(&ctx, &m)
);
}
#[test]
fn match_with_wrong_case_count_is_independently_rejected() {
let ctx = std_ctx();
let bad = Term::Match {
discriminant: Box::new(g("true")),
motive: Box::new(g("Bool")),
cases: vec![g("true")],
};
assert!(
matches!(recheck(&ctx, &bad), Err(ReCheckError::Ill(_))),
"a non-exhaustive match must be rejected by the re-checker's coverage check"
);
assert_eq!(double_check(&ctx, &bad), DoubleCheck::Agreed, "both reject it");
}
fn nat_arrow() -> Term {
pi("_", g("Nat"), g("Nat"))
}
#[test]
fn trivially_terminating_fix_is_fully_double_checked() {
let ctx = std_ctx();
let f = Term::Fix { name: "f".to_string(), body: Box::new(lam("n", g("Nat"), g("Zero"))) };
assert!(recheck(&ctx, &f).is_ok(), "a terminating fix must be accepted");
assert_eq!(double_check(&ctx, &f), DoubleCheck::Agreed);
}
#[test]
fn genuine_structural_recursion_is_accepted() {
let ctx = std_ctx();
let body = lam(
"n",
g("Nat"),
Term::Match {
discriminant: Box::new(var("n")),
motive: Box::new(lam("_", g("Nat"), g("Nat"))),
cases: vec![g("Zero"), lam("k", g("Nat"), app(var("f"), var("k")))],
},
);
let f = Term::Fix { name: "f".to_string(), body: Box::new(body) };
assert!(
recheck(&ctx, &f).is_ok(),
"genuine structural recursion must be accepted: {:?}",
recheck(&ctx, &f)
);
assert_eq!(double_check(&ctx, &f), DoubleCheck::Agreed);
}
#[test]
fn non_decreasing_self_call_is_independently_rejected() {
let ctx = std_ctx();
let f = Term::Fix {
name: "f".to_string(),
body: Box::new(lam("n", g("Nat"), app(var("f"), var("n")))),
};
assert!(
matches!(recheck(&ctx, &f), Err(ReCheckError::Ill(_))),
"a non-decreasing self-call must be rejected by the re-checker's guard, got {:?}",
recheck(&ctx, &f)
);
assert_eq!(double_check(&ctx, &f), DoubleCheck::Agreed, "both reject it");
}
#[test]
fn higher_order_escape_that_inhabits_false_is_independently_rejected() {
let ctx = std_ctx();
let nat_to_false = pi("_", g("Nat"), g("False"));
let zero_case = app(lam("g", nat_to_false, app(var("g"), g("Zero"))), var("f"));
let succ_case = lam("k", g("Nat"), app(var("f"), var("k")));
let body = lam(
"n",
g("Nat"),
Term::Match {
discriminant: Box::new(var("n")),
motive: Box::new(lam("_", g("Nat"), g("False"))),
cases: vec![zero_case, succ_case],
},
);
let f = Term::Fix { name: "f".to_string(), body: Box::new(body) };
assert!(
matches!(recheck(&ctx, &f), Err(ReCheckError::Ill(_))),
"the higher-order escape inhabits False and MUST be rejected by the re-checker, got {:?}",
recheck(&ctx, &f)
);
assert_eq!(double_check(&ctx, &f), DoubleCheck::Agreed);
}
#[test]
fn bare_recursive_name_returned_from_a_branch_is_independently_rejected() {
let ctx = std_ctx();
let body = lam(
"n",
g("Nat"),
Term::Match {
discriminant: Box::new(var("n")),
motive: Box::new(lam("_", g("Nat"), nat_arrow())),
cases: vec![var("f"), lam("k", g("Nat"), app(var("f"), var("k")))],
},
);
let f = Term::Fix { name: "f".to_string(), body: Box::new(body) };
assert!(
matches!(recheck(&ctx, &f), Err(ReCheckError::Ill(_))),
"a branch returning the bare fixpoint must be rejected, got {:?}",
recheck(&ctx, &f)
);
}
#[test]
fn differential_corpus_never_disagrees() {
let ctx = std_ctx();
let corpus = vec![
ty(0),
prop(),
pi("_", g("Nat"), g("Nat")),
pi("x", g("Entity"), prop()),
lam("A", ty(0), lam("x", var("A"), var("x"))),
app(g("Succ"), g("Zero")),
lam("p", prop(), var("p")),
pi("A", ty(0), pi("_", var("A"), var("A"))),
Term::Lit(Literal::Int(7)),
];
for (i, term) in corpus.iter().enumerate() {
let verdict = double_check(&ctx, term);
assert_ne!(
verdict,
DoubleCheck::Disagree(String::new()),
"corpus[{i}] produced a disagreement shape"
);
assert!(
!matches!(verdict, DoubleCheck::Disagree(_)),
"corpus[{i}] = {term} DISAGREED: {verdict:?}"
);
}
}