use crate::context::Context;
use crate::error::{KernelError, KernelResult};
use crate::reduction::normalize;
use crate::term::{Literal, Term, Universe};
pub fn infer_type(ctx: &Context, term: &Term) -> KernelResult<Term> {
match term {
Term::Sort(u) => Ok(Term::Sort(u.succ())),
Term::Var(name) => ctx
.get(name)
.cloned()
.ok_or_else(|| KernelError::UnboundVariable(name.clone())),
Term::Global(name) => ctx
.get_global(name)
.cloned()
.ok_or_else(|| KernelError::UnboundVariable(name.clone())),
Term::Const { name, levels } => {
let (params, ty, _body) = ctx
.get_universe_poly(name)
.ok_or_else(|| KernelError::UnboundVariable(name.clone()))?;
if params.len() != levels.len() {
return Err(KernelError::CertificationError(format!(
"universe-polymorphic '{}' expects {} level argument(s), got {}",
name,
params.len(),
levels.len()
)));
}
let subst: std::collections::HashMap<String, Universe> =
params.iter().cloned().zip(levels.iter().cloned()).collect();
Ok(crate::term::instantiate_universes(&ty.clone(), &subst))
}
Term::Pi {
param,
param_type,
body_type,
} => {
let a_sort = infer_sort(ctx, param_type)?;
let extended_ctx = ctx.extend(param, (**param_type).clone());
let b_sort = infer_sort(&extended_ctx, body_type)?;
let pi_sort = a_sort.imax(&b_sort);
Ok(Term::Sort(pi_sort))
}
Term::Lambda {
param,
param_type,
body,
} => {
let _ = infer_sort(ctx, param_type)?;
let extended_ctx = ctx.extend(param, (**param_type).clone());
let body_type = infer_type(&extended_ctx, body)?;
Ok(Term::Pi {
param: param.clone(),
param_type: param_type.clone(),
body_type: Box::new(body_type),
})
}
Term::Let { name, ty, value, body } => {
let _ = infer_sort(ctx, ty)?;
check_type(ctx, value, ty)?;
let unfolded = substitute(body, name, value);
infer_type(ctx, &unfolded)
}
Term::App(func, arg) => {
let func_type = infer_type(ctx, func)?;
let func_type = match func_type {
Term::Pi { .. } => func_type,
other => normalize(ctx, &other),
};
match func_type {
Term::Pi {
param,
param_type,
body_type,
} => {
check_type(ctx, arg, ¶m_type)?;
Ok(substitute(&body_type, ¶m, arg))
}
_ => Err(KernelError::NotAFunction(format!("{}", func)))
}
}
Term::Match {
discriminant,
motive,
cases,
} => {
let disc_type = normalize(ctx, &infer_type(ctx, discriminant)?);
let inductive_name = extract_inductive_name(ctx, &disc_type)
.ok_or_else(|| KernelError::NotAnInductive(format!("{}", disc_type)))?;
let disc_args = extract_type_args(&disc_type);
let num_params = ctx.inductive_num_params(&inductive_name).min(disc_args.len());
if disc_args.len() > num_params {
return infer_indexed_match(
ctx,
discriminant,
motive,
cases,
&disc_type,
&inductive_name,
num_params,
&disc_args,
);
}
let motive_type = infer_type(ctx, motive)?;
let effective_motive = match &motive_type {
Term::Pi {
param_type,
body_type,
..
} => {
if !types_equal(param_type, &disc_type) {
return Err(KernelError::InvalidMotive(format!(
"motive parameter {} doesn't match discriminant type {}",
param_type, disc_type
)));
}
match infer_type(ctx, body_type) {
Ok(Term::Sort(_)) => {}
_ => {
return Err(KernelError::InvalidMotive(format!(
"motive body {} is not a type",
body_type
)));
}
}
(**motive).clone()
}
Term::Sort(_) => {
Term::Lambda {
param: "_".to_string(),
param_type: Box::new(disc_type.clone()),
body: motive.clone(),
}
}
_ => {
return Err(KernelError::InvalidMotive(format!(
"motive {} is not a function or type",
motive
)));
}
};
let constructors = ctx.get_constructors(&inductive_name);
if cases.len() != constructors.len() {
return Err(KernelError::WrongNumberOfCases {
expected: constructors.len(),
found: cases.len(),
});
}
for (case, (ctor_name, ctor_type)) in cases.iter().zip(constructors.iter()) {
let expected_case_type = compute_case_type(&effective_motive, ctor_name, ctor_type, &disc_type);
check_type(ctx, case, &expected_case_type)?;
}
let return_type = beta_reduce(&Term::App(Box::new(effective_motive), discriminant.clone()));
if matches!(normalize(ctx, &infer_type(ctx, &disc_type)?), Term::Sort(Universe::Prop)) {
let large = !matches!(normalize(ctx, &infer_type(ctx, &return_type)?), Term::Sort(Universe::Prop));
if large && !is_subsingleton_prop(ctx, &inductive_name)? {
return Err(KernelError::InvalidMotive(format!(
"large elimination of proposition '{}' into a larger sort is not allowed: only \
subsingleton propositions (empty, or one constructor with propositional arguments \
— e.g. False, And, eq) may be eliminated into Type",
inductive_name
)));
}
}
Ok(return_type)
}
Term::Lit(lit) => {
match lit {
Literal::Int(_) | Literal::BigInt(_) => Ok(Term::Global("Int".to_string())),
Literal::Nat(n) if *n < logicaffeine_base::BigInt::from_i64(0) => {
Err(KernelError::CertificationError(
"a `Nat` literal must be non-negative".to_string(),
))
}
Literal::Nat(_) => Ok(Term::Global("Nat".to_string())),
Literal::Float(_) => Ok(Term::Global("Float".to_string())),
Literal::Text(_) => Ok(Term::Global("Text".to_string())),
Literal::Duration(_) => Ok(Term::Global("Duration".to_string())),
Literal::Date(_) => Ok(Term::Global("Date".to_string())),
Literal::Moment(_) => Ok(Term::Global("Moment".to_string())),
}
}
Term::Hole => Err(KernelError::CannotInferHole),
Term::Fix { name, body } => {
let structural_type = infer_fix_type_structurally(ctx, body)?;
crate::termination::check_termination(ctx, name, body)?;
let extended = ctx.extend(name, structural_type.clone());
let _ = infer_type(&extended, body)?;
Ok(structural_type)
}
Term::MutualFix { defs, index } => {
if defs.is_empty() || *index >= defs.len() {
return Err(KernelError::CertificationError(
"mutual fixpoint with an empty block or out-of-range index".to_string(),
));
}
let mut types = Vec::with_capacity(defs.len());
for (_, body) in defs {
types.push(infer_fix_type_structurally(ctx, body)?);
}
crate::termination::check_termination_mutual(ctx, defs)?;
let mut extended = ctx.clone();
for ((name, _), ty) in defs.iter().zip(types.iter()) {
extended = extended.extend(name, ty.clone());
}
for (_, body) in defs {
let _ = infer_type(&extended, body)?;
}
Ok(types[*index].clone())
}
}
}
fn infer_fix_type_structurally(ctx: &Context, term: &Term) -> KernelResult<Term> {
match term {
Term::Lambda {
param,
param_type,
body,
} => {
let _ = infer_sort(ctx, param_type)?;
let extended = ctx.extend(param, (**param_type).clone());
let body_type = infer_fix_type_structurally(&extended, body)?;
Ok(Term::Pi {
param: param.clone(),
param_type: param_type.clone(),
body_type: Box::new(body_type),
})
}
Term::Match { discriminant, motive, .. } => {
if let Ok(mt) = infer_type(ctx, motive) {
if matches!(normalize(ctx, &mt), Term::Sort(_)) {
return Ok(normalize(ctx, motive));
}
}
let mut applied = (**motive).clone();
if let Ok(dt) = infer_type(ctx, discriminant) {
let dt = normalize(ctx, &dt);
if let Some(ind) = extract_inductive_name(ctx, &dt) {
let args = extract_type_args(&dt);
let p = ctx.inductive_num_params(&ind).min(args.len());
for idx in &args[p..] {
applied = Term::App(Box::new(applied), Box::new(idx.clone()));
}
}
}
applied = Term::App(Box::new(applied), discriminant.clone());
Ok(normalize(ctx, &applied))
}
_ => infer_type(ctx, term),
}
}
fn check_type(ctx: &Context, term: &Term, expected: &Term) -> KernelResult<()> {
if matches!(term, Term::Hole) {
if matches!(expected, Term::Sort(_)) {
return Ok(());
}
return Err(KernelError::TypeMismatch {
expected: format!("{}", expected),
found: "_".to_string(),
});
}
if matches!(expected, Term::Hole) {
let _ = infer_type(ctx, term)?; return Ok(());
}
if let Term::Lambda {
param,
param_type,
body,
} = term
{
if let Term::Global(name) = param_type.as_ref() {
if name == "_" {
if let Term::Pi {
param_type: expected_param_type,
body_type: expected_body_type,
param: expected_param,
} = expected
{
let extended_ctx = ctx.extend(param, (**expected_param_type).clone());
let body_expected = if param != expected_param {
substitute(expected_body_type, expected_param, &Term::Var(param.clone()))
} else {
(**expected_body_type).clone()
};
return check_type(&extended_ctx, body, &body_expected);
}
}
}
}
let inferred = infer_type(ctx, term)?;
if is_subtype(ctx, &inferred, expected) {
Ok(())
} else {
Err(KernelError::TypeMismatch {
expected: format!("{}", expected),
found: format!("{}", inferred),
})
}
}
fn infer_sort(ctx: &Context, term: &Term) -> KernelResult<Universe> {
let ty = infer_type(ctx, term)?;
match ty {
Term::Sort(u) => Ok(u),
_ => Err(KernelError::NotAType(format!("{}", term))),
}
}
fn result_sort_universe(t: &Term) -> Option<Universe> {
let mut cur = t;
while let Term::Pi { body_type, .. } = cur {
cur = body_type;
}
match cur {
Term::Sort(u) => Some(u.clone()),
_ => None,
}
}
pub fn check_constructor_universes(
ctx: &Context,
ind: &str,
ctor: &str,
ty: &Term,
) -> KernelResult<()> {
let ind_ty = match ctx.get_global(ind) {
Some(t) => t.clone(),
None => return Ok(()),
};
let target = match result_sort_universe(&ind_ty) {
Some(Universe::Prop) => return Ok(()),
Some(u) => u,
None => return Ok(()),
};
let num_params = ctx.inductive_num_params(ind);
let mut ext = ctx.clone();
let mut cur = ty;
let mut i = 0usize;
while let Term::Pi { param, param_type, body_type } = cur {
if i >= num_params {
let s = infer_sort(&ext, param_type)?;
if !s.is_subtype_of(&target) {
return Err(KernelError::CertificationError(format!(
"universe inconsistency: constructor '{ctor}' stores an argument in sort \
{s}, which exceeds the sort {target} of its inductive '{ind}'"
)));
}
}
ext = ext.extend(param, (**param_type).clone());
cur = body_type;
i += 1;
}
Ok(())
}
fn beta_reduce(term: &Term) -> Term {
match term {
Term::App(func, arg) => {
match func.as_ref() {
Term::Lambda { param, body, .. } => {
substitute(body, param, arg)
}
_ => term.clone(),
}
}
_ => term.clone(),
}
}
#[allow(clippy::too_many_arguments)]
fn infer_indexed_match(
ctx: &Context,
discriminant: &Term,
motive: &Term,
cases: &[Term],
disc_type: &Term,
inductive_name: &str,
num_params: usize,
disc_args: &[Term],
) -> KernelResult<Term> {
let disc_params = &disc_args[0..num_params];
let disc_indices = &disc_args[num_params..];
let _ = infer_type(ctx, motive)?;
let constructors = ctx.get_constructors(inductive_name);
if cases.len() != constructors.len() {
return Err(KernelError::WrongNumberOfCases {
expected: constructors.len(),
found: cases.len(),
});
}
for (case, (ctor_name, ctor_type)) in cases.iter().zip(constructors.iter()) {
let expected = compute_indexed_case_type(motive, ctor_name, ctor_type, num_params, disc_params);
check_type(ctx, case, &expected)?;
}
let mut ret = motive.clone();
for idx in disc_indices {
ret = Term::App(Box::new(ret), Box::new(idx.clone()));
}
ret = Term::App(Box::new(ret), Box::new(discriminant.clone()));
let ret = normalize(ctx, &ret);
match normalize(ctx, &infer_type(ctx, &ret)?) {
Term::Sort(_) => {}
other => {
return Err(KernelError::InvalidMotive(format!(
"indexed match on '{}' has non-type result {} — the motive is not a family into a sort",
inductive_name, other
)));
}
}
if matches!(normalize(ctx, &infer_type(ctx, disc_type)?), Term::Sort(Universe::Prop)) {
let large = !matches!(normalize(ctx, &infer_type(ctx, &ret)?), Term::Sort(Universe::Prop));
if large && !is_subsingleton_prop(ctx, inductive_name)? {
return Err(KernelError::InvalidMotive(format!(
"large elimination of proposition '{}' into a larger sort is not allowed",
inductive_name
)));
}
}
Ok(ret)
}
fn compute_indexed_case_type(
motive: &Term,
ctor_name: &str,
ctor_type: &Term,
num_params: usize,
disc_params: &[Term],
) -> Term {
let mut all_params: Vec<(String, Term)> = Vec::new();
let mut current = ctor_type;
while let Term::Pi { param, param_type, body_type } = current {
all_params.push((param.clone(), (**param_type).clone()));
current = body_type;
}
let result_args = extract_type_args(current);
let split = num_params.min(all_params.len());
let param_binders = &all_params[0..split];
let value_named: Vec<(String, String, Term)> = all_params[split..]
.iter()
.enumerate()
.map(|(i, (orig, ty))| (orig.clone(), format!("__arg{}", i), ty.clone()))
.collect();
let rewrite = |t: &Term, upto: usize| -> Term {
let mut out = t.clone();
for (i, (name, _)) in param_binders.iter().enumerate() {
if name != "_" {
out = substitute(&out, name, &disc_params[i]);
}
}
for (orig, fresh, _) in value_named.iter().take(upto) {
if orig != "_" {
out = substitute(&out, orig, &Term::Var(fresh.clone()));
}
}
out
};
let index_exprs: Vec<Term> = result_args
.iter()
.skip(split)
.map(|e| beta_reduce(&rewrite(e, value_named.len())))
.collect();
let mut ctor_applied = Term::Global(ctor_name.to_string());
for pa in disc_params {
ctor_applied = Term::App(Box::new(ctor_applied), Box::new(pa.clone()));
}
for (_, fresh, _) in &value_named {
ctor_applied = Term::App(Box::new(ctor_applied), Box::new(Term::Var(fresh.clone())));
}
let mut body = motive.clone();
for e in &index_exprs {
body = Term::App(Box::new(body), Box::new(e.clone()));
}
body = Term::App(Box::new(body), Box::new(ctor_applied));
let mut case_type = beta_reduce(&body);
for k in (0..value_named.len()).rev() {
let (_, fresh, ty_k) = &value_named[k];
let pty = beta_reduce(&rewrite(ty_k, k));
case_type = Term::Pi {
param: fresh.clone(),
param_type: Box::new(pty),
body_type: Box::new(case_type),
};
}
case_type
}
fn compute_case_type(motive: &Term, ctor_name: &str, ctor_type: &Term, disc_type: &Term) -> Term {
let type_args = extract_type_args(disc_type);
let num_type_args = type_args.len();
let mut all_params: Vec<(String, Term)> = Vec::new();
let mut current = ctor_type;
while let Term::Pi {
param,
param_type,
body_type,
} = current
{
all_params.push((param.clone(), (**param_type).clone()));
current = body_type;
}
let type_params: Vec<(String, Term)> = all_params
.iter()
.take(num_type_args)
.map(|(n, t)| (n.clone(), t.clone()))
.collect();
let value_named: Vec<(String, String, Term)> = all_params
.into_iter()
.skip(num_type_args)
.enumerate()
.map(|(i, (orig, ty))| (orig, format!("__arg{}", i), ty))
.collect();
let mut ctor_applied = Term::Global(ctor_name.to_string());
for type_arg in &type_args {
ctor_applied = Term::App(Box::new(ctor_applied), Box::new(type_arg.clone()));
}
for (_, new_name, _) in &value_named {
ctor_applied = Term::App(Box::new(ctor_applied), Box::new(Term::Var(new_name.clone())));
}
let result_type = beta_reduce(&Term::App(Box::new(motive.clone()), Box::new(ctor_applied)));
let mut case_type = result_type;
for k in (0..value_named.len()).rev() {
let (_, new_name, ty_k) = &value_named[k];
let mut pty = ty_k.clone();
for ((tp_name, _), type_arg) in type_params.iter().zip(type_args.iter()) {
pty = substitute(&pty, tp_name, type_arg);
}
for (orig_j, new_j, _) in value_named.iter().take(k) {
pty = substitute(&pty, orig_j, &Term::Var(new_j.clone()));
}
let pty = beta_reduce(&pty);
case_type = Term::Pi {
param: new_name.clone(),
param_type: Box::new(pty),
body_type: Box::new(case_type),
};
}
case_type
}
fn extract_type_args(ty: &Term) -> Vec<Term> {
let mut args = Vec::new();
let mut current = ty;
while let Term::App(func, arg) = current {
args.push((**arg).clone());
current = func;
}
args.reverse();
args
}
pub fn substitute(body: &Term, var: &str, replacement: &Term) -> Term {
if !occurs_free(body, var) {
return body.clone();
}
let replacement_fvs = free_vars(replacement);
substitute_avoiding(body, var, replacement, &replacement_fvs)
}
fn occurs_free(term: &Term, var: &str) -> bool {
match term {
Term::Var(name) => name == var,
Term::Sort(_) | Term::Lit(_) | Term::Hole | Term::Global(_) | Term::Const { .. } => false,
Term::App(func, arg) => occurs_free(func, var) || occurs_free(arg, var),
Term::Pi { param, param_type, body_type } => {
occurs_free(param_type, var) || (param != var && occurs_free(body_type, var))
}
Term::Lambda { param, param_type, body } => {
occurs_free(param_type, var) || (param != var && occurs_free(body, var))
}
Term::Fix { name, body } => name != var && occurs_free(body, var),
Term::MutualFix { defs, .. } => {
!defs.iter().any(|(n, _)| n == var) && defs.iter().any(|(_, b)| occurs_free(b, var))
}
Term::Let { name, ty, value, body } => {
occurs_free(ty, var)
|| occurs_free(value, var)
|| (name != var && occurs_free(body, var))
}
Term::Match { discriminant, motive, cases } => {
occurs_free(discriminant, var)
|| occurs_free(motive, var)
|| cases.iter().any(|c| occurs_free(c, var))
}
}
}
fn free_vars(term: &Term) -> std::collections::HashSet<String> {
fn go(term: &Term, bound: &mut Vec<String>, acc: &mut std::collections::HashSet<String>) {
match term {
Term::Var(name) => {
if !bound.iter().any(|b| b == name) {
acc.insert(name.clone());
}
}
Term::Sort(_) | Term::Lit(_) | Term::Hole | Term::Global(_) | Term::Const { .. } => {}
Term::App(func, arg) => {
go(func, bound, acc);
go(arg, bound, acc);
}
Term::Pi { param, param_type, body_type } => {
go(param_type, bound, acc);
bound.push(param.clone());
go(body_type, bound, acc);
bound.pop();
}
Term::Lambda { param, param_type, body } => {
go(param_type, bound, acc);
bound.push(param.clone());
go(body, bound, acc);
bound.pop();
}
Term::Fix { name, body } => {
bound.push(name.clone());
go(body, bound, acc);
bound.pop();
}
Term::MutualFix { defs, .. } => {
for (n, _) in defs {
bound.push(n.clone());
}
for (_, b) in defs {
go(b, bound, acc);
}
for _ in defs {
bound.pop();
}
}
Term::Let { name, ty, value, body } => {
go(ty, bound, acc);
go(value, bound, acc);
bound.push(name.clone());
go(body, bound, acc);
bound.pop();
}
Term::Match { discriminant, motive, cases } => {
go(discriminant, bound, acc);
go(motive, bound, acc);
for c in cases {
go(c, bound, acc);
}
}
}
}
let mut acc = std::collections::HashSet::new();
let mut bound = Vec::new();
go(term, &mut bound, &mut acc);
acc
}
fn all_var_names(term: &Term, acc: &mut std::collections::HashSet<String>) {
match term {
Term::Var(name) => {
acc.insert(name.clone());
}
Term::Sort(_) | Term::Lit(_) | Term::Hole | Term::Global(_) | Term::Const { .. } => {}
Term::App(func, arg) => {
all_var_names(func, acc);
all_var_names(arg, acc);
}
Term::Pi { param, param_type, body_type } => {
acc.insert(param.clone());
all_var_names(param_type, acc);
all_var_names(body_type, acc);
}
Term::Lambda { param, param_type, body } => {
acc.insert(param.clone());
all_var_names(param_type, acc);
all_var_names(body, acc);
}
Term::Fix { name, body } => {
acc.insert(name.clone());
all_var_names(body, acc);
}
Term::MutualFix { defs, .. } => {
for (n, b) in defs {
acc.insert(n.clone());
all_var_names(b, acc);
}
}
Term::Let { name, ty, value, body } => {
acc.insert(name.clone());
all_var_names(ty, acc);
all_var_names(value, acc);
all_var_names(body, acc);
}
Term::Match { discriminant, motive, cases } => {
all_var_names(discriminant, acc);
all_var_names(motive, acc);
for c in cases {
all_var_names(c, acc);
}
}
}
}
fn fresh_name(base: &str, avoid: &std::collections::HashSet<String>) -> String {
let mut candidate = format!("{}'", base);
let mut counter: u32 = 0;
while avoid.contains(&candidate) {
counter += 1;
candidate = format!("{}'{}", base, counter);
}
candidate
}
fn freshen(
param: &str,
body: &Term,
replacement_fvs: &std::collections::HashSet<String>,
var: &str,
) -> String {
let mut avoid = replacement_fvs.clone();
all_var_names(body, &mut avoid);
avoid.insert(var.to_string());
fresh_name(param, &avoid)
}
fn rename_var(term: &Term, from: &str, to: &str) -> Term {
let repl = Term::Var(to.to_string());
let mut fvs = std::collections::HashSet::new();
fvs.insert(to.to_string());
substitute_avoiding(term, from, &repl, &fvs)
}
fn substitute_avoiding(
body: &Term,
var: &str,
replacement: &Term,
replacement_fvs: &std::collections::HashSet<String>,
) -> Term {
match body {
Term::Sort(u) => Term::Sort(u.clone()),
Term::Const { .. } => body.clone(),
Term::Lit(lit) => Term::Lit(lit.clone()),
Term::Hole => Term::Hole,
Term::Var(name) if name == var => replacement.clone(),
Term::Var(name) => Term::Var(name.clone()),
Term::Global(name) => Term::Global(name.clone()),
Term::Pi {
param,
param_type,
body_type,
} => {
let new_param_type = substitute_avoiding(param_type, var, replacement, replacement_fvs);
if param == var {
Term::Pi {
param: param.clone(),
param_type: Box::new(new_param_type),
body_type: (*body_type).clone(),
}
} else if replacement_fvs.contains(param) {
let fresh = freshen(param, body_type, replacement_fvs, var);
let renamed = rename_var(body_type, param, &fresh);
Term::Pi {
param: fresh,
param_type: Box::new(new_param_type),
body_type: Box::new(substitute_avoiding(&renamed, var, replacement, replacement_fvs)),
}
} else {
Term::Pi {
param: param.clone(),
param_type: Box::new(new_param_type),
body_type: Box::new(substitute_avoiding(body_type, var, replacement, replacement_fvs)),
}
}
}
Term::Lambda {
param,
param_type,
body,
} => {
let new_param_type = substitute_avoiding(param_type, var, replacement, replacement_fvs);
if param == var {
Term::Lambda {
param: param.clone(),
param_type: Box::new(new_param_type),
body: (*body).clone(),
}
} else if replacement_fvs.contains(param) {
let fresh = freshen(param, body, replacement_fvs, var);
let renamed = rename_var(body, param, &fresh);
Term::Lambda {
param: fresh,
param_type: Box::new(new_param_type),
body: Box::new(substitute_avoiding(&renamed, var, replacement, replacement_fvs)),
}
} else {
Term::Lambda {
param: param.clone(),
param_type: Box::new(new_param_type),
body: Box::new(substitute_avoiding(body, var, replacement, replacement_fvs)),
}
}
}
Term::App(func, arg) => Term::App(
Box::new(substitute_avoiding(func, var, replacement, replacement_fvs)),
Box::new(substitute_avoiding(arg, var, replacement, replacement_fvs)),
),
Term::Match {
discriminant,
motive,
cases,
} => Term::Match {
discriminant: Box::new(substitute_avoiding(discriminant, var, replacement, replacement_fvs)),
motive: Box::new(substitute_avoiding(motive, var, replacement, replacement_fvs)),
cases: cases
.iter()
.map(|c| substitute_avoiding(c, var, replacement, replacement_fvs))
.collect(),
},
Term::Fix { name, body } => {
if name == var {
Term::Fix {
name: name.clone(),
body: body.clone(),
}
} else if replacement_fvs.contains(name) {
let fresh = freshen(name, body, replacement_fvs, var);
let renamed = rename_var(body, name, &fresh);
Term::Fix {
name: fresh,
body: Box::new(substitute_avoiding(&renamed, var, replacement, replacement_fvs)),
}
} else {
Term::Fix {
name: name.clone(),
body: Box::new(substitute_avoiding(body, var, replacement, replacement_fvs)),
}
}
}
Term::MutualFix { defs, index } => {
if defs.iter().any(|(n, _)| n == var) {
return body.clone();
}
let mut names: Vec<String> = defs.iter().map(|(n, _)| n.clone()).collect();
let mut bodies: Vec<Term> = defs.iter().map(|(_, b)| b.clone()).collect();
for i in 0..names.len() {
if replacement_fvs.contains(&names[i]) {
let mut avoid = replacement_fvs.clone();
for b in &bodies {
all_var_names(b, &mut avoid);
}
for n in &names {
avoid.insert(n.clone());
}
avoid.insert(var.to_string());
let fresh = fresh_name(&names[i], &avoid);
for b in bodies.iter_mut() {
*b = rename_var(b, &names[i], &fresh);
}
names[i] = fresh;
}
}
let new_defs = names
.into_iter()
.zip(bodies.iter())
.map(|(n, b)| (n, substitute_avoiding(b, var, replacement, replacement_fvs)))
.collect();
Term::MutualFix { defs: new_defs, index: *index }
}
Term::Let { name, ty, value, body } => {
let new_ty = substitute_avoiding(ty, var, replacement, replacement_fvs);
let new_value = substitute_avoiding(value, var, replacement, replacement_fvs);
if name == var {
Term::Let {
name: name.clone(),
ty: Box::new(new_ty),
value: Box::new(new_value),
body: body.clone(),
}
} else if replacement_fvs.contains(name) {
let fresh = freshen(name, body, replacement_fvs, var);
let renamed = rename_var(body, name, &fresh);
Term::Let {
name: fresh,
ty: Box::new(new_ty),
value: Box::new(new_value),
body: Box::new(substitute_avoiding(&renamed, var, replacement, replacement_fvs)),
}
} else {
Term::Let {
name: name.clone(),
ty: Box::new(new_ty),
value: Box::new(new_value),
body: Box::new(substitute_avoiding(body, var, replacement, replacement_fvs)),
}
}
}
}
}
pub fn is_subtype(ctx: &Context, a: &Term, b: &Term) -> bool {
if types_equal(a, b) {
return true;
}
let a_norm = normalize(ctx, a);
let b_norm = normalize(ctx, b);
is_subtype_normalized(ctx, &a_norm, &b_norm)
}
fn is_subtype_normalized(ctx: &Context, a: &Term, b: &Term) -> bool {
match (a, b) {
(Term::Sort(u1), Term::Sort(u2)) => u1.is_subtype_of(u2),
(
Term::Pi {
param: p1,
param_type: t1,
body_type: b1,
},
Term::Pi {
param: p2,
param_type: t2,
body_type: b2,
},
) => {
is_subtype_normalized(ctx, t2, t1) && {
let ext = ctx.extend(p1, (**t1).clone());
let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
is_subtype_normalized(&ext, b1, &b2_renamed)
}
}
_ => def_eq_normalized(ctx, a, b),
}
}
pub(crate) fn def_eq(ctx: &Context, a: &Term, b: &Term) -> bool {
if types_equal(a, b) {
return true;
}
let a_norm = normalize(ctx, a);
let b_norm = normalize(ctx, b);
def_eq_normalized(ctx, &a_norm, &b_norm)
}
fn spine_of(t: &Term) -> (&Term, Vec<&Term>) {
let mut args = Vec::new();
let mut head = t;
while let Term::App(f, a) = head {
args.push(a.as_ref());
head = f;
}
args.reverse();
(head, args)
}
fn try_structure_eta(ctx: &Context, mk_term: &Term, other: &Term) -> Option<bool> {
let (head, args) = spine_of(mk_term);
let Term::Global(hname) = head else { return None };
let (_sname, info) = ctx.struct_of_constructor(hname)?;
let nfields = info.projections.len();
if args.len() != info.num_params + nfields {
return None;
}
let (ohead, _) = spine_of(other);
if matches!(ohead, Term::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 proj_applied = Term::Global(proj.clone());
for p in params {
proj_applied = Term::App(Box::new(proj_applied), Box::new((*p).clone()));
}
proj_applied = Term::App(Box::new(proj_applied), Box::new(other.clone()));
def_eq(ctx, field_args[i], &proj_applied)
}))
}
fn is_nat_peano_headed(t: &Term) -> bool {
match t {
Term::Global(n) => n == "Zero",
Term::App(f, _) => matches!(f.as_ref(), Term::Global(n) if n == "Succ"),
_ => false,
}
}
fn nat_peano_step(t: &Term) -> Term {
match t {
Term::Lit(Literal::Nat(n)) if *n <= logicaffeine_base::BigInt::from_i64(0) => {
Term::Global("Zero".to_string())
}
Term::Lit(Literal::Nat(n)) => Term::App(
Box::new(Term::Global("Succ".to_string())),
Box::new(Term::Lit(Literal::Nat(n.sub(&logicaffeine_base::BigInt::from_i64(1))))),
),
other => other.clone(),
}
}
fn def_eq_normalized(ctx: &Context, a: &Term, b: &Term) -> bool {
if types_equal(a, b) {
return true;
}
if let Term::Lambda { param, param_type, body } = a {
if !matches!(b, Term::Lambda { .. }) {
let ext = ctx.extend(param, (**param_type).clone());
let bx = normalize(ctx, &Term::App(Box::new(b.clone()), Box::new(Term::Var(param.clone()))));
return def_eq_normalized(&ext, body, &bx);
}
}
if let Term::Lambda { param, param_type, body } = b {
if !matches!(a, Term::Lambda { .. }) {
let ext = ctx.extend(param, (**param_type).clone());
let ax = normalize(ctx, &Term::App(Box::new(a.clone()), Box::new(Term::Var(param.clone()))));
return def_eq_normalized(&ext, &ax, body);
}
}
if let Some(eq) = try_structure_eta(ctx, a, b) {
return eq;
}
if let Some(eq) = try_structure_eta(ctx, b, a) {
return eq;
}
match (a, b) {
(Term::Lit(Literal::Nat(x)), Term::Lit(Literal::Nat(y))) => return x == y,
(Term::Lit(Literal::Nat(_)), _) if is_nat_peano_headed(b) => {
return def_eq_normalized(ctx, &nat_peano_step(a), b);
}
(_, Term::Lit(Literal::Nat(_))) if is_nat_peano_headed(a) => {
return def_eq_normalized(ctx, a, &nat_peano_step(b));
}
_ => {}
}
let congruent = match (a, b) {
(Term::Sort(u1), Term::Sort(u2)) => u1.equiv(u2),
(
Term::Pi { param: p1, param_type: t1, body_type: b1 },
Term::Pi { param: p2, param_type: t2, body_type: b2 },
) => {
def_eq_normalized(ctx, t1, t2) && {
let ext = ctx.extend(p1, (**t1).clone());
let b2r = substitute(b2, p2, &Term::Var(p1.clone()));
def_eq_normalized(&ext, b1, &b2r)
}
}
(
Term::Lambda { param: p1, param_type: t1, body: b1 },
Term::Lambda { param: p2, param_type: t2, body: b2 },
) => {
def_eq_normalized(ctx, t1, t2) && {
let ext = ctx.extend(p1, (**t1).clone());
let b2r = substitute(b2, p2, &Term::Var(p1.clone()));
def_eq_normalized(&ext, b1, &b2r)
}
}
(Term::App(f1, a1), Term::App(f2, a2)) => {
def_eq_normalized(ctx, f1, f2) && def_eq_normalized(ctx, a1, a2)
}
(
Term::Match { discriminant: d1, motive: m1, cases: c1 },
Term::Match { discriminant: d2, motive: m2, cases: c2 },
) => {
def_eq_normalized(ctx, d1, d2)
&& def_eq_normalized(ctx, m1, m2)
&& c1.len() == c2.len()
&& c1.iter().zip(c2.iter()).all(|(x, y)| def_eq_normalized(ctx, x, y))
}
(Term::Fix { name: n1, body: b1 }, Term::Fix { name: n2, body: b2 }) => {
let b2r = substitute(b2, n2, &Term::Var(n1.clone()));
def_eq_normalized(ctx, b1, &b2r)
}
_ => false,
};
if congruent {
return true;
}
proof_irrelevant(ctx, a, b)
}
fn proof_irrelevant(ctx: &Context, a: &Term, b: &Term) -> bool {
let ta = match infer_type(ctx, a) {
Ok(t) => t,
Err(_) => return false,
};
match infer_type(ctx, &ta) {
Ok(sort)
if matches!(
normalize(ctx, &sort),
Term::Sort(Universe::Prop) | Term::Sort(Universe::SProp)
) => {}
_ => return false,
}
match infer_type(ctx, b) {
Ok(tb) => def_eq(ctx, &ta, &tb),
Err(_) => false,
}
}
fn extract_inductive_name(ctx: &Context, ty: &Term) -> Option<String> {
match ty {
Term::Global(name) if ctx.is_inductive(name) => Some(name.clone()),
Term::App(func, _) => extract_inductive_name(ctx, func),
_ => None,
}
}
fn types_equal(a: &Term, b: &Term) -> bool {
if matches!(a, Term::Hole) || matches!(b, Term::Hole) {
return true;
}
match (a, b) {
(Term::Sort(u1), Term::Sort(u2)) => u1.equiv(u2),
(Term::Lit(l1), Term::Lit(l2)) => l1 == l2,
(Term::Var(n1), Term::Var(n2)) => n1 == n2,
(Term::Global(n1), Term::Global(n2)) => n1 == n2,
(
Term::Pi {
param: p1,
param_type: t1,
body_type: b1,
},
Term::Pi {
param: p2,
param_type: t2,
body_type: b2,
},
) => {
types_equal(t1, t2) && {
let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
types_equal(b1, &b2_renamed)
}
}
(
Term::Lambda {
param: p1,
param_type: t1,
body: b1,
},
Term::Lambda {
param: p2,
param_type: t2,
body: b2,
},
) => {
types_equal(t1, t2) && {
let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
types_equal(b1, &b2_renamed)
}
}
(Term::App(f1, a1), Term::App(f2, a2)) => types_equal(f1, f2) && types_equal(a1, a2),
(
Term::Match {
discriminant: d1,
motive: m1,
cases: c1,
},
Term::Match {
discriminant: d2,
motive: m2,
cases: c2,
},
) => {
types_equal(d1, d2)
&& types_equal(m1, m2)
&& c1.len() == c2.len()
&& c1.iter().zip(c2.iter()).all(|(a, b)| types_equal(a, b))
}
(
Term::Fix {
name: n1,
body: b1,
},
Term::Fix {
name: n2,
body: b2,
},
) => {
let b2_renamed = substitute(b2, n2, &Term::Var(n1.clone()));
types_equal(b1, &b2_renamed)
}
_ => false,
}
}
pub(crate) fn is_subsingleton_prop(ctx: &Context, inductive_name: &str) -> KernelResult<bool> {
let ctors = ctx.get_constructors(inductive_name);
match ctors.len() {
0 => Ok(true),
1 => {
let ctor_type = ctors[0].1.clone();
let ind_type = ctx
.get_global(inductive_name)
.cloned()
.ok_or_else(|| KernelError::UnboundVariable(inductive_name.to_string()))?;
let arity = pi_param_count(&ind_type);
let mut local = ctx.clone();
let mut t = ctor_type;
let mut i = 0;
while let Term::Pi { param, param_type, body_type } = t {
if i >= arity && infer_sort(&local, ¶m_type)? != Universe::Prop {
return Ok(false);
}
local = local.extend(¶m, (*param_type).clone());
t = *body_type;
i += 1;
}
Ok(true)
}
_ => Ok(false),
}
}
fn pi_param_count(ty: &Term) -> usize {
match ty {
Term::Pi { body_type, .. } => 1 + pi_param_count(body_type),
_ => 0,
}
}
#[cfg(test)]
mod large_elim_tests {
use super::*;
use crate::context::Context;
use crate::prelude::StandardLibrary;
use crate::term::{Term, Universe};
fn g(s: &str) -> Term { Term::Global(s.to_string()) }
fn app(f: Term, x: Term) -> Term { Term::App(Box::new(f), Box::new(x)) }
fn lam(p: &str, ty: Term, body: Term) -> Term {
Term::Lambda { param: p.to_string(), param_type: Box::new(ty), body: Box::new(body) }
}
fn or_tt() -> Term { app(app(g("Or"), g("True")), g("True")) }
#[test]
fn large_elimination_of_or_into_type_is_rejected() {
let mut ctx = Context::new();
StandardLibrary::register(&mut ctx);
let case = lam("_", g("True"), g("Zero")); let m = Term::Match {
discriminant: Box::new(Term::Var("h".to_string())),
motive: Box::new(lam("_", or_tt(), g("Nat"))), cases: vec![case.clone(), case],
};
let term = lam("h", or_tt(), m);
assert!(
infer_type(&ctx, &term).is_err(),
"large elimination of Or (2 constructors) into Type must be rejected for consistency"
);
}
#[test]
fn ex_falso_large_elimination_of_false_still_allowed() {
let mut ctx = Context::new();
StandardLibrary::register(&mut ctx);
let m = Term::Match {
discriminant: Box::new(Term::Var("h".to_string())),
motive: Box::new(lam("_", g("False"), g("Nat"))), cases: vec![],
};
let term = lam("h", g("False"), m);
assert!(infer_type(&ctx, &term).is_ok(), "ex falso (large elim of empty False) must stay legal");
}
#[test]
fn small_elimination_of_or_into_prop_still_allowed() {
let mut ctx = Context::new();
StandardLibrary::register(&mut ctx);
let case = lam("_", g("True"), g("I")); let m = Term::Match {
discriminant: Box::new(Term::Var("h".to_string())),
motive: Box::new(lam("_", or_tt(), g("True"))), cases: vec![case.clone(), case],
};
let term = lam("h", or_tt(), m);
assert!(infer_type(&ctx, &term).is_ok(), "small elimination of Or into Prop must stay legal");
}
}