Skip to main content

logicaffeine_kernel/
recheck.rs

1//! R1 — an independent proof-term re-checker (the de Bruijn criterion taken seriously).
2//!
3//! The kernel's [`infer_type`](crate::infer_type) is the entire trust base. This module
4//! is a SECOND, deliberately separate implementation of the CIC type checker: trust then
5//! rests on two small independent kernels agreeing, not on one. For the independence to
6//! be meaningful the two must not share their checking logic — so this re-checker uses a
7//! DIFFERENT term representation (de Bruijn indices, no names) with its own reduction and
8//! its own cumulative conversion. A variable-capture or substitution bug in the main
9//! kernel's name-based machinery would surface here as a type error rather than slipping
10//! through identically — Logos's analog of `lean4checker`/`nanoda`.
11//!
12//! Scope: the CIC logical core — `Sort`/`Var`/`Global`/`Pi`/`Lambda`/`App`/`Lit` (incl.
13//! `BigInt`/`Nat` literal arithmetic and the Nat↔Peano bridge) with β/ι/ζ/δ-reduction and
14//! cumulative conversion — PLUS the inductive eliminator `Match` (coverage, per-constructor
15//! case typing via de Bruijn telescope instantiation, ι-reduction, large-elimination
16//! restriction) AND both single (`Fix`) and MUTUAL (`MutualFix`) fixpoints, each with an
17//! INDEPENDENT structural-termination guard. Transparent definitions ARE δ-unfolded during
18//! whnf; only axioms/constructors/inductives stay opaque. The only fragment reported
19//! HONESTLY as [`ReCheckError::Unsupported`] — rather than silently passed — is `Hole`
20//! (an unelaborated metavariable) and a `match` on an inductive this checker cannot
21//! resolve: such a proof is single-checked by the main kernel and [`double_check`] says so.
22//! The re-checker never reports a term as independently verified unless it actually
23//! verified every part of it.
24
25use std::collections::{HashMap, HashSet};
26
27use crate::term::{int_lit, lit_bigint, Literal, Term, Universe};
28use crate::Context;
29
30/// Why the re-checker could not produce a verdict, split by soundness meaning.
31#[derive(Debug, Clone, PartialEq)]
32pub enum ReCheckError {
33    /// A construct outside the covered fragment — a `Hole` (unelaborated metavariable) or a
34    /// `match` on an inductive whose head this checker does not recognize. NOT a soundness
35    /// signal: the term is still fully checked by the main kernel, and [`double_check`]
36    /// flags it as single-checked rather than claiming agreement.
37    Unsupported(String),
38    /// A genuine ill-typedness the re-checker is confident about (a non-function in
39    /// application position, a sort/type mismatch, wrong case count, unbound variable).
40    /// If the main kernel accepted the same term, the two kernels DISAGREE — an alarm.
41    Ill(String),
42}
43
44impl ReCheckError {
45    fn ill(msg: impl Into<String>) -> Self {
46        ReCheckError::Ill(msg.into())
47    }
48    fn unsupported(msg: impl Into<String>) -> Self {
49        ReCheckError::Unsupported(msg.into())
50    }
51}
52
53type RResult<T> = Result<T, ReCheckError>;
54
55/// A locally-nameless term: every bound variable is a de Bruijn INDEX (0 = innermost
56/// binder), so α-equivalence is syntactic identity and there are no names to capture.
57/// This is the independence-bearing representation — distinct from the main kernel's
58/// named [`Term`].
59#[derive(Debug, Clone, PartialEq)]
60enum Db {
61    Sort(Universe),
62    /// de Bruijn index of a `λ`/`Π`-bound variable (0 = nearest enclosing binder).
63    Var(usize),
64    /// A reference into the global environment (inductive, constructor, definition,
65    /// or declaration) — kept opaque.
66    Global(String),
67    /// A universe-polymorphic global at explicit levels — `name.{ℓ…}`.
68    Const { name: String, levels: Vec<Universe> },
69    /// `Π(_:dom). body` — `body` is one binder deeper than `dom`.
70    Pi(Box<Db>, Box<Db>),
71    /// `λ(_:dom). body`.
72    Lam(Box<Db>, Box<Db>),
73    App(Box<Db>, Box<Db>),
74    /// `match disc return motive with cases` — the node binds nothing itself; each case
75    /// and the motive are `λ`s that introduce their own binders.
76    Match { disc: Box<Db>, motive: Box<Db>, cases: Vec<Db> },
77    /// `fix rec. body` — binds the recursive self-reference (index 0 inside `body`).
78    Fix(Box<Db>),
79    /// `mutualfix { b₀, …, b_{n-1} }.index` — a block of `n` mutually-recursive bodies.
80    /// ALL `n` names bind in EVERY body: entering a body pushes `n` levels, with def `j`
81    /// at level `base + j` (`base` = enclosing depth). The node reduces to the
82    /// `index`-th body once an argument is constructor-headed.
83    MutualFix { defs: Vec<Db>, index: usize },
84    /// `let _:ty := value in body` — `body` is one binder deeper than `ty`/`value`.
85    Let(Box<Db>, Box<Db>, Box<Db>),
86    Lit(Literal),
87}
88
89// ---------------------------------------------------------------------------
90// Named `Term` ⟷ de Bruijn `Db`
91// ---------------------------------------------------------------------------
92
93/// Lower a named [`Term`] to a de Bruijn [`Db`], resolving each `Var` to the index of
94/// its nearest enclosing binder. `scope` holds the binder names, innermost LAST. A
95/// `Var` with no matching binder is a free local — illegal in a closed certificate
96/// term, so it is rejected. `Fix`/`Hole` are outside the covered fragment.
97fn to_db(term: &Term, scope: &mut Vec<String>) -> RResult<Db> {
98    match term {
99        Term::Sort(u) => Ok(Db::Sort(u.clone())),
100        Term::Var(name) => {
101            for (depth_from_inner, bound) in scope.iter().rev().enumerate() {
102                if bound == name {
103                    return Ok(Db::Var(depth_from_inner));
104                }
105            }
106            Err(ReCheckError::ill(format!("unbound local variable '{}'", name)))
107        }
108        Term::Global(name) => Ok(Db::Global(name.clone())),
109        Term::Const { name, levels } => {
110            Ok(Db::Const { name: name.clone(), levels: levels.clone() })
111        }
112        Term::Pi { param, param_type, body_type } => {
113            let dom = to_db(param_type, scope)?;
114            scope.push(param.clone());
115            let body = to_db(body_type, scope);
116            scope.pop();
117            Ok(Db::Pi(Box::new(dom), Box::new(body?)))
118        }
119        Term::Lambda { param, param_type, body } => {
120            let dom = to_db(param_type, scope)?;
121            scope.push(param.clone());
122            let inner = to_db(body, scope);
123            scope.pop();
124            Ok(Db::Lam(Box::new(dom), Box::new(inner?)))
125        }
126        Term::App(f, a) => Ok(Db::App(Box::new(to_db(f, scope)?), Box::new(to_db(a, scope)?))),
127        Term::Match { discriminant, motive, cases } => {
128            let disc = to_db(discriminant, scope)?;
129            let mot = to_db(motive, scope)?;
130            let cs = cases.iter().map(|c| to_db(c, scope)).collect::<RResult<Vec<_>>>()?;
131            Ok(Db::Match { disc: Box::new(disc), motive: Box::new(mot), cases: cs })
132        }
133        Term::Fix { name, body } => {
134            scope.push(name.clone());
135            let b = to_db(body, scope);
136            scope.pop();
137            Ok(Db::Fix(Box::new(b?)))
138        }
139        // Mutual fixpoint: all `n` names bind in every body, so push all `n` before
140        // converting any body (innermost-last, so def `j` sits at level `base + j`).
141        Term::MutualFix { defs, index } => {
142            let names: Vec<String> = defs.iter().map(|(n, _)| n.clone()).collect();
143            for n in &names {
144                scope.push(n.clone());
145            }
146            let mut dbs = Vec::with_capacity(defs.len());
147            let mut err = None;
148            for (_, body) in defs {
149                match to_db(body, scope) {
150                    Ok(d) => dbs.push(d),
151                    Err(e) => {
152                        err = Some(e);
153                        break;
154                    }
155                }
156            }
157            for _ in &names {
158                scope.pop();
159            }
160            match err {
161                Some(e) => Err(e),
162                None => Ok(Db::MutualFix { defs: dbs, index: *index }),
163            }
164        }
165        Term::Let { name, ty, value, body } => {
166            let d_ty = to_db(ty, scope)?;
167            let d_val = to_db(value, scope)?;
168            scope.push(name.clone());
169            let d_body = to_db(body, scope);
170            scope.pop();
171            Ok(Db::Let(Box::new(d_ty), Box::new(d_val), Box::new(d_body?)))
172        }
173        Term::Lit(l) => Ok(Db::Lit(l.clone())),
174        Term::Hole => Err(ReCheckError::unsupported("Hole (unelaborated implicit)")),
175    }
176}
177
178/// Raise a de Bruijn [`Db`] back to a named [`Term`], naming binders `v{depth}` by their
179/// binding depth (used only to report an inferred type back; comparisons stay in
180/// de Bruijn).
181fn from_db(t: &Db, depth: usize) -> Term {
182    match t {
183        Db::Sort(u) => Term::Sort(u.clone()),
184        Db::Var(k) => {
185            let binder = depth.saturating_sub(1).saturating_sub(*k);
186            Term::Var(format!("v{}", binder))
187        }
188        Db::Global(n) => Term::Global(n.clone()),
189        Db::Const { name, levels } => Term::Const { name: name.clone(), levels: levels.clone() },
190        Db::Pi(a, b) => Term::Pi {
191            param: format!("v{}", depth),
192            param_type: Box::new(from_db(a, depth)),
193            body_type: Box::new(from_db(b, depth + 1)),
194        },
195        Db::Lam(a, b) => Term::Lambda {
196            param: format!("v{}", depth),
197            param_type: Box::new(from_db(a, depth)),
198            body: Box::new(from_db(b, depth + 1)),
199        },
200        Db::App(f, a) => Term::App(Box::new(from_db(f, depth)), Box::new(from_db(a, depth))),
201        Db::Match { disc, motive, cases } => Term::Match {
202            discriminant: Box::new(from_db(disc, depth)),
203            motive: Box::new(from_db(motive, depth)),
204            cases: cases.iter().map(|c| from_db(c, depth)).collect(),
205        },
206        Db::Fix(body) => Term::Fix {
207            name: format!("rec{}", depth),
208            body: Box::new(from_db(body, depth + 1)),
209        },
210        Db::MutualFix { defs, index } => {
211            let n = defs.len();
212            Term::MutualFix {
213                defs: defs
214                    .iter()
215                    .enumerate()
216                    .map(|(j, b)| (format!("rec{depth}_{j}"), from_db(b, depth + n)))
217                    .collect(),
218                index: *index,
219            }
220        }
221        Db::Let(ty, value, body) => Term::Let {
222            name: format!("v{}", depth),
223            ty: Box::new(from_db(ty, depth)),
224            value: Box::new(from_db(value, depth)),
225            body: Box::new(from_db(body, depth + 1)),
226        },
227        Db::Lit(l) => Term::Lit(l.clone()),
228    }
229}
230
231// ---------------------------------------------------------------------------
232// de Bruijn shifting and substitution (the capture-free core)
233// ---------------------------------------------------------------------------
234
235/// Shift every free index `≥ cutoff` by `d` (which may be negative). Binders raise the
236/// cutoff so bound indices are untouched (the classic TAPL shift).
237fn shift(t: &Db, d: isize, cutoff: usize) -> Db {
238    match t {
239        Db::Var(k) => {
240            if *k >= cutoff {
241                Db::Var((*k as isize + d) as usize)
242            } else {
243                Db::Var(*k)
244            }
245        }
246        Db::Pi(a, b) => Db::Pi(Box::new(shift(a, d, cutoff)), Box::new(shift(b, d, cutoff + 1))),
247        Db::Lam(a, b) => Db::Lam(Box::new(shift(a, d, cutoff)), Box::new(shift(b, d, cutoff + 1))),
248        Db::App(f, a) => Db::App(Box::new(shift(f, d, cutoff)), Box::new(shift(a, d, cutoff))),
249        Db::Match { disc, motive, cases } => Db::Match {
250            disc: Box::new(shift(disc, d, cutoff)),
251            motive: Box::new(shift(motive, d, cutoff)),
252            cases: cases.iter().map(|c| shift(c, d, cutoff)).collect(),
253        },
254        Db::Fix(body) => Db::Fix(Box::new(shift(body, d, cutoff + 1))),
255        Db::MutualFix { defs, index } => Db::MutualFix {
256            // Every body lives beneath ALL `n` mutual binders, so the cutoff rises by `n`.
257            defs: defs.iter().map(|b| shift(b, d, cutoff + defs.len())).collect(),
258            index: *index,
259        },
260        Db::Let(ty, value, body) => Db::Let(
261            Box::new(shift(ty, d, cutoff)),
262            Box::new(shift(value, d, cutoff)),
263            Box::new(shift(body, d, cutoff + 1)),
264        ),
265        Db::Sort(_) | Db::Global(_) | Db::Const { .. } | Db::Lit(_) => t.clone(),
266    }
267}
268
269/// Substitute the variable with index `j` by `s` (capture-free: `s` is shifted as it
270/// descends under binders, `j` rises in step).
271fn subst(t: &Db, j: usize, s: &Db) -> Db {
272    match t {
273        Db::Var(k) => {
274            if *k == j {
275                s.clone()
276            } else {
277                Db::Var(*k)
278            }
279        }
280        Db::Pi(a, b) => Db::Pi(
281            Box::new(subst(a, j, s)),
282            Box::new(subst(b, j + 1, &shift(s, 1, 0))),
283        ),
284        Db::Lam(a, b) => Db::Lam(
285            Box::new(subst(a, j, s)),
286            Box::new(subst(b, j + 1, &shift(s, 1, 0))),
287        ),
288        Db::App(f, a) => Db::App(Box::new(subst(f, j, s)), Box::new(subst(a, j, s))),
289        Db::Match { disc, motive, cases } => Db::Match {
290            disc: Box::new(subst(disc, j, s)),
291            motive: Box::new(subst(motive, j, s)),
292            cases: cases.iter().map(|c| subst(c, j, s)).collect(),
293        },
294        Db::Fix(body) => Db::Fix(Box::new(subst(body, j + 1, &shift(s, 1, 0)))),
295        Db::MutualFix { defs, index } => {
296            // Under `n` mutual binders: the substituted index rises by `n` and `s` is
297            // shifted by `n` to keep its free variables aligned.
298            let n = defs.len();
299            Db::MutualFix {
300                defs: defs.iter().map(|b| subst(b, j + n, &shift(s, n as isize, 0))).collect(),
301                index: *index,
302            }
303        }
304        Db::Let(ty, value, body) => Db::Let(
305            Box::new(subst(ty, j, s)),
306            Box::new(subst(value, j, s)),
307            Box::new(subst(body, j + 1, &shift(s, 1, 0))),
308        ),
309        Db::Sort(_) | Db::Global(_) | Db::Const { .. } | Db::Lit(_) => t.clone(),
310    }
311}
312
313/// β-substitution of the argument into a binder body: `(λ. body) arg ⤳ body[0 := arg]`.
314fn beta_open(body: &Db, arg: &Db) -> Db {
315    shift(&subst(body, 0, &shift(arg, 1, 0)), -1, 0)
316}
317
318/// Unfold the `index`-th body of a mutual block: replace each of the `n` mutual names by
319/// its own (closed) `MutualFix` projection and drop the `n` binders. The mutual analog of
320/// `beta_open` — the innermost binder (index 0) is name `n-1`, so it receives projection
321/// `n-1`, down to name `0`. Each projection is closed, so iterated `beta_open` is exact.
322fn open_mutual(defs: &[Db], index: usize) -> Db {
323    let n = defs.len();
324    let mut result = defs[index].clone();
325    for k in 0..n {
326        let proj = Db::MutualFix { defs: defs.to_vec(), index: n - 1 - k };
327        result = beta_open(&result, &proj);
328    }
329    result
330}
331
332/// Decompose an application spine into its head and arguments (in source order).
333fn spine(t: &Db) -> (Db, Vec<Db>) {
334    let mut args = Vec::new();
335    let mut cur = t.clone();
336    while let Db::App(f, a) = cur {
337        args.push(*a);
338        cur = *f;
339    }
340    args.reverse();
341    (cur, args)
342}
343
344/// Whether `t` reduces to an application headed by a constructor — the guard that lets a
345/// fixpoint unfold (its decreasing argument is a value) without risking a loop.
346fn ctor_headed(genv: &Context, t: &Db) -> bool {
347    let (head, _) = spine(&whnf(genv, t));
348    matches!(head, Db::Global(n) if genv.is_constructor(&n))
349}
350
351/// Reduce a primitive Int/Bool operation applied to two literal arguments — the main
352/// kernel's hardware-ALU builtins, replicated here so the re-checker computes the same
353/// arithmetic (`le 2 3 ⤳ true`, `add 2 4 ⤳ 6`). The arguments are themselves reduced
354/// first (so `le (add 1 1) 3` works). `None` if `t` is not such a primitive application.
355fn try_builtin(genv: &Context, t: &Db) -> Option<Db> {
356    let (head, args) = spine(t);
357    let op = match &head {
358        Db::Global(n) if args.len() == 2 => n.as_str(),
359        _ => return None,
360    };
361    if !matches!(op, "add" | "sub" | "mul" | "div" | "mod" | "le" | "lt" | "ge" | "gt") {
362        return None;
363    }
364    let (xl, yl) = match (whnf(genv, &args[0]), whnf(genv, &args[1])) {
365        (Db::Lit(xl), Db::Lit(yl)) => (xl, yl),
366        _ => return None,
367    };
368    let bool_op = |b: bool| Some(Db::Global(if b { "true" } else { "false" }.to_string()));
369
370    // Fast i64 path; an overflowing arithmetic op falls through to exact BigInt (K6).
371    if let (Literal::Int(x), Literal::Int(y)) = (&xl, &yl) {
372        let fast = match op {
373            "add" => x.checked_add(*y),
374            "sub" => x.checked_sub(*y),
375            "mul" => x.checked_mul(*y),
376            "div" => x.checked_div(*y),
377            "mod" => x.checked_rem(*y),
378            _ => None,
379        };
380        if let Some(r) = fast {
381            return Some(Db::Lit(Literal::Int(r)));
382        }
383        match op {
384            "le" => return bool_op(*x <= *y),
385            "lt" => return bool_op(*x < *y),
386            "ge" => return bool_op(*x >= *y),
387            "gt" => return bool_op(*x > *y),
388            _ => {}
389        }
390    }
391
392    // Exact arbitrary-precision path — the independent kernel's copy of the main kernel's
393    // BigInt arithmetic (`try_primitive_reduce`), canonicalised by `int_lit`.
394    let (xb, yb) = (lit_bigint(&xl)?, lit_bigint(&yl)?);
395    let big = match op {
396        "add" => Some(xb.add(&yb)),
397        "sub" => Some(xb.sub(&yb)),
398        "mul" => Some(xb.mul(&yb)),
399        "div" => xb.div_rem(&yb).map(|(q, _)| q),
400        "mod" => xb.div_rem(&yb).map(|(_, r)| r),
401        _ => None,
402    };
403    if let Some(r) = big {
404        return Some(Db::Lit(int_lit(r)));
405    }
406    match op {
407        "le" => bool_op(xb <= yb),
408        "lt" => bool_op(xb < yb),
409        "ge" => bool_op(xb >= yb),
410        "gt" => bool_op(xb > yb),
411        _ => None,
412    }
413}
414
415/// Count the leading `Π` binders of a de Bruijn type (an inductive's arity / a
416/// constructor's parameter count).
417fn pi_count(t: &Db) -> usize {
418    match t {
419        Db::Pi(_, b) => 1 + pi_count(b),
420        _ => 0,
421    }
422}
423
424/// The number of leading parameters (arity) of an inductive — how many of a
425/// constructor's leading arguments are type parameters fixed by the discriminant.
426fn inductive_arity(genv: &Context, ind: &str) -> usize {
427    match genv.get_global(ind) {
428        Some(ty) => to_db(&ty.clone(), &mut Vec::new()).map(|d| pi_count(&d)).unwrap_or(0),
429        None => 0,
430    }
431}
432
433/// Weak head normal form under β and ι (globals stay opaque — no δ). `ι`: a `match`
434/// whose discriminant reduces to a constructor application selects and applies the
435/// corresponding case to the constructor's value arguments. Terminates on the supported
436/// (non-`Fix`) fragment.
437/// The quotient computation rule in de Bruijn form: `Quot_lift A r B f h (Quot_mk A r a) ⤳
438/// f a`. Returns the reduct when `t` is that spine, else `None`.
439fn try_quot_lift_db(genv: &Context, t: &Db) -> Option<Db> {
440    let (head, args) = spine(t);
441    if !matches!(&head, Db::Global(n) if n == "Quot_lift") || args.len() != 6 {
442        return None;
443    }
444    let q = whnf(genv, &args[5]);
445    let (qh, qa) = spine(&q);
446    if !matches!(&qh, Db::Global(n) if n == "Quot_mk") || qa.len() != 3 {
447        return None;
448    }
449    Some(Db::App(Box::new(args[3].clone()), Box::new(qa[2].clone())))
450}
451
452fn whnf(genv: &Context, t: &Db) -> Db {
453    let mut cur = t.clone();
454    // Strong normalization guarantees termination on well-typed terms; the fuel is a
455    // backstop so a (hypothetical) ill-typed input can never hang the re-checker.
456    let mut fuel: usize = 1_000_000;
457    loop {
458        if fuel == 0 {
459            return cur;
460        }
461        fuel -= 1;
462        // Quotient computation: `Quot_lift A r B f h (Quot_mk A r a) ⤳ f a` (mirrors the
463        // main kernel, so quotient proofs are two-kernel verified).
464        if matches!(cur, Db::App(..)) {
465            if let Some(reduced) = try_quot_lift_db(genv, &cur) {
466                cur = reduced;
467                continue;
468            }
469        }
470        // Native-reduction hook: the main kernel reduces `reduceBool t` with its
471        // trusted evaluator; the re-checker reduces `t` with its OWN machinery
472        // (δ/ι/builtins) — an independent computation of the same value, so a
473        // `native_decide` proof is genuinely two-kernel verified.
474        if let Db::App(f, a) = &cur {
475            if matches!(f.as_ref(), Db::Global(n) if n == "reduceBool") {
476                let av = whnf(genv, a);
477                if matches!(&av, Db::Global(n) if n == "true" || n == "false") {
478                    cur = av;
479                    continue;
480                }
481            }
482        }
483        match cur {
484            // δ: unfold a transparent definition (a global with a body) — so the
485            // re-checker COMPUTES `le 2 3 ⇝ true` etc., independently of the main kernel.
486            // Axioms/declarations (no body) stay opaque.
487            Db::Global(name) => {
488                if let Some(body) = genv.get_definition_body(&name) {
489                    if let Ok(db) = to_db(&body.clone(), &mut Vec::new()) {
490                        cur = db;
491                        continue;
492                    }
493                }
494                return Db::Global(name);
495            }
496            Db::App(f, a) => {
497                let fw = whnf(genv, &f);
498                match fw {
499                    Db::Lam(_, body) => {
500                        cur = beta_open(&body, &a);
501                    }
502                    // Guarded fix-unfolding: `(fix rec. body) args ⤳ body[rec := fix] args`,
503                    // but ONLY when some argument is constructor-headed, so the unfolded
504                    // `match` makes progress and a stuck fixpoint cannot loop.
505                    Db::Fix(fbody) => {
506                        let (_, args) = spine(&Db::App(Box::new(Db::Fix(fbody.clone())), a.clone()));
507                        if args.iter().any(|x| ctor_headed(genv, x)) {
508                            let unfolded = beta_open(&fbody, &Db::Fix(fbody.clone()));
509                            cur = Db::App(Box::new(unfolded), a);
510                        } else {
511                            return Db::App(Box::new(Db::Fix(fbody)), a);
512                        }
513                    }
514                    // Mutual-fix unfolding: same guarded rule, but unfold the `index`-th
515                    // body, substituting every sibling by its own projection.
516                    Db::MutualFix { defs, index } => {
517                        let applied =
518                            Db::App(Box::new(Db::MutualFix { defs: defs.clone(), index }), a.clone());
519                        let (_, args) = spine(&applied);
520                        if args.iter().any(|x| ctor_headed(genv, x)) {
521                            cur = Db::App(Box::new(open_mutual(&defs, index)), a);
522                        } else {
523                            return Db::App(Box::new(Db::MutualFix { defs, index }), a);
524                        }
525                    }
526                    other => {
527                        // A primitive Int/Bool operation on literal arguments
528                        // (`le 2 3 ⤳ true`, `add 2 4 ⤳ 6`) — the re-checker independently
529                        // replicates the main kernel's ALU builtins so it can verify
530                        // arithmetic certificates.
531                        let full = Db::App(Box::new(other), a);
532                        if let Some(r) = try_builtin(genv, &full) {
533                            cur = r;
534                        } else {
535                            return full;
536                        }
537                    }
538                }
539            }
540            Db::Match { disc, motive, cases } => {
541                let mut d = whnf(genv, &disc);
542                // Peano bridge (K6): a `Nat(n)` literal is `Zero`/`Succ(Nat(n-1))` — expand
543                // one step so the constructor-selection below fires (the recursor computes
544                // on Nat literals, peeling one `Succ` per match).
545                if matches!(&d, Db::Lit(Literal::Nat(_))) {
546                    d = db_nat_peano_step(&d);
547                }
548                let (head, args) = spine(&d);
549                if let Db::Global(cname) = &head {
550                    if let Some(ind) = genv.constructor_inductive(cname) {
551                        let ctor_names: Vec<String> =
552                            genv.get_constructors(ind).iter().map(|(n, _)| n.to_string()).collect();
553                        if let Some(idx) = ctor_names.iter().position(|n| n == cname) {
554                            if idx < cases.len() {
555                                let arity = inductive_arity(genv, ind);
556                                let val_args =
557                                    if args.len() >= arity { &args[arity..] } else { &args[..] };
558                                let mut res = cases[idx].clone();
559                                for a in val_args {
560                                    res = Db::App(Box::new(res), Box::new(a.clone()));
561                                }
562                                cur = res;
563                                continue;
564                            }
565                        }
566                    }
567                }
568                return Db::Match { disc: Box::new(d), motive, cases };
569            }
570            // Zeta: `let _:_ := v in b` ⤳ `b[0 := v]`.
571            Db::Let(_ty, value, body) => {
572                cur = beta_open(&body, &value);
573            }
574            other => return other,
575        }
576    }
577}
578
579// ---------------------------------------------------------------------------
580// Conversion (definitional equality + cumulativity)
581// ---------------------------------------------------------------------------
582
583/// One Peano-unfolding step of a de Bruijn `Nat` literal: `Nat(0) → Zero`,
584/// `Nat(n) → Succ (Nat(n-1))` (K6). Non-Nat terms are returned unchanged.
585fn db_nat_peano_step(t: &Db) -> Db {
586    match t {
587        // `n ≤ 0` collapses to `Zero` so the peel TERMINATES on any (incl. malformed
588        // negative) literal — critical here, where the re-checker consumes untrusted input.
589        Db::Lit(Literal::Nat(n)) if *n <= logicaffeine_base::BigInt::from_i64(0) => {
590            Db::Global("Zero".to_string())
591        }
592        Db::Lit(Literal::Nat(n)) => Db::App(
593            Box::new(Db::Global("Succ".to_string())),
594            Box::new(Db::Lit(Literal::Nat(n.sub(&logicaffeine_base::BigInt::from_i64(1))))),
595        ),
596        other => other.clone(),
597    }
598}
599
600/// True if `t` heads a `Nat` Peano value — `Zero` or `Succ _` — the shape a `Nat` literal
601/// bridges against.
602fn db_nat_peano_headed(t: &Db) -> bool {
603    match t {
604        Db::Global(n) => n == "Zero",
605        Db::App(f, _) => matches!(f.as_ref(), Db::Global(n) if n == "Succ"),
606        _ => false,
607    }
608}
609
610/// Definitional equality: β/ι-convertible up to α (syntactic in de Bruijn). Globals are
611/// compared by name (opaque — no δ).
612fn def_eq(genv: &Context, lctx: &[Db], a: &Db, b: &Db) -> bool {
613    let a = whnf(genv, a);
614    let b = whnf(genv, b);
615
616    // Peano bridge (K6): a `Nat(n)` literal is definitionally `Succ^n Zero`. Two Nat
617    // literals are equal iff their counts are; a Nat literal against a `Zero`/`Succ`-headed
618    // term is compared by peeling one `Succ` (terminating at `Nat(0) ≡ Zero`).
619    match (&a, &b) {
620        (Db::Lit(Literal::Nat(x)), Db::Lit(Literal::Nat(y))) => return x == y,
621        (Db::Lit(Literal::Nat(_)), _) if db_nat_peano_headed(&b) => {
622            return def_eq(genv, lctx, &db_nat_peano_step(&a), &b);
623        }
624        (_, Db::Lit(Literal::Nat(_))) if db_nat_peano_headed(&a) => {
625            return def_eq(genv, lctx, &a, &db_nat_peano_step(&b));
626        }
627        _ => {}
628    }
629
630    // η-conversion: `λ. f (Var 0) ≡ g`. When exactly one side is a `λ`, compare its body
631    // against the other side (shifted under the binder) applied to `Var 0`.
632    if let Db::Lam(dom, body) = &a {
633        if !matches!(b, Db::Lam(..)) {
634            let bx = whnf(genv, &Db::App(Box::new(shift(&b, 1, 0)), Box::new(Db::Var(0))));
635            let mut ext = lctx.to_vec();
636            ext.push((**dom).clone());
637            return def_eq(genv, &ext, body, &bx);
638        }
639    }
640    if let Db::Lam(dom, body) = &b {
641        if !matches!(a, Db::Lam(..)) {
642            let ax = whnf(genv, &Db::App(Box::new(shift(&a, 1, 0)), Box::new(Db::Var(0))));
643            let mut ext = lctx.to_vec();
644            ext.push((**dom).clone());
645            return def_eq(genv, &ext, &ax, body);
646        }
647    }
648
649    // Structure η (the second kernel's copy of the main kernel's rule): a
650    // fully-applied structure constructor is convertible with any term whose
651    // projections match it field-wise.
652    if let Some(eq) = try_struct_eta_db(genv, lctx, &a, &b) {
653        return eq;
654    }
655    if let Some(eq) = try_struct_eta_db(genv, lctx, &b, &a) {
656        return eq;
657    }
658
659    let congruent = match (&a, &b) {
660        (Db::Sort(x), Db::Sort(y)) => x.equiv(y),
661        (Db::Var(i), Db::Var(j)) => i == j,
662        (Db::Global(m), Db::Global(n)) => m == n,
663        (Db::Lit(x), Db::Lit(y)) => x == y,
664        (Db::App(f1, a1), Db::App(f2, a2)) => {
665            def_eq(genv, lctx, f1, f2) && def_eq(genv, lctx, a1, a2)
666        }
667        (Db::Pi(d1, b1), Db::Pi(d2, b2)) => {
668            def_eq(genv, lctx, d1, d2) && {
669                let mut ext = lctx.to_vec();
670                ext.push((**d1).clone());
671                def_eq(genv, &ext, b1, b2)
672            }
673        }
674        (Db::Lam(d1, b1), Db::Lam(d2, b2)) => {
675            def_eq(genv, lctx, d1, d2) && {
676                let mut ext = lctx.to_vec();
677                ext.push((**d1).clone());
678                def_eq(genv, &ext, b1, b2)
679            }
680        }
681        (
682            Db::Match { disc: d1, motive: m1, cases: c1 },
683            Db::Match { disc: d2, motive: m2, cases: c2 },
684        ) => {
685            def_eq(genv, lctx, d1, d2)
686                && def_eq(genv, lctx, m1, m2)
687                && c1.len() == c2.len()
688                && c1.iter().zip(c2.iter()).all(|(x, y)| def_eq(genv, lctx, x, y))
689        }
690        (Db::Fix(b1), Db::Fix(b2)) => {
691            let mut ext = lctx.to_vec();
692            ext.push(Db::Sort(Universe::Prop)); // `rec` placeholder (its type is never consulted)
693            def_eq(genv, &ext, b1, b2)
694        }
695        (Db::MutualFix { defs: d1, index: i1 }, Db::MutualFix { defs: d2, index: i2 }) => {
696            i1 == i2
697                && d1.len() == d2.len()
698                && {
699                    let mut ext = lctx.to_vec();
700                    for _ in 0..d1.len() {
701                        ext.push(Db::Sort(Universe::Prop)); // the `n` recursive-name placeholders
702                    }
703                    d1.iter().zip(d2.iter()).all(|(a, b)| def_eq(genv, &ext, a, b))
704                }
705        }
706        (Db::Const { name: n1, levels: l1 }, Db::Const { name: n2, levels: l2 }) => {
707            n1 == n2 && l1.len() == l2.len() && l1.iter().zip(l2.iter()).all(|(a, b)| a.equiv(b))
708        }
709        _ => false,
710    };
711    if congruent {
712        return true;
713    }
714
715    // Proof irrelevance: two proofs of the same proposition are equal.
716    proof_irrel(genv, lctx, &a, &b)
717}
718
719/// Structure η in the de Bruijn re-checker, ONE direction (see the main kernel's
720/// `try_structure_eta`). `None` when `mk_term` is not a fully-applied registered
721/// structure constructor, or `other` is the same constructor (ordinary congruence).
722fn try_struct_eta_db(genv: &Context, lctx: &[Db], mk_term: &Db, other: &Db) -> Option<bool> {
723    let (head, args) = spine(mk_term);
724    let Db::Global(hname) = &head else { return None };
725    let (_sname, info) = genv.struct_of_constructor(hname)?;
726    let nfields = info.projections.len();
727    if args.len() != info.num_params + nfields {
728        return None;
729    }
730    let (ohead, _) = spine(other);
731    if matches!(&ohead, Db::Global(n) if n == hname) {
732        return None;
733    }
734    let params = &args[..info.num_params];
735    let field_args = &args[info.num_params..];
736    Some(info.projections.iter().enumerate().all(|(i, proj)| {
737        // `proj params… other` as a Db application.
738        let mut app = Db::Global(proj.clone());
739        for p in params {
740            app = Db::App(Box::new(app), Box::new(p.clone()));
741        }
742        app = Db::App(Box::new(app), Box::new(other.clone()));
743        def_eq(genv, lctx, &field_args[i], &app)
744    }))
745}
746
747/// Proof irrelevance for the re-checker: `a ≡ b` if `a : A`, `A : Prop`, and `b`'s type is
748/// definitionally equal to `A`. Mirrors the main kernel's [`crate::type_checker::def_eq`].
749fn proof_irrel(genv: &Context, lctx: &[Db], a: &Db, b: &Db) -> bool {
750    let ta = match infer(genv, lctx, a) {
751        Ok(t) => t,
752        Err(_) => return false,
753    };
754    match infer(genv, lctx, &ta) {
755        Ok(s) if matches!(
756            whnf(genv, &s),
757            Db::Sort(Universe::Prop) | Db::Sort(Universe::SProp)
758        ) => {}
759        _ => return false,
760    }
761    match infer(genv, lctx, b) {
762        Ok(tb) => def_eq(genv, lctx, &ta, &tb),
763        Err(_) => false,
764    }
765}
766
767/// Cumulative subtyping `sub ≤ sup`: sorts follow `Prop ≤ Type i ≤ Type j`; a `Π` is
768/// covariant in its codomain and invariant in its domain; everything else is `def_eq`.
769fn is_sub(genv: &Context, lctx: &[Db], sub: &Db, sup: &Db) -> bool {
770    let s = whnf(genv, sub);
771    let t = whnf(genv, sup);
772    match (&s, &t) {
773        (Db::Sort(x), Db::Sort(y)) => x.is_subtype_of(y),
774        (Db::Pi(d1, b1), Db::Pi(d2, b2)) => {
775            def_eq(genv, lctx, d1, d2) && {
776                let mut ext = lctx.to_vec();
777                ext.push((**d1).clone());
778                is_sub(genv, &ext, b1, b2)
779            }
780        }
781        _ => def_eq(genv, lctx, &s, &t),
782    }
783}
784
785// ---------------------------------------------------------------------------
786// Inference
787// ---------------------------------------------------------------------------
788
789/// The type of de Bruijn variable `k`, read from `lctx` (binder types, innermost LAST,
790/// each stored in the scope of ITS binding point) and lifted by `k+1` to current depth.
791fn var_type(lctx: &[Db], k: usize) -> RResult<Db> {
792    if k >= lctx.len() {
793        return Err(ReCheckError::ill(format!("de Bruijn index {} out of range", k)));
794    }
795    let stored = &lctx[lctx.len() - 1 - k];
796    Ok(shift(stored, (k + 1) as isize, 0))
797}
798
799/// Infer the type of `t` under global env `genv` and local context `lctx`.
800fn infer(genv: &Context, lctx: &[Db], t: &Db) -> RResult<Db> {
801    match t {
802        Db::Sort(u) => Ok(Db::Sort(u.succ())),
803        Db::Var(k) => var_type(lctx, *k),
804        Db::Global(name) => {
805            let ty = genv
806                .get_global(name)
807                .ok_or_else(|| ReCheckError::ill(format!("unknown global '{}'", name)))?;
808            to_db(&ty.clone(), &mut Vec::new())
809        }
810        Db::Const { name, levels } => {
811            let (params, ty, _body) = genv
812                .get_universe_poly(name)
813                .ok_or_else(|| ReCheckError::ill(format!("unknown universe-poly global '{}'", name)))?;
814            if params.len() != levels.len() {
815                return Err(ReCheckError::ill(format!(
816                    "universe-poly '{}' expects {} levels, got {}",
817                    name,
818                    params.len(),
819                    levels.len()
820                )));
821            }
822            let subst: std::collections::HashMap<String, Universe> =
823                params.iter().cloned().zip(levels.iter().cloned()).collect();
824            let instantiated = crate::term::instantiate_universes(&ty.clone(), &subst);
825            to_db(&instantiated, &mut Vec::new())
826        }
827        Db::Lit(Literal::Nat(n)) if *n < logicaffeine_base::BigInt::from_i64(0) => {
828            Err(ReCheckError::ill("a `Nat` literal must be non-negative".to_string()))
829        }
830        Db::Lit(l) => Ok(Db::Global(lit_type_name(l).to_string())),
831        Db::Pi(dom, body) => {
832            let dom_sort = infer_sort(genv, lctx, dom)?;
833            let mut ext = lctx.to_vec();
834            ext.push((**dom).clone());
835            let body_sort = infer_sort(genv, &ext, body)?;
836            // Impredicative product formation via `imax` (see the main kernel's
837            // `Pi` arm) — the second kernel decides the same level algebra.
838            let pi_sort = dom_sort.imax(&body_sort);
839            Ok(Db::Sort(pi_sort))
840        }
841        Db::Lam(dom, body) => {
842            let _ = infer_sort(genv, lctx, dom)?;
843            let mut ext = lctx.to_vec();
844            ext.push((**dom).clone());
845            let body_ty = infer(genv, &ext, body)?;
846            Ok(Db::Pi(dom.clone(), Box::new(body_ty)))
847        }
848        Db::App(f, a) => {
849            let f_ty = whnf(genv, &infer(genv, lctx, f)?);
850            match f_ty {
851                Db::Pi(dom, body) => {
852                    check(genv, lctx, a, &dom)?;
853                    Ok(beta_open(&body, a))
854                }
855                other => Err(ReCheckError::ill(format!(
856                    "application of a non-function (type {})",
857                    from_db(&other, lctx.len())
858                ))),
859            }
860        }
861        Db::Match { disc, motive, cases } => infer_match(genv, lctx, disc, motive, cases),
862        Db::Fix(body) => infer_fix(genv, lctx, body),
863        Db::MutualFix { defs, index } => infer_mutual_fix(genv, lctx, defs, *index),
864        // Let: check `ty` is a sort and `value : ty`, then type the body with the
865        // value substituted in (zeta) — exactly the main kernel's by-substitution
866        // rule, so both kernels agree.
867        Db::Let(ty, value, body) => {
868            let _ = infer_sort(genv, lctx, ty)?;
869            check(genv, lctx, value, ty)?;
870            infer(genv, lctx, &beta_open(body, value))
871        }
872    }
873}
874
875/// The effective motive of a `match`: a function motive `λx:I.T` (checked: its domain
876/// matches the discriminant type, its codomain is a type) is used as-is; a raw type `T`
877/// (a `Sort`-typed motive) is wrapped as `λ_:I. T` (shifting `T` under the new binder).
878fn effective_motive(genv: &Context, lctx: &[Db], motive: &Db, disc_ty: &Db) -> RResult<Db> {
879    let motive_ty = whnf(genv, &infer(genv, lctx, motive)?);
880    match &motive_ty {
881        Db::Pi(dom, cod) => {
882            if !def_eq(genv, lctx, dom, disc_ty) {
883                return Err(ReCheckError::ill(format!(
884                    "motive domain {} does not match discriminant type {}",
885                    from_db(dom, lctx.len()),
886                    from_db(disc_ty, lctx.len())
887                )));
888            }
889            let mut ext = lctx.to_vec();
890            ext.push(disc_ty.clone());
891            infer_sort(genv, &ext, cod)?;
892            Ok(motive.clone())
893        }
894        Db::Sort(_) => Ok(Db::Lam(Box::new(disc_ty.clone()), Box::new(shift(motive, 1, 0)))),
895        other => Err(ReCheckError::ill(format!(
896            "motive is neither a function nor a type (type {})",
897            from_db(other, lctx.len())
898        ))),
899    }
900}
901
902/// Type a `fix rec. body`: compute its (structural) type, INDEPENDENTLY verify the
903/// termination guard, sanity-check the body under `rec : T`, and return `T`. The guard
904/// is the load-bearing soundness check — without it `fix f. f` inhabits every type.
905fn infer_fix(genv: &Context, lctx: &[Db], body: &Db) -> RResult<Db> {
906    let fix_level = lctx.len();
907
908    // 1. The fixpoint's type, read from the body's λ-telescope and its `match` codomain
909    //    (which does not depend on `rec`). The body's indices are relative to a context
910    //    that INCLUDES the `rec` binder, so `fix_type` must run with that slot present
911    //    (a placeholder — its type is never consulted), or every index is off by one.
912    let mut inner = lctx.to_vec();
913    inner.push(Db::Sort(Universe::Prop));
914    let fix_ty_inner = fix_type(genv, &inner, body)?;
915    // The type does not reference `rec` (level `fix_level`, index 0 at this base), so drop
916    // that slot to express the type in the enclosing context.
917    let fix_ty = shift(&fix_ty_inner, -1, 0);
918
919    // 2. THE GUARDIAN — an independent structural-decrease check. The body lives one
920    //    binder (`rec`, at level `fix_level`) beneath the current depth.
921    check_terminates(genv, body, fix_level)?;
922
923    // 3. Sanity: the body type-checks with `rec : T` in scope.
924    let mut ext = lctx.to_vec();
925    ext.push(fix_ty.clone());
926    let _ = infer(genv, &ext, body)?;
927
928    Ok(fix_ty)
929}
930
931/// Type a `mutualfix { b₀ … b_{n-1} }.index` — the mutual analog of [`infer_fix`]. Each
932/// body's structural type is read from its λ-telescope (independent of the recursive
933/// names), the MUTUAL termination guard is verified over the whole block, the bodies are
934/// sanity-checked with all `n` names bound, and the `index`-th type is returned.
935fn infer_mutual_fix(genv: &Context, lctx: &[Db], defs: &[Db], index: usize) -> RResult<Db> {
936    let n = defs.len();
937    if n == 0 || index >= n {
938        return Err(ReCheckError::ill("malformed mutual fixpoint".to_string()));
939    }
940    let base = lctx.len();
941
942    // 1. Structural type of each body, computed with `n` placeholder binders in scope
943    //    (the bodies' indices assume all `n` mutual names are present). The types do not
944    //    mention those names, so drop the `n` slots (shift down by `n`) into the
945    //    enclosing context — each `types[j]` is then expressed relative to depth `base`.
946    let mut inner = lctx.to_vec();
947    for _ in 0..n {
948        inner.push(Db::Sort(Universe::Prop));
949    }
950    let mut types: Vec<Db> = Vec::with_capacity(n);
951    for body in defs {
952        let ty_inner = fix_type(genv, &inner, body)?;
953        types.push(shift(&ty_inner, -(n as isize), 0));
954    }
955
956    // 2. THE GUARDIAN — the mutual structural-decrease check.
957    check_terminates_mutual(genv, defs, base)?;
958
959    // 3. Sanity: every body type-checks with all `n` names bound. Binder `j` sits at level
960    //    `base + j`, so its type must be expressed relative to depth `base + j` (shift up
961    //    by `j` from the `base`-relative `types[j]`).
962    let mut ext = lctx.to_vec();
963    for (j, ty) in types.iter().enumerate() {
964        ext.push(shift(ty, j as isize, 0));
965    }
966    for body in defs {
967        let _ = infer(genv, &ext, body)?;
968    }
969
970    Ok(types[index].clone())
971}
972
973/// The structural type of a fixpoint body: each leading `λ(_:A). …` contributes a
974/// `Π(_:A). …`, and the innermost `match` contributes its return type `motive(disc)`
975/// (independent of `rec`). A non-`match` tail is typed directly.
976fn fix_type(genv: &Context, lctx: &[Db], body: &Db) -> RResult<Db> {
977    match body {
978        Db::Lam(dom, inner) => {
979            let _ = infer_sort(genv, lctx, dom)?;
980            let mut ext = lctx.to_vec();
981            ext.push((**dom).clone());
982            let inner_ty = fix_type(genv, &ext, inner)?;
983            Ok(Db::Pi(dom.clone(), Box::new(inner_ty)))
984        }
985        Db::Match { disc, motive, .. } => {
986            let disc_ty = whnf(genv, &infer(genv, lctx, disc)?);
987            match_return_type(genv, lctx, motive, disc, &disc_ty)
988        }
989        other => infer(genv, lctx, other),
990    }
991}
992
993// ---------------------------------------------------------------------------
994// The termination guard (Giménez 1995 / the Coq guard) — independently re-derived.
995//
996// A fixpoint is sound only if every recursive call decreases a STRUCTURAL argument.
997// Without this, `fix f. f` (and the higher-order escape `(λg. g Zero) f`) inhabit every
998// type, including `False`. This is a SEPARATE implementation of the main kernel's
999// `termination` module. Crucially, the main kernel tracks shadowing with explicit
1000// `struct_param_live`/`fix_name_live` flags because it works with NAMES; here, working
1001// in de Bruijn LEVELS, shadowing is automatic — an inner binder gets a fresh level, so a
1002// reference can never be mistaken for the recursive name or the structural parameter.
1003// That structural simplicity is the point: the two guards cannot share a shadowing bug.
1004// ---------------------------------------------------------------------------
1005
1006/// The de Bruijn LEVEL referred to by index `k` at the given `depth` (`None` if the
1007/// index is out of range). A level is absolute, so it identifies a binder independently
1008/// of how deep the reference is — which is what makes shadowing a non-issue.
1009fn level_of(depth: usize, k: usize) -> Option<usize> {
1010    depth.checked_sub(1)?.checked_sub(k)
1011}
1012
1013/// Count the leading `Π` parameters of a (named) constructor type — its arity.
1014fn count_pi_named(t: &Term) -> usize {
1015    match t {
1016        Term::Pi { body_type, .. } => 1 + count_pi_named(body_type),
1017        _ => 0,
1018    }
1019}
1020
1021/// Locate a fixpoint body's structural parameter: peel the λ-telescope from `base_depth`,
1022/// pick the scrutinee of the innermost `match` if inductive-typed (else the first
1023/// inductive-typed binder). Returns the parameter's LEVEL, its inductive name, the body
1024/// just past it, and its argument POSITION in the telescope.
1025fn locate_struct<'a>(
1026    genv: &Context,
1027    body: &'a Db,
1028    base_depth: usize,
1029) -> RResult<(usize, String, &'a Db, usize)> {
1030    let mut chain: Vec<(usize, Option<String>, &Db)> = Vec::new();
1031    let mut cur = body;
1032    let mut depth = base_depth;
1033    while let Db::Lam(dom, inner) = cur {
1034        let dom_h = whnf(genv, dom);
1035        chain.push((depth, extract_inductive(genv, &dom_h).map(|(n, _)| n), inner));
1036        cur = inner;
1037        depth += 1;
1038    }
1039    let scrutinee = match cur {
1040        Db::Match { disc, .. } => match &**disc {
1041            Db::Var(k) => {
1042                level_of(depth, *k).and_then(|lvl| chain.iter().position(|(l, ..)| *l == lvl))
1043            }
1044            _ => None,
1045        },
1046        _ => None,
1047    };
1048    let idx = scrutinee
1049        .filter(|&i| chain[i].1.is_some())
1050        .or_else(|| chain.iter().position(|(_, ind, _)| ind.is_some()))
1051        .ok_or_else(|| {
1052            ReCheckError::ill(
1053                "fixpoint has no inductive parameter for structural recursion".to_string(),
1054            )
1055        })?;
1056    Ok((chain[idx].0, chain[idx].1.clone().unwrap(), chain[idx].2, idx))
1057}
1058
1059/// Verify the fixpoint `body` (with `rec` bound at level `fix_level`) terminates: locate
1060/// the structural parameter and check that every recursive call decreases it. A single
1061/// fixpoint is the one-entry case of the block guard.
1062fn check_terminates(genv: &Context, body: &Db, fix_level: usize) -> RResult<()> {
1063    let (struct_level, ind, guard_body, struct_pos) =
1064        locate_struct(genv, body, fix_level + 1)?;
1065    let mut fix_positions = HashMap::new();
1066    fix_positions.insert(fix_level, struct_pos);
1067    guard(genv, guard_body, &fix_positions, struct_level, &ind, &HashSet::new(), struct_level + 1)
1068}
1069
1070/// Verify a MUTUAL block of `n` fixpoint bodies terminates. The `n` names occupy levels
1071/// `base .. base+n-1`, so each body lives beneath all of them. Every member's structural
1072/// position is found first (assembling `level → position`); then each body is guarded so
1073/// that a call to ANY member decreases the CURRENT body's structural parameter — the
1074/// mutual Giménez guard, independently re-derived alongside the main kernel's.
1075fn check_terminates_mutual(genv: &Context, defs: &[Db], base: usize) -> RResult<()> {
1076    let n = defs.len();
1077    let base_depth = base + n;
1078    let mut fix_positions = HashMap::new();
1079    let mut located: Vec<(usize, String, &Db)> = Vec::with_capacity(n);
1080    for (j, body) in defs.iter().enumerate() {
1081        let (struct_level, ind, guard_body, struct_pos) = locate_struct(genv, body, base_depth)?;
1082        fix_positions.insert(base + j, struct_pos);
1083        located.push((struct_level, ind, guard_body));
1084    }
1085    for (struct_level, ind, guard_body) in &located {
1086        guard(
1087            genv,
1088            guard_body,
1089            &fix_positions,
1090            *struct_level,
1091            ind,
1092            &HashSet::new(),
1093            struct_level + 1,
1094        )?;
1095    }
1096    Ok(())
1097}
1098
1099/// Walk `term` (at `depth`) checking every recursive call — to ANY block member whose
1100/// level → structural-position mapping is in `fix_positions` — applies a structurally-
1101/// smaller argument (a variable whose level is in `smaller`, the set of constructor-bound
1102/// variables from matching on the structural parameter `struct_level` of `struct_type`).
1103fn guard(
1104    genv: &Context,
1105    term: &Db,
1106    fix_positions: &HashMap<usize, usize>,
1107    struct_level: usize,
1108    struct_type: &str,
1109    smaller: &HashSet<usize>,
1110    depth: usize,
1111) -> RResult<()> {
1112    match term {
1113        Db::App(..) => {
1114            let (head, args) = spine(term);
1115            if let Db::Var(k) = &head {
1116                if let Some(&pos) = level_of(depth, *k).and_then(|lvl| fix_positions.get(&lvl)) {
1117                    // A recursive call to some member: the argument at THAT member's
1118                    // structural position must be structurally smaller — a constructor-
1119                    // bound variable, or an APPLICATION `h a…` whose head `h` is one (the
1120                    // applied-smaller / Giménez rule, sound by strict positivity — see the
1121                    // main kernel's `verify_structural_arg_smaller`).
1122                    match args.get(pos) {
1123                        Some(arg) => {
1124                            let mut h: &Db = arg;
1125                            while let Db::App(f, _) = h {
1126                                h = f;
1127                            }
1128                            match h {
1129                                Db::Var(j)
1130                                    if level_of(depth, *j)
1131                                        .is_some_and(|l| smaller.contains(&l)) => {}
1132                                Db::Var(_) => {
1133                                    return Err(ReCheckError::ill(
1134                                        "recursive call on an argument not headed by a \
1135                                         structurally-smaller variable"
1136                                            .to_string(),
1137                                    ))
1138                                }
1139                                _ => {
1140                                    return Err(ReCheckError::ill(
1141                                        "recursive call whose structural argument is not a \
1142                                         variable or an application of one — cannot certify it \
1143                                         decreases"
1144                                            .to_string(),
1145                                    ))
1146                                }
1147                            }
1148                        }
1149                        None => {
1150                            return Err(ReCheckError::ill(
1151                                "recursive call is missing its structural argument".to_string(),
1152                            ))
1153                        }
1154                    }
1155                    for a in &args {
1156                        guard(genv, a, fix_positions, struct_level, struct_type, smaller, depth)?;
1157                    }
1158                    return Ok(());
1159                }
1160            }
1161            guard(genv, &head, fix_positions, struct_level, struct_type, smaller, depth)?;
1162            for a in &args {
1163                guard(genv, a, fix_positions, struct_level, struct_type, smaller, depth)?;
1164            }
1165            Ok(())
1166        }
1167        Db::Match { disc, motive, cases } => {
1168            // The return motive is an ordinary subterm and MUST be guarded too (a recursive
1169            // occurrence in the return predicate would otherwise evade the check).
1170            guard(genv, motive, fix_positions, struct_level, struct_type, smaller, depth)?;
1171            // A match on the (un-shadowed) structural parameter guards the recursive
1172            // calls in its cases: each constructor argument is a structural subterm.
1173            if let Db::Var(k) = &**disc {
1174                if level_of(depth, *k) == Some(struct_level) {
1175                    return guard_match_cases(
1176                        genv, cases, struct_type, fix_positions, struct_level, smaller, depth,
1177                    );
1178                }
1179            }
1180            guard(genv, disc, fix_positions, struct_level, struct_type, smaller, depth)?;
1181            for c in cases {
1182                guard(genv, c, fix_positions, struct_level, struct_type, smaller, depth)?;
1183            }
1184            Ok(())
1185        }
1186        // Guard a binder's DOMAIN annotation (current depth) as well as its body (one
1187        // deeper). `Fix` has no domain.
1188        Db::Lam(dom, inner) | Db::Pi(dom, inner) => {
1189            guard(genv, dom, fix_positions, struct_level, struct_type, smaller, depth)?;
1190            guard(genv, inner, fix_positions, struct_level, struct_type, smaller, depth + 1)
1191        }
1192        Db::Fix(inner) => {
1193            guard(genv, inner, fix_positions, struct_level, struct_type, smaller, depth + 1)
1194        }
1195        // A nested mutual block introduces `n` fresh levels; its own termination is checked
1196        // when it is type-checked. Descend into each body, past those `n` binders — an
1197        // outer recursive call from within it must still decrease.
1198        Db::MutualFix { defs, .. } => {
1199            for b in defs {
1200                guard(genv, b, fix_positions, struct_level, struct_type, smaller, depth + defs.len())?;
1201            }
1202            Ok(())
1203        }
1204        Db::Let(ty, value, body) => {
1205            // `ty`/`value` at the current depth; `body` one binder deeper. The
1206            // bound value is NOT marked smaller (conservative guard).
1207            guard(genv, ty, fix_positions, struct_level, struct_type, smaller, depth)?;
1208            guard(genv, value, fix_positions, struct_level, struct_type, smaller, depth)?;
1209            guard(genv, body, fix_positions, struct_level, struct_type, smaller, depth + 1)
1210        }
1211        Db::Var(k) => {
1212            // A bare member name (not the head of a guarded call) is the higher-order
1213            // escape — a recursive name used as a first-class value. Reject it.
1214            if level_of(depth, *k).is_some_and(|lvl| fix_positions.contains_key(&lvl)) {
1215                Err(ReCheckError::ill(
1216                    "recursive name occurs as a first-class value, not applied to a \
1217                     structurally-smaller argument"
1218                        .to_string(),
1219                ))
1220            } else {
1221                Ok(())
1222            }
1223        }
1224        Db::Sort(_) | Db::Global(_) | Db::Const { .. } | Db::Lit(_) => Ok(()),
1225    }
1226}
1227
1228/// Guard each case of a match on the structural parameter: mark every constructor
1229/// argument it binds as structurally smaller, then check the case body.
1230fn guard_match_cases(
1231    genv: &Context,
1232    cases: &[Db],
1233    struct_type: &str,
1234    fix_positions: &HashMap<usize, usize>,
1235    struct_level: usize,
1236    smaller: &HashSet<usize>,
1237    depth: usize,
1238) -> RResult<()> {
1239    let arities: Vec<usize> =
1240        genv.get_constructors(struct_type).iter().map(|(_, t)| count_pi_named(t)).collect();
1241    for (i, case) in cases.iter().enumerate() {
1242        let arity = arities.get(i).copied().unwrap_or(0);
1243        let mut smaller2 = smaller.clone();
1244        let mut cur = case;
1245        let mut d = depth;
1246        // Peel up to `arity` λs (a parametric inductive's case binds fewer than its
1247        // constructor's total parameter count — type parameters are fixed), marking each
1248        // bound variable's level as structurally smaller.
1249        for _ in 0..arity {
1250            if let Db::Lam(_, inner) = cur {
1251                smaller2.insert(d);
1252                cur = inner;
1253                d += 1;
1254            } else {
1255                break;
1256            }
1257        }
1258        guard(genv, cur, fix_positions, struct_level, struct_type, &smaller2, d)?;
1259    }
1260    Ok(())
1261}
1262
1263/// Type a `match`: discriminant must be inductive; the motive (function `λx:I.T` or a
1264/// raw type) gives the return type; case count must equal constructor count; each case
1265/// is checked against the type derived from its constructor's signature; and a `Prop`
1266/// discriminant may only large-eliminate into `Type` if it is a subsingleton.
1267fn infer_match(
1268    genv: &Context,
1269    lctx: &[Db],
1270    disc: &Db,
1271    motive: &Db,
1272    cases: &[Db],
1273) -> RResult<Db> {
1274    // 1. Discriminant's inductive type and its type arguments.
1275    let disc_ty = whnf(genv, &infer(genv, lctx, disc)?);
1276    let (ind_name, type_args) = extract_inductive(genv, &disc_ty).ok_or_else(|| {
1277        ReCheckError::unsupported(format!(
1278            "match discriminant of unrecognized type {}",
1279            from_db(&disc_ty, lctx.len())
1280        ))
1281    })?;
1282
1283    // 2. Parameter/index split: the first `p` type arguments are uniform parameters; any
1284    // remaining ones are INDICES the motive abstracts over (an indexed family like `Eq`).
1285    // `indexed == false` is the ordinary eliminator, handled exactly as before.
1286    let p = genv.inductive_num_params(&ind_name).min(type_args.len());
1287    let indexed = type_args.len() > p;
1288    let eff_motive = if indexed {
1289        let _ = infer(genv, lctx, motive)?; // the motive must at least type-check
1290        None
1291    } else {
1292        Some(effective_motive(genv, lctx, motive, &disc_ty)?)
1293    };
1294
1295    // 3. Coverage: exactly one case per constructor, in registration order.
1296    let ctor_names: Vec<String> =
1297        genv.get_constructors(&ind_name).iter().map(|(n, _)| n.to_string()).collect();
1298    if cases.len() != ctor_names.len() {
1299        return Err(ReCheckError::ill(format!(
1300            "match on {} has {} cases but {} constructors",
1301            ind_name,
1302            cases.len(),
1303            ctor_names.len()
1304        )));
1305    }
1306
1307    // 4. Each case against its constructor-derived type.
1308    for (case, cname) in cases.iter().zip(ctor_names.iter()) {
1309        let case_ty = match &eff_motive {
1310            Some(eff) => case_type(genv, eff, cname, &type_args)?,
1311            None => case_type_indexed(genv, motive, cname, &type_args[..p])?,
1312        };
1313        check_case(genv, lctx, case, &case_ty)?;
1314    }
1315
1316    // 5. Return type: the motive applied to the discriminant's indices, then the scrutinee.
1317    let ret = match_return_type(genv, lctx, motive, disc, &disc_ty)?;
1318
1319    // 6. Large-elimination restriction: a `Prop` may be eliminated into `Type` only if it
1320    // is a subsingleton (empty, or one constructor with purely propositional arguments).
1321    let disc_sort = whnf(genv, &infer(genv, lctx, &disc_ty)?);
1322    if matches!(disc_sort, Db::Sort(Universe::Prop)) {
1323        let ret_sort = whnf(genv, &infer(genv, lctx, &ret)?);
1324        let large = !matches!(ret_sort, Db::Sort(Universe::Prop));
1325        if large && !is_subsingleton(genv, &ind_name)? {
1326            return Err(ReCheckError::ill(format!(
1327                "large elimination of non-subsingleton proposition '{}' into a larger sort",
1328                ind_name
1329            )));
1330        }
1331    }
1332
1333    Ok(ret)
1334}
1335
1336/// Extract the inductive name and its type arguments from a (head-normal) type: peel the
1337/// application spine; the head must be a `Global` registered as an inductive.
1338fn extract_inductive(genv: &Context, ty: &Db) -> Option<(String, Vec<Db>)> {
1339    let (head, args) = spine(ty);
1340    match head {
1341        Db::Global(name) if genv.is_inductive(&name) => Some((name, args)),
1342        _ => None,
1343    }
1344}
1345
1346/// The expected type of the case for `ctor_name`, given the `motive` and the
1347/// discriminant's `type_args`. The constructor's type `Π(p₀:U₀)…Π(pₘ₋₁:Uₘ₋₁). Ind …`
1348/// has its leading `|type_args|` parameters instantiated by the type arguments
1349/// (β-style), and its final codomain replaced by `motive (Ctor type_args value_args)`,
1350/// leaving `Π(value params). motive (Ctor …)`. Pure de Bruijn — no names to capture.
1351fn case_type(genv: &Context, motive: &Db, ctor_name: &str, type_args: &[Db]) -> RResult<Db> {
1352    let ctor_named = genv
1353        .get_global(ctor_name)
1354        .ok_or_else(|| ReCheckError::ill(format!("unknown constructor '{}'", ctor_name)))?
1355        .clone();
1356    let ctor_db = to_db(&ctor_named, &mut Vec::new())?;
1357
1358    // Instantiate the type parameters (each `type_args[i]` is closed over the telescope).
1359    let mut body = ctor_db;
1360    for ta in type_args {
1361        match body {
1362            Db::Pi(_dom, rest) => body = beta_open(&rest, ta),
1363            _ => {
1364                return Err(ReCheckError::ill(format!(
1365                    "constructor '{}' has fewer parameters than the discriminant's type arguments",
1366                    ctor_name
1367                )))
1368            }
1369        }
1370    }
1371
1372    // Peel the remaining value-parameter telescope.
1373    let mut value_doms = Vec::new();
1374    let mut cur = body;
1375    while let Db::Pi(dom, rest) = cur {
1376        value_doms.push(*dom);
1377        cur = *rest;
1378    }
1379    let num_val = value_doms.len();
1380    let lift = num_val as isize;
1381
1382    // Build `Ctor type_args value_vars` at the bottom of the value telescope.
1383    let mut applied = Db::Global(ctor_name.to_string());
1384    for ta in type_args {
1385        applied = Db::App(Box::new(applied), Box::new(shift(ta, lift, 0)));
1386    }
1387    for p in 0..num_val {
1388        applied = Db::App(Box::new(applied), Box::new(Db::Var(num_val - 1 - p)));
1389    }
1390
1391    let motive_bot = shift(motive, lift, 0);
1392    let cod = whnf(genv, &Db::App(Box::new(motive_bot), Box::new(applied)));
1393
1394    // Re-wrap the value parameters.
1395    let mut case_ty = cod;
1396    for dom in value_doms.into_iter().rev() {
1397        case_ty = Db::Pi(Box::new(dom), Box::new(case_ty));
1398    }
1399    Ok(case_ty)
1400}
1401
1402/// A match's return type, indexed-aware. For an ordinary (non-indexed) inductive it is the
1403/// motive applied to the discriminant. For an INDEXED family the motive abstracts over the
1404/// indices too, so it is applied to the discriminant's own index arguments and THEN the
1405/// discriminant — `Eq.rec`'s motive `P` yields `P y h` for a scrutinee `h : Eq A x y`.
1406fn match_return_type(genv: &Context, lctx: &[Db], motive: &Db, disc: &Db, disc_ty: &Db) -> RResult<Db> {
1407    let (ind_name, type_args) = extract_inductive(genv, disc_ty).ok_or_else(|| {
1408        ReCheckError::unsupported(format!(
1409            "match discriminant of unrecognized type {}",
1410            from_db(disc_ty, lctx.len())
1411        ))
1412    })?;
1413    let p = genv.inductive_num_params(&ind_name).min(type_args.len());
1414    if type_args.len() > p {
1415        let mut ret = motive.clone();
1416        for idx in &type_args[p..] {
1417            ret = Db::App(Box::new(ret), Box::new(idx.clone()));
1418        }
1419        ret = Db::App(Box::new(ret), Box::new(disc.clone()));
1420        Ok(whnf(genv, &ret))
1421    } else {
1422        let eff = effective_motive(genv, lctx, motive, disc_ty)?;
1423        Ok(whnf(genv, &Db::App(Box::new(eff), Box::new(disc.clone()))))
1424    }
1425}
1426
1427/// The expected case type for `ctor_name` in an INDEXED match. Only the inductive's
1428/// `params` are instantiated (not the trailing indices); the constructor's remaining value
1429/// arguments become the case's `Π` binders; and the codomain is the `motive` applied to
1430/// the constructor's RESULT indices (its declared return-type arguments past the
1431/// parameters) and then the constructor value. Pure de Bruijn — mirrors [`case_type`].
1432fn case_type_indexed(genv: &Context, motive: &Db, ctor_name: &str, params: &[Db]) -> RResult<Db> {
1433    let ctor_named = genv
1434        .get_global(ctor_name)
1435        .ok_or_else(|| ReCheckError::ill(format!("unknown constructor '{}'", ctor_name)))?
1436        .clone();
1437    let ctor_db = to_db(&ctor_named, &mut Vec::new())?;
1438
1439    // Instantiate the inductive's PARAMETERS only (the leading `params.len()` binders).
1440    let mut body = ctor_db;
1441    for pa in params {
1442        match body {
1443            Db::Pi(_dom, rest) => body = beta_open(&rest, pa),
1444            _ => {
1445                return Err(ReCheckError::ill(format!(
1446                    "constructor '{}' has fewer parameters than the inductive",
1447                    ctor_name
1448                )))
1449            }
1450        }
1451    }
1452
1453    // Peel the value-parameter telescope; the residual is the constructor's result type.
1454    let mut value_doms = Vec::new();
1455    let mut cur = body;
1456    while let Db::Pi(dom, rest) = cur {
1457        value_doms.push(*dom);
1458        cur = *rest;
1459    }
1460    let num_val = value_doms.len();
1461    let lift = num_val as isize;
1462
1463    // The constructor's RESULT indices: its result-type spine arguments past the params
1464    // (already expressed under the value binders, so no shift).
1465    let (_head, res_args) = spine(&cur);
1466    let result_indices: Vec<Db> = res_args.into_iter().skip(params.len()).collect();
1467
1468    // Build `Ctor params value_vars` at the bottom of the value telescope.
1469    let mut applied = Db::Global(ctor_name.to_string());
1470    for pa in params {
1471        applied = Db::App(Box::new(applied), Box::new(shift(pa, lift, 0)));
1472    }
1473    for i in 0..num_val {
1474        applied = Db::App(Box::new(applied), Box::new(Db::Var(num_val - 1 - i)));
1475    }
1476
1477    // `motive result_indices… applied`, all under the value binders.
1478    let mut cod = shift(motive, lift, 0);
1479    for ri in &result_indices {
1480        cod = Db::App(Box::new(cod), Box::new(ri.clone()));
1481    }
1482    cod = Db::App(Box::new(cod), Box::new(applied));
1483    let cod = whnf(genv, &cod);
1484
1485    // Re-wrap the value parameters.
1486    let mut case_ty = cod;
1487    for dom in value_doms.into_iter().rev() {
1488        case_ty = Db::Pi(Box::new(dom), Box::new(case_ty));
1489    }
1490    Ok(case_ty)
1491}
1492
1493/// Whether an inductive is a subsingleton `Prop`: zero constructors (e.g. `False`), or
1494/// exactly one whose arguments beyond the inductive's parameters are all propositional
1495/// (e.g. `And`, `eq`). Only these may be large-eliminated into `Type`.
1496fn is_subsingleton(genv: &Context, ind: &str) -> RResult<bool> {
1497    let ctors: Vec<(String, Term)> = genv
1498        .get_constructors(ind)
1499        .iter()
1500        .map(|(n, t)| (n.to_string(), (*t).clone()))
1501        .collect();
1502    match ctors.len() {
1503        0 => Ok(true),
1504        1 => {
1505            let arity = inductive_arity(genv, ind);
1506            let ctor_db = to_db(&ctors[0].1, &mut Vec::new())?;
1507            let mut lctx: Vec<Db> = Vec::new();
1508            let mut cur = ctor_db;
1509            let mut i = 0;
1510            while let Db::Pi(dom, rest) = cur {
1511                if i >= arity && infer_sort(genv, &lctx, &dom)? != Universe::Prop {
1512                    return Ok(false);
1513                }
1514                lctx.push(*dom);
1515                cur = *rest;
1516                i += 1;
1517            }
1518            Ok(true)
1519        }
1520        _ => Ok(false),
1521    }
1522}
1523
1524/// Infer `t`'s type and require it to be a sort; return that universe.
1525fn infer_sort(genv: &Context, lctx: &[Db], t: &Db) -> RResult<Universe> {
1526    let ty = whnf(genv, &infer(genv, lctx, t)?);
1527    match ty {
1528        Db::Sort(u) => Ok(u),
1529        other => Err(ReCheckError::ill(format!(
1530            "expected a type, got something of type {}",
1531            from_db(&other, lctx.len())
1532        ))),
1533    }
1534}
1535
1536/// Check `t` against `expected` under cumulativity.
1537/// Check a `match` case against its constructor-derived type. A case's pattern binders
1538/// carry only PLACEHOLDER types from the surface parser (`λx:_. …`), so — exactly as the
1539/// main kernel does — we ignore them and take each binder's type from the constructor
1540/// telescope in `case_ty` (`Π(a:Aₖ). … motive (Ctor …)`): peel the case `λ` and the `Π`
1541/// in lockstep, pushing the constructor-derived domains into the context, then check the
1542/// case body against the residual codomain. On a real (non-placeholder) case `λ` this is
1543/// identical to `check`, since the binder type equals the telescope domain.
1544fn check_case(genv: &Context, lctx: &[Db], case: &Db, case_ty: &Db) -> RResult<()> {
1545    match (case, case_ty) {
1546        (Db::Lam(_, body), Db::Pi(dom, cod)) => {
1547            let mut ext = lctx.to_vec();
1548            ext.push((**dom).clone());
1549            check_case(genv, &ext, body, cod)
1550        }
1551        _ => check(genv, lctx, case, case_ty),
1552    }
1553}
1554
1555fn check(genv: &Context, lctx: &[Db], t: &Db, expected: &Db) -> RResult<()> {
1556    let inferred = infer(genv, lctx, t)?;
1557    if is_sub(genv, lctx, &inferred, expected) {
1558        Ok(())
1559    } else {
1560        Err(ReCheckError::ill(format!(
1561            "type mismatch: have {}, expected {}",
1562            from_db(&whnf(genv, &inferred), lctx.len()),
1563            from_db(&whnf(genv, expected), lctx.len())
1564        )))
1565    }
1566}
1567
1568fn lit_type_name(l: &Literal) -> &'static str {
1569    match l {
1570        Literal::Int(_) | Literal::BigInt(_) => "Int",
1571        Literal::Nat(_) => "Nat",
1572        Literal::Float(_) => "Float",
1573        Literal::Text(_) => "Text",
1574        Literal::Duration(_) => "Duration",
1575        Literal::Date(_) => "Date",
1576        Literal::Moment(_) => "Moment",
1577    }
1578}
1579
1580// ---------------------------------------------------------------------------
1581// Public API
1582// ---------------------------------------------------------------------------
1583
1584/// Independently re-check `term` in `ctx`, returning its inferred type on success.
1585///
1586/// A SEPARATE implementation from [`infer_type`](crate::infer_type): a de Bruijn core
1587/// (now including the `Match` eliminator) with its own reduction and cumulative
1588/// conversion, so a bug in one checker is unlikely to be shared by the other. Returns
1589/// [`ReCheckError::Unsupported`] for the fragment it does not yet cover (`Fix`/`Hole`,
1590/// or an unrecognized inductive) — never a false pass.
1591pub fn recheck(ctx: &Context, term: &Term) -> RResult<Term> {
1592    let db = to_db(term, &mut Vec::new())?;
1593    let ty = infer(ctx, &[], &db)?;
1594    Ok(from_db(&ty, 0))
1595}
1596
1597/// The verdict of cross-checking a term with BOTH kernels.
1598#[derive(Debug, Clone, PartialEq)]
1599pub enum DoubleCheck {
1600    /// Both kernels accepted and inferred definitionally-equal types — the strongest
1601    /// guarantee: two independent checkers concur.
1602    Agreed,
1603    /// The main kernel accepted, but the re-checker hit a construct outside its current
1604    /// fragment (a `Fix`/`Hole`, or an inductive it does not recognize). The term is
1605    /// single-checked, honestly flagged — not a soundness failure, just incomplete
1606    /// redundancy.
1607    MainOnlyReCheckerIncomplete(String),
1608    /// The two kernels DISAGREE: one accepted and the other rejected, or they inferred
1609    /// non-equal types. A soundness alarm that must never fire on a valid proof.
1610    Disagree(String),
1611}
1612
1613/// Cross-check `term` against both [`infer_type`](crate::infer_type) and `recheck`.
1614///
1615/// The de Bruijn criterion in action: a proof term is most trustworthy when two
1616/// independently-written kernels, on different representations, agree on its type.
1617/// Disagreement is surfaced loudly; an incomplete re-check (the `Fix` fragment) is
1618/// surfaced honestly rather than dressed up as agreement.
1619pub fn double_check(ctx: &Context, term: &Term) -> DoubleCheck {
1620    let main = crate::infer_type(ctx, term);
1621    let recheck_result = recheck(ctx, term);
1622    match (main, recheck_result) {
1623        (Ok(main_ty), Ok(re_ty)) => {
1624            match (to_db(&main_ty, &mut Vec::new()), to_db(&re_ty, &mut Vec::new())) {
1625                (Ok(m), Ok(r)) if def_eq(ctx, &[], &m, &r) => DoubleCheck::Agreed,
1626                (Ok(_), Ok(_)) => DoubleCheck::Disagree(format!(
1627                    "kernels inferred different types: main={}, recheck={}",
1628                    main_ty, re_ty
1629                )),
1630                _ => DoubleCheck::MainOnlyReCheckerIncomplete(
1631                    "inferred type outside re-checker fragment".to_string(),
1632                ),
1633            }
1634        }
1635        (Ok(_), Err(ReCheckError::Unsupported(why))) => {
1636            DoubleCheck::MainOnlyReCheckerIncomplete(why)
1637        }
1638        (Ok(_), Err(ReCheckError::Ill(why))) => DoubleCheck::Disagree(format!(
1639            "main kernel accepted but re-checker rejected: {}",
1640            why
1641        )),
1642        (Err(e), Ok(_)) => DoubleCheck::Disagree(format!(
1643            "re-checker accepted but main kernel rejected: {:?}",
1644            e
1645        )),
1646        (Err(_), Err(_)) => DoubleCheck::Agreed,
1647    }
1648}