Skip to main content

logicaffeine_proof/
certifier.rs

1//! The Certifier: Converts DerivationTrees to Kernel Terms.
2//!
3//! This is the Great Bridge between the Proof Engine and the Kernel.
4//! Via Curry-Howard, each proof rule maps to a term constructor:
5//!
6//! | DerivationTree Rule     | Kernel Term                           |
7//! |-------------------------|---------------------------------------|
8//! | Axiom / PremiseMatch    | Term::Global(name)                    |
9//! | ModusPonens [impl, arg] | Term::App(certify(impl), certify(arg))|
10//! | ConjunctionIntro [p, q] | conj P Q p_proof q_proof              |
11
12use logicaffeine_kernel::{Context, KernelError, KernelResult, Term};
13
14use crate::{DerivationTree, InferenceRule, ProofExpr, ProofTerm};
15
16// =============================================================================
17// INDUCTION STATE - Tracks context during induction certification
18// =============================================================================
19
20/// State for tracking induction during certification.
21/// When certifying a recursive case, IH references become recursive calls.
22/// A single case may bind several recursive arguments (e.g. `Node(l, r)`), so it
23/// carries one (recursive-argument variable, IH conclusion) entry per hypothesis.
24struct InductionState {
25    /// Name for self-reference in the fixpoint (e.g., "rec_n")
26    fix_name: String,
27    /// One entry per recursive argument: the bound variable and the IH it proves.
28    /// A `PremiseMatch` whose conclusion matches an entry's IH resolves to
29    /// `rec_name <var>` — the recursive call on that argument.
30    ihs: Vec<(String, ProofExpr)>,
31}
32
33// =============================================================================
34// CERTIFICATION CONTEXT
35// =============================================================================
36
37/// Context for certifying derivation trees into kernel terms.
38///
39/// Wraps a kernel [`Context`] and tracks additional state
40/// needed during certification:
41///
42/// - **Local variables**: Variables bound by lambda abstractions during traversal
43/// - **Induction state**: For resolving IH (inductive hypothesis) references in step cases
44///
45/// # Lifetime
46///
47/// The `'a` lifetime ties this context to the kernel context it wraps.
48/// The certification context borrows the kernel context immutably.
49///
50/// # Example
51///
52/// ```
53/// use logicaffeine_proof::certifier::CertificationContext;
54/// use logicaffeine_kernel::Context;
55///
56/// let kernel_ctx = Context::new();
57/// let cert_ctx = CertificationContext::new(&kernel_ctx);
58/// ```
59///
60/// # See Also
61///
62/// * [`certify`] - The main function that uses this context
63/// * [`Context`] - The underlying kernel context
64pub struct CertificationContext<'a> {
65    kernel_ctx: &'a Context,
66    /// Local variables in scope (from lambda abstractions)
67    locals: Vec<String>,
68    /// Local *hypotheses* in scope, keyed by the proposition they prove and
69    /// paired with the kernel term that witnesses it. Introduced by reductio
70    /// (the assumed proposition), case analysis (the case formula / its negation
71    /// in each branch), and existential elimination (the witness body and its
72    /// projected conjuncts). A `PremiseMatch` whose conclusion matches one of
73    /// these resolves to its witness term rather than to a global axiom. The
74    /// witness is usually a bound variable (`Term::Var`), but for a projected
75    /// conjunct of an `And` hypothesis it is a `Match` that eliminates the
76    /// conjunction.
77    /// Each entry is shared behind `Arc` so extending the context at a binder copies
78    /// pointers, not whole (proposition, witness) pairs — building a large certified
79    /// proof extends this thousands of times.
80    local_hyps: Vec<std::sync::Arc<(ProofExpr, Term)>>,
81    /// Induction state for IH resolution (only set during step case)
82    induction_state: Option<InductionState>,
83}
84
85impl<'a> CertificationContext<'a> {
86    pub fn new(kernel_ctx: &'a Context) -> Self {
87        Self {
88            kernel_ctx,
89            locals: Vec::new(),
90            local_hyps: Vec::new(),
91            induction_state: None,
92        }
93    }
94
95    /// Create a new context with an additional local variable.
96    fn with_local(&self, name: &str) -> Self {
97        let mut new_locals = self.locals.clone();
98        new_locals.push(name.to_string());
99        Self {
100            kernel_ctx: self.kernel_ctx,
101            locals: new_locals,
102            local_hyps: self.local_hyps.clone(),
103            induction_state: self.induction_state.clone(),
104        }
105    }
106
107    /// Create a new context with an additional local hypothesis: a proof of
108    /// `prop` is available under the name `var`.
109    fn with_local_hyp(&self, prop: &ProofExpr, var: &str) -> Self {
110        self.with_local_hyp_term(prop, Term::Var(var.to_string()))
111    }
112
113    /// Create a new context with an additional local hypothesis: a proof of
114    /// `prop` is given by the kernel term `witness`. Unlike [`with_local_hyp`],
115    /// the witness need not be a bare variable — e.g. a projection that
116    /// eliminates an `And` to recover one conjunct.
117    fn with_local_hyp_term(&self, prop: &ProofExpr, witness: Term) -> Self {
118        let mut new_hyps = self.local_hyps.clone();
119        new_hyps.push(std::sync::Arc::new((prop.clone(), witness)));
120        Self {
121            kernel_ctx: self.kernel_ctx,
122            locals: self.locals.clone(),
123            local_hyps: new_hyps,
124            induction_state: self.induction_state.clone(),
125        }
126    }
127
128    /// A fresh hypothesis variable name, unique along the current branch.
129    fn fresh_hyp_name(&self) -> String {
130        format!("_hyp{}", self.local_hyps.len())
131    }
132
133    /// If `conclusion` is proved by a local hypothesis, return its witness term.
134    fn get_local_hyp(&self, conclusion: &ProofExpr) -> Option<Term> {
135        self.local_hyps
136            .iter()
137            .rev()
138            .find(|h| h.0 == *conclusion)
139            .map(|h| h.1.clone())
140    }
141
142    /// Create a new context with single-IH induction state (the Nat/List step case).
143    fn with_induction(&self, fix_name: &str, step_var: &str, ih: ProofExpr) -> Self {
144        self.with_induction_multi(fix_name, vec![(step_var.to_string(), ih)])
145    }
146
147    /// Create a new context whose induction hypotheses are `ihs` — one entry per
148    /// recursive argument of the constructor case being certified.
149    fn with_induction_multi(&self, fix_name: &str, ihs: Vec<(String, ProofExpr)>) -> Self {
150        Self {
151            kernel_ctx: self.kernel_ctx,
152            locals: self.locals.clone(),
153            local_hyps: self.local_hyps.clone(),
154            induction_state: Some(InductionState {
155                fix_name: fix_name.to_string(),
156                ihs,
157            }),
158        }
159    }
160
161    /// Check if a name is a local variable.
162    fn is_local(&self, name: &str) -> bool {
163        self.locals.iter().any(|n| n == name)
164    }
165
166    /// Check if this conclusion matches one of the IHs in the current induction
167    /// context. Returns the corresponding recursive call `rec <var>` if it matches.
168    fn get_ih_term(&self, conclusion: &ProofExpr) -> Option<Term> {
169        let state = self.induction_state.as_ref()?;
170        for (var, ih) in &state.ihs {
171            if conclusions_match(conclusion, ih) {
172                // IH becomes a recursive call on that argument: rec <var>
173                return Some(Term::App(
174                    Box::new(Term::Var(state.fix_name.clone())),
175                    Box::new(Term::Var(var.clone())),
176                ));
177            }
178        }
179        None
180    }
181}
182
183impl Clone for InductionState {
184    fn clone(&self) -> Self {
185        Self {
186            fix_name: self.fix_name.clone(),
187            ihs: self.ihs.clone(),
188        }
189    }
190}
191
192/// Check if two ProofExprs are structurally equivalent (for IH matching).
193/// Uses PartialEq derivation.
194fn conclusions_match(a: &ProofExpr, b: &ProofExpr) -> bool {
195    a == b
196}
197
198/// Certify a derivation tree, producing a kernel term.
199///
200/// Converts a proof (derivation tree) into a typed lambda calculus term
201/// via the Curry-Howard correspondence. The resulting term, when type-checked
202/// by the kernel, should have the type corresponding to the tree's conclusion.
203///
204/// # Arguments
205///
206/// * `tree` - The derivation tree to certify
207/// * `ctx` - The certification context (wraps kernel context)
208///
209/// # Returns
210///
211/// * `Ok(term)` - The kernel term representing this proof
212/// * `Err(_)` - If certification fails (missing hypothesis, wrong premises, etc.)
213///
214/// # Curry-Howard Mapping
215///
216/// | Inference Rule | Kernel Term |
217/// |----------------|-------------|
218/// | `Axiom` / `PremiseMatch` | `Term::Global(name)` (hypothesis lookup) |
219/// | `ModusPonens` | `Term::App(certify(impl), certify(arg))` |
220/// | `ConjunctionIntro` | `conj P Q p_proof q_proof` |
221/// | `UniversalInst(w)` | `Term::App(forall_proof, w)` |
222/// | `UniversalIntro { var, type }` | `Term::Lambda { param: var, ... }` |
223/// | `StructuralInduction` | `Term::Fix { name, body: λn. match n ... }` |
224/// | `Rewrite { from, to }` | `Eq_rec A from P proof to eq_proof` |
225/// | `ExistentialIntro` | `witness A P w proof` |
226///
227/// # Induction Handling
228///
229/// Structural induction produces fixpoint terms. The step case certification
230/// tracks the induction variable so that IH references become recursive calls.
231///
232/// # See Also
233///
234/// * [`CertificationContext`] - The context passed to this function
235/// * [`DerivationTree`] - The input proof structure
236/// * [`InferenceRule`] - The rules being certified
237pub fn certify(tree: &DerivationTree, ctx: &CertificationContext) -> KernelResult<Term> {
238    match &tree.rule {
239        // Axiom or direct hypothesis reference
240        InferenceRule::Axiom | InferenceRule::PremiseMatch => {
241            certify_hypothesis(&tree.conclusion, ctx)
242        }
243
244        // Modus Ponens: App(impl_proof, arg_proof)
245        // P → Q, P ⊢ Q becomes (h1 h2) : Q
246        InferenceRule::ModusPonens => {
247            if tree.premises.len() != 2 {
248                return Err(KernelError::CertificationError(
249                    "ModusPonens requires exactly 2 premises".to_string(),
250                ));
251            }
252            let impl_term = certify(&tree.premises[0], ctx)?;
253            let arg_term = certify(&tree.premises[1], ctx)?;
254            Ok(Term::App(Box::new(impl_term), Box::new(arg_term)))
255        }
256
257        // Modus Tollens: P → Q, ¬Q ⊢ ¬P.  Since ¬P unfolds to P → False, the
258        // proof is λ(hp:P). neg_q (impl hp) — feed a hypothetical P through the
259        // implication to get Q, then through ¬Q (= Q → False) to get False.
260        InferenceRule::ModusTollens => {
261            if tree.premises.len() != 2 {
262                return Err(KernelError::CertificationError(
263                    "ModusTollens requires exactly 2 premises (implication, negated consequent)"
264                        .to_string(),
265                ));
266            }
267            let p = match &tree.conclusion {
268                ProofExpr::Not(p) => p.as_ref().clone(),
269                _ => {
270                    return Err(KernelError::CertificationError(
271                        "ModusTollens conclusion must be a negation".to_string(),
272                    ))
273                }
274            };
275            let p_type = proof_expr_to_type(&p)?;
276            let impl_proof = certify(&tree.premises[0], ctx)?;
277            let neg_q_proof = certify(&tree.premises[1], ctx)?;
278            let hp = "__mt_hp".to_string();
279            // λ(hp:P). neg_q_proof (impl_proof hp)  :  P → False  =  ¬P
280            Ok(Term::Lambda {
281                param: hp.clone(),
282                param_type: Box::new(p_type),
283                body: Box::new(Term::App(
284                    Box::new(neg_q_proof),
285                    Box::new(Term::App(
286                        Box::new(impl_proof),
287                        Box::new(Term::Var(hp)),
288                    )),
289                )),
290            })
291        }
292
293        // Reflexivity of equality: a = a.  Curry-Howard: `refl T a`, where
294        // `refl : Π(A:Type). Π(x:A). Eq A x x`. The two sides are definitionally
295        // equal, so `refl T l : Eq T l l` reconciles with `Eq T l r`. The domain `T`
296        // is inferred from the operands (so a ground `le 2 5 = true`, where both
297        // sides reduce to `true : Bool`, certifies as `refl Bool (le 2 5)`).
298        InferenceRule::Reflexivity => {
299            let (l, r) = match &tree.conclusion {
300                ProofExpr::Identity(l, r) => (l, r),
301                _ => {
302                    return Err(KernelError::CertificationError(
303                        "Reflexivity conclusion must be an Identity".to_string(),
304                    ))
305                }
306            };
307            let domain = Term::Global(identity_domain(l, r).to_string());
308            let term = proof_term_to_kernel_term(l)?;
309            Ok(Term::App(
310                Box::new(Term::App(
311                    Box::new(Term::Global("refl".to_string())),
312                    Box::new(domain),
313                )),
314                Box::new(term),
315            ))
316        }
317
318        // Ex falso quodlibet: ⊥ ⊢ G.  False has no constructors, so its eliminator
319        // is a `match` with zero cases — `match false_proof return (λ_:False. G) {}`.
320        InferenceRule::ExFalso => {
321            if tree.premises.len() != 1 {
322                return Err(KernelError::CertificationError(
323                    "ExFalso requires exactly 1 premise (a proof of False)".to_string(),
324                ));
325            }
326            let false_proof = certify(&tree.premises[0], ctx)?;
327            let goal_type = proof_expr_to_type(&tree.conclusion)?;
328            Ok(Term::Match {
329                discriminant: Box::new(false_proof),
330                motive: Box::new(Term::Lambda {
331                    param: "_".to_string(),
332                    param_type: Box::new(Term::Global("False".to_string())),
333                    body: Box::new(goal_type),
334                }),
335                cases: vec![],
336            })
337        }
338
339        // Conjunction Introduction: conj P Q p_proof q_proof
340        // P, Q ⊢ P ∧ Q becomes (conj P Q p q) : And P Q
341        InferenceRule::ConjunctionIntro => {
342            if tree.premises.len() != 2 {
343                return Err(KernelError::CertificationError(
344                    "ConjunctionIntro requires exactly 2 premises".to_string(),
345                ));
346            }
347            let (p_type, q_type) = extract_and_types(&tree.conclusion)?;
348            let p_term = certify(&tree.premises[0], ctx)?;
349            let q_term = certify(&tree.premises[1], ctx)?;
350
351            // Build: conj P Q p_proof q_proof (fully curried)
352            let conj = Term::Global("conj".to_string());
353            let applied = Term::App(
354                Box::new(Term::App(
355                    Box::new(Term::App(
356                        Box::new(Term::App(Box::new(conj), Box::new(p_type))),
357                        Box::new(q_type),
358                    )),
359                    Box::new(p_term),
360                )),
361                Box::new(q_term),
362            );
363            Ok(applied)
364        }
365
366        // Disjunction Introduction: P ⊢ P ∨ Q  (or Q ⊢ P ∨ Q)
367        // Curry-Howard: `left P Q p` or `right P Q q`. The rule is side-agnostic;
368        // we recover the side by matching the proved premise against the disjuncts.
369        InferenceRule::DisjunctionIntro => {
370            if tree.premises.len() != 1 {
371                return Err(KernelError::CertificationError(
372                    "DisjunctionIntro requires exactly 1 premise".to_string(),
373                ));
374            }
375            let (p, q) = match &tree.conclusion {
376                ProofExpr::Or(p, q) => (p.as_ref().clone(), q.as_ref().clone()),
377                _ => {
378                    return Err(KernelError::CertificationError(
379                        "DisjunctionIntro conclusion must be Or".to_string(),
380                    ))
381                }
382            };
383            let proved = &tree.premises[0].conclusion;
384            let ctor = if proved == &p {
385                "left"
386            } else if proved == &q {
387                "right"
388            } else {
389                return Err(KernelError::CertificationError(
390                    "DisjunctionIntro premise proves neither disjunct".to_string(),
391                ));
392            };
393            let p_type = proof_expr_to_type(&p)?;
394            let q_type = proof_expr_to_type(&q)?;
395            let proof_term = certify(&tree.premises[0], ctx)?;
396
397            // left/right : Π(P:Prop). Π(Q:Prop). (P|Q) → Or P Q
398            let applied = Term::App(
399                Box::new(Term::App(
400                    Box::new(Term::App(
401                        Box::new(Term::Global(ctor.to_string())),
402                        Box::new(p_type),
403                    )),
404                    Box::new(q_type),
405                )),
406                Box::new(proof_term),
407            );
408            Ok(applied)
409        }
410
411        // Universal Instantiation: forall x.P(x) |- P(t)
412        // Curry-Howard: Apply the forall-proof (a function) to the witness term
413        InferenceRule::UniversalInst(witness) => {
414            if tree.premises.len() != 1 {
415                return Err(KernelError::CertificationError(
416                    "UniversalInst requires exactly 1 premise".to_string(),
417                ));
418            }
419            let forall_proof = certify(&tree.premises[0], ctx)?;
420            // Check if witness is a local variable (inside a lambda body)
421            let witness_term = if ctx.is_local(witness) {
422                Term::Var(witness.clone())
423            } else {
424                Term::Global(witness.clone())
425            };
426            Ok(Term::App(Box::new(forall_proof), Box::new(witness_term)))
427        }
428
429        // Universal Instantiation at an arbitrary witness TERM: the same
430        // Curry-Howard application, with the witness converted as a full term
431        // (compound witnesses like `add(a, Zero)` included).
432        InferenceRule::UniversalInstTerm(witness) => {
433            if tree.premises.len() != 1 {
434                return Err(KernelError::CertificationError(
435                    "UniversalInstTerm requires exactly 1 premise".to_string(),
436                ));
437            }
438            let forall_proof = certify(&tree.premises[0], ctx)?;
439            let witness_term = match witness {
440                ProofTerm::Constant(name) | ProofTerm::Variable(name)
441                    if ctx.is_local(name) =>
442                {
443                    Term::Var(name.clone())
444                }
445                _ => proof_term_to_kernel_term(witness)?,
446            };
447            Ok(Term::App(Box::new(forall_proof), Box::new(witness_term)))
448        }
449
450        // Universal Introduction: Γ, x:T ⊢ P(x) implies Γ ⊢ ∀x:T. P(x)
451        // Curry-Howard: Wrap the body proof in a Lambda
452        InferenceRule::UniversalIntro { variable, var_type } => {
453            if tree.premises.len() != 1 {
454                return Err(KernelError::CertificationError(
455                    "UniversalIntro requires exactly 1 premise".to_string(),
456                ));
457            }
458
459            // Build the type term
460            let type_term = Term::Global(var_type.clone());
461
462            // Certify body with variable in scope
463            let extended_ctx = ctx.with_local(variable);
464            let body_term = certify(&tree.premises[0], &extended_ctx)?;
465
466            // Wrap in Lambda
467            Ok(Term::Lambda {
468                param: variable.clone(),
469                param_type: Box::new(type_term),
470                body: Box::new(body_term),
471            })
472        }
473
474        // Structural Induction: P(0), ∀k(P(k) → P(S(k))) ⊢ ∀n P(n)
475        // Curry-Howard: fix rec. λn. match n { Zero => base, Succ k => step }
476        InferenceRule::StructuralInduction {
477            variable: var_name,
478            ind_type,
479            step_var,
480        } => {
481            if tree.premises.len() != 2 {
482                return Err(KernelError::CertificationError(
483                    "StructuralInduction requires exactly 2 premises (base, step)".to_string(),
484                ));
485            }
486
487            // Extract motive body from ForAll conclusion
488            let motive_body = extract_motive_body(&tree.conclusion, var_name)?;
489
490            // Generate fix name
491            let fix_name = format!("rec_{}", var_name);
492
493            // === Certify Base Case ===
494            // Base case is certified in the current context (no IH)
495            let base_term = certify(&tree.premises[0], ctx)?;
496
497            // === Certify Step Case ===
498            // IH: P(k) - compute what IH looks like by substituting n -> k
499            let ih_conclusion = compute_ih_conclusion(&tree.conclusion, var_name, step_var)?;
500
501            // Create context with:
502            // 1. step_var as a local (bound by the Succ case lambda)
503            // 2. induction state (for IH resolution)
504            let step_ctx = ctx
505                .with_local(step_var)
506                .with_induction(&fix_name, step_var, ih_conclusion);
507
508            let step_body = certify(&tree.premises[1], &step_ctx)?;
509
510            // Wrap step body in lambda: λk. step_body
511            let step_term = Term::Lambda {
512                param: step_var.clone(),
513                param_type: Box::new(Term::Global(ind_type.clone())),
514                body: Box::new(step_body),
515            };
516
517            // === Build Match ===
518            // match n return (λn:Nat. P(n)) with { Zero => base, Succ k => step }
519            // The motive uses var_name so body references are properly bound
520            let match_term = Term::Match {
521                discriminant: Box::new(Term::Var(var_name.clone())),
522                motive: Box::new(build_motive(ind_type, &motive_body, var_name)),
523                cases: vec![base_term, step_term],
524            };
525
526            // === Build Lambda ===
527            // λn:Nat. match n ...
528            let lambda_term = Term::Lambda {
529                param: var_name.clone(),
530                param_type: Box::new(Term::Global(ind_type.clone())),
531                body: Box::new(match_term),
532            };
533
534            // === Build Fixpoint ===
535            // fix rec_n. λn. match n ...
536            Ok(Term::Fix {
537                name: fix_name,
538                body: Box::new(lambda_term),
539            })
540        }
541
542        // Generic structural induction over an arbitrary inductive type: one case
543        // per constructor (in registration order), each recursive argument carrying
544        // its own induction hypothesis. Certifies to `fix rec. λx. match x { … }`,
545        // the dependent eliminator — the kernel re-checks coverage, case types, and
546        // the termination guard, so an ill-formed scheme is rejected here.
547        InferenceRule::InductionScheme {
548            variable,
549            ind_type,
550            cases,
551        } => {
552            if tree.premises.len() != cases.len() {
553                return Err(KernelError::CertificationError(format!(
554                    "InductionScheme requires one premise per constructor: {} cases, {} premises",
555                    cases.len(),
556                    tree.premises.len()
557                )));
558            }
559
560            let motive_body = extract_motive_body(&tree.conclusion, variable)?;
561            let fix_name = format!("rec_{}", variable);
562
563            let mut case_terms = Vec::with_capacity(cases.len());
564            for (case, premise) in cases.iter().zip(tree.premises.iter()) {
565                // Argument kernel types, peeled from the constructor's signature.
566                let arg_types = constructor_arg_types(ctx.kernel_ctx, &case.constructor)?;
567                if arg_types.len() != case.args.len() {
568                    return Err(KernelError::CertificationError(format!(
569                        "constructor {} takes {} arguments, case binds {}",
570                        case.constructor,
571                        arg_types.len(),
572                        case.args.len()
573                    )));
574                }
575
576                // Each recursive argument contributes an IH: the motive at that
577                // argument (the induction variable substituted by the argument name).
578                let mut ihs = Vec::new();
579                for arg in &case.args {
580                    if arg.recursive {
581                        let ih = compute_ih_conclusion(&tree.conclusion, variable, &arg.name)?;
582                        ihs.push((arg.name.clone(), ih));
583                    }
584                }
585
586                // Certify the case under a context binding every argument as a local
587                // and exposing the recursive IHs (so IH references become `rec <arg>`).
588                let mut case_ctx = ctx.with_induction_multi(&fix_name, ihs);
589                for arg in &case.args {
590                    case_ctx = case_ctx.with_local(&arg.name);
591                }
592                let case_body = certify(premise, &case_ctx)?;
593
594                // Wrap the body in one lambda per constructor argument:
595                // λ(a0:T0). … λ(aN:TN). body  (nullary constructors stay bare).
596                let mut term = case_body;
597                for (arg, ty) in case.args.iter().zip(arg_types.iter()).rev() {
598                    term = Term::Lambda {
599                        param: arg.name.clone(),
600                        param_type: Box::new(ty.clone()),
601                        body: Box::new(term),
602                    };
603                }
604                case_terms.push(term);
605            }
606
607            // match x return (λx:Ind. P(x)) with { case₀, …, caseₙ }
608            let match_term = Term::Match {
609                discriminant: Box::new(Term::Var(variable.clone())),
610                motive: Box::new(build_motive(ind_type, &motive_body, variable)),
611                cases: case_terms,
612            };
613
614            // fix rec_x. λx:Ind. match x { … }
615            let lambda_term = Term::Lambda {
616                param: variable.clone(),
617                param_type: Box::new(Term::Global(ind_type.clone())),
618                body: Box::new(match_term),
619            };
620
621            Ok(Term::Fix {
622                name: fix_name,
623                body: Box::new(lambda_term),
624            })
625        }
626
627        // Existential Introduction: P(w) ⊢ ∃x.P(x)
628        // Curry-Howard: witness A P w proof
629        InferenceRule::ExistentialIntro {
630            witness: witness_str,
631            witness_type,
632        } => {
633            if tree.premises.len() != 1 {
634                return Err(KernelError::CertificationError(
635                    "ExistentialIntro requires exactly 1 premise".to_string(),
636                ));
637            }
638
639            // Extract variable and body from Exists conclusion
640            let (variable, body) = match &tree.conclusion {
641                ProofExpr::Exists { variable, body } => (variable.clone(), body.as_ref().clone()),
642                _ => {
643                    return Err(KernelError::CertificationError(
644                        "ExistentialIntro conclusion must be Exists".to_string(),
645                    ))
646                }
647            };
648
649            // Determine witness term (local or global)
650            let witness_term = if ctx.is_local(witness_str) {
651                Term::Var(witness_str.clone())
652            } else {
653                Term::Global(witness_str.clone())
654            };
655
656            // Certify the premise (proof of P(witness))
657            let proof_term = certify(&tree.premises[0], ctx)?;
658
659            // The type A comes from the InferenceRule (EXPLICIT INTENT)
660            let type_a = Term::Global(witness_type.clone());
661
662            // Build predicate P = λvar:A. body_type — matching `proof_expr_to_type`'s
663            // encoding of `Exists` EXACTLY (no eta-contracted `P`-direct shortcut for
664            // the `∃y.P(y)` case), so the certified term's type and the goal type
665            // agree structurally without relying on eta-conversion in the kernel.
666            let predicate = Term::Lambda {
667                param: variable.clone(),
668                param_type: Box::new(type_a.clone()),
669                body: Box::new(proof_expr_to_type(&body)?),
670            };
671
672            // Build: witness A P w proof
673            // witness : Π(A:Type). Π(P:A→Prop). Π(x:A). P(x) → Ex A P
674            let witness_ctor = Term::Global("witness".to_string());
675
676            let applied = Term::App(
677                Box::new(Term::App(
678                    Box::new(Term::App(
679                        Box::new(Term::App(Box::new(witness_ctor), Box::new(type_a))),
680                        Box::new(predicate),
681                    )),
682                    Box::new(witness_term),
683                )),
684                Box::new(proof_term),
685            );
686
687            Ok(applied)
688        }
689
690        // Existential Elimination: from ∃x.P(x) and a proof of the goal that uses
691        // a fresh witness `c` with hypothesis P(c), eliminate the existential.
692        // Curry-Howard: pattern-match the existential —
693        //   match ex return (λ_:Ex Entity P. Goal) with witness c h => <Goal proof>
694        // The witness `c` is the Match-bound variable, so within the body we turn
695        // the placeholder constant `c` into that bound variable. Two shapes flow
696        // through here: the prove-⊥ form (`cert_derive_falsum`, conclusion ⊥, body
697        // references the whole `P(c)`) and the forward form (`try_existential_
698        // elimination`, conclusion = the actual goal, body references the FLATTENED
699        // conjuncts of `P(c)`). The motive's body is the conclusion's type — ⊥
700        // gives `False`, recovering the prove-⊥ case unchanged — and the body
701        // proof sees both the whole `P(c)` and each of its conjuncts (the latter
702        // recovered by genuine ∧-elimination of `h`).
703        InferenceRule::ExistentialElim { witness } => {
704            if tree.premises.len() != 2 {
705                return Err(KernelError::CertificationError(
706                    "ExistentialElim requires exactly 2 premises (existential, body)".to_string(),
707                ));
708            }
709            let exist_premise = &tree.premises[0];
710            let body = &tree.premises[1];
711
712            let (var, p_body) = match &exist_premise.conclusion {
713                ProofExpr::Exists { variable, body } => (variable.clone(), body.as_ref().clone()),
714                _ => {
715                    return Err(KernelError::CertificationError(
716                        "ExistentialElim first premise must conclude an Exists".to_string(),
717                    ))
718                }
719            };
720
721            // P(c) as a ProofExpr — the witness substituted for the bound var.
722            let mut subst = crate::unify::Substitution::new();
723            subst.insert(var.clone(), ProofTerm::Constant(witness.clone()));
724            let p_c_expr = crate::unify::apply_subst_to_expr(&p_body, &subst);
725
726            // Discriminant: the proof of the existential (type Ex Entity P).
727            let disc = certify(exist_premise, ctx)?;
728
729            // The witness body is proved by the bound hypothesis `h : P(c)`. Bind
730            // the whole body (the prove-⊥ form references it directly) AND every
731            // flattened conjunct, each recovered by ∧-eliminating `h` (the forward
732            // form references the conjuncts separately).
733            let h = format!("_exh_{}", witness);
734            let h_var = Term::Var(h.clone());
735            let mut body_ctx = ctx.with_local_hyp_term(&p_c_expr, h_var.clone());
736            for (conjunct, proj) in collect_conjunct_hyps(&p_c_expr, &h_var)? {
737                body_ctx = body_ctx.with_local_hyp_term(&conjunct, proj);
738            }
739
740            let body_raw = certify(body, &body_ctx)?;
741            let c_global = Term::Global(witness.clone());
742            let c_var = Term::Var(witness.clone());
743            let body_term = substitute_term_in_kernel(&body_raw, &c_global, &c_var);
744            let p_c_type =
745                substitute_term_in_kernel(&proof_expr_to_type(&p_c_expr)?, &c_global, &c_var);
746
747            // CONSTANT motive: the witness does not escape — the goal is closed (it does
748            // not mention `c`) — so pass the goal type raw and let the kernel synthesize
749            // `λ_:(Ex Entity P). Goal` from the discriminant's own type. (⊥ ↦ `False`,
750            // keeping the prove-⊥ form intact.)
751            let motive = proof_expr_to_type(&tree.conclusion)?;
752
753            // Case for `witness`: λ(c:Entity). λ(h:P(c)). <Goal proof>
754            let case = Term::Lambda {
755                param: witness.clone(),
756                param_type: Box::new(Term::Global("Entity".to_string())),
757                body: Box::new(Term::Lambda {
758                    param: h,
759                    param_type: Box::new(p_c_type),
760                    body: Box::new(body_term),
761                }),
762            };
763
764            Ok(Term::Match {
765                discriminant: Box::new(disc),
766                motive: Box::new(motive),
767                cases: vec![case],
768            })
769        }
770
771        // Equality Rewriting (Leibniz's Law)
772        // a = b, P(a) ⊢ P(b)
773        // Uses: Eq_rec A x P proof y eq_proof
774        InferenceRule::Rewrite { from, to } => {
775            if tree.premises.len() != 2 {
776                return Err(KernelError::CertificationError(
777                    "Rewrite requires exactly 2 premises (equality, source)".to_string(),
778                ));
779            }
780
781            // Certify both premises
782            let eq_proof = certify(&tree.premises[0], ctx)?;
783            let source_proof = certify(&tree.premises[1], ctx)?;
784
785            // Convert terms
786            let from_term = proof_term_to_kernel_term(from)?;
787            let to_term = proof_term_to_kernel_term(to)?;
788
789            // Build the predicate P as a lambda that wraps the goal
790            // P = λz. (goal with 'to' replaced by z)
791            let predicate = build_equality_predicate(&tree.conclusion, to)?;
792
793            // Eq_rec : Π(A:Type). Π(x:A). Π(P:A→Prop). P x → Π(y:A). Eq A x y → P y
794            // Build: Eq_rec D from P source_proof to eq_proof, where D is the domain
795            // of the rewritten terms (Int for arithmetic, else Entity).
796            let eq_rec = Term::Global("Eq_rec".to_string());
797            let domain = Term::Global(term_domain(from).to_string());
798
799            let applied = Term::App(
800                Box::new(Term::App(
801                    Box::new(Term::App(
802                        Box::new(Term::App(
803                            Box::new(Term::App(
804                                Box::new(Term::App(Box::new(eq_rec), Box::new(domain))),
805                                Box::new(from_term),
806                            )),
807                            Box::new(predicate),
808                        )),
809                        Box::new(source_proof),
810                    )),
811                    Box::new(to_term),
812                )),
813                Box::new(eq_proof),
814            );
815
816            Ok(applied)
817        }
818
819        // Equality Symmetry: a = b ⊢ b = a
820        // Uses: Eq_sym A x y proof
821        InferenceRule::EqualitySymmetry => {
822            if tree.premises.len() != 1 {
823                return Err(KernelError::CertificationError(
824                    "EqualitySymmetry requires exactly 1 premise".to_string(),
825                ));
826            }
827
828            let premise_proof = certify(&tree.premises[0], ctx)?;
829
830            // Extract x and y from the premise conclusion (x = y)
831            let (x, y, domain) = match &tree.premises[0].conclusion {
832                ProofExpr::Identity(l, r) => (
833                    proof_term_to_kernel_term(l)?,
834                    proof_term_to_kernel_term(r)?,
835                    Term::Global(identity_domain(l, r).to_string()),
836                ),
837                _ => {
838                    return Err(KernelError::CertificationError(
839                        "EqualitySymmetry premise must be an Identity".to_string(),
840                    ))
841                }
842            };
843
844            // Eq_sym : Π(A:Type). Π(x:A). Π(y:A). Eq A x y → Eq A y x
845            // Build: Eq_sym D x y proof, with D the operands' domain
846            // (Int for arithmetic, Bool for comparisons, else Entity).
847            let eq_sym = Term::Global("Eq_sym".to_string());
848
849            Ok(Term::App(
850                Box::new(Term::App(
851                    Box::new(Term::App(
852                        Box::new(Term::App(Box::new(eq_sym), Box::new(domain))),
853                        Box::new(x),
854                    )),
855                    Box::new(y),
856                )),
857                Box::new(premise_proof),
858            ))
859        }
860
861        // Equality Transitivity: a = b, b = c ⊢ a = c
862        // Uses: Eq_trans A x y z proof1 proof2
863        InferenceRule::EqualityTransitivity => {
864            if tree.premises.len() != 2 {
865                return Err(KernelError::CertificationError(
866                    "EqualityTransitivity requires exactly 2 premises".to_string(),
867                ));
868            }
869
870            let proof1 = certify(&tree.premises[0], ctx)?;
871            let proof2 = certify(&tree.premises[1], ctx)?;
872
873            // Extract x, y from first premise (x = y)
874            let (x, y, domain) = match &tree.premises[0].conclusion {
875                ProofExpr::Identity(l, r) => (
876                    proof_term_to_kernel_term(l)?,
877                    proof_term_to_kernel_term(r)?,
878                    Term::Global(identity_domain(l, r).to_string()),
879                ),
880                _ => {
881                    return Err(KernelError::CertificationError(
882                        "EqualityTransitivity first premise must be Identity".to_string(),
883                    ))
884                }
885            };
886
887            // Extract z from second premise (y = z)
888            let z = match &tree.premises[1].conclusion {
889                ProofExpr::Identity(_, r) => proof_term_to_kernel_term(r)?,
890                _ => {
891                    return Err(KernelError::CertificationError(
892                        "EqualityTransitivity second premise must be Identity".to_string(),
893                    ))
894                }
895            };
896
897            // Eq_trans : Π(A:Type). Π(x:A). Π(y:A). Π(z:A). Eq A x y → Eq A y z → Eq A x z
898            // Build: Eq_trans D x y z proof1 proof2, with D the operands' domain
899            // (Int for arithmetic, Bool for comparisons, else Entity).
900            let eq_trans = Term::Global("Eq_trans".to_string());
901
902            Ok(Term::App(
903                Box::new(Term::App(
904                    Box::new(Term::App(
905                        Box::new(Term::App(
906                            Box::new(Term::App(
907                                Box::new(Term::App(Box::new(eq_trans), Box::new(domain))),
908                                Box::new(x),
909                            )),
910                            Box::new(y),
911                        )),
912                        Box::new(z),
913                    )),
914                    Box::new(proof1),
915                )),
916                Box::new(proof2),
917            ))
918        }
919
920        // `a ≤ b`, `b ≤ c` ⊢ `a ≤ c`:  le_trans a b c p₀ p₁.  Inequalities are the
921        // Prop `Eq Bool (le a b) true`; the middle term comes from the first premise.
922        InferenceRule::LeTrans => {
923            if tree.premises.len() != 2 {
924                return Err(KernelError::CertificationError(
925                    "LeTrans requires exactly 2 premises".to_string(),
926                ));
927            }
928            let (a, b) = le_terms(&tree.conclusion)?;
929            let (_, mid) = le_terms(&tree.premises[0].conclusion)?;
930            let p0 = certify(&tree.premises[0], ctx)?;
931            let p1 = certify(&tree.premises[1], ctx)?;
932            let mut t = Term::Global("le_trans".to_string());
933            for arg in [
934                proof_term_to_kernel_term(&a)?,
935                proof_term_to_kernel_term(&mid)?,
936                proof_term_to_kernel_term(&b)?,
937                p0,
938                p1,
939            ] {
940                t = Term::App(Box::new(t), Box::new(arg));
941            }
942            Ok(t)
943        }
944
945        // `⊢ a ≤ a`:  le_refl a.
946        InferenceRule::LeRefl => {
947            let (a, _) = le_terms(&tree.conclusion)?;
948            Ok(Term::App(
949                Box::new(Term::Global("le_refl".to_string())),
950                Box::new(proof_term_to_kernel_term(&a)?),
951            ))
952        }
953
954        // `a ≤ b`, `c ≤ d` ⊢ `a + c ≤ b + d`:  le_add_mono a b c d p₀ p₁.
955        InferenceRule::LeAddMono => {
956            if tree.premises.len() != 2 {
957                return Err(KernelError::CertificationError(
958                    "LeAddMono requires exactly 2 premises".to_string(),
959                ));
960            }
961            let (lhs, rhs) = le_terms(&tree.conclusion)?;
962            let (a, c) = add_terms(&lhs)?;
963            let (b, d) = add_terms(&rhs)?;
964            let p0 = certify(&tree.premises[0], ctx)?;
965            let p1 = certify(&tree.premises[1], ctx)?;
966            let mut t = Term::Global("le_add_mono".to_string());
967            for arg in [
968                proof_term_to_kernel_term(&a)?,
969                proof_term_to_kernel_term(&b)?,
970                proof_term_to_kernel_term(&c)?,
971                proof_term_to_kernel_term(&d)?,
972                p0,
973                p1,
974            ] {
975                t = Term::App(Box::new(t), Box::new(arg));
976            }
977            Ok(t)
978        }
979
980        // Linear contradiction → ⊥: `premise[0] : le(m,n) = true` with `m > n` is, by
981        // computation, a proof of `Eq Bool false true`; the Bool no-confusion
982        // discriminator turns it into `False`.
983        InferenceRule::LinFalse => {
984            if tree.premises.len() != 1 {
985                return Err(KernelError::CertificationError(
986                    "LinFalse requires exactly 1 premise".to_string(),
987                ));
988            }
989            let le_proof = certify(&tree.premises[0], ctx)?;
990            Ok(build_bool_discriminator(le_proof))
991        }
992
993        // `0 ≤ k`, `a ≤ b` ⊢ `k·a ≤ k·b`:  le_mul_nonneg k a b p₀ p₁.
994        InferenceRule::LeMulNonneg => {
995            if tree.premises.len() != 2 {
996                return Err(KernelError::CertificationError(
997                    "LeMulNonneg requires exactly 2 premises".to_string(),
998                ));
999            }
1000            let (lhs, rhs) = le_terms(&tree.conclusion)?;
1001            let (k, a) = binop_terms("mul", &lhs)?;
1002            let (_, b) = binop_terms("mul", &rhs)?;
1003            let p0 = certify(&tree.premises[0], ctx)?;
1004            let p1 = certify(&tree.premises[1], ctx)?;
1005            let mut t = Term::Global("le_mul_nonneg".to_string());
1006            for arg in [
1007                proof_term_to_kernel_term(&k)?,
1008                proof_term_to_kernel_term(&a)?,
1009                proof_term_to_kernel_term(&b)?,
1010                p0,
1011                p1,
1012            ] {
1013                t = Term::App(Box::new(t), Box::new(arg));
1014            }
1015            Ok(t)
1016        }
1017
1018        // `a ≤ b` ⊢ `0 ≤ b + (-1)·a`:  le_sub a b p₀.
1019        InferenceRule::LeSub => {
1020            if tree.premises.len() != 1 {
1021                return Err(KernelError::CertificationError(
1022                    "LeSub requires exactly 1 premise".to_string(),
1023                ));
1024            }
1025            let (_zero, diff) = le_terms(&tree.conclusion)?;
1026            let (b, neg_a) = binop_terms("add", &diff)?;
1027            let (_neg_one, a) = binop_terms("mul", &neg_a)?;
1028            let p0 = certify(&tree.premises[0], ctx)?;
1029            let mut t = Term::Global("le_sub".to_string());
1030            for arg in [proof_term_to_kernel_term(&a)?, proof_term_to_kernel_term(&b)?, p0] {
1031                t = Term::App(Box::new(t), Box::new(arg));
1032            }
1033            Ok(t)
1034        }
1035
1036        // `a < b` ⊢ `(a + 1) ≤ b` — integer discreteness. The conclusion is
1037        // `le(add a 1, b) = true`, so `a` is the first summand of the sum and `b`
1038        // the right operand; `premise[0]` proves `lt(a, b) = true`. Certifies to
1039        // `lt_succ_le a b p₀` — the kernel axiom application.
1040        InferenceRule::LtSuccLe => {
1041            if tree.premises.len() != 1 {
1042                return Err(KernelError::CertificationError(
1043                    "LtSuccLe requires exactly 1 premise".to_string(),
1044                ));
1045            }
1046            let (succ_a, b) = le_terms(&tree.conclusion)?;
1047            let (a, _one) = binop_terms("add", &succ_a)?;
1048            let p0 = certify(&tree.premises[0], ctx)?;
1049            let mut t = Term::Global("lt_succ_le".to_string());
1050            for arg in [proof_term_to_kernel_term(&a)?, proof_term_to_kernel_term(&b)?, p0] {
1051                t = Term::App(Box::new(t), Box::new(arg));
1052            }
1053            Ok(t)
1054        }
1055
1056        // `a < b + 1` ⊢ `a ≤ b`. The conclusion is `le(a, b) = true`; `premise[0]`
1057        // proves `lt(a, add b 1) = true`. Certifies to `lt_add1_le a b p₀`.
1058        InferenceRule::LtAdd1Le => {
1059            if tree.premises.len() != 1 {
1060                return Err(KernelError::CertificationError(
1061                    "LtAdd1Le requires exactly 1 premise".to_string(),
1062                ));
1063            }
1064            let (a, b) = le_terms(&tree.conclusion)?;
1065            let p0 = certify(&tree.premises[0], ctx)?;
1066            let mut t = Term::Global("lt_add1_le".to_string());
1067            for arg in [proof_term_to_kernel_term(&a)?, proof_term_to_kernel_term(&b)?, p0] {
1068                t = Term::App(Box::new(t), Box::new(arg));
1069            }
1070            Ok(t)
1071        }
1072
1073        // Arithmetic decision: discharge an Int equality with the proof-producing
1074        // oracle. The oracle is untrusted — the term it returns is type-checked by
1075        // the kernel (here, by the caller's `infer_type`), so a wrong proof is
1076        // rejected. Proofs are built from computation + the registered ring axioms.
1077        InferenceRule::ArithDecision => match &tree.conclusion {
1078            ProofExpr::Identity(l, r) => {
1079                let kl = proof_term_to_kernel_term(l)?;
1080                let kr = proof_term_to_kernel_term(r)?;
1081                crate::arith::prove_int_eq(ctx.kernel_ctx, &kl, &kr).ok_or_else(|| {
1082                    KernelError::CertificationError(
1083                        "ArithDecision: arithmetic oracle found no proof".to_string(),
1084                    )
1085                })
1086            }
1087            _ => Err(KernelError::CertificationError(
1088                "ArithDecision conclusion must be an Identity".to_string(),
1089            )),
1090        },
1091
1092        // Proof by kernel evaluation: the leaf carries only the claim. Build
1093        // the kernel proposition and its `Decidable` instance, then let
1094        // `native_decide` construct the `of_decide_eq_true`-shaped term — the
1095        // kernel re-checks it (via the `reduceBool` hook), so a false claim
1096        // certifies to nothing.
1097        InferenceRule::NativeDecide => {
1098            if !tree.premises.is_empty() {
1099                return Err(KernelError::CertificationError(
1100                    "NativeDecide is a leaf (no premises)".to_string(),
1101                ));
1102            }
1103            let prop = proof_expr_to_type_ctx(&tree.conclusion, ctx.kernel_ctx)?;
1104            let inst = decidable_instance_for(&prop).ok_or_else(|| {
1105                KernelError::CertificationError(
1106                    "NativeDecide: no Decidable instance for this proposition".to_string(),
1107                )
1108            })?;
1109            logicaffeine_kernel::native_decide(ctx.kernel_ctx, &prop, &inst).ok_or_else(|| {
1110                KernelError::CertificationError(
1111                    "NativeDecide: evaluation did not decide the goal true".to_string(),
1112                )
1113            })
1114        }
1115
1116        // Contradiction: P and ¬P jointly yield ⊥.
1117        // Curry-Howard: ¬P is `P → False`, so applying the proof of ¬P to the
1118        // proof of P gives `False`. We detect which premise is the negation.
1119        InferenceRule::Contradiction => {
1120            if tree.premises.len() != 2 {
1121                return Err(KernelError::CertificationError(
1122                    "Contradiction requires exactly 2 premises (P and ¬P)".to_string(),
1123                ));
1124            }
1125            let a = &tree.premises[0];
1126            let b = &tree.premises[1];
1127            let (pos, neg) = if is_negation_of(&b.conclusion, &a.conclusion) {
1128                (a, b)
1129            } else if is_negation_of(&a.conclusion, &b.conclusion) {
1130                (b, a)
1131            } else {
1132                return Err(KernelError::CertificationError(format!(
1133                    "Contradiction premises are not a proposition and its negation: {:?} vs {:?}",
1134                    a.conclusion, b.conclusion
1135                )));
1136            };
1137            let pos_term = certify(pos, ctx)?;
1138            let neg_term = certify(neg, ctx)?;
1139            // (¬P) P : False
1140            Ok(Term::App(Box::new(neg_term), Box::new(pos_term)))
1141        }
1142
1143        // Reductio ad absurdum: assume P, derive ⊥, conclude ¬P.
1144        // Curry-Howard: λ(h:P). <⊥-proof using h> : P → False = Not P.
1145        // The assumption becomes a local hypothesis available to the ⊥-derivation.
1146        InferenceRule::ReductioAdAbsurdum => {
1147            if tree.premises.len() != 2 {
1148                return Err(KernelError::CertificationError(
1149                    "ReductioAdAbsurdum requires exactly 2 premises (assumption, ⊥-derivation)"
1150                        .to_string(),
1151                ));
1152            }
1153            let assumed = &tree.premises[0].conclusion;
1154            let contradiction = &tree.premises[1];
1155            let hyp_name = ctx.fresh_hyp_name();
1156            let branch_ctx = ctx.with_local_hyp(assumed, &hyp_name);
1157            let body = certify(contradiction, &branch_ctx)?;
1158            Ok(Term::Lambda {
1159                param: hyp_name,
1160                param_type: Box::new(proof_expr_to_type(assumed)?),
1161                body: Box::new(body),
1162            })
1163        }
1164
1165        // Case analysis where both cases reach ⊥. Intuitionistic form, needing
1166        // NO excluded middle: from the C-branch build `¬C : C → False`, from the
1167        // ¬C-branch build `¬¬C : (Not C) → False`, then `(¬¬C) (¬C) : False`.
1168        InferenceRule::CaseAnalysis { case_formula } => {
1169            if tree.premises.len() != 2 {
1170                return Err(KernelError::CertificationError(
1171                    "CaseAnalysis requires exactly 2 premises (C-branch, ¬C-branch)".to_string(),
1172                ));
1173            }
1174            let c = case_formula.as_ref();
1175            let not_c = ProofExpr::Not(Box::new(c.clone()));
1176
1177            let branch_c = unwrap_case_branch(&tree.premises[0]);
1178            let branch_nc = unwrap_case_branch(&tree.premises[1]);
1179
1180            // ¬C : C → False
1181            let h1 = ctx.fresh_hyp_name();
1182            let ctx_c = ctx.with_local_hyp(c, &h1);
1183            let neg_c = Term::Lambda {
1184                param: h1,
1185                param_type: Box::new(proof_expr_to_type(c)?),
1186                body: Box::new(certify(branch_c, &ctx_c)?),
1187            };
1188
1189            // ¬¬C : (Not C) → False  (fresh name distinct from h1)
1190            let h2 = ctx_c.fresh_hyp_name();
1191            let ctx_nc = ctx.with_local_hyp(&not_c, &h2);
1192            let neg_neg_c = Term::Lambda {
1193                param: h2,
1194                param_type: Box::new(proof_expr_to_type(&not_c)?),
1195                body: Box::new(certify(branch_nc, &ctx_nc)?),
1196            };
1197
1198            // (¬¬C) (¬C) : False
1199            Ok(Term::App(Box::new(neg_neg_c), Box::new(neg_c)))
1200        }
1201
1202        // Disjunctive syllogism: from `A ∨ B` and `¬A`, conclude `B` (and the
1203        // mirror `¬B` ⊢ `A`). Curry-Howard, intuitionistically (no excluded
1204        // middle): match the `Or A B` proof. In the branch whose disjunct is
1205        // refuted, apply the negation `Not X = X → False` to the bound proof of
1206        // `X`, obtaining `False`, then eliminate `False` into the goal (a match
1207        // with no cases — `False` has no constructors). The surviving branch
1208        // returns its bound proof, which IS the goal.
1209        InferenceRule::DisjunctionElim => {
1210            if tree.premises.len() != 2 {
1211                return Err(KernelError::CertificationError(
1212                    "DisjunctionElim requires exactly 2 premises (disjunction, negation)"
1213                        .to_string(),
1214                ));
1215            }
1216            let disj_premise = &tree.premises[0];
1217            let neg_premise = &tree.premises[1];
1218
1219            let (left, right) = match &disj_premise.conclusion {
1220                ProofExpr::Or(l, r) => (l.as_ref().clone(), r.as_ref().clone()),
1221                other => {
1222                    return Err(KernelError::CertificationError(format!(
1223                        "DisjunctionElim first premise must conclude a disjunction, got {:?}",
1224                        other
1225                    )))
1226                }
1227            };
1228            let refuted = match &neg_premise.conclusion {
1229                ProofExpr::Not(inner) => inner.as_ref().clone(),
1230                other => {
1231                    return Err(KernelError::CertificationError(format!(
1232                        "DisjunctionElim second premise must conclude a negation, got {:?}",
1233                        other
1234                    )))
1235                }
1236            };
1237
1238            let left_is_refuted = conclusions_match(&refuted, &left);
1239            let right_is_refuted = conclusions_match(&refuted, &right);
1240            if left_is_refuted == right_is_refuted {
1241                return Err(KernelError::CertificationError(format!(
1242                    "DisjunctionElim negation {:?} matches neither (or both) disjuncts of {:?}",
1243                    refuted, disj_premise.conclusion
1244                )));
1245            }
1246
1247            let goal_type = proof_expr_to_type(&tree.conclusion)?;
1248            let disc = certify(disj_premise, ctx)?;
1249            let neg_term = certify(neg_premise, ctx)?;
1250
1251            // CONSTANT motive: pass the goal type raw (see DisjunctionCases) — the kernel
1252            // rebuilds `λ_:disc_type. goal` from the discriminant's `Or` type.
1253            let motive = goal_type.clone();
1254
1255            // A branch that returns its bound proof directly (the surviving
1256            // disjunct equals the goal): λ(__d:D). __d.
1257            let return_case = |disjunct: &ProofExpr, binder: &str| -> KernelResult<Term> {
1258                Ok(Term::Lambda {
1259                    param: binder.to_string(),
1260                    param_type: Box::new(proof_expr_to_type(disjunct)?),
1261                    body: Box::new(Term::Var(binder.to_string())),
1262                })
1263            };
1264            // A branch whose disjunct is refuted: λ(__d:D). False_elim G ((¬D) __d).
1265            let absurd_case = |disjunct: &ProofExpr, binder: &str| -> KernelResult<Term> {
1266                let falsum = Term::App(
1267                    Box::new(neg_term.clone()),
1268                    Box::new(Term::Var(binder.to_string())),
1269                );
1270                let ex_falso = Term::Match {
1271                    discriminant: Box::new(falsum),
1272                    motive: Box::new(Term::Lambda {
1273                        param: "_".to_string(),
1274                        param_type: Box::new(Term::Global("False".to_string())),
1275                        body: Box::new(goal_type.clone()),
1276                    }),
1277                    cases: vec![],
1278                };
1279                Ok(Term::Lambda {
1280                    param: binder.to_string(),
1281                    param_type: Box::new(proof_expr_to_type(disjunct)?),
1282                    body: Box::new(ex_falso),
1283                })
1284            };
1285
1286            let left_case = if left_is_refuted {
1287                absurd_case(&left, "__dl")?
1288            } else {
1289                return_case(&left, "__dl")?
1290            };
1291            let right_case = if right_is_refuted {
1292                absurd_case(&right, "__dr")?
1293            } else {
1294                return_case(&right, "__dr")?
1295            };
1296
1297            Ok(Term::Match {
1298                discriminant: Box::new(disc),
1299                motive: Box::new(motive),
1300                cases: vec![left_case, right_case],
1301            })
1302        }
1303
1304        // Disjunction elimination to a COMMON conclusion (here always ⊥): from
1305        // `A ∨ B`, a proof of the goal assuming `A`, and a proof assuming `B`,
1306        // Curry-Howard matches the `Or` and runs the matching branch. Each branch
1307        // binds its disjunct — and, when the disjunct is a conjunction, each
1308        // conjunct (recovered by ∧-elimination, as in `ExistentialElim`) — as a
1309        // local hypothesis, so the branch proof may cite them directly. This is the
1310        // case analysis a grid's of-pair / either-or / closure clause needs, and it
1311        // is intuitionistic (no excluded middle).
1312        InferenceRule::DisjunctionCases => {
1313            if tree.premises.len() != 3 {
1314                return Err(KernelError::CertificationError(
1315                    "DisjunctionCases requires exactly 3 premises (disjunction, A-branch, B-branch)"
1316                        .to_string(),
1317                ));
1318            }
1319            let disj_premise = &tree.premises[0];
1320            let (left, right) = match &disj_premise.conclusion {
1321                ProofExpr::Or(l, r) => (l.as_ref().clone(), r.as_ref().clone()),
1322                other => {
1323                    return Err(KernelError::CertificationError(format!(
1324                        "DisjunctionCases first premise must conclude a disjunction, got {:?}",
1325                        other
1326                    )))
1327                }
1328            };
1329            let disc = certify(disj_premise, ctx)?;
1330            // CONSTANT motive (the goal is independent of which disjunct held): pass the
1331            // goal TYPE raw and let the kernel synthesize `λ_:disc_type. goal` from the
1332            // discriminant's `Or` type. This keeps the large, left-nested `Or` OUT of the
1333            // emitted term at EVERY nested case split — the O(n²) certification blowup.
1334            let motive = proof_expr_to_type(&tree.conclusion)?;
1335
1336            let build_arm = |disjunct: &ProofExpr,
1337                             branch: &DerivationTree,
1338                             binder: &str|
1339             -> KernelResult<Term> {
1340                let h_var = Term::Var(binder.to_string());
1341                let mut bctx = ctx.with_local_hyp_term(disjunct, h_var.clone());
1342                for (conjunct, proj) in collect_conjunct_hyps(disjunct, &h_var)? {
1343                    bctx = bctx.with_local_hyp_term(&conjunct, proj);
1344                }
1345                let body = certify(branch, &bctx)?;
1346                Ok(Term::Lambda {
1347                    param: binder.to_string(),
1348                    param_type: Box::new(proof_expr_to_type(disjunct)?),
1349                    body: Box::new(body),
1350                })
1351            };
1352
1353            // FRESH binder names (`_hyp{depth}`), not fixed `__dcl`/`__dcr`: a branch
1354            // may itself contain a nested case-analysis, and a reused name would let the
1355            // inner binder CAPTURE an outer disjunct hypothesis in the kernel term (a
1356            // leaf citing the outer disjunct resolves to the inner λ of the wrong type).
1357            let lname = ctx.fresh_hyp_name();
1358            let rname = ctx.fresh_hyp_name();
1359            let left_case = build_arm(&left, &tree.premises[1], &lname)?;
1360            let right_case = build_arm(&right, &tree.premises[2], &rname)?;
1361
1362            Ok(Term::Match {
1363                discriminant: Box::new(disc),
1364                motive: Box::new(motive),
1365                cases: vec![left_case, right_case],
1366            })
1367        }
1368
1369        // Biconditional introduction (↔I): `conj (P→Q) (Q→P) <pq> <qp>` — the pair of
1370        // direction proofs combined, matching the `Iff ≡ And (P→Q) (Q→P)` encoding.
1371        InferenceRule::BicondIntro => {
1372            if tree.premises.len() != 2 {
1373                return Err(KernelError::CertificationError(
1374                    "BicondIntro requires exactly 2 premises (P→Q, Q→P)".to_string(),
1375                ));
1376            }
1377            let (p, q) = match &tree.conclusion {
1378                ProofExpr::Iff(p, q) => (p.as_ref().clone(), q.as_ref().clone()),
1379                other => {
1380                    return Err(KernelError::CertificationError(format!(
1381                        "BicondIntro conclusion must be a biconditional, got {:?}",
1382                        other
1383                    )))
1384                }
1385            };
1386            let pq_type =
1387                proof_expr_to_type(&ProofExpr::Implies(Box::new(p.clone()), Box::new(q.clone())))?;
1388            let qp_type =
1389                proof_expr_to_type(&ProofExpr::Implies(Box::new(q), Box::new(p)))?;
1390            let pq_proof = certify(&tree.premises[0], ctx)?;
1391            let qp_proof = certify(&tree.premises[1], ctx)?;
1392            Ok(Term::App(
1393                Box::new(Term::App(
1394                    Box::new(Term::App(
1395                        Box::new(Term::App(
1396                            Box::new(Term::Global("conj".to_string())),
1397                            Box::new(pq_type),
1398                        )),
1399                        Box::new(qp_type),
1400                    )),
1401                    Box::new(pq_proof),
1402                )),
1403                Box::new(qp_proof),
1404            ))
1405        }
1406
1407        // Double-negation introduction (constructive): P ⊢ ¬¬P. With `¬X ≡ X→False`,
1408        // `¬¬P = (P→False)→False`, proved by `λ(hnp:¬P). hnp p`.
1409        InferenceRule::DoubleNegation => {
1410            if tree.premises.len() != 1 {
1411                return Err(KernelError::CertificationError(
1412                    "DoubleNegation requires exactly 1 premise (a proof of P)".to_string(),
1413                ));
1414            }
1415            let p = match &tree.conclusion {
1416                ProofExpr::Not(inner) => match inner.as_ref() {
1417                    ProofExpr::Not(core) => core.as_ref().clone(),
1418                    other => {
1419                        return Err(KernelError::CertificationError(format!(
1420                            "DoubleNegation conclusion must be ¬¬P, got ¬{:?}",
1421                            other
1422                        )))
1423                    }
1424                },
1425                other => {
1426                    return Err(KernelError::CertificationError(format!(
1427                        "DoubleNegation conclusion must be ¬¬P, got {:?}",
1428                        other
1429                    )))
1430                }
1431            };
1432            let p_type = proof_expr_to_type(&p)?;
1433            let p_proof = certify(&tree.premises[0], ctx)?;
1434            let not_p_type = Term::Pi {
1435                param: "_".to_string(),
1436                param_type: Box::new(p_type),
1437                body_type: Box::new(Term::Global("False".to_string())),
1438            };
1439            let hnp = "__dn_hnp".to_string();
1440            Ok(Term::Lambda {
1441                param: hnp.clone(),
1442                param_type: Box::new(not_p_type),
1443                body: Box::new(Term::App(Box::new(Term::Var(hnp)), Box::new(p_proof))),
1444            })
1445        }
1446
1447        // Classical reductio: assume ¬G, derive ⊥, conclude G via the `dne` axiom.
1448        // Build `dne G (λ(hng:¬G). <⊥-proof>)`, where `λ(hng:¬G). ⊥ : ¬¬G`.
1449        InferenceRule::ClassicalReductio => {
1450            if tree.premises.len() != 1 {
1451                return Err(KernelError::CertificationError(
1452                    "ClassicalReductio requires exactly 1 premise (a proof of False)".to_string(),
1453                ));
1454            }
1455            let g = tree.conclusion.clone();
1456            let g_type = proof_expr_to_type(&g)?;
1457            let neg_g = ProofExpr::Not(Box::new(g));
1458            let neg_g_type = proof_expr_to_type(&neg_g)?; // G → False
1459            let binder = ctx.fresh_hyp_name();
1460            let bctx = ctx.with_local_hyp_term(&neg_g, Term::Var(binder.clone()));
1461            let false_proof = certify(&tree.premises[0], &bctx)?;
1462            let nn_g = Term::Lambda {
1463                param: binder,
1464                param_type: Box::new(neg_g_type),
1465                body: Box::new(false_proof),
1466            };
1467            Ok(Term::App(
1468                Box::new(Term::App(
1469                    Box::new(Term::Global("dne".to_string())),
1470                    Box::new(g_type),
1471                )),
1472                Box::new(nn_g),
1473            ))
1474        }
1475
1476        // Implication introduction (→I): `λ(hp:P). <Q-proof>`, binding the antecedent
1477        // P as a local hypothesis the consequent proof may cite — and, when P is a
1478        // conjunction, each conjunct by ∧-elimination (same as DisjunctionCases).
1479        InferenceRule::ImpliesIntro => {
1480            if tree.premises.len() != 1 {
1481                return Err(KernelError::CertificationError(
1482                    "ImpliesIntro requires exactly 1 premise (the consequent proof)".to_string(),
1483                ));
1484            }
1485            let ant = match &tree.conclusion {
1486                ProofExpr::Implies(a, _) => a.as_ref().clone(),
1487                other => {
1488                    return Err(KernelError::CertificationError(format!(
1489                        "ImpliesIntro conclusion must be an implication, got {:?}",
1490                        other
1491                    )))
1492                }
1493            };
1494            let binder = ctx.fresh_hyp_name();
1495            let h_var = Term::Var(binder.clone());
1496            let mut bctx = ctx.with_local_hyp_term(&ant, h_var.clone());
1497            for (conjunct, proj) in collect_conjunct_hyps(&ant, &h_var)? {
1498                bctx = bctx.with_local_hyp_term(&conjunct, proj);
1499            }
1500            let body = certify(&tree.premises[0], &bctx)?;
1501            Ok(Term::Lambda {
1502                param: binder,
1503                param_type: Box::new(proof_expr_to_type(&ant)?),
1504                body: Box::new(body),
1505            })
1506        }
1507
1508        // Conjunction elimination: from a proof of `A ∧ B`, recover the conjunct
1509        // (`A` or `B`, whichever the conclusion names) by `∧`-elimination.
1510        InferenceRule::ConjunctionElim => {
1511            if tree.premises.len() != 1 {
1512                return Err(KernelError::CertificationError(
1513                    "ConjunctionElim requires exactly 1 premise (the conjunction)".to_string(),
1514                ));
1515            }
1516            let conj_premise = &tree.premises[0];
1517            let (a, b) = match &conj_premise.conclusion {
1518                ProofExpr::And(l, r) => (l.as_ref().clone(), r.as_ref().clone()),
1519                // Iff P Q ≡ And (P → Q) (Q → P) in the kernel encoding, so an
1520                // Iff premise projects to either implication.
1521                ProofExpr::Iff(l, r) => (
1522                    ProofExpr::Implies(l.clone(), r.clone()),
1523                    ProofExpr::Implies(r.clone(), l.clone()),
1524                ),
1525                other => {
1526                    return Err(KernelError::CertificationError(format!(
1527                        "ConjunctionElim premise must conclude a conjunction, got {:?}",
1528                        other
1529                    )))
1530                }
1531            };
1532            let take_left = conclusions_match(&tree.conclusion, &a);
1533            if !take_left && !conclusions_match(&tree.conclusion, &b) {
1534                return Err(KernelError::CertificationError(format!(
1535                    "ConjunctionElim conclusion {:?} matches neither conjunct of {:?}",
1536                    tree.conclusion, conj_premise.conclusion
1537                )));
1538            }
1539            let disc = certify(conj_premise, ctx)?;
1540            let left_type = proof_expr_to_type(&a)?;
1541            let right_type = proof_expr_to_type(&b)?;
1542            Ok(project_conjunct(&disc, &left_type, &right_type, take_left))
1543        }
1544
1545        // Z3 oracle results are deliberately NOT certifiable. The oracle attests
1546        // *satisfiability* but hands back no checkable proof term — so it can
1547        // never be turned into a kernel certificate, and a goal discharged only
1548        // by the oracle is reported as unverified by the trusted door. (For
1549        // arithmetic, the proof-producing `ArithDecision` path yields a real
1550        // certificate instead.) Making this explicit keeps Z3 firmly outside the
1551        // trusted base.
1552        InferenceRule::OracleVerification(detail) => Err(KernelError::CertificationError(format!(
1553            "oracle (Z3) results are not kernel-certifiable — they attest \
1554             satisfiability but produce no checkable proof term ({})",
1555            detail
1556        ))),
1557
1558        // Fallback for unimplemented rules
1559        rule => Err(KernelError::CertificationError(format!(
1560            "Certification not implemented for {:?}",
1561            rule
1562        ))),
1563    }
1564}
1565
1566/// Whether `neg` is syntactically the negation of `pos` (i.e. `neg = ¬pos`).
1567fn is_negation_of(neg: &ProofExpr, pos: &ProofExpr) -> bool {
1568    matches!(neg, ProofExpr::Not(inner) if inner.as_ref() == pos)
1569}
1570
1571/// Unwrap a case-analysis branch. The engine wraps each branch's ⊥-derivation
1572/// in a trivial `PremiseMatch(⊥)` node with a single child; the real proof is
1573/// that child. Any other shape is returned as-is.
1574fn unwrap_case_branch(tree: &DerivationTree) -> &DerivationTree {
1575    if matches!(tree.rule, InferenceRule::PremiseMatch) && tree.premises.len() == 1 {
1576        &tree.premises[0]
1577    } else {
1578        tree
1579    }
1580}
1581
1582/// Build the equality predicate for rewriting.
1583/// Given goal P(y) and term y, extracts P.
1584///
1585/// For simple predicates like `mortal(Superman)`, returns `mortal` directly.
1586/// For complex goals, builds `λz. (goal with y replaced by z)`.
1587fn build_equality_predicate(goal: &ProofExpr, replace_term: &ProofTerm) -> KernelResult<Term> {
1588    // Simple case: if the goal is P(y), just use P as the predicate
1589    // This avoids the beta-reduction issue with (λz. P(z)) x
1590    if let ProofExpr::Predicate { name, args, .. } = goal {
1591        if args.len() == 1 && &args[0] == replace_term {
1592            return Ok(Term::Global(name.clone()));
1593        }
1594    }
1595
1596    // General case: build λz. (goal with replace_term replaced by z)
1597    let goal_type = proof_expr_to_type(goal)?;
1598    let param_name = "_eq_var".to_string();
1599    let substituted = substitute_term_in_kernel(
1600        &goal_type,
1601        &proof_term_to_kernel_term(replace_term)?,
1602        &Term::Var(param_name.clone()),
1603    );
1604
1605    Ok(Term::Lambda {
1606        param: param_name,
1607        // The bound variable replaces `replace_term`, so it has that term's domain
1608        // (Int when rewriting arithmetic, else Entity).
1609        param_type: Box::new(Term::Global(term_domain(replace_term).to_string())),
1610        body: Box::new(substituted),
1611    })
1612}
1613
1614/// Substitute a kernel term for another in a kernel Term.
1615fn substitute_term_in_kernel(term: &Term, from: &Term, to: &Term) -> Term {
1616    if term == from {
1617        return to.clone();
1618    }
1619    match term {
1620        Term::App(f, a) => Term::App(
1621            Box::new(substitute_term_in_kernel(f, from, to)),
1622            Box::new(substitute_term_in_kernel(a, from, to)),
1623        ),
1624        Term::Pi {
1625            param,
1626            param_type,
1627            body_type,
1628        } => Term::Pi {
1629            param: param.clone(),
1630            param_type: Box::new(substitute_term_in_kernel(param_type, from, to)),
1631            body_type: Box::new(substitute_term_in_kernel(body_type, from, to)),
1632        },
1633        Term::Lambda {
1634            param,
1635            param_type,
1636            body,
1637        } => Term::Lambda {
1638            param: param.clone(),
1639            param_type: Box::new(substitute_term_in_kernel(param_type, from, to)),
1640            body: Box::new(substitute_term_in_kernel(body, from, to)),
1641        },
1642        Term::Match {
1643            discriminant,
1644            motive,
1645            cases,
1646        } => Term::Match {
1647            discriminant: Box::new(substitute_term_in_kernel(discriminant, from, to)),
1648            motive: Box::new(substitute_term_in_kernel(motive, from, to)),
1649            cases: cases
1650                .iter()
1651                .map(|c| substitute_term_in_kernel(c, from, to))
1652                .collect(),
1653        },
1654        Term::Fix { name, body } => Term::Fix {
1655            name: name.clone(),
1656            body: Box::new(substitute_term_in_kernel(body, from, to)),
1657        },
1658        other => other.clone(),
1659    }
1660}
1661
1662/// Eliminate an `And A B` proof `disc` to recover one conjunct.
1663///
1664/// Builds `match disc return C with conj => λ(__l:A).λ(__r:B). __x`
1665/// (a CONSTANT motive — the kernel re-derives `λ_:And A B. C` from `disc`'s type)
1666/// where `C` is the chosen conjunct's type and `__x` is the corresponding bound
1667/// proof. This is genuine ∧-elimination — `And` is the inductive with the single
1668/// constructor `conj : Π P Q. P → Q → And P Q`, so the singleton-eliminator match
1669/// is total and the recovered term has exactly the conjunct's type.
1670fn project_conjunct(
1671    disc: &Term,
1672    left_type: &Term,
1673    right_type: &Term,
1674    take_left: bool,
1675) -> Term {
1676    let chosen = if take_left { left_type } else { right_type };
1677    let result = if take_left { "__l" } else { "__r" };
1678    let case = Term::Lambda {
1679        param: "__l".to_string(),
1680        param_type: Box::new(left_type.clone()),
1681        body: Box::new(Term::Lambda {
1682            param: "__r".to_string(),
1683            param_type: Box::new(right_type.clone()),
1684            body: Box::new(Term::Var(result.to_string())),
1685        }),
1686    };
1687    Term::Match {
1688        discriminant: Box::new(disc.clone()),
1689        // CONSTANT motive: pass the chosen conjunct's type raw; the kernel synthesizes
1690        // `λ_:disc_type. C` from the discriminant's own `And A B` type (type_checker's
1691        // Match arm wraps a Sort-typed motive automatically), keeping the conjunction
1692        // — which carries the of-pair's nested XOR — OUT of the emitted term.
1693        motive: Box::new(chosen.clone()),
1694        cases: vec![case],
1695    }
1696}
1697
1698/// Collect each leaf conjunct of `expr` paired with a kernel term that proves it,
1699/// derived from `witness` (a proof of `proof_expr_to_type(expr)`). A non-`And`
1700/// proposition is its own single leaf, proved directly by `witness`. An
1701/// `And(l, r)` yields the leaves of `l` (proved by projecting `l` out of
1702/// `witness`) followed by the leaves of `r` (projecting `r`). This mirrors the
1703/// engine's `flatten_conjunction`, so the body proof — which references the
1704/// flattened conjuncts as separate premises — finds each one as a local
1705/// hypothesis backed by a sound ∧-elimination.
1706fn collect_conjunct_hyps(expr: &ProofExpr, witness: &Term) -> KernelResult<Vec<(ProofExpr, Term)>> {
1707    match expr {
1708        ProofExpr::And(l, r) => {
1709            let left_type = proof_expr_to_type(l)?;
1710            let right_type = proof_expr_to_type(r)?;
1711
1712            let left_proj = project_conjunct(witness, &left_type, &right_type, true);
1713            let right_proj = project_conjunct(witness, &left_type, &right_type, false);
1714
1715            let mut hyps = collect_conjunct_hyps(l, &left_proj)?;
1716            hyps.extend(collect_conjunct_hyps(r, &right_proj)?);
1717            Ok(hyps)
1718        }
1719        _ => Ok(vec![(expr.clone(), witness.clone())]),
1720    }
1721}
1722
1723/// Certify a hypothesis reference (Axiom or PremiseMatch).
1724fn certify_hypothesis(conclusion: &ProofExpr, ctx: &CertificationContext) -> KernelResult<Term> {
1725    // Check if this is an IH reference (MUST check first!)
1726    if let Some(ih_term) = ctx.get_ih_term(conclusion) {
1727        return Ok(ih_term);
1728    }
1729
1730    // A locally-assumed proposition (from reductio / case analysis) resolves to
1731    // its bound variable, regardless of the proposition's shape.
1732    if let Some(hyp) = ctx.get_local_hyp(conclusion) {
1733        return Ok(hyp);
1734    }
1735
1736    match conclusion {
1737        ProofExpr::Atom(name) => {
1738            // Check locals first (for lambda-bound variables)
1739            if ctx.is_local(name) {
1740                return Ok(Term::Var(name.clone()));
1741            }
1742            // Then check globals
1743            if ctx.kernel_ctx.get_global(name).is_some() {
1744                Ok(Term::Global(name.clone()))
1745            } else {
1746                Err(KernelError::CertificationError(format!(
1747                    "Unknown hypothesis: {}",
1748                    name
1749                )))
1750            }
1751        }
1752        // For predicate hypotheses, look up by type in kernel context
1753        ProofExpr::Predicate { name, args, .. } => {
1754            // Build the target type: P(a, b, ...) as nested application. Entity-position
1755            // lowering (predicate arguments are entities) so the lookup type matches the
1756            // hypothesis as registered — a numeric label `2004` is `Global`, not `Int`.
1757            let mut target_type = Term::Global(name.clone());
1758            for arg in args {
1759                let arg_term = proof_term_to_kernel_term_entity(arg)?;
1760                target_type = Term::App(Box::new(target_type), Box::new(arg_term));
1761            }
1762
1763            // Search declarations for one with matching type
1764            for (decl_name, decl_type) in ctx.kernel_ctx.iter_declarations() {
1765                if types_structurally_match(&target_type, decl_type) {
1766                    return Ok(Term::Global(decl_name.to_string()));
1767                }
1768            }
1769
1770            Err(KernelError::CertificationError(format!(
1771                "Cannot find hypothesis with type: {:?}",
1772                conclusion
1773            )))
1774        }
1775        // Any other proposition (ForAll, Implies, Identity, Not, Or, And, …):
1776        // convert to its kernel type and look it up among the registered
1777        // hypotheses by structural match.
1778        _ => {
1779            // Context-aware so a `∀`-premise registered with a `Nat` binder (induction)
1780            // is found — its lookup type must match the binder domain it was declared with.
1781            let target_type = proof_expr_to_type_ctx(conclusion, ctx.kernel_ctx).map_err(|_| {
1782                KernelError::CertificationError(format!(
1783                    "Cannot certify hypothesis: {:?}",
1784                    conclusion
1785                ))
1786            })?;
1787
1788            for (name, decl_type) in ctx.kernel_ctx.iter_declarations() {
1789                if types_structurally_match(&target_type, decl_type) {
1790                    return Ok(Term::Global(name.to_string()));
1791                }
1792            }
1793
1794            Err(KernelError::CertificationError(format!(
1795                "Cannot find hypothesis with type matching: {:?}",
1796                conclusion
1797            )))
1798        }
1799    }
1800}
1801
1802/// Check if two kernel terms are alpha-equivalent.
1803/// Two terms are alpha-equivalent if they are the same up to renaming of bound variables.
1804fn types_structurally_match(a: &Term, b: &Term) -> bool {
1805    // Use a helper with a mapping from bound vars in a to bound vars in b
1806    types_alpha_equiv(a, b, &mut Vec::new())
1807}
1808
1809/// Check alpha-equivalence with a mapping of bound variable pairs.
1810/// `bindings` tracks pairs of corresponding bound variable names.
1811fn types_alpha_equiv(a: &Term, b: &Term, bindings: &mut Vec<(String, String)>) -> bool {
1812    match (a, b) {
1813        (Term::Sort(u1), Term::Sort(u2)) => u1 == u2,
1814        (Term::Var(v1), Term::Var(v2)) => {
1815            // Check if these are corresponding bound variables
1816            for (bound_a, bound_b) in bindings.iter().rev() {
1817                if v1 == bound_a {
1818                    return v2 == bound_b;
1819                }
1820                if v2 == bound_b {
1821                    return false; // v2 is bound but v1 doesn't match
1822                }
1823            }
1824            // Both are free variables - must have same name
1825            v1 == v2
1826        }
1827        (Term::Global(g1), Term::Global(g2)) => g1 == g2,
1828        (Term::Lit(l1), Term::Lit(l2)) => l1 == l2,
1829        (Term::App(f1, a1), Term::App(f2, a2)) => {
1830            types_alpha_equiv(f1, f2, bindings) && types_alpha_equiv(a1, a2, bindings)
1831        }
1832        (
1833            Term::Pi {
1834                param: p1,
1835                param_type: pt1,
1836                body_type: bt1,
1837            },
1838            Term::Pi {
1839                param: p2,
1840                param_type: pt2,
1841                body_type: bt2,
1842            },
1843        ) => {
1844            // Parameter types must match in current scope
1845            if !types_alpha_equiv(pt1, pt2, bindings) {
1846                return false;
1847            }
1848            // Body types must match with the new binding
1849            bindings.push((p1.clone(), p2.clone()));
1850            let result = types_alpha_equiv(bt1, bt2, bindings);
1851            bindings.pop();
1852            result
1853        }
1854        (
1855            Term::Lambda {
1856                param: p1,
1857                param_type: pt1,
1858                body: b1,
1859            },
1860            Term::Lambda {
1861                param: p2,
1862                param_type: pt2,
1863                body: b2,
1864            },
1865        ) => {
1866            // Parameter types must match in current scope
1867            if !types_alpha_equiv(pt1, pt2, bindings) {
1868                return false;
1869            }
1870            // Bodies must match with the new binding
1871            bindings.push((p1.clone(), p2.clone()));
1872            let result = types_alpha_equiv(b1, b2, bindings);
1873            bindings.pop();
1874            result
1875        }
1876        _ => false,
1877    }
1878}
1879
1880/// Extract P and Q types from an And(P, Q) conclusion.
1881fn extract_and_types(conclusion: &ProofExpr) -> KernelResult<(Term, Term)> {
1882    match conclusion {
1883        ProofExpr::And(p, q) => {
1884            let p_term = proof_expr_to_type(p)?;
1885            let q_term = proof_expr_to_type(q)?;
1886            Ok((p_term, q_term))
1887        }
1888        _ => Err(KernelError::CertificationError(format!(
1889            "Expected And, got {:?}",
1890            conclusion
1891        ))),
1892    }
1893}
1894
1895/// The domain (the `Global` type name) of parameter `pos` in the registered
1896/// signature of `name`: peel its `Π`s and return the `pos`-th parameter type when it
1897/// is a `Global`. The seam that lets binder typing read a symbol's declared shape
1898/// instead of hardcoding constructor names.
1899fn param_domain(ctx: &Context, name: &str, pos: usize) -> Option<String> {
1900    let mut ty = ctx.get_global(name)?;
1901    let mut i = 0;
1902    while let Term::Pi { param_type, body_type, .. } = ty {
1903        if i == pos {
1904            return match param_type.as_ref() {
1905                Term::Global(g) => Some(g.clone()),
1906                _ => None,
1907            };
1908        }
1909        ty = body_type;
1910        i += 1;
1911    }
1912    None
1913}
1914
1915/// Collect every kernel domain `variable` is constrained to within `t`: a DIRECT
1916/// argument at position `j` of a function/constructor whose signature types that
1917/// parameter `Global(g)` contributes `g`. Recurses through nested arguments, so a
1918/// variable under `Succ` (⇒ `Nat`) or in `Cons`'s tail (⇒ `List`) is found.
1919fn collect_var_domains_term(t: &ProofTerm, variable: &str, ctx: &Context, out: &mut Vec<String>) {
1920    match t {
1921        ProofTerm::Function(name, args) => {
1922            for (j, a) in args.iter().enumerate() {
1923                if matches!(a, ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == variable) {
1924                    if let Some(g) = param_domain(ctx, name, j) {
1925                        out.push(g);
1926                    }
1927                }
1928                collect_var_domains_term(a, variable, ctx, out);
1929            }
1930        }
1931        ProofTerm::Group(args) => {
1932            for a in args {
1933                collect_var_domains_term(a, variable, ctx, out);
1934            }
1935        }
1936        _ => {}
1937    }
1938}
1939
1940/// Like [`collect_var_domains_term`] but over a proposition: a variable fed DIRECTLY
1941/// to a predicate at position `j` is constrained to that predicate's `j`-th parameter
1942/// domain (so `P(t)` with `P : List → Prop` makes `t` a `List`).
1943fn collect_var_domains_expr(e: &ProofExpr, variable: &str, ctx: &Context, out: &mut Vec<String>) {
1944    match e {
1945        ProofExpr::Predicate { name, args, .. } => {
1946            for (j, a) in args.iter().enumerate() {
1947                if matches!(a, ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == variable) {
1948                    if let Some(g) = param_domain(ctx, name, j) {
1949                        out.push(g);
1950                    }
1951                }
1952                collect_var_domains_term(a, variable, ctx, out);
1953            }
1954        }
1955        ProofExpr::Identity(l, r) => {
1956            collect_var_domains_term(l, variable, ctx, out);
1957            collect_var_domains_term(r, variable, ctx, out);
1958        }
1959        ProofExpr::And(l, r)
1960        | ProofExpr::Or(l, r)
1961        | ProofExpr::Implies(l, r)
1962        | ProofExpr::Iff(l, r) => {
1963            collect_var_domains_expr(l, variable, ctx, out);
1964            collect_var_domains_expr(r, variable, ctx, out);
1965        }
1966        ProofExpr::Not(p)
1967        | ProofExpr::ForAll { body: p, .. }
1968        | ProofExpr::Exists { body: p, .. } => collect_var_domains_expr(p, variable, ctx, out),
1969        _ => {}
1970    }
1971}
1972
1973/// Infer the kernel domain of a `∀`-bound `variable` from how `body` uses it, by
1974/// reading the registered signatures of the predicates/constructors it is an argument
1975/// of: an inductive constraint (`Nat` from `Succ` or a `Nat`-predicate, `List` from a
1976/// `List`-predicate or `Cons`'s tail) wins over the `Entity` default, while `Cons`'s
1977/// head position correctly stays `Entity`. The slice of elaboration that lets a
1978/// context-free proposition acquire the binder type its symbols imply.
1979fn binder_domain(variable: &str, body: &ProofExpr, ctx: &Context) -> String {
1980    let mut domains = Vec::new();
1981    collect_var_domains_expr(body, variable, ctx, &mut domains);
1982    domains
1983        .into_iter()
1984        .find(|d| d != "Entity")
1985        .unwrap_or_else(|| "Entity".to_string())
1986}
1987
1988/// Like [`proof_expr_to_type`] but resolves `∀`-binder domains against `ctx` (so a
1989/// quantifier over a `Nat`-used variable types as `Π(n:Nat). …`). Used where the
1990/// binder type is load-bearing — premise registration and the goal-type check —
1991/// while the context-free version still serves the inner certifier arms.
1992pub(crate) fn proof_expr_to_type_ctx(expr: &ProofExpr, ctx: &Context) -> KernelResult<Term> {
1993    if let ProofExpr::ForAll { variable, body } = expr {
1994        let dom = binder_domain(variable, body, ctx);
1995        let body_type = proof_expr_to_type_ctx(body, ctx)?;
1996        return Ok(Term::Pi {
1997            param: variable.clone(),
1998            param_type: Box::new(Term::Global(dom)),
1999            body_type: Box::new(body_type),
2000        });
2001    }
2002    proof_expr_to_type(expr)
2003}
2004
2005pub(crate) fn proof_expr_to_type(expr: &ProofExpr) -> KernelResult<Term> {
2006    match expr {
2007        // Falsum maps to the kernel's `False : Prop`; other atoms are
2008        // propositional constants named directly.
2009        ProofExpr::Atom(name) if name == "⊥" || name == "False" || name == "false" => {
2010            Ok(Term::Global("False".to_string()))
2011        }
2012        ProofExpr::Atom(name) => Ok(Term::Global(name.clone())),
2013        // ¬P is `Not P`, which the kernel *defines* as `P → False`. We emit the
2014        // unfolded Pi form directly so the proposition is syntactically a
2015        // function in every position (application, hypothesis lookup, lambda
2016        // parameter types) without relying on delta-unfolding mid-typecheck.
2017        ProofExpr::Not(p) => Ok(Term::Pi {
2018            param: "_".to_string(),
2019            param_type: Box::new(proof_expr_to_type(p)?),
2020            body_type: Box::new(Term::Global("False".to_string())),
2021        }),
2022        ProofExpr::And(p, q) => Ok(Term::App(
2023            Box::new(Term::App(
2024                Box::new(Term::Global("And".to_string())),
2025                Box::new(proof_expr_to_type(p)?),
2026            )),
2027            Box::new(proof_expr_to_type(q)?),
2028        )),
2029        ProofExpr::Or(p, q) => Ok(Term::App(
2030            Box::new(Term::App(
2031                Box::new(Term::Global("Or".to_string())),
2032                Box::new(proof_expr_to_type(p)?),
2033            )),
2034            Box::new(proof_expr_to_type(q)?),
2035        )),
2036        ProofExpr::Implies(p, q) => Ok(Term::Pi {
2037            param: "_".to_string(),
2038            param_type: Box::new(proof_expr_to_type(p)?),
2039            body_type: Box::new(proof_expr_to_type(q)?),
2040        }),
2041        // Iff P Q ≡ And (P → Q) (Q → P) — the same encoding `verify.rs:expand_iff`
2042        // uses for Iff premises, so Iff goals and Iff hypotheses agree.
2043        ProofExpr::Iff(p, q) => {
2044            let pt = proof_expr_to_type(p)?;
2045            let qt = proof_expr_to_type(q)?;
2046            let pq = Term::Pi {
2047                param: "_".to_string(),
2048                param_type: Box::new(pt.clone()),
2049                body_type: Box::new(qt.clone()),
2050            };
2051            let qp = Term::Pi {
2052                param: "_".to_string(),
2053                param_type: Box::new(qt),
2054                body_type: Box::new(pt),
2055            };
2056            Ok(Term::App(
2057                Box::new(Term::App(Box::new(Term::Global("And".to_string())), Box::new(pq))),
2058                Box::new(qp),
2059            ))
2060        }
2061        // ForAll ∀x.P(x) becomes Π(x:Entity). P(x)
2062        ProofExpr::ForAll { variable, body } => {
2063            let body_type = proof_expr_to_type(body)?;
2064            Ok(Term::Pi {
2065                param: variable.clone(),
2066                param_type: Box::new(Term::Global("Entity".to_string())),
2067                body_type: Box::new(body_type),
2068            })
2069        }
2070        // Predicate P(x, y, ...) becomes (P x y ...). A predicate is an uninterpreted
2071        // `Entity → … → Prop` relation, so its arguments are entities — a numeric
2072        // argument is an opaque label (a grid year `2004`), not an arithmetic literal.
2073        ProofExpr::Predicate { name, args, .. } => {
2074            let mut result = Term::Global(name.clone());
2075            for arg in args {
2076                let arg_term = proof_term_to_kernel_term_entity(arg)?;
2077                result = Term::App(Box::new(result), Box::new(arg_term));
2078            }
2079            Ok(result)
2080        }
2081        // Identity t1 = t2 becomes (Eq T t1 t2). The domain T is inferred from the
2082        // operands: a comparison (`le`/`lt`/…) or a Bool literal makes it `Eq Bool`
2083        // (the encoding `le a b = true` for `a ≤ b`); an arithmetic operator or
2084        // integer literal makes it `Eq Int`; otherwise the FOL default `Eq Entity`.
2085        ProofExpr::Identity(l, r) => {
2086            let l_term = proof_term_to_kernel_term(l)?;
2087            let r_term = proof_term_to_kernel_term(r)?;
2088            let domain = Term::Global(identity_domain(l, r).to_string());
2089            Ok(Term::App(
2090                Box::new(Term::App(
2091                    Box::new(Term::App(
2092                        Box::new(Term::Global("Eq".to_string())),
2093                        Box::new(domain),
2094                    )),
2095                    Box::new(l_term),
2096                )),
2097                Box::new(r_term),
2098            ))
2099        }
2100        // Exists ∃x.P(x) becomes Ex Entity (λx.P(x)). FOL quantifies over
2101        // entities (matching the ForAll arm); `ExistentialIntro` may still carry
2102        // an explicit witness type for other domains.
2103        ProofExpr::Exists { variable, body } => {
2104            let var_type = Term::Global("Entity".to_string());
2105            let body_type = proof_expr_to_type(body)?;
2106
2107            // Build: Ex VarType (λvar. body_type)
2108            let predicate = Term::Lambda {
2109                param: variable.clone(),
2110                param_type: Box::new(var_type.clone()),
2111                body: Box::new(body_type),
2112            };
2113
2114            Ok(Term::App(
2115                Box::new(Term::App(
2116                    Box::new(Term::Global("Ex".to_string())),
2117                    Box::new(var_type),
2118                )),
2119                Box::new(predicate),
2120            ))
2121        }
2122        // A temporal modality `Op(P)` — "he was seen" is `Past(see(butler))` —
2123        // is an opaque unary operator on propositions. It lowers to `(Op P)` with
2124        // `Op : Prop → Prop` (registered by the symbol collector), keeping
2125        // `Past(P)` a distinct proposition from `P` so a modus-tollens chain over
2126        // tensed premises certifies without conflating the two.
2127        ProofExpr::Temporal { operator, body } => Ok(Term::App(
2128            Box::new(Term::Global(operator.clone())),
2129            Box::new(proof_expr_to_type(body)?),
2130        )),
2131        _ => Err(KernelError::CertificationError(format!(
2132            "Cannot convert {:?} to kernel type",
2133            expr
2134        ))),
2135    }
2136}
2137
2138/// Convert a ProofTerm to a kernel Term.
2139///
2140/// This bridges the proof engine's term representation to the kernel's.
2141/// Used for converting witness terms in quantifier instantiation.
2142/// Translate the shared IR's canonical arithmetic function names to the kernel's
2143/// internal ring vocabulary. The proof IR is canonicalised to `Add`/`Sub`/`Mul`/
2144/// `Div` (so the SMT oracle recognises it); the kernel's LIA prover ([`crate::arith`])
2145/// and its ring axioms are written in lowercase `add`/`sub`/`mul`/`div`. This is the
2146/// single translation boundary — every other name passes through verbatim.
2147fn kernel_arith_name(name: &str) -> &str {
2148    match name {
2149        "Add" => "add",
2150        "Sub" => "sub",
2151        "Mul" => "mul",
2152        "Div" => "div",
2153        other => other,
2154    }
2155}
2156
2157/// Is `name` an arithmetic or comparison builtin? Its operands are `Int` (so the
2158/// kernel's δ-rules compute on them — `add 2 3 ⇝ 5`, `le 2 5 ⇝ true`); every other
2159/// function is uninterpreted and `Entity`-valued. Mirrors the operand classification in
2160/// `verify::SymbolCollector` so registration and lowering agree on which numerics are Int.
2161fn is_arith_builtin(name: &str) -> bool {
2162    matches!(
2163        name,
2164        "le" | "lt" | "ge" | "gt" | "add" | "sub" | "mul" | "div" | "mod" | "Add" | "Sub"
2165            | "Mul" | "Div"
2166    )
2167}
2168
2169/// Lower a proof term at an ENTITY position — a predicate argument, an uninterpreted
2170/// function's argument, an entity-domain identity operand. A bare numeric constant here
2171/// is an opaque `Entity` LABEL (a grid year `2004`, a jersey number) carried by a
2172/// first-order relation, NOT an arithmetic literal: it lowers to `Global "2004"` (which
2173/// resolves to the `Entity` constant `verify.rs` registers for it) so a monomorphic
2174/// relation like `in : Entity → Entity → Prop` — shared by a grid's year and state
2175/// columns — stays well-typed. A numeric nested under an arithmetic builtin still lowers
2176/// to an `Int` literal so the δ-rules fire. The position chooses the sort; the same
2177/// constant is one or the other by where it sits, never both. The entity-position dual of
2178/// [`proof_term_to_kernel_term`] (which lowers numerics as `Int`).
2179fn proof_term_to_kernel_term_entity(term: &ProofTerm) -> KernelResult<Term> {
2180    match term {
2181        ProofTerm::Constant(name) => Ok(Term::Global(name.clone())),
2182        ProofTerm::Variable(name) | ProofTerm::BoundVarRef(name) => Ok(Term::Var(name.clone())),
2183        ProofTerm::Function(name, args) => {
2184            let arith = is_arith_builtin(name);
2185            let mut result = Term::Global(kernel_arith_name(name).to_string());
2186            for arg in args {
2187                let arg_term = if arith {
2188                    proof_term_to_kernel_term(arg)?
2189                } else {
2190                    proof_term_to_kernel_term_entity(arg)?
2191                };
2192                result = Term::App(Box::new(result), Box::new(arg_term));
2193            }
2194            Ok(result)
2195        }
2196        ProofTerm::Group(_) => Err(KernelError::CertificationError(
2197            "Cannot convert Group to kernel term".to_string(),
2198        )),
2199    }
2200}
2201
2202pub(crate) fn proof_term_to_kernel_term(term: &ProofTerm) -> KernelResult<Term> {
2203    match term {
2204        // A numeric constant is an integer literal — it must be `Lit(Int)` (not an
2205        // opaque `Global`) so the kernel's arithmetic/comparison delta rules fire
2206        // (`add 2 3 ⇝ 5`, `le 2 5 ⇝ true`), deciding ground facts by computation.
2207        ProofTerm::Constant(name) => match name.parse::<i64>() {
2208            Ok(n) => Ok(Term::Lit(logicaffeine_kernel::Literal::Int(n))),
2209            Err(_) => Ok(Term::Global(name.clone())),
2210        },
2211        ProofTerm::Variable(name) => Ok(Term::Var(name.clone())),
2212        ProofTerm::BoundVarRef(name) => Ok(Term::Var(name.clone())),
2213        ProofTerm::Function(name, args) => {
2214            // Build nested applications: f(a, b) -> ((f a) b). Canonical arithmetic
2215            // names are lowered to the kernel's ring vocabulary at this boundary.
2216            let mut result = Term::Global(kernel_arith_name(name).to_string());
2217            for arg in args {
2218                let arg_term = proof_term_to_kernel_term(arg)?;
2219                result = Term::App(Box::new(result), Box::new(arg_term));
2220            }
2221            Ok(result)
2222        }
2223        ProofTerm::Group(_) => Err(KernelError::CertificationError(
2224            "Cannot convert Group to kernel term".to_string(),
2225        )),
2226    }
2227}
2228
2229/// The `Decidable` instance term for a kernel proposition, when one is
2230/// registered: `Eq Bool a b ↦ decEqBool a b`, `Eq Nat a b ↦ decEqNat a b`.
2231/// (Comparisons arrive here already lowered to `Eq Bool (le a b) true`.)
2232fn decidable_instance_for(prop: &Term) -> Option<Term> {
2233    let Term::App(f1, b) = prop else { return None };
2234    let Term::App(f2, a) = f1.as_ref() else { return None };
2235    let Term::App(eq, dom) = f2.as_ref() else { return None };
2236    let Term::Global(eq_name) = eq.as_ref() else { return None };
2237    if eq_name != "Eq" {
2238        return None;
2239    }
2240    let Term::Global(domain) = dom.as_ref() else { return None };
2241    let inst = match domain.as_str() {
2242        "Bool" => "decEqBool",
2243        "Nat" => "decEqNat",
2244        _ => return None,
2245    };
2246    Some(Term::App(
2247        Box::new(Term::App(
2248            Box::new(Term::Global(inst.to_string())),
2249            a.clone(),
2250        )),
2251        b.clone(),
2252    ))
2253}
2254
2255/// Infer the `Eq` domain for an `Identity` from its operands (see the `Identity`
2256/// arm of [`proof_expr_to_type`]). A comparison (`le`/`lt`/…) or a Bool literal →
2257/// `Bool`; an arithmetic operator or integer literal → `Int`; otherwise the
2258/// first-order default `Entity`.
2259fn identity_domain(l: &ProofTerm, r: &ProofTerm) -> &'static str {
2260    match (term_domain(l), term_domain(r)) {
2261        ("Bool", _) | (_, "Bool") => "Bool",
2262        ("Int", _) | (_, "Int") => "Int",
2263        _ => "Entity",
2264    }
2265}
2266
2267/// The kernel domain a single term belongs to: a comparison / Bool literal → `Bool`;
2268/// an arithmetic operator or integer literal → `Int`; otherwise `Entity`.
2269fn term_domain(t: &ProofTerm) -> &'static str {
2270    match t {
2271        ProofTerm::Function(n, _) if matches!(n.as_str(), "le" | "lt" | "ge" | "gt") => "Bool",
2272        ProofTerm::Function(n, _)
2273            if matches!(
2274                n.as_str(),
2275                "add" | "sub" | "mul" | "div" | "mod" | "Add" | "Sub" | "Mul" | "Div"
2276            ) =>
2277        {
2278            "Int"
2279        }
2280        ProofTerm::Constant(s) if s == "true" || s == "false" => "Bool",
2281        ProofTerm::Constant(s) if s.parse::<i64>().is_ok() => "Int",
2282        _ => "Entity",
2283    }
2284}
2285
2286/// Extract `(a, b)` from an inequality conclusion `le(a, b) = true`, encoded in the
2287/// proof layer as `Identity(Function("le", [a, b]), Constant("true"))`.
2288fn le_terms(expr: &ProofExpr) -> KernelResult<(ProofTerm, ProofTerm)> {
2289    if let ProofExpr::Identity(lhs, _) = expr {
2290        if let ProofTerm::Function(name, args) = lhs {
2291            if name == "le" && args.len() == 2 {
2292                return Ok((args[0].clone(), args[1].clone()));
2293            }
2294        }
2295    }
2296    Err(KernelError::CertificationError(format!(
2297        "expected an `le(a, b) = true` inequality, got {:?}",
2298        expr
2299    )))
2300}
2301
2302/// Extract `(x, y)` from a sum `add(x, y)`.
2303fn add_terms(t: &ProofTerm) -> KernelResult<(ProofTerm, ProofTerm)> {
2304    binop_terms("add", t)
2305}
2306
2307/// Extract `(x, y)` from a binary application `op(x, y)`.
2308fn binop_terms(op: &str, t: &ProofTerm) -> KernelResult<(ProofTerm, ProofTerm)> {
2309    if let ProofTerm::Function(name, args) = t {
2310        if name == op && args.len() == 2 {
2311            return Ok((args[0].clone(), args[1].clone()));
2312        }
2313    }
2314    Err(KernelError::CertificationError(format!(
2315        "expected `{}(x, y)`, got {:?}",
2316        op, t
2317    )))
2318}
2319
2320/// `Eq_rec Bool false P I true h : False`, where `h : Eq Bool false true` (or, by
2321/// computation, any `Eq Bool (le m n) true` with `m > n`). The motive
2322/// `P b = match b with true ⇒ False | false ⇒ True` is the Bool no-confusion
2323/// discriminator — turning a derived ground-false inequality into `⊥`.
2324fn build_bool_discriminator(h: Term) -> Term {
2325    let g = |s: &str| Term::Global(s.to_string());
2326    // P = λb:Bool. match b return (λ_:Bool. Prop) with | true ⇒ False | false ⇒ True
2327    let motive = Term::Lambda {
2328        param: "b".to_string(),
2329        param_type: Box::new(g("Bool")),
2330        body: Box::new(Term::Match {
2331            discriminant: Box::new(Term::Var("b".to_string())),
2332            motive: Box::new(Term::Lambda {
2333                param: "_".to_string(),
2334                param_type: Box::new(g("Bool")),
2335                body: Box::new(Term::Sort(logicaffeine_kernel::Universe::Prop)),
2336            }),
2337            cases: vec![g("False"), g("True")],
2338        }),
2339    };
2340    [g("Bool"), g("false"), motive, g("I"), g("true"), h]
2341        .into_iter()
2342        .fold(g("Eq_rec"), |acc, arg| Term::App(Box::new(acc), Box::new(arg)))
2343}
2344
2345// =============================================================================
2346// INDUCTION HELPER FUNCTIONS
2347// =============================================================================
2348
2349/// Extract motive body from ForAll conclusion.
2350/// The motive is the predicate body of the ForAll that we're proving by induction.
2351fn extract_motive_body(conclusion: &ProofExpr, var_name: &str) -> KernelResult<Term> {
2352    match conclusion {
2353        ProofExpr::ForAll { variable, body } if variable == var_name => {
2354            // Convert body to kernel type (the motive body)
2355            proof_expr_to_type(body)
2356        }
2357        _ => Err(KernelError::CertificationError(format!(
2358            "StructuralInduction conclusion must be ForAll over {}, got {:?}",
2359            var_name, conclusion
2360        ))),
2361    }
2362}
2363
2364/// Compute what the IH conclusion looks like: P(k) when proving P(n).
2365/// Substitutes the induction variable with the step variable in the ForAll body.
2366fn compute_ih_conclusion(
2367    conclusion: &ProofExpr,
2368    orig_var: &str,
2369    step_var: &str,
2370) -> KernelResult<ProofExpr> {
2371    match conclusion {
2372        ProofExpr::ForAll { body, .. } => Ok(substitute_var_in_expr(body, orig_var, step_var)),
2373        _ => Err(KernelError::CertificationError(
2374            "Expected ForAll for IH computation".to_string(),
2375        )),
2376    }
2377}
2378
2379/// Peel a constructor's argument types from its signature. A monomorphic
2380/// inductive's constructor has type `Π(a₀:T₀). … Π(aₙ:Tₙ). Ind`, so the argument
2381/// types are exactly the `Pi` parameter types in order (a nullary constructor
2382/// yields an empty list). Used to type the case lambdas of a generic
2383/// [`InferenceRule::InductionScheme`] eliminator.
2384fn constructor_arg_types(ctx: &Context, constructor: &str) -> KernelResult<Vec<Term>> {
2385    let mut ty = ctx
2386        .get_global(constructor)
2387        .ok_or_else(|| {
2388            KernelError::CertificationError(format!("unknown constructor {}", constructor))
2389        })?
2390        .clone();
2391    let mut args = Vec::new();
2392    while let Term::Pi {
2393        param_type,
2394        body_type,
2395        ..
2396    } = ty
2397    {
2398        args.push(*param_type);
2399        ty = *body_type;
2400    }
2401    Ok(args)
2402}
2403
2404/// Build the match motive: λn:IndType. ResultType
2405/// The motive_param should be the same variable name used in the body
2406/// so that the body's references are properly captured.
2407fn build_motive(ind_type: &str, result_type: &Term, motive_param: &str) -> Term {
2408    Term::Lambda {
2409        param: motive_param.to_string(),
2410        param_type: Box::new(Term::Global(ind_type.to_string())),
2411        body: Box::new(result_type.clone()),
2412    }
2413}
2414
2415/// Substitute variable name in ProofExpr (recursive).
2416fn substitute_var_in_expr(expr: &ProofExpr, from: &str, to: &str) -> ProofExpr {
2417    match expr {
2418        ProofExpr::Atom(s) if s == from => ProofExpr::Atom(to.to_string()),
2419        ProofExpr::Atom(s) => ProofExpr::Atom(s.clone()),
2420
2421        ProofExpr::TypedVar { name, typename } => ProofExpr::TypedVar {
2422            name: if name == from {
2423                to.to_string()
2424            } else {
2425                name.clone()
2426            },
2427            typename: typename.clone(),
2428        },
2429
2430        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
2431            name: name.clone(),
2432            args: args
2433                .iter()
2434                .map(|a| substitute_var_in_term(a, from, to))
2435                .collect(),
2436            world: world.clone(),
2437        },
2438
2439        ProofExpr::Identity(l, r) => ProofExpr::Identity(
2440            substitute_var_in_term(l, from, to),
2441            substitute_var_in_term(r, from, to),
2442        ),
2443
2444        ProofExpr::And(l, r) => ProofExpr::And(
2445            Box::new(substitute_var_in_expr(l, from, to)),
2446            Box::new(substitute_var_in_expr(r, from, to)),
2447        ),
2448
2449        ProofExpr::Or(l, r) => ProofExpr::Or(
2450            Box::new(substitute_var_in_expr(l, from, to)),
2451            Box::new(substitute_var_in_expr(r, from, to)),
2452        ),
2453
2454        ProofExpr::Implies(l, r) => ProofExpr::Implies(
2455            Box::new(substitute_var_in_expr(l, from, to)),
2456            Box::new(substitute_var_in_expr(r, from, to)),
2457        ),
2458
2459        ProofExpr::Iff(l, r) => ProofExpr::Iff(
2460            Box::new(substitute_var_in_expr(l, from, to)),
2461            Box::new(substitute_var_in_expr(r, from, to)),
2462        ),
2463
2464        ProofExpr::Not(inner) => {
2465            ProofExpr::Not(Box::new(substitute_var_in_expr(inner, from, to)))
2466        }
2467
2468        // Don't substitute inside binding that shadows
2469        ProofExpr::ForAll { variable, body } if variable != from => ProofExpr::ForAll {
2470            variable: variable.clone(),
2471            body: Box::new(substitute_var_in_expr(body, from, to)),
2472        },
2473
2474        ProofExpr::Exists { variable, body } if variable != from => ProofExpr::Exists {
2475            variable: variable.clone(),
2476            body: Box::new(substitute_var_in_expr(body, from, to)),
2477        },
2478
2479        ProofExpr::Lambda { variable, body } if variable != from => ProofExpr::Lambda {
2480            variable: variable.clone(),
2481            body: Box::new(substitute_var_in_expr(body, from, to)),
2482        },
2483
2484        ProofExpr::App(f, a) => ProofExpr::App(
2485            Box::new(substitute_var_in_expr(f, from, to)),
2486            Box::new(substitute_var_in_expr(a, from, to)),
2487        ),
2488
2489        ProofExpr::Term(t) => ProofExpr::Term(substitute_var_in_term(t, from, to)),
2490
2491        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
2492            name: name.clone(),
2493            args: args
2494                .iter()
2495                .map(|a| substitute_var_in_expr(a, from, to))
2496                .collect(),
2497        },
2498
2499        // For anything else (shadowed bindings, modals, temporals, etc.), clone as-is
2500        other => other.clone(),
2501    }
2502}
2503
2504/// Substitute variable name in ProofTerm.
2505fn substitute_var_in_term(term: &ProofTerm, from: &str, to: &str) -> ProofTerm {
2506    match term {
2507        ProofTerm::Variable(s) if s == from => ProofTerm::Variable(to.to_string()),
2508        ProofTerm::Variable(s) => ProofTerm::Variable(s.clone()),
2509        ProofTerm::Constant(s) => ProofTerm::Constant(s.clone()),
2510        ProofTerm::BoundVarRef(s) if s == from => ProofTerm::BoundVarRef(to.to_string()),
2511        ProofTerm::BoundVarRef(s) => ProofTerm::BoundVarRef(s.clone()),
2512        ProofTerm::Function(name, args) => ProofTerm::Function(
2513            name.clone(),
2514            args.iter()
2515                .map(|a| substitute_var_in_term(a, from, to))
2516                .collect(),
2517        ),
2518        ProofTerm::Group(terms) => ProofTerm::Group(
2519            terms
2520                .iter()
2521                .map(|t| substitute_var_in_term(t, from, to))
2522                .collect(),
2523        ),
2524    }
2525}
2526
2527#[cfg(test)]
2528mod canonical_arith_boundary_tests {
2529    use super::*;
2530
2531    #[test]
2532    fn kernel_arith_name_maps_canonical_to_ring() {
2533        // The shared IR is canonicalised to Add/Sub/Mul/Div; the kernel ring axioms
2534        // and LIA prover speak lowercase add/sub/mul/div. The boundary translates.
2535        assert_eq!(kernel_arith_name("Add"), "add");
2536        assert_eq!(kernel_arith_name("Sub"), "sub");
2537        assert_eq!(kernel_arith_name("Mul"), "mul");
2538        assert_eq!(kernel_arith_name("Div"), "div");
2539        // Everything else passes through verbatim (measure fns, relations, names).
2540        assert_eq!(kernel_arith_name("Score"), "Score");
2541        assert_eq!(kernel_arith_name("add"), "add");
2542    }
2543
2544    #[test]
2545    fn canonical_add_lowers_to_kernel_ring_head() {
2546        // ProofTerm::Function("Add", [x, y]) must lower to ((add x) y) so the
2547        // kernel arith prover (which matches Global("add")) can discharge it.
2548        let t = proof_term_to_kernel_term(&ProofTerm::Function(
2549            "Add".to_string(),
2550            vec![
2551                ProofTerm::Variable("x".to_string()),
2552                ProofTerm::Variable("y".to_string()),
2553            ],
2554        ))
2555        .expect("Add term converts to a kernel term");
2556        match t {
2557            Term::App(f, _) => match *f {
2558                Term::App(g, _) => match *g {
2559                    Term::Global(name) => assert_eq!(
2560                        name, "add",
2561                        "canonical Add must lower to the kernel ring name 'add'; got {name}"
2562                    ),
2563                    other => panic!("expected a Global head, got {:?}", other),
2564                },
2565                other => panic!("expected a nested App, got {:?}", other),
2566            },
2567            other => panic!("expected an App, got {:?}", other),
2568        }
2569    }
2570
2571    // A numeric constant is ambiguous: an arithmetic `Int` literal in an Int position,
2572    // an opaque `Entity` LABEL (a grid year `2004`) in a relation position. The position
2573    // decides — this is what keeps a monomorphic relation (`in : Entity → Entity → Prop`,
2574    // shared by a logic grid's year and state columns) well-typed. These pin both arms;
2575    // the regression they guard froze the Studio's Simon puzzle by making `in(Beta, 2002)`
2576    // fail certification (`expected Entity, found Int`), which fell through to an unbounded
2577    // backward-chainer grind.
2578
2579    /// The second argument of `in(Beta, 2002)` is the YEAR ENTITY `2002`, so it must lower
2580    /// to `Global("2002")` (resolving to the `Entity` constant `verify.rs` registers),
2581    /// never an `Int` literal — otherwise it clashes with `in : Entity → Entity → Prop`.
2582    #[test]
2583    fn numeric_predicate_argument_lowers_to_entity_label() {
2584        let in_beta_2002 = ProofExpr::Predicate {
2585            name: "in".to_string(),
2586            args: vec![
2587                ProofTerm::Constant("Beta".to_string()),
2588                ProofTerm::Constant("2002".to_string()),
2589            ],
2590            world: None,
2591        };
2592        let t = proof_expr_to_type(&in_beta_2002).expect("predicate lowers to a kernel type");
2593        // (in Beta) 2002 — the outer App's argument is the year term.
2594        match t {
2595            Term::App(_, year) => assert!(
2596                matches!(*year, Term::Global(ref n) if n == "2002"),
2597                "year `2002` in a relation must be an Entity Global, got {:?}",
2598                year
2599            ),
2600            other => panic!("expected `(in Beta) 2002` application, got {:?}", other),
2601        }
2602    }
2603
2604    /// The dual: a numeric under an arithmetic builtin stays an `Int` literal so the
2605    /// kernel's δ-rules fire (`le 2 5 ⇝ true`). The fix must not touch this path.
2606    #[test]
2607    fn numeric_arithmetic_operand_stays_int_literal() {
2608        use logicaffeine_kernel::Literal;
2609        let le_2_5 = ProofTerm::Function(
2610            "le".to_string(),
2611            vec![
2612                ProofTerm::Constant("2".to_string()),
2613                ProofTerm::Constant("5".to_string()),
2614            ],
2615        );
2616        let t = proof_term_to_kernel_term(&le_2_5).expect("le term converts");
2617        match t {
2618            Term::App(_, five) => assert!(
2619                matches!(*five, Term::Lit(Literal::Int(5))),
2620                "an arithmetic operand must stay an Int literal, got {:?}",
2621                five
2622            ),
2623            other => panic!("expected `(le 2) 5` application, got {:?}", other),
2624        }
2625    }
2626
2627    /// Entity-position lowering keeps arithmetic builtins nested inside it on the `Int`
2628    /// path (their operands still compute), while a bare numeric becomes an Entity label.
2629    #[test]
2630    fn entity_lowering_is_numeric_aware_but_arith_preserving() {
2631        use logicaffeine_kernel::Literal;
2632        assert!(
2633            matches!(
2634                proof_term_to_kernel_term_entity(&ProofTerm::Constant("2004".to_string())).unwrap(),
2635                Term::Global(ref n) if n == "2004"
2636            ),
2637            "a bare numeric in an entity position is an Entity label"
2638        );
2639        // add(2, 3) under an entity position still lowers its operands as Int literals.
2640        let add = ProofTerm::Function(
2641            "add".to_string(),
2642            vec![
2643                ProofTerm::Constant("2".to_string()),
2644                ProofTerm::Constant("3".to_string()),
2645            ],
2646        );
2647        match proof_term_to_kernel_term_entity(&add).unwrap() {
2648            Term::App(_, three) => assert!(
2649                matches!(*three, Term::Lit(Literal::Int(3))),
2650                "an arithmetic operand stays Int even inside an entity position, got {:?}",
2651                three
2652            ),
2653            other => panic!("expected `(add 2) 3`, got {:?}", other),
2654        }
2655    }
2656}