use std::collections::{HashMap, HashSet};
use crate::term::{int_lit, lit_bigint, Literal, Term, Universe};
use crate::Context;
#[derive(Debug, Clone, PartialEq)]
pub enum ReCheckError {
Unsupported(String),
Ill(String),
}
impl ReCheckError {
fn ill(msg: impl Into<String>) -> Self {
ReCheckError::Ill(msg.into())
}
fn unsupported(msg: impl Into<String>) -> Self {
ReCheckError::Unsupported(msg.into())
}
}
type RResult<T> = Result<T, ReCheckError>;
#[derive(Debug, Clone, PartialEq)]
enum Db {
Sort(Universe),
Var(usize),
Global(String),
Const { name: String, levels: Vec<Universe> },
Pi(Box<Db>, Box<Db>),
Lam(Box<Db>, Box<Db>),
App(Box<Db>, Box<Db>),
Match { disc: Box<Db>, motive: Box<Db>, cases: Vec<Db> },
Fix(Box<Db>),
MutualFix { defs: Vec<Db>, index: usize },
Let(Box<Db>, Box<Db>, Box<Db>),
Lit(Literal),
}
fn to_db(term: &Term, scope: &mut Vec<String>) -> RResult<Db> {
match term {
Term::Sort(u) => Ok(Db::Sort(u.clone())),
Term::Var(name) => {
for (depth_from_inner, bound) in scope.iter().rev().enumerate() {
if bound == name {
return Ok(Db::Var(depth_from_inner));
}
}
Err(ReCheckError::ill(format!("unbound local variable '{}'", name)))
}
Term::Global(name) => Ok(Db::Global(name.clone())),
Term::Const { name, levels } => {
Ok(Db::Const { name: name.clone(), levels: levels.clone() })
}
Term::Pi { param, param_type, body_type } => {
let dom = to_db(param_type, scope)?;
scope.push(param.clone());
let body = to_db(body_type, scope);
scope.pop();
Ok(Db::Pi(Box::new(dom), Box::new(body?)))
}
Term::Lambda { param, param_type, body } => {
let dom = to_db(param_type, scope)?;
scope.push(param.clone());
let inner = to_db(body, scope);
scope.pop();
Ok(Db::Lam(Box::new(dom), Box::new(inner?)))
}
Term::App(f, a) => Ok(Db::App(Box::new(to_db(f, scope)?), Box::new(to_db(a, scope)?))),
Term::Match { discriminant, motive, cases } => {
let disc = to_db(discriminant, scope)?;
let mot = to_db(motive, scope)?;
let cs = cases.iter().map(|c| to_db(c, scope)).collect::<RResult<Vec<_>>>()?;
Ok(Db::Match { disc: Box::new(disc), motive: Box::new(mot), cases: cs })
}
Term::Fix { name, body } => {
scope.push(name.clone());
let b = to_db(body, scope);
scope.pop();
Ok(Db::Fix(Box::new(b?)))
}
Term::MutualFix { defs, index } => {
let names: Vec<String> = defs.iter().map(|(n, _)| n.clone()).collect();
for n in &names {
scope.push(n.clone());
}
let mut dbs = Vec::with_capacity(defs.len());
let mut err = None;
for (_, body) in defs {
match to_db(body, scope) {
Ok(d) => dbs.push(d),
Err(e) => {
err = Some(e);
break;
}
}
}
for _ in &names {
scope.pop();
}
match err {
Some(e) => Err(e),
None => Ok(Db::MutualFix { defs: dbs, index: *index }),
}
}
Term::Let { name, ty, value, body } => {
let d_ty = to_db(ty, scope)?;
let d_val = to_db(value, scope)?;
scope.push(name.clone());
let d_body = to_db(body, scope);
scope.pop();
Ok(Db::Let(Box::new(d_ty), Box::new(d_val), Box::new(d_body?)))
}
Term::Lit(l) => Ok(Db::Lit(l.clone())),
Term::Hole => Err(ReCheckError::unsupported("Hole (unelaborated implicit)")),
}
}
fn from_db(t: &Db, depth: usize) -> Term {
match t {
Db::Sort(u) => Term::Sort(u.clone()),
Db::Var(k) => {
let binder = depth.saturating_sub(1).saturating_sub(*k);
Term::Var(format!("v{}", binder))
}
Db::Global(n) => Term::Global(n.clone()),
Db::Const { name, levels } => Term::Const { name: name.clone(), levels: levels.clone() },
Db::Pi(a, b) => Term::Pi {
param: format!("v{}", depth),
param_type: Box::new(from_db(a, depth)),
body_type: Box::new(from_db(b, depth + 1)),
},
Db::Lam(a, b) => Term::Lambda {
param: format!("v{}", depth),
param_type: Box::new(from_db(a, depth)),
body: Box::new(from_db(b, depth + 1)),
},
Db::App(f, a) => Term::App(Box::new(from_db(f, depth)), Box::new(from_db(a, depth))),
Db::Match { disc, motive, cases } => Term::Match {
discriminant: Box::new(from_db(disc, depth)),
motive: Box::new(from_db(motive, depth)),
cases: cases.iter().map(|c| from_db(c, depth)).collect(),
},
Db::Fix(body) => Term::Fix {
name: format!("rec{}", depth),
body: Box::new(from_db(body, depth + 1)),
},
Db::MutualFix { defs, index } => {
let n = defs.len();
Term::MutualFix {
defs: defs
.iter()
.enumerate()
.map(|(j, b)| (format!("rec{depth}_{j}"), from_db(b, depth + n)))
.collect(),
index: *index,
}
}
Db::Let(ty, value, body) => Term::Let {
name: format!("v{}", depth),
ty: Box::new(from_db(ty, depth)),
value: Box::new(from_db(value, depth)),
body: Box::new(from_db(body, depth + 1)),
},
Db::Lit(l) => Term::Lit(l.clone()),
}
}
fn shift(t: &Db, d: isize, cutoff: usize) -> Db {
match t {
Db::Var(k) => {
if *k >= cutoff {
Db::Var((*k as isize + d) as usize)
} else {
Db::Var(*k)
}
}
Db::Pi(a, b) => Db::Pi(Box::new(shift(a, d, cutoff)), Box::new(shift(b, d, cutoff + 1))),
Db::Lam(a, b) => Db::Lam(Box::new(shift(a, d, cutoff)), Box::new(shift(b, d, cutoff + 1))),
Db::App(f, a) => Db::App(Box::new(shift(f, d, cutoff)), Box::new(shift(a, d, cutoff))),
Db::Match { disc, motive, cases } => Db::Match {
disc: Box::new(shift(disc, d, cutoff)),
motive: Box::new(shift(motive, d, cutoff)),
cases: cases.iter().map(|c| shift(c, d, cutoff)).collect(),
},
Db::Fix(body) => Db::Fix(Box::new(shift(body, d, cutoff + 1))),
Db::MutualFix { defs, index } => Db::MutualFix {
defs: defs.iter().map(|b| shift(b, d, cutoff + defs.len())).collect(),
index: *index,
},
Db::Let(ty, value, body) => Db::Let(
Box::new(shift(ty, d, cutoff)),
Box::new(shift(value, d, cutoff)),
Box::new(shift(body, d, cutoff + 1)),
),
Db::Sort(_) | Db::Global(_) | Db::Const { .. } | Db::Lit(_) => t.clone(),
}
}
fn subst(t: &Db, j: usize, s: &Db) -> Db {
match t {
Db::Var(k) => {
if *k == j {
s.clone()
} else {
Db::Var(*k)
}
}
Db::Pi(a, b) => Db::Pi(
Box::new(subst(a, j, s)),
Box::new(subst(b, j + 1, &shift(s, 1, 0))),
),
Db::Lam(a, b) => Db::Lam(
Box::new(subst(a, j, s)),
Box::new(subst(b, j + 1, &shift(s, 1, 0))),
),
Db::App(f, a) => Db::App(Box::new(subst(f, j, s)), Box::new(subst(a, j, s))),
Db::Match { disc, motive, cases } => Db::Match {
disc: Box::new(subst(disc, j, s)),
motive: Box::new(subst(motive, j, s)),
cases: cases.iter().map(|c| subst(c, j, s)).collect(),
},
Db::Fix(body) => Db::Fix(Box::new(subst(body, j + 1, &shift(s, 1, 0)))),
Db::MutualFix { defs, index } => {
let n = defs.len();
Db::MutualFix {
defs: defs.iter().map(|b| subst(b, j + n, &shift(s, n as isize, 0))).collect(),
index: *index,
}
}
Db::Let(ty, value, body) => Db::Let(
Box::new(subst(ty, j, s)),
Box::new(subst(value, j, s)),
Box::new(subst(body, j + 1, &shift(s, 1, 0))),
),
Db::Sort(_) | Db::Global(_) | Db::Const { .. } | Db::Lit(_) => t.clone(),
}
}
fn beta_open(body: &Db, arg: &Db) -> Db {
shift(&subst(body, 0, &shift(arg, 1, 0)), -1, 0)
}
fn open_mutual(defs: &[Db], index: usize) -> Db {
let n = defs.len();
let mut result = defs[index].clone();
for k in 0..n {
let proj = Db::MutualFix { defs: defs.to_vec(), index: n - 1 - k };
result = beta_open(&result, &proj);
}
result
}
fn spine(t: &Db) -> (Db, Vec<Db>) {
let mut args = Vec::new();
let mut cur = t.clone();
while let Db::App(f, a) = cur {
args.push(*a);
cur = *f;
}
args.reverse();
(cur, args)
}
fn ctor_headed(genv: &Context, t: &Db) -> bool {
let (head, _) = spine(&whnf(genv, t));
matches!(head, Db::Global(n) if genv.is_constructor(&n))
}
fn try_builtin(genv: &Context, t: &Db) -> Option<Db> {
let (head, args) = spine(t);
let op = match &head {
Db::Global(n) if args.len() == 2 => n.as_str(),
_ => return None,
};
if !matches!(op, "add" | "sub" | "mul" | "div" | "mod" | "le" | "lt" | "ge" | "gt") {
return None;
}
let (xl, yl) = match (whnf(genv, &args[0]), whnf(genv, &args[1])) {
(Db::Lit(xl), Db::Lit(yl)) => (xl, yl),
_ => return None,
};
let bool_op = |b: bool| Some(Db::Global(if b { "true" } else { "false" }.to_string()));
if let (Literal::Int(x), Literal::Int(y)) = (&xl, &yl) {
let fast = match op {
"add" => x.checked_add(*y),
"sub" => x.checked_sub(*y),
"mul" => x.checked_mul(*y),
"div" => x.checked_div(*y),
"mod" => x.checked_rem(*y),
_ => None,
};
if let Some(r) = fast {
return Some(Db::Lit(Literal::Int(r)));
}
match op {
"le" => return bool_op(*x <= *y),
"lt" => return bool_op(*x < *y),
"ge" => return bool_op(*x >= *y),
"gt" => return bool_op(*x > *y),
_ => {}
}
}
let (xb, yb) = (lit_bigint(&xl)?, lit_bigint(&yl)?);
let big = match op {
"add" => Some(xb.add(&yb)),
"sub" => Some(xb.sub(&yb)),
"mul" => Some(xb.mul(&yb)),
"div" => xb.div_rem(&yb).map(|(q, _)| q),
"mod" => xb.div_rem(&yb).map(|(_, r)| r),
_ => None,
};
if let Some(r) = big {
return Some(Db::Lit(int_lit(r)));
}
match op {
"le" => bool_op(xb <= yb),
"lt" => bool_op(xb < yb),
"ge" => bool_op(xb >= yb),
"gt" => bool_op(xb > yb),
_ => None,
}
}
fn pi_count(t: &Db) -> usize {
match t {
Db::Pi(_, b) => 1 + pi_count(b),
_ => 0,
}
}
fn inductive_arity(genv: &Context, ind: &str) -> usize {
match genv.get_global(ind) {
Some(ty) => to_db(&ty.clone(), &mut Vec::new()).map(|d| pi_count(&d)).unwrap_or(0),
None => 0,
}
}
fn try_quot_lift_db(genv: &Context, t: &Db) -> Option<Db> {
let (head, args) = spine(t);
if !matches!(&head, Db::Global(n) if n == "Quot_lift") || args.len() != 6 {
return None;
}
let q = whnf(genv, &args[5]);
let (qh, qa) = spine(&q);
if !matches!(&qh, Db::Global(n) if n == "Quot_mk") || qa.len() != 3 {
return None;
}
Some(Db::App(Box::new(args[3].clone()), Box::new(qa[2].clone())))
}
fn whnf(genv: &Context, t: &Db) -> Db {
let mut cur = t.clone();
let mut fuel: usize = 1_000_000;
loop {
if fuel == 0 {
return cur;
}
fuel -= 1;
if matches!(cur, Db::App(..)) {
if let Some(reduced) = try_quot_lift_db(genv, &cur) {
cur = reduced;
continue;
}
}
if let Db::App(f, a) = &cur {
if matches!(f.as_ref(), Db::Global(n) if n == "reduceBool") {
let av = whnf(genv, a);
if matches!(&av, Db::Global(n) if n == "true" || n == "false") {
cur = av;
continue;
}
}
}
match cur {
Db::Global(name) => {
if let Some(body) = genv.get_definition_body(&name) {
if let Ok(db) = to_db(&body.clone(), &mut Vec::new()) {
cur = db;
continue;
}
}
return Db::Global(name);
}
Db::App(f, a) => {
let fw = whnf(genv, &f);
match fw {
Db::Lam(_, body) => {
cur = beta_open(&body, &a);
}
Db::Fix(fbody) => {
let (_, args) = spine(&Db::App(Box::new(Db::Fix(fbody.clone())), a.clone()));
if args.iter().any(|x| ctor_headed(genv, x)) {
let unfolded = beta_open(&fbody, &Db::Fix(fbody.clone()));
cur = Db::App(Box::new(unfolded), a);
} else {
return Db::App(Box::new(Db::Fix(fbody)), a);
}
}
Db::MutualFix { defs, index } => {
let applied =
Db::App(Box::new(Db::MutualFix { defs: defs.clone(), index }), a.clone());
let (_, args) = spine(&applied);
if args.iter().any(|x| ctor_headed(genv, x)) {
cur = Db::App(Box::new(open_mutual(&defs, index)), a);
} else {
return Db::App(Box::new(Db::MutualFix { defs, index }), a);
}
}
other => {
let full = Db::App(Box::new(other), a);
if let Some(r) = try_builtin(genv, &full) {
cur = r;
} else {
return full;
}
}
}
}
Db::Match { disc, motive, cases } => {
let mut d = whnf(genv, &disc);
if matches!(&d, Db::Lit(Literal::Nat(_))) {
d = db_nat_peano_step(&d);
}
let (head, args) = spine(&d);
if let Db::Global(cname) = &head {
if let Some(ind) = genv.constructor_inductive(cname) {
let ctor_names: Vec<String> =
genv.get_constructors(ind).iter().map(|(n, _)| n.to_string()).collect();
if let Some(idx) = ctor_names.iter().position(|n| n == cname) {
if idx < cases.len() {
let arity = inductive_arity(genv, ind);
let val_args =
if args.len() >= arity { &args[arity..] } else { &args[..] };
let mut res = cases[idx].clone();
for a in val_args {
res = Db::App(Box::new(res), Box::new(a.clone()));
}
cur = res;
continue;
}
}
}
}
return Db::Match { disc: Box::new(d), motive, cases };
}
Db::Let(_ty, value, body) => {
cur = beta_open(&body, &value);
}
other => return other,
}
}
}
fn db_nat_peano_step(t: &Db) -> Db {
match t {
Db::Lit(Literal::Nat(n)) if *n <= logicaffeine_base::BigInt::from_i64(0) => {
Db::Global("Zero".to_string())
}
Db::Lit(Literal::Nat(n)) => Db::App(
Box::new(Db::Global("Succ".to_string())),
Box::new(Db::Lit(Literal::Nat(n.sub(&logicaffeine_base::BigInt::from_i64(1))))),
),
other => other.clone(),
}
}
fn db_nat_peano_headed(t: &Db) -> bool {
match t {
Db::Global(n) => n == "Zero",
Db::App(f, _) => matches!(f.as_ref(), Db::Global(n) if n == "Succ"),
_ => false,
}
}
fn def_eq(genv: &Context, lctx: &[Db], a: &Db, b: &Db) -> bool {
let a = whnf(genv, a);
let b = whnf(genv, b);
match (&a, &b) {
(Db::Lit(Literal::Nat(x)), Db::Lit(Literal::Nat(y))) => return x == y,
(Db::Lit(Literal::Nat(_)), _) if db_nat_peano_headed(&b) => {
return def_eq(genv, lctx, &db_nat_peano_step(&a), &b);
}
(_, Db::Lit(Literal::Nat(_))) if db_nat_peano_headed(&a) => {
return def_eq(genv, lctx, &a, &db_nat_peano_step(&b));
}
_ => {}
}
if let Db::Lam(dom, body) = &a {
if !matches!(b, Db::Lam(..)) {
let bx = whnf(genv, &Db::App(Box::new(shift(&b, 1, 0)), Box::new(Db::Var(0))));
let mut ext = lctx.to_vec();
ext.push((**dom).clone());
return def_eq(genv, &ext, body, &bx);
}
}
if let Db::Lam(dom, body) = &b {
if !matches!(a, Db::Lam(..)) {
let ax = whnf(genv, &Db::App(Box::new(shift(&a, 1, 0)), Box::new(Db::Var(0))));
let mut ext = lctx.to_vec();
ext.push((**dom).clone());
return def_eq(genv, &ext, &ax, body);
}
}
if let Some(eq) = try_struct_eta_db(genv, lctx, &a, &b) {
return eq;
}
if let Some(eq) = try_struct_eta_db(genv, lctx, &b, &a) {
return eq;
}
let congruent = match (&a, &b) {
(Db::Sort(x), Db::Sort(y)) => x.equiv(y),
(Db::Var(i), Db::Var(j)) => i == j,
(Db::Global(m), Db::Global(n)) => m == n,
(Db::Lit(x), Db::Lit(y)) => x == y,
(Db::App(f1, a1), Db::App(f2, a2)) => {
def_eq(genv, lctx, f1, f2) && def_eq(genv, lctx, a1, a2)
}
(Db::Pi(d1, b1), Db::Pi(d2, b2)) => {
def_eq(genv, lctx, d1, d2) && {
let mut ext = lctx.to_vec();
ext.push((**d1).clone());
def_eq(genv, &ext, b1, b2)
}
}
(Db::Lam(d1, b1), Db::Lam(d2, b2)) => {
def_eq(genv, lctx, d1, d2) && {
let mut ext = lctx.to_vec();
ext.push((**d1).clone());
def_eq(genv, &ext, b1, b2)
}
}
(
Db::Match { disc: d1, motive: m1, cases: c1 },
Db::Match { disc: d2, motive: m2, cases: c2 },
) => {
def_eq(genv, lctx, d1, d2)
&& def_eq(genv, lctx, m1, m2)
&& c1.len() == c2.len()
&& c1.iter().zip(c2.iter()).all(|(x, y)| def_eq(genv, lctx, x, y))
}
(Db::Fix(b1), Db::Fix(b2)) => {
let mut ext = lctx.to_vec();
ext.push(Db::Sort(Universe::Prop)); def_eq(genv, &ext, b1, b2)
}
(Db::MutualFix { defs: d1, index: i1 }, Db::MutualFix { defs: d2, index: i2 }) => {
i1 == i2
&& d1.len() == d2.len()
&& {
let mut ext = lctx.to_vec();
for _ in 0..d1.len() {
ext.push(Db::Sort(Universe::Prop)); }
d1.iter().zip(d2.iter()).all(|(a, b)| def_eq(genv, &ext, a, b))
}
}
(Db::Const { name: n1, levels: l1 }, Db::Const { name: n2, levels: l2 }) => {
n1 == n2 && l1.len() == l2.len() && l1.iter().zip(l2.iter()).all(|(a, b)| a.equiv(b))
}
_ => false,
};
if congruent {
return true;
}
proof_irrel(genv, lctx, &a, &b)
}
fn try_struct_eta_db(genv: &Context, lctx: &[Db], mk_term: &Db, other: &Db) -> Option<bool> {
let (head, args) = spine(mk_term);
let Db::Global(hname) = &head else { return None };
let (_sname, info) = genv.struct_of_constructor(hname)?;
let nfields = info.projections.len();
if args.len() != info.num_params + nfields {
return None;
}
let (ohead, _) = spine(other);
if matches!(&ohead, Db::Global(n) if n == hname) {
return None;
}
let params = &args[..info.num_params];
let field_args = &args[info.num_params..];
Some(info.projections.iter().enumerate().all(|(i, proj)| {
let mut app = Db::Global(proj.clone());
for p in params {
app = Db::App(Box::new(app), Box::new(p.clone()));
}
app = Db::App(Box::new(app), Box::new(other.clone()));
def_eq(genv, lctx, &field_args[i], &app)
}))
}
fn proof_irrel(genv: &Context, lctx: &[Db], a: &Db, b: &Db) -> bool {
let ta = match infer(genv, lctx, a) {
Ok(t) => t,
Err(_) => return false,
};
match infer(genv, lctx, &ta) {
Ok(s) if matches!(
whnf(genv, &s),
Db::Sort(Universe::Prop) | Db::Sort(Universe::SProp)
) => {}
_ => return false,
}
match infer(genv, lctx, b) {
Ok(tb) => def_eq(genv, lctx, &ta, &tb),
Err(_) => false,
}
}
fn is_sub(genv: &Context, lctx: &[Db], sub: &Db, sup: &Db) -> bool {
let s = whnf(genv, sub);
let t = whnf(genv, sup);
match (&s, &t) {
(Db::Sort(x), Db::Sort(y)) => x.is_subtype_of(y),
(Db::Pi(d1, b1), Db::Pi(d2, b2)) => {
def_eq(genv, lctx, d1, d2) && {
let mut ext = lctx.to_vec();
ext.push((**d1).clone());
is_sub(genv, &ext, b1, b2)
}
}
_ => def_eq(genv, lctx, &s, &t),
}
}
fn var_type(lctx: &[Db], k: usize) -> RResult<Db> {
if k >= lctx.len() {
return Err(ReCheckError::ill(format!("de Bruijn index {} out of range", k)));
}
let stored = &lctx[lctx.len() - 1 - k];
Ok(shift(stored, (k + 1) as isize, 0))
}
fn infer(genv: &Context, lctx: &[Db], t: &Db) -> RResult<Db> {
match t {
Db::Sort(u) => Ok(Db::Sort(u.succ())),
Db::Var(k) => var_type(lctx, *k),
Db::Global(name) => {
let ty = genv
.get_global(name)
.ok_or_else(|| ReCheckError::ill(format!("unknown global '{}'", name)))?;
to_db(&ty.clone(), &mut Vec::new())
}
Db::Const { name, levels } => {
let (params, ty, _body) = genv
.get_universe_poly(name)
.ok_or_else(|| ReCheckError::ill(format!("unknown universe-poly global '{}'", name)))?;
if params.len() != levels.len() {
return Err(ReCheckError::ill(format!(
"universe-poly '{}' expects {} levels, got {}",
name,
params.len(),
levels.len()
)));
}
let subst: std::collections::HashMap<String, Universe> =
params.iter().cloned().zip(levels.iter().cloned()).collect();
let instantiated = crate::term::instantiate_universes(&ty.clone(), &subst);
to_db(&instantiated, &mut Vec::new())
}
Db::Lit(Literal::Nat(n)) if *n < logicaffeine_base::BigInt::from_i64(0) => {
Err(ReCheckError::ill("a `Nat` literal must be non-negative".to_string()))
}
Db::Lit(l) => Ok(Db::Global(lit_type_name(l).to_string())),
Db::Pi(dom, body) => {
let dom_sort = infer_sort(genv, lctx, dom)?;
let mut ext = lctx.to_vec();
ext.push((**dom).clone());
let body_sort = infer_sort(genv, &ext, body)?;
let pi_sort = dom_sort.imax(&body_sort);
Ok(Db::Sort(pi_sort))
}
Db::Lam(dom, body) => {
let _ = infer_sort(genv, lctx, dom)?;
let mut ext = lctx.to_vec();
ext.push((**dom).clone());
let body_ty = infer(genv, &ext, body)?;
Ok(Db::Pi(dom.clone(), Box::new(body_ty)))
}
Db::App(f, a) => {
let f_ty = whnf(genv, &infer(genv, lctx, f)?);
match f_ty {
Db::Pi(dom, body) => {
check(genv, lctx, a, &dom)?;
Ok(beta_open(&body, a))
}
other => Err(ReCheckError::ill(format!(
"application of a non-function (type {})",
from_db(&other, lctx.len())
))),
}
}
Db::Match { disc, motive, cases } => infer_match(genv, lctx, disc, motive, cases),
Db::Fix(body) => infer_fix(genv, lctx, body),
Db::MutualFix { defs, index } => infer_mutual_fix(genv, lctx, defs, *index),
Db::Let(ty, value, body) => {
let _ = infer_sort(genv, lctx, ty)?;
check(genv, lctx, value, ty)?;
infer(genv, lctx, &beta_open(body, value))
}
}
}
fn effective_motive(genv: &Context, lctx: &[Db], motive: &Db, disc_ty: &Db) -> RResult<Db> {
let motive_ty = whnf(genv, &infer(genv, lctx, motive)?);
match &motive_ty {
Db::Pi(dom, cod) => {
if !def_eq(genv, lctx, dom, disc_ty) {
return Err(ReCheckError::ill(format!(
"motive domain {} does not match discriminant type {}",
from_db(dom, lctx.len()),
from_db(disc_ty, lctx.len())
)));
}
let mut ext = lctx.to_vec();
ext.push(disc_ty.clone());
infer_sort(genv, &ext, cod)?;
Ok(motive.clone())
}
Db::Sort(_) => Ok(Db::Lam(Box::new(disc_ty.clone()), Box::new(shift(motive, 1, 0)))),
other => Err(ReCheckError::ill(format!(
"motive is neither a function nor a type (type {})",
from_db(other, lctx.len())
))),
}
}
fn infer_fix(genv: &Context, lctx: &[Db], body: &Db) -> RResult<Db> {
let fix_level = lctx.len();
let mut inner = lctx.to_vec();
inner.push(Db::Sort(Universe::Prop));
let fix_ty_inner = fix_type(genv, &inner, body)?;
let fix_ty = shift(&fix_ty_inner, -1, 0);
check_terminates(genv, body, fix_level)?;
let mut ext = lctx.to_vec();
ext.push(fix_ty.clone());
let _ = infer(genv, &ext, body)?;
Ok(fix_ty)
}
fn infer_mutual_fix(genv: &Context, lctx: &[Db], defs: &[Db], index: usize) -> RResult<Db> {
let n = defs.len();
if n == 0 || index >= n {
return Err(ReCheckError::ill("malformed mutual fixpoint".to_string()));
}
let base = lctx.len();
let mut inner = lctx.to_vec();
for _ in 0..n {
inner.push(Db::Sort(Universe::Prop));
}
let mut types: Vec<Db> = Vec::with_capacity(n);
for body in defs {
let ty_inner = fix_type(genv, &inner, body)?;
types.push(shift(&ty_inner, -(n as isize), 0));
}
check_terminates_mutual(genv, defs, base)?;
let mut ext = lctx.to_vec();
for (j, ty) in types.iter().enumerate() {
ext.push(shift(ty, j as isize, 0));
}
for body in defs {
let _ = infer(genv, &ext, body)?;
}
Ok(types[index].clone())
}
fn fix_type(genv: &Context, lctx: &[Db], body: &Db) -> RResult<Db> {
match body {
Db::Lam(dom, inner) => {
let _ = infer_sort(genv, lctx, dom)?;
let mut ext = lctx.to_vec();
ext.push((**dom).clone());
let inner_ty = fix_type(genv, &ext, inner)?;
Ok(Db::Pi(dom.clone(), Box::new(inner_ty)))
}
Db::Match { disc, motive, .. } => {
let disc_ty = whnf(genv, &infer(genv, lctx, disc)?);
match_return_type(genv, lctx, motive, disc, &disc_ty)
}
other => infer(genv, lctx, other),
}
}
fn level_of(depth: usize, k: usize) -> Option<usize> {
depth.checked_sub(1)?.checked_sub(k)
}
fn count_pi_named(t: &Term) -> usize {
match t {
Term::Pi { body_type, .. } => 1 + count_pi_named(body_type),
_ => 0,
}
}
fn locate_struct<'a>(
genv: &Context,
body: &'a Db,
base_depth: usize,
) -> RResult<(usize, String, &'a Db, usize)> {
let mut chain: Vec<(usize, Option<String>, &Db)> = Vec::new();
let mut cur = body;
let mut depth = base_depth;
while let Db::Lam(dom, inner) = cur {
let dom_h = whnf(genv, dom);
chain.push((depth, extract_inductive(genv, &dom_h).map(|(n, _)| n), inner));
cur = inner;
depth += 1;
}
let scrutinee = match cur {
Db::Match { disc, .. } => match &**disc {
Db::Var(k) => {
level_of(depth, *k).and_then(|lvl| chain.iter().position(|(l, ..)| *l == lvl))
}
_ => None,
},
_ => None,
};
let idx = scrutinee
.filter(|&i| chain[i].1.is_some())
.or_else(|| chain.iter().position(|(_, ind, _)| ind.is_some()))
.ok_or_else(|| {
ReCheckError::ill(
"fixpoint has no inductive parameter for structural recursion".to_string(),
)
})?;
Ok((chain[idx].0, chain[idx].1.clone().unwrap(), chain[idx].2, idx))
}
fn check_terminates(genv: &Context, body: &Db, fix_level: usize) -> RResult<()> {
let (struct_level, ind, guard_body, struct_pos) =
locate_struct(genv, body, fix_level + 1)?;
let mut fix_positions = HashMap::new();
fix_positions.insert(fix_level, struct_pos);
guard(genv, guard_body, &fix_positions, struct_level, &ind, &HashSet::new(), struct_level + 1)
}
fn check_terminates_mutual(genv: &Context, defs: &[Db], base: usize) -> RResult<()> {
let n = defs.len();
let base_depth = base + n;
let mut fix_positions = HashMap::new();
let mut located: Vec<(usize, String, &Db)> = Vec::with_capacity(n);
for (j, body) in defs.iter().enumerate() {
let (struct_level, ind, guard_body, struct_pos) = locate_struct(genv, body, base_depth)?;
fix_positions.insert(base + j, struct_pos);
located.push((struct_level, ind, guard_body));
}
for (struct_level, ind, guard_body) in &located {
guard(
genv,
guard_body,
&fix_positions,
*struct_level,
ind,
&HashSet::new(),
struct_level + 1,
)?;
}
Ok(())
}
fn guard(
genv: &Context,
term: &Db,
fix_positions: &HashMap<usize, usize>,
struct_level: usize,
struct_type: &str,
smaller: &HashSet<usize>,
depth: usize,
) -> RResult<()> {
match term {
Db::App(..) => {
let (head, args) = spine(term);
if let Db::Var(k) = &head {
if let Some(&pos) = level_of(depth, *k).and_then(|lvl| fix_positions.get(&lvl)) {
match args.get(pos) {
Some(arg) => {
let mut h: &Db = arg;
while let Db::App(f, _) = h {
h = f;
}
match h {
Db::Var(j)
if level_of(depth, *j)
.is_some_and(|l| smaller.contains(&l)) => {}
Db::Var(_) => {
return Err(ReCheckError::ill(
"recursive call on an argument not headed by a \
structurally-smaller variable"
.to_string(),
))
}
_ => {
return Err(ReCheckError::ill(
"recursive call whose structural argument is not a \
variable or an application of one — cannot certify it \
decreases"
.to_string(),
))
}
}
}
None => {
return Err(ReCheckError::ill(
"recursive call is missing its structural argument".to_string(),
))
}
}
for a in &args {
guard(genv, a, fix_positions, struct_level, struct_type, smaller, depth)?;
}
return Ok(());
}
}
guard(genv, &head, fix_positions, struct_level, struct_type, smaller, depth)?;
for a in &args {
guard(genv, a, fix_positions, struct_level, struct_type, smaller, depth)?;
}
Ok(())
}
Db::Match { disc, motive, cases } => {
guard(genv, motive, fix_positions, struct_level, struct_type, smaller, depth)?;
if let Db::Var(k) = &**disc {
if level_of(depth, *k) == Some(struct_level) {
return guard_match_cases(
genv, cases, struct_type, fix_positions, struct_level, smaller, depth,
);
}
}
guard(genv, disc, fix_positions, struct_level, struct_type, smaller, depth)?;
for c in cases {
guard(genv, c, fix_positions, struct_level, struct_type, smaller, depth)?;
}
Ok(())
}
Db::Lam(dom, inner) | Db::Pi(dom, inner) => {
guard(genv, dom, fix_positions, struct_level, struct_type, smaller, depth)?;
guard(genv, inner, fix_positions, struct_level, struct_type, smaller, depth + 1)
}
Db::Fix(inner) => {
guard(genv, inner, fix_positions, struct_level, struct_type, smaller, depth + 1)
}
Db::MutualFix { defs, .. } => {
for b in defs {
guard(genv, b, fix_positions, struct_level, struct_type, smaller, depth + defs.len())?;
}
Ok(())
}
Db::Let(ty, value, body) => {
guard(genv, ty, fix_positions, struct_level, struct_type, smaller, depth)?;
guard(genv, value, fix_positions, struct_level, struct_type, smaller, depth)?;
guard(genv, body, fix_positions, struct_level, struct_type, smaller, depth + 1)
}
Db::Var(k) => {
if level_of(depth, *k).is_some_and(|lvl| fix_positions.contains_key(&lvl)) {
Err(ReCheckError::ill(
"recursive name occurs as a first-class value, not applied to a \
structurally-smaller argument"
.to_string(),
))
} else {
Ok(())
}
}
Db::Sort(_) | Db::Global(_) | Db::Const { .. } | Db::Lit(_) => Ok(()),
}
}
fn guard_match_cases(
genv: &Context,
cases: &[Db],
struct_type: &str,
fix_positions: &HashMap<usize, usize>,
struct_level: usize,
smaller: &HashSet<usize>,
depth: usize,
) -> RResult<()> {
let arities: Vec<usize> =
genv.get_constructors(struct_type).iter().map(|(_, t)| count_pi_named(t)).collect();
for (i, case) in cases.iter().enumerate() {
let arity = arities.get(i).copied().unwrap_or(0);
let mut smaller2 = smaller.clone();
let mut cur = case;
let mut d = depth;
for _ in 0..arity {
if let Db::Lam(_, inner) = cur {
smaller2.insert(d);
cur = inner;
d += 1;
} else {
break;
}
}
guard(genv, cur, fix_positions, struct_level, struct_type, &smaller2, d)?;
}
Ok(())
}
fn infer_match(
genv: &Context,
lctx: &[Db],
disc: &Db,
motive: &Db,
cases: &[Db],
) -> RResult<Db> {
let disc_ty = whnf(genv, &infer(genv, lctx, disc)?);
let (ind_name, type_args) = extract_inductive(genv, &disc_ty).ok_or_else(|| {
ReCheckError::unsupported(format!(
"match discriminant of unrecognized type {}",
from_db(&disc_ty, lctx.len())
))
})?;
let p = genv.inductive_num_params(&ind_name).min(type_args.len());
let indexed = type_args.len() > p;
let eff_motive = if indexed {
let _ = infer(genv, lctx, motive)?; None
} else {
Some(effective_motive(genv, lctx, motive, &disc_ty)?)
};
let ctor_names: Vec<String> =
genv.get_constructors(&ind_name).iter().map(|(n, _)| n.to_string()).collect();
if cases.len() != ctor_names.len() {
return Err(ReCheckError::ill(format!(
"match on {} has {} cases but {} constructors",
ind_name,
cases.len(),
ctor_names.len()
)));
}
for (case, cname) in cases.iter().zip(ctor_names.iter()) {
let case_ty = match &eff_motive {
Some(eff) => case_type(genv, eff, cname, &type_args)?,
None => case_type_indexed(genv, motive, cname, &type_args[..p])?,
};
check_case(genv, lctx, case, &case_ty)?;
}
let ret = match_return_type(genv, lctx, motive, disc, &disc_ty)?;
let disc_sort = whnf(genv, &infer(genv, lctx, &disc_ty)?);
if matches!(disc_sort, Db::Sort(Universe::Prop)) {
let ret_sort = whnf(genv, &infer(genv, lctx, &ret)?);
let large = !matches!(ret_sort, Db::Sort(Universe::Prop));
if large && !is_subsingleton(genv, &ind_name)? {
return Err(ReCheckError::ill(format!(
"large elimination of non-subsingleton proposition '{}' into a larger sort",
ind_name
)));
}
}
Ok(ret)
}
fn extract_inductive(genv: &Context, ty: &Db) -> Option<(String, Vec<Db>)> {
let (head, args) = spine(ty);
match head {
Db::Global(name) if genv.is_inductive(&name) => Some((name, args)),
_ => None,
}
}
fn case_type(genv: &Context, motive: &Db, ctor_name: &str, type_args: &[Db]) -> RResult<Db> {
let ctor_named = genv
.get_global(ctor_name)
.ok_or_else(|| ReCheckError::ill(format!("unknown constructor '{}'", ctor_name)))?
.clone();
let ctor_db = to_db(&ctor_named, &mut Vec::new())?;
let mut body = ctor_db;
for ta in type_args {
match body {
Db::Pi(_dom, rest) => body = beta_open(&rest, ta),
_ => {
return Err(ReCheckError::ill(format!(
"constructor '{}' has fewer parameters than the discriminant's type arguments",
ctor_name
)))
}
}
}
let mut value_doms = Vec::new();
let mut cur = body;
while let Db::Pi(dom, rest) = cur {
value_doms.push(*dom);
cur = *rest;
}
let num_val = value_doms.len();
let lift = num_val as isize;
let mut applied = Db::Global(ctor_name.to_string());
for ta in type_args {
applied = Db::App(Box::new(applied), Box::new(shift(ta, lift, 0)));
}
for p in 0..num_val {
applied = Db::App(Box::new(applied), Box::new(Db::Var(num_val - 1 - p)));
}
let motive_bot = shift(motive, lift, 0);
let cod = whnf(genv, &Db::App(Box::new(motive_bot), Box::new(applied)));
let mut case_ty = cod;
for dom in value_doms.into_iter().rev() {
case_ty = Db::Pi(Box::new(dom), Box::new(case_ty));
}
Ok(case_ty)
}
fn match_return_type(genv: &Context, lctx: &[Db], motive: &Db, disc: &Db, disc_ty: &Db) -> RResult<Db> {
let (ind_name, type_args) = extract_inductive(genv, disc_ty).ok_or_else(|| {
ReCheckError::unsupported(format!(
"match discriminant of unrecognized type {}",
from_db(disc_ty, lctx.len())
))
})?;
let p = genv.inductive_num_params(&ind_name).min(type_args.len());
if type_args.len() > p {
let mut ret = motive.clone();
for idx in &type_args[p..] {
ret = Db::App(Box::new(ret), Box::new(idx.clone()));
}
ret = Db::App(Box::new(ret), Box::new(disc.clone()));
Ok(whnf(genv, &ret))
} else {
let eff = effective_motive(genv, lctx, motive, disc_ty)?;
Ok(whnf(genv, &Db::App(Box::new(eff), Box::new(disc.clone()))))
}
}
fn case_type_indexed(genv: &Context, motive: &Db, ctor_name: &str, params: &[Db]) -> RResult<Db> {
let ctor_named = genv
.get_global(ctor_name)
.ok_or_else(|| ReCheckError::ill(format!("unknown constructor '{}'", ctor_name)))?
.clone();
let ctor_db = to_db(&ctor_named, &mut Vec::new())?;
let mut body = ctor_db;
for pa in params {
match body {
Db::Pi(_dom, rest) => body = beta_open(&rest, pa),
_ => {
return Err(ReCheckError::ill(format!(
"constructor '{}' has fewer parameters than the inductive",
ctor_name
)))
}
}
}
let mut value_doms = Vec::new();
let mut cur = body;
while let Db::Pi(dom, rest) = cur {
value_doms.push(*dom);
cur = *rest;
}
let num_val = value_doms.len();
let lift = num_val as isize;
let (_head, res_args) = spine(&cur);
let result_indices: Vec<Db> = res_args.into_iter().skip(params.len()).collect();
let mut applied = Db::Global(ctor_name.to_string());
for pa in params {
applied = Db::App(Box::new(applied), Box::new(shift(pa, lift, 0)));
}
for i in 0..num_val {
applied = Db::App(Box::new(applied), Box::new(Db::Var(num_val - 1 - i)));
}
let mut cod = shift(motive, lift, 0);
for ri in &result_indices {
cod = Db::App(Box::new(cod), Box::new(ri.clone()));
}
cod = Db::App(Box::new(cod), Box::new(applied));
let cod = whnf(genv, &cod);
let mut case_ty = cod;
for dom in value_doms.into_iter().rev() {
case_ty = Db::Pi(Box::new(dom), Box::new(case_ty));
}
Ok(case_ty)
}
fn is_subsingleton(genv: &Context, ind: &str) -> RResult<bool> {
let ctors: Vec<(String, Term)> = genv
.get_constructors(ind)
.iter()
.map(|(n, t)| (n.to_string(), (*t).clone()))
.collect();
match ctors.len() {
0 => Ok(true),
1 => {
let arity = inductive_arity(genv, ind);
let ctor_db = to_db(&ctors[0].1, &mut Vec::new())?;
let mut lctx: Vec<Db> = Vec::new();
let mut cur = ctor_db;
let mut i = 0;
while let Db::Pi(dom, rest) = cur {
if i >= arity && infer_sort(genv, &lctx, &dom)? != Universe::Prop {
return Ok(false);
}
lctx.push(*dom);
cur = *rest;
i += 1;
}
Ok(true)
}
_ => Ok(false),
}
}
fn infer_sort(genv: &Context, lctx: &[Db], t: &Db) -> RResult<Universe> {
let ty = whnf(genv, &infer(genv, lctx, t)?);
match ty {
Db::Sort(u) => Ok(u),
other => Err(ReCheckError::ill(format!(
"expected a type, got something of type {}",
from_db(&other, lctx.len())
))),
}
}
fn check_case(genv: &Context, lctx: &[Db], case: &Db, case_ty: &Db) -> RResult<()> {
match (case, case_ty) {
(Db::Lam(_, body), Db::Pi(dom, cod)) => {
let mut ext = lctx.to_vec();
ext.push((**dom).clone());
check_case(genv, &ext, body, cod)
}
_ => check(genv, lctx, case, case_ty),
}
}
fn check(genv: &Context, lctx: &[Db], t: &Db, expected: &Db) -> RResult<()> {
let inferred = infer(genv, lctx, t)?;
if is_sub(genv, lctx, &inferred, expected) {
Ok(())
} else {
Err(ReCheckError::ill(format!(
"type mismatch: have {}, expected {}",
from_db(&whnf(genv, &inferred), lctx.len()),
from_db(&whnf(genv, expected), lctx.len())
)))
}
}
fn lit_type_name(l: &Literal) -> &'static str {
match l {
Literal::Int(_) | Literal::BigInt(_) => "Int",
Literal::Nat(_) => "Nat",
Literal::Float(_) => "Float",
Literal::Text(_) => "Text",
Literal::Duration(_) => "Duration",
Literal::Date(_) => "Date",
Literal::Moment(_) => "Moment",
}
}
pub fn recheck(ctx: &Context, term: &Term) -> RResult<Term> {
let db = to_db(term, &mut Vec::new())?;
let ty = infer(ctx, &[], &db)?;
Ok(from_db(&ty, 0))
}
#[derive(Debug, Clone, PartialEq)]
pub enum DoubleCheck {
Agreed,
MainOnlyReCheckerIncomplete(String),
Disagree(String),
}
pub fn double_check(ctx: &Context, term: &Term) -> DoubleCheck {
let main = crate::infer_type(ctx, term);
let recheck_result = recheck(ctx, term);
match (main, recheck_result) {
(Ok(main_ty), Ok(re_ty)) => {
match (to_db(&main_ty, &mut Vec::new()), to_db(&re_ty, &mut Vec::new())) {
(Ok(m), Ok(r)) if def_eq(ctx, &[], &m, &r) => DoubleCheck::Agreed,
(Ok(_), Ok(_)) => DoubleCheck::Disagree(format!(
"kernels inferred different types: main={}, recheck={}",
main_ty, re_ty
)),
_ => DoubleCheck::MainOnlyReCheckerIncomplete(
"inferred type outside re-checker fragment".to_string(),
),
}
}
(Ok(_), Err(ReCheckError::Unsupported(why))) => {
DoubleCheck::MainOnlyReCheckerIncomplete(why)
}
(Ok(_), Err(ReCheckError::Ill(why))) => DoubleCheck::Disagree(format!(
"main kernel accepted but re-checker rejected: {}",
why
)),
(Err(e), Ok(_)) => DoubleCheck::Disagree(format!(
"re-checker accepted but main kernel rejected: {:?}",
e
)),
(Err(_), Err(_)) => DoubleCheck::Agreed,
}
}