use logicaffeine_kernel::prelude::StandardLibrary;
use logicaffeine_kernel::{
derive_recursor, double_check, infer_type, is_subtype, normalize, recheck, Context,
DoubleCheck, MutualInductive, Term, Universe,
};
fn same_type(ctx: &Context, a: &Term, b: &Term) -> bool {
is_subtype(ctx, a, b) && is_subtype(ctx, b, a)
}
fn g(n: &str) -> Term {
Term::Global(n.to_string())
}
fn v(n: &str) -> Term {
Term::Var(n.to_string())
}
fn app(f: Term, x: Term) -> Term {
Term::App(Box::new(f), Box::new(x))
}
fn pi(p: &str, t: Term, b: Term) -> Term {
Term::Pi { param: p.to_string(), param_type: Box::new(t), body_type: Box::new(b) }
}
fn arrow(a: Term, b: Term) -> Term {
pi("_", a, b)
}
fn lam(p: &str, t: Term, b: Term) -> Term {
Term::Lambda { param: p.to_string(), param_type: Box::new(t), body: Box::new(b) }
}
fn match_(d: Term, motive: Term, cases: Vec<Term>) -> Term {
Term::Match { discriminant: Box::new(d), motive: Box::new(motive), cases }
}
fn mutfix(defs: &[(&str, Term)], index: usize) -> Term {
Term::MutualFix {
defs: defs.iter().map(|(n, b)| (n.to_string(), b.clone())).collect(),
index,
}
}
fn apps(f: Term, xs: &[Term]) -> Term {
xs.iter().fold(f, |a, x| app(a, x.clone()))
}
fn nat() -> Term {
g("Nat")
}
fn bool_t() -> Term {
g("Bool")
}
fn prop() -> Term {
Term::Sort(Universe::Prop)
}
fn ty0() -> Term {
Term::Sort(Universe::Type(0))
}
fn succ(n: Term) -> Term {
app(g("Succ"), n)
}
fn tree_forest_block() -> Vec<MutualInductive> {
vec![
MutualInductive {
name: "Tree".to_string(),
sort: ty0(),
num_params: 0,
constructors: vec![("node".to_string(), arrow(nat(), arrow(g("Forest"), g("Tree"))))],
},
MutualInductive {
name: "Forest".to_string(),
sort: ty0(),
num_params: 0,
constructors: vec![
("fnil".to_string(), g("Forest")),
("fcons".to_string(), arrow(g("Tree"), arrow(g("Forest"), g("Forest")))),
],
},
]
}
fn std_ctx() -> Context {
let mut ctx = Context::new();
StandardLibrary::register(&mut ctx);
ctx
}
fn is_even_odd(index: usize, bad: bool) -> Term {
let bool_motive = lam("_", nat(), bool_t());
let iseven_body = lam(
"n",
nat(),
match_(
v("n"),
bool_motive.clone(),
vec![g("true"), lam("m", nat(), app(v("isOdd"), if bad { v("n") } else { v("m") }))],
),
);
let isodd_body = lam(
"n",
nat(),
match_(
v("n"),
bool_motive,
vec![g("false"), lam("m", nat(), app(v("isEven"), v("m")))],
),
);
mutfix(&[("isEven", iseven_body), ("isOdd", isodd_body)], index)
}
fn even_odd_block() -> Vec<MutualInductive> {
vec![
MutualInductive {
name: "Even".to_string(),
sort: pi("n", nat(), prop()),
num_params: 0,
constructors: vec![
("even_zero".to_string(), app(g("Even"), g("Zero"))),
(
"even_succ".to_string(),
pi("n", nat(), arrow(app(g("Odd"), v("n")), app(g("Even"), succ(v("n"))))),
),
],
},
MutualInductive {
name: "Odd".to_string(),
sort: pi("n", nat(), prop()),
num_params: 0,
constructors: vec![(
"odd_succ".to_string(),
pi("n", nat(), arrow(app(g("Even"), v("n")), app(g("Odd"), succ(v("n"))))),
)],
},
]
}
#[test]
fn even_odd_block_registers_and_typechecks() {
let mut ctx = std_ctx();
ctx.add_mutual_inductives(&even_odd_block()).expect("Even/Odd block registers");
for name in ["Even", "Odd", "even_zero", "even_succ", "odd_succ"] {
assert!(infer_type(&ctx, &g(name)).is_ok(), "{name} must be registered and type-check");
}
let even_two = app(g("Even"), succ(succ(g("Zero"))));
let sort = infer_type(&ctx, &even_two).expect("Even 2 type-checks");
assert_eq!(sort, prop(), "Even 2 must be a Prop");
}
#[test]
fn cross_block_negative_occurrence_is_rejected() {
let mut block = even_odd_block();
block[1].constructors.push((
"bad".to_string(),
pi(
"n",
nat(),
arrow(arrow(app(g("Even"), v("n")), g("False")), app(g("Odd"), succ(v("n")))),
),
));
let mut ctx = std_ctx();
assert!(
ctx.add_mutual_inductives(&block).is_err(),
"a cross-block negative occurrence (Even n → False) → Odd n must be rejected"
);
assert!(infer_type(&ctx, &g("Even")).is_err(), "a rejected block must register nothing");
assert!(infer_type(&ctx, &g("odd_succ")).is_err(), "a rejected block must register nothing");
}
#[test]
fn mutual_fixpoint_typechecks_computes_and_is_guarded() {
let ctx = std_ctx();
let iseven = is_even_odd(0, false);
let ty = infer_type(&ctx, &iseven).expect("isEven type-checks (mutual guard accepts)");
assert!(same_type(&ctx, &ty, &arrow(nat(), bool_t())), "isEven : Nat → Bool, got {ty}");
let two = succ(succ(g("Zero")));
let three = succ(two.clone());
assert_eq!(normalize(&ctx, &app(iseven.clone(), two)), g("true"), "isEven 2 = true");
assert_eq!(normalize(&ctx, &app(iseven.clone(), succ(g("Zero")))), g("false"), "isEven 1 = false");
assert_eq!(normalize(&ctx, &app(iseven, three)), g("false"), "isEven 3 = false");
let isodd = is_even_odd(1, false);
assert!(
same_type(&ctx, &infer_type(&ctx, &isodd).unwrap(), &arrow(nat(), bool_t())),
"isOdd : Nat → Bool"
);
assert_eq!(normalize(&ctx, &app(isodd, succ(succ(g("Zero"))))), g("false"), "isOdd 2 = false");
}
#[test]
fn tree_forest_mutual_recursor_derives_and_typechecks() {
let mut ctx = std_ctx();
ctx.add_mutual_inductives(&tree_forest_block()).expect("Tree/Forest registers");
let (tree_ty, tree_rec) = derive_recursor(&ctx, "Tree").expect("Tree_rec derives");
let (_forest_ty, forest_rec) = derive_recursor(&ctx, "Forest").expect("Forest_rec derives");
assert!(infer_type(&ctx, &tree_rec).is_ok(), "Tree_rec body type-checks: {tree_ty}");
assert!(infer_type(&ctx, &forest_rec).is_ok(), "Forest_rec body type-checks");
if let Term::Pi { param, .. } = &tree_ty {
assert_eq!(param, "P0", "Tree_rec's first binder is the Tree motive P0");
} else {
panic!("Tree_rec type must be a Π, got {tree_ty}");
}
}
#[test]
fn tree_forest_mutual_recursor_computes() {
let mut ctx = std_ctx();
ctx.add_mutual_inductives(&tree_forest_block()).expect("Tree/Forest registers");
let (_forest_ty, forest_rec) = derive_recursor(&ctx, "Forest").expect("Forest_rec derives");
let p_tree = lam("_", g("Tree"), nat());
let p_forest = lam("_", g("Forest"), nat());
let f_node = lam("n", nat(), lam("f", g("Forest"), lam("ihf", nat(), g("Zero"))));
let f_fnil = g("Zero");
let f_fcons = lam(
"t",
g("Tree"),
lam("f", g("Forest"), lam("iht", nat(), lam("ihf", nat(), succ(v("ihf"))))),
);
let leaf = apps(g("node"), &[g("Zero"), g("fnil")]);
let forest = apps(g("fcons"), &[leaf.clone(), apps(g("fcons"), &[leaf, g("fnil")])]);
let length =
apps(forest_rec, &[p_tree, p_forest, f_node, f_fnil, f_fcons, forest]);
let ty = infer_type(&ctx, &length).expect("forestLength application type-checks");
assert!(same_type(&ctx, &ty, &nat()), "forestLength forest : Nat, got {ty}");
assert_eq!(
normalize(&ctx, &length),
succ(succ(g("Zero"))),
"a forest of two trees has length 2"
);
}
fn tree_forest_a_block() -> Vec<MutualInductive> {
vec![
MutualInductive {
name: "TreeA".to_string(),
sort: pi("A", ty0(), ty0()), num_params: 1,
constructors: vec![(
"nodeA".to_string(),
pi("A", ty0(), arrow(v("A"), arrow(app(g("ForestA"), v("A")), app(g("TreeA"), v("A"))))),
)],
},
MutualInductive {
name: "ForestA".to_string(),
sort: pi("A", ty0(), ty0()),
num_params: 1,
constructors: vec![
("fnilA".to_string(), pi("A", ty0(), app(g("ForestA"), v("A")))),
(
"fconsA".to_string(),
pi(
"A",
ty0(),
arrow(
app(g("TreeA"), v("A")),
arrow(app(g("ForestA"), v("A")), app(g("ForestA"), v("A"))),
),
),
),
],
},
]
}
#[test]
fn parametric_mutual_block_derives_a_two_kernel_recursor() {
let mut ctx = std_ctx();
ctx.add_mutual_inductives(&tree_forest_a_block()).expect("parametric block registers");
for member in ["TreeA", "ForestA"] {
let (ty, rec) = derive_recursor(&ctx, member).expect("parametric mutual recursor derives");
assert!(infer_type(&ctx, &rec).is_ok(), "{member}_rec type-checks: {ty}");
if let Term::Pi { param, .. } = &ty {
assert_eq!(param, "A0", "{member}_rec's first binder is the shared parameter");
} else {
panic!("recursor type must be a Π");
}
match double_check(&ctx, &rec) {
DoubleCheck::Agreed => {}
other => panic!("both kernels must certify {member}_rec, got {other:?}"),
}
}
}
#[test]
fn non_terminating_mutual_fixpoint_is_rejected() {
let ctx = std_ctx();
let bad = is_even_odd(0, true);
assert!(
infer_type(&ctx, &bad).is_err(),
"a mutual call on the un-decremented parameter must be rejected by the main guard"
);
assert!(
recheck(&ctx, &bad).is_err(),
"the independent re-checker's mutual guard must reject it too"
);
}
#[test]
fn mutual_fixpoint_is_two_kernel_verified() {
let ctx = std_ctx();
for index in [0, 1] {
match double_check(&ctx, &is_even_odd(index, false)) {
DoubleCheck::Agreed => {}
other => panic!("both kernels must agree on is_even_odd({index}), got {other:?}"),
}
}
}
#[test]
fn tree_forest_recursor_is_two_kernel_verified() {
let mut ctx = std_ctx();
ctx.add_mutual_inductives(&tree_forest_block()).expect("Tree/Forest registers");
for member in ["Tree", "Forest"] {
let (_ty, rec) = derive_recursor(&ctx, member).expect("recursor derives");
match double_check(&ctx, &rec) {
DoubleCheck::Agreed => {}
other => panic!("both kernels must agree on {member}_rec, got {other:?}"),
}
}
}