use logicaffeine_kernel::prelude::StandardLibrary;
use logicaffeine_kernel::{
double_check, infer_type, is_subtype, normalize, Context, DoubleCheck, Term, Universe,
};
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 apps(f: Term, xs: &[Term]) -> Term {
xs.iter().fold(f, |acc, x| app(acc, x.clone()))
}
fn ty0() -> Term {
Term::Sort(Universe::Type(0))
}
fn prod_ctx() -> Context {
let mut ctx = Context::new();
StandardLibrary::register(&mut ctx);
ctx.add_structure(
"Prod",
&[("A", ty0()), ("B", ty0())],
&[("fst", v("A")), ("snd", v("B"))],
);
ctx
}
#[test]
fn structure_registers_as_single_ctor_inductive() {
let ctx = prod_ctx();
assert!(infer_type(&ctx, &g("Prod")).is_ok(), "Prod : Type→Type→Type");
assert!(infer_type(&ctx, &g("Prod_mk")).is_ok(), "Prod_mk registered");
assert!(infer_type(&ctx, &g("Prod_fst")).is_ok(), "Prod_fst registered");
assert!(infer_type(&ctx, &g("Prod_snd")).is_ok(), "Prod_snd registered");
}
#[test]
fn projections_compute() {
let ctx = prod_ctx();
let pair = apps(g("Prod_mk"), &[g("Nat"), g("Bool"), g("Zero"), g("true")]);
let fst = apps(g("Prod_fst"), &[g("Nat"), g("Bool"), pair.clone()]);
assert_eq!(normalize(&ctx, &fst), g("Zero"), "fst computes to Zero");
let snd = apps(g("Prod_snd"), &[g("Nat"), g("Bool"), pair]);
assert_eq!(normalize(&ctx, &snd), g("true"), "snd computes to true");
}
#[test]
fn projection_types_are_correct() {
let ctx = prod_ctx();
let pair = apps(g("Prod_mk"), &[g("Nat"), g("Bool"), g("Zero"), g("true")]);
let fst = apps(g("Prod_fst"), &[g("Nat"), g("Bool"), pair.clone()]);
let fst_ty = infer_type(&ctx, &fst).expect("fst type-checks");
assert!(is_subtype(&ctx, &fst_ty, &g("Nat")), "fst : Nat, got {fst_ty}");
}
#[test]
fn structure_eta_defeq() {
let ctx = {
let mut c = prod_ctx();
c.add_declaration("p", apps(g("Prod"), &[g("Nat"), g("Bool")]));
c
};
let p = g("p");
let expanded = apps(
g("Prod_mk"),
&[
g("Nat"),
g("Bool"),
apps(g("Prod_fst"), &[g("Nat"), g("Bool"), p.clone()]),
apps(g("Prod_snd"), &[g("Nat"), g("Bool"), p.clone()]),
],
);
assert!(
logicaffeine_kernel::defeq_for_test(&ctx, &p, &expanded),
"structure eta: p ≡ ⟨p.fst, p.snd⟩"
);
}
#[test]
fn eta_only_for_registered_structures() {
let ctx = {
let mut c = prod_ctx();
c.add_declaration("n", g("Nat"));
c
};
let n = g("n");
let succ_pred = app(g("Succ"), app(g("pred"), n.clone()));
assert!(
!logicaffeine_kernel::defeq_for_test(&ctx, &n, &succ_pred),
"eta must not fire for the non-structure Nat"
);
}
#[test]
fn structure_eta_is_two_kernel() {
let ctx = {
let mut c = prod_ctx();
c.add_declaration("p", apps(g("Prod"), &[g("Nat"), g("Bool")]));
c
};
let body = apps(
g("Prod_mk"),
&[
g("Nat"),
g("Bool"),
apps(g("Prod_fst"), &[g("Nat"), g("Bool"), v("x")]),
apps(g("Prod_snd"), &[g("Nat"), g("Bool"), v("x")]),
],
);
let repack = Term::Lambda {
param: "x".to_string(),
param_type: Box::new(apps(g("Prod"), &[g("Nat"), g("Bool")])),
body: Box::new(body),
};
let applied = app(repack, g("p"));
assert!(
logicaffeine_kernel::defeq_for_test(&ctx, &applied, &g("p")),
"repack p ≡ p by eta"
);
match double_check(&ctx, &applied) {
DoubleCheck::Agreed => {}
other => panic!("both kernels must agree on the eta term, got {other:?}"),
}
}
#[test]
fn dependent_field_structure_projects_and_computes() {
let mut ctx = Context::new();
StandardLibrary::register(&mut ctx);
let arrow_ty = Term::Pi {
param: "_".to_string(),
param_type: Box::new(v("A")),
body_type: Box::new(ty0()),
};
ctx.add_structure(
"Sig",
&[("A", ty0()), ("B", arrow_ty)],
&[("fst", v("A")), ("snd", app(v("B"), v("fst")))],
);
let b_fam = Term::Lambda {
param: "_".to_string(),
param_type: Box::new(g("Nat")),
body: Box::new(g("Bool")),
};
let pair = apps(g("Sig_mk"), &[g("Nat"), b_fam.clone(), g("Zero"), g("true")]);
let fst = apps(g("Sig_fst"), &[g("Nat"), b_fam.clone(), pair.clone()]);
assert_eq!(normalize(&ctx, &fst), g("Zero"), "Sig_fst computes");
let snd = apps(g("Sig_snd"), &[g("Nat"), b_fam.clone(), pair.clone()]);
assert_eq!(normalize(&ctx, &snd), g("true"), "Sig_snd computes");
assert!(infer_type(&ctx, &pair).is_ok(), "dependent pair type-checks");
let snd_ty = infer_type(&ctx, &snd).expect("Sig_snd type-checks");
assert!(is_subtype(&ctx, &snd_ty, &g("Bool")), "Sig_snd : Bool, got {snd_ty}");
}
#[test]
fn typeclass_as_structure_end_to_end() {
let mut ctx = Context::new();
StandardLibrary::register(&mut ctx);
let binop = Term::Pi {
param: "_".to_string(),
param_type: Box::new(v("M")),
body_type: Box::new(Term::Pi {
param: "_".to_string(),
param_type: Box::new(v("M")),
body_type: Box::new(v("M")),
}),
};
ctx.add_structure("Monoid", &[("M", ty0())], &[("unit", v("M")), ("op", binop)]);
let nat_op = Term::Lambda {
param: "x".to_string(),
param_type: Box::new(g("Nat")),
body: Box::new(Term::Lambda {
param: "y".to_string(),
param_type: Box::new(g("Nat")),
body: Box::new(v("x")),
}),
};
let nat_monoid = apps(g("Monoid_mk"), &[g("Nat"), g("Zero"), nat_op]);
let monoid_nat = app(g("Monoid"), g("Nat"));
assert!(
is_subtype(&ctx, &infer_type(&ctx, &nat_monoid).unwrap(), &monoid_nat),
"the Nat monoid instance has type Monoid Nat"
);
ctx.add_instance(monoid_nat.clone(), nat_monoid.clone());
let mut mctx = logicaffeine_kernel::MetaCtx::default();
let resolved = logicaffeine_kernel::resolve_instance(&ctx, &mut mctx, &monoid_nat)
.expect("instance resolution finds the Nat monoid");
let unit = apps(g("Monoid_unit"), &[g("Nat"), resolved]);
assert_eq!(normalize(&ctx, &unit), g("Zero"), "the monoid's unit is Zero");
}
#[test]
fn structure_eta_required_for_typing_is_two_kernel() {
let ctx = {
let mut c = prod_ctx();
c.add_declaration("p", apps(g("Prod"), &[g("Nat"), g("Bool")]));
c
};
let prod_ab = apps(g("Prod"), &[g("Nat"), g("Bool")]);
let fst_p = apps(g("Prod_fst"), &[g("Nat"), g("Bool"), g("p")]);
let snd_p = apps(g("Prod_snd"), &[g("Nat"), g("Bool"), g("p")]);
let mk_p = apps(g("Prod_mk"), &[g("Nat"), g("Bool"), fst_p, snd_p]);
let eq_expected = apps(g("Eq"), &[prod_ab.clone(), mk_p, g("p")]);
let refl_p = apps(g("refl"), &[prod_ab.clone(), g("p")]);
let coerce = Term::Lambda {
param: "y".to_string(),
param_type: Box::new(eq_expected),
body: Box::new(v("y")),
};
let attack = app(coerce, refl_p);
assert!(infer_type(&ctx, &attack).is_ok(), "eta-requiring coercion must type-check");
match double_check(&ctx, &attack) {
DoubleCheck::Agreed => {}
other => panic!("both kernels must do structure eta, got {other:?}"),
}
}