Skip to main content

logicaffeine_proof/
verify.rs

1//! Unified theorem verification — the single door.
2//!
3//! Every public theorem entry point in the workspace funnels through
4//! [`prove_certify_check`], so they all share one trust guarantee:
5//!
6//! > A proof is `verified` **iff** the backward chainer produced a derivation,
7//! > the certifier turned it into a kernel term, **and** the kernel type-checked
8//! > that term.
9//!
10//! A derivation alone never counts as verified — `verified == true` is always
11//! backed by a [`Term`] that re-checks under [`infer_type`] in the returned
12//! [`Context`]. This core lives in the proof crate (not the compile crate) so
13//! both `logicaffeine_language` and `logicaffeine_compile` can reach it without
14//! a dependency cycle.
15
16use std::collections::{HashMap, HashSet};
17
18use logicaffeine_kernel::{
19    double_check, infer_type, is_subtype, prelude::StandardLibrary, Context, DoubleCheck, Term,
20    Universe,
21};
22
23use crate::certifier::{certify, proof_expr_to_type, proof_expr_to_type_ctx, CertificationContext};
24use crate::{BackwardChainer, DerivationTree, ProofExpr, ProofTerm};
25
26/// Outcome of running a goal through prove → certify → kernel type-check.
27///
28/// The invariant: `verified == true` **iff** `proof_term.is_some()` and that
29/// term type-checks in `kernel_ctx`. When verification fails, `derivation` may
30/// still be present (the chainer found *a* derivation) but it is never reported
31/// as verified — `verification_error` explains where the chain broke.
32pub struct VerifiedProof {
33    /// The derivation found by backward chaining, if any.
34    pub derivation: Option<DerivationTree>,
35    /// The certified kernel proof term, present only when `verified`.
36    pub proof_term: Option<Term>,
37    /// The kernel context the proof term was checked in (predicates,
38    /// constants, and premise hypotheses registered).
39    pub kernel_ctx: Context,
40    /// True iff a proof term was certified and kernel type-checked.
41    pub verified: bool,
42    /// Where the chain broke (search, certification, or type-check), if it did.
43    pub verification_error: Option<String>,
44}
45
46/// A user-introduced predicate definition (Rung 0a), expressed in the proof
47/// layer's own vocabulary so `logicaffeine_proof` keeps its
48/// no-language-dependency invariant. Read as `name(params) :↔ definiens`.
49///
50/// Registered as a δ-unfoldable kernel definition (not an inlined expansion), so
51/// the definiendum stays a first-class, citable node: `bachelor(x)` remains
52/// `bachelor(x)` in the proposition, defeq to its meaning by δ-reduction.
53#[derive(Debug, Clone)]
54pub struct Definition {
55    /// The definiendum predicate name (e.g. `"bachelor"`).
56    pub name: String,
57    /// The parameter names the definiendum binds (e.g. `["x"]`).
58    pub params: Vec<String>,
59    /// The definiens — the body the definiendum abbreviates.
60    pub definiens: ProofExpr,
61}
62
63/// Prove `goal` from `premises`, certify the derivation, and kernel-check it.
64///
65/// This is the canonical pipeline. Symbols are extracted from the premises and
66/// goal and registered in a fresh kernel context (predicates as `Entity → Prop`,
67/// constants as `Entity`); each premise is registered as a hypothesis using the
68/// **same** conversion the certifier uses for hypothesis lookup, so a registered
69/// premise is guaranteed to match.
70pub fn prove_certify_check(premises: &[ProofExpr], goal: &ProofExpr) -> VerifiedProof {
71    prove_certify_check_bounded(premises, goal, 100)
72}
73
74/// One theorem in a dependency-ordered library: proved from its own `premises` plus
75/// the conclusions of the theorems it `cites`. This is the unit the multi-theorem
76/// driver discharges in citation order (the Euclid-graph engine).
77#[derive(Debug, Clone, PartialEq)]
78pub struct LibraryTheorem {
79    pub name: String,
80    pub premises: Vec<ProofExpr>,
81    pub goal: ProofExpr,
82    /// Names of earlier theorems whose conclusions this proof relies on.
83    pub cites: Vec<String>,
84}
85
86/// The outcome of proving one theorem in a library.
87pub struct LibraryResult {
88    pub name: String,
89    pub verified: bool,
90    pub verification_error: Option<String>,
91}
92
93/// Order theorems so every citation precedes its citer. A theorem is ready once all
94/// the theorems it cites (by name; unknown names are treated as external givens) are
95/// already ordered. Any theorems left in a citation cycle are appended last (they
96/// will simply fail to find their cyclic lemma). Stable: independent theorems keep
97/// their input order.
98pub(crate) fn citation_order(theorems: &[LibraryTheorem]) -> Vec<usize> {
99    use std::collections::HashSet;
100    let known: HashSet<&str> = theorems.iter().map(|t| t.name.as_str()).collect();
101    let mut placed: HashSet<usize> = HashSet::new();
102    let mut placed_names: HashSet<&str> = HashSet::new();
103    let mut order = Vec::with_capacity(theorems.len());
104    loop {
105        let mut progressed = false;
106        for (i, t) in theorems.iter().enumerate() {
107            if placed.contains(&i) {
108                continue;
109            }
110            let ready = t
111                .cites
112                .iter()
113                .all(|c| !known.contains(c.as_str()) || placed_names.contains(c.as_str()));
114            if ready {
115                order.push(i);
116                placed.insert(i);
117                placed_names.insert(t.name.as_str());
118                progressed = true;
119            }
120        }
121        if !progressed {
122            break;
123        }
124    }
125    for i in 0..theorems.len() {
126        if !placed.contains(&i) {
127            order.push(i);
128        }
129    }
130    order
131}
132
133/// Discharge a library of theorems in citation order. Each theorem is proved from
134/// its own premises plus the conclusions of the (already-proved) theorems it cites,
135/// so a citation graph is walked exactly like the scraped Euclid dependency graph.
136/// Results are returned in the INPUT order. A citation of an unproved/failed theorem
137/// simply isn't available as a premise (so the dependent proof fails too).
138pub fn prove_library(theorems: &[LibraryTheorem]) -> Vec<LibraryResult> {
139    prove_library_with_axioms(&[], theorems)
140}
141
142/// Like [`prove_library`] but with a shared `axioms` base in scope for every
143/// theorem — a named theory (e.g. the Tarski geometry axioms) on which the whole
144/// dependency graph is discharged.
145pub fn prove_library_with_axioms(
146    axioms: &[ProofExpr],
147    theorems: &[LibraryTheorem],
148) -> Vec<LibraryResult> {
149    use std::collections::HashMap;
150    let mut proved: HashMap<&str, &ProofExpr> = HashMap::new();
151    let mut by_name: HashMap<&str, LibraryResult> = HashMap::new();
152
153    for &i in &citation_order(theorems) {
154        let t = &theorems[i];
155        let mut premises = axioms.to_vec();
156        premises.extend(t.premises.iter().cloned());
157        for cite in &t.cites {
158            if let Some(goal) = proved.get(cite.as_str()) {
159                premises.push((*goal).clone());
160            }
161        }
162        let vp = prove_certify_check(&premises, &t.goal);
163        if vp.verified {
164            proved.insert(t.name.as_str(), &t.goal);
165        }
166        by_name.insert(
167            t.name.as_str(),
168            LibraryResult {
169                name: t.name.clone(),
170                verified: vp.verified,
171                verification_error: vp.verification_error,
172            },
173        );
174    }
175
176    theorems
177        .iter()
178        .map(|t| by_name.remove(t.name.as_str()).expect("every theorem produces a result"))
179        .collect()
180}
181
182/// Like [`prove_certify_check`] but with user [`Definition`]s in scope (Rung 0a).
183pub fn prove_certify_check_with_defs(
184    premises: &[ProofExpr],
185    goal: &ProofExpr,
186    definitions: &[Definition],
187) -> VerifiedProof {
188    prove_certify_check_bounded_with_defs(premises, goal, definitions, 100)
189}
190
191/// Like [`prove_certify_check`] but caps the backward-chainer search depth, so a
192/// goal the kernel cannot reach fails FAST instead of exhausting the default depth.
193/// Keeps "prove-with-ours-first" cheap when answering a grid cell by cell: a
194/// shallow kernel attempt certifies what it can, then falls through to the oracle.
195pub fn prove_certify_check_bounded(
196    premises: &[ProofExpr],
197    goal: &ProofExpr,
198    max_depth: usize,
199) -> VerifiedProof {
200    prove_certify_check_bounded_with_defs(premises, goal, &[], max_depth)
201}
202
203/// The depth-bounded pipeline with user definitions (Rung 0a). Definitions are
204/// validated (recursion + lowering) up front, registered as δ-unfoldable kernel
205/// definitions in the context, then the goal is proved and kernel-checked. The
206/// engine still treats the definiendum opaquely during *search* (Stride 3 adds
207/// expand-for-search); δ reconciles the certified term with the folded goal type
208/// at the [`finish_check`] root.
209pub fn prove_certify_check_bounded_with_defs(
210    premises: &[ProofExpr],
211    goal: &ProofExpr,
212    definitions: &[Definition],
213    max_depth: usize,
214) -> VerifiedProof {
215    // The entire search + certify + kernel-check pipeline runs on ONE large-stack
216    // thread, so a legitimately deep proof never overflows the native stack and the
217    // common (shallow) case pays a single thread spawn.
218    on_big_stack(|| {
219        prove_certify_check_bounded_with_defs_inner(premises, goal, definitions, max_depth)
220    })
221}
222
223fn prove_certify_check_bounded_with_defs_inner(
224    premises: &[ProofExpr],
225    goal: &ProofExpr,
226    definitions: &[Definition],
227    max_depth: usize,
228) -> VerifiedProof {
229    let abstracted = abstract_definitions(definitions);
230    let definitions = &abstracted[..];
231    if let Err(e) = validate_definitions(definitions) {
232        return definition_error(e);
233    }
234
235    // Expand-for-search: the backward chainer treats a definiendum opaquely, so
236    // δ-expand defined predicates in the premises and goal before search — the
237    // engine then proves over ordinary predicates. The goal *type* stays FOLDED
238    // (abstracted from the ORIGINAL goal), so the certified proposition is still
239    // `glorp(A)`, reconciled to its unfolding by δ at the `finish_check` root.
240    //
241    // FAST PATH: with no definitions, the originals pass through untouched — no
242    // expansion walk, no clone — so ordinary theorems pay nothing for Rung 0a.
243    let expanded: Option<(Vec<ProofExpr>, ProofExpr)> = if definitions.is_empty() {
244        None
245    } else {
246        let defmap: HashMap<&str, &Definition> =
247            definitions.iter().map(|d| (d.name.as_str(), d)).collect();
248        let ep = premises
249            .iter()
250            .map(|p| expand_defs_in_expr(p, &defmap, 0))
251            .collect();
252        let eg = expand_defs_in_expr(goal, &defmap, 0);
253        Some((ep, eg))
254    };
255    let (search_premises, search_goal): (&[ProofExpr], &ProofExpr) = match &expanded {
256        Some((ep, eg)) => (ep, eg),
257        None => (premises, goal),
258    };
259
260    // Build the context and hypotheses from the (expanded) search premises so the
261    // certifier's PremiseMatch leaves resolve.
262    let (kernel_ctx, flat_premises, prepared_goal) =
263        prepare_ctx_with_defs(search_premises, search_goal, definitions);
264    // Folded goal type: when nothing was expanded, `prepared_goal` is already the
265    // folded form; otherwise re-abstract the ORIGINAL goal (the definiendum stays
266    // a citable node).
267    let abstracted_goal = if expanded.is_none() {
268        prepared_goal
269    } else {
270        BackwardChainer::new().abstract_all_events(goal)
271    };
272
273    // === Prove ===
274    let mut engine = BackwardChainer::new();
275    engine.set_max_depth(max_depth);
276    for premise in &flat_premises {
277        engine.add_axiom(premise.clone());
278    }
279    // The engine searches over event-ABSTRACTED premises, so the goal must be
280    // abstracted to the same form (`Alice admires Bob` ⇒ `admire(Alice, Bob)`).
281    let search_goal = engine.abstract_all_events(search_goal);
282    let derivation = match engine.prove(search_goal) {
283        Ok(d) => d,
284        Err(e) => {
285            return VerifiedProof {
286                derivation: None,
287                proof_term: None,
288                kernel_ctx,
289                verified: false,
290                verification_error: Some(format!("Proof search failed: {}", e)),
291            };
292        }
293    };
294
295    finish_check(kernel_ctx, &abstracted_goal, derivation)
296}
297
298/// Certify and kernel-check a derivation built EXTERNALLY (e.g. by the fast grid
299/// solver) against `premises ⊢ goal`, WITHOUT running the backward chainer. The
300/// trust guarantee is identical to [`prove_certify_check`]: `verified` is true only
301/// when the certifier produced a kernel term whose inferred type IS the goal type.
302/// So an external solver sits OUTSIDE the trusted base — it hands us a
303/// `DerivationTree`, the kernel re-checks it, and a wrong tree yields
304/// `verified == false`, never a false claim.
305pub fn check_derivation(
306    premises: &[ProofExpr],
307    goal: &ProofExpr,
308    derivation: DerivationTree,
309) -> VerifiedProof {
310    check_derivation_with_defs(premises, goal, &[], derivation)
311}
312
313/// Like [`check_derivation`] but with user [`Definition`]s in scope (Rung 0a), so
314/// an externally-built derivation of a definiens can certify against a goal
315/// stated with the definiendum — δ reconciles them at the root.
316pub fn check_derivation_with_defs(
317    premises: &[ProofExpr],
318    goal: &ProofExpr,
319    definitions: &[Definition],
320    derivation: DerivationTree,
321) -> VerifiedProof {
322    on_big_stack(|| check_derivation_with_defs_inner(premises, goal, definitions, derivation))
323}
324
325fn check_derivation_with_defs_inner(
326    premises: &[ProofExpr],
327    goal: &ProofExpr,
328    definitions: &[Definition],
329    derivation: DerivationTree,
330) -> VerifiedProof {
331    let abstracted = abstract_definitions(definitions);
332    let definitions = &abstracted[..];
333    if let Err(e) = validate_definitions(definitions) {
334        return definition_error(e);
335    }
336    let (kernel_ctx, _flat_premises, abstracted_goal) =
337        prepare_ctx_with_defs(premises, goal, definitions);
338    finish_check(kernel_ctx, &abstracted_goal, derivation)
339}
340
341/// Build the kernel context shared by [`prove_certify_check_bounded`] and
342/// [`check_derivation`]: register the standard library, any user [`Definition`]s
343/// (Rung 0a), the event-ABSTRACTED, conjunction-SPLIT premises as hypotheses
344/// `h1, h2, …`, and every predicate/constant referenced. Returns the context, the
345/// flattened premises, and the abstracted goal.
346fn prepare_ctx_with_defs(
347    premises: &[ProofExpr],
348    goal: &ProofExpr,
349    definitions: &[Definition],
350) -> (Context, Vec<ProofExpr>, ProofExpr) {
351    let mut kernel_ctx = Context::new();
352    StandardLibrary::register(&mut kernel_ctx);
353
354    // Rung 0a: register user definitions FIRST, so a definiendum becomes a
355    // δ-unfoldable definition (with a body) rather than an opaque axiom. The
356    // opaque `register_predicate` pass below then skips it (idempotent on
357    // `get_global`). `validate_definitions` already rejected recursive /
358    // unlowerable definitions, so registration here cannot loop.
359    for def in definitions {
360        register_definition(&mut kernel_ctx, def);
361    }
362
363    // The search, the hypotheses, and the goal type must all live in ONE
364    // language: the engine's event-ABSTRACTED form (∃e(Lie(e) ∧ Agent(e, m))
365    // ↦ lie(m)). Hypotheses registered in the raw event form could never be
366    // found by the certifier, because derivation leaves cite abstracted
367    // conclusions. Premises are also split at top-level conjunctions (the
368    // standard sequent move: Γ, A ∧ B ⊢ φ iff Γ, A, B ⊢ φ) so a projected
369    // presupposition conjunct is an exactly-matchable hypothesis.
370    fn split_conjuncts(expr: &ProofExpr, out: &mut Vec<ProofExpr>) {
371        if let ProofExpr::And(l, r) = expr {
372            split_conjuncts(l, out);
373            split_conjuncts(r, out);
374        } else {
375            out.push(expr.clone());
376        }
377    }
378    let engine_for_abstraction = BackwardChainer::new();
379    let mut flat_premises = Vec::new();
380    for premise in premises {
381        let abstracted = engine_for_abstraction.abstract_all_events(premise);
382        // Biconditional elimination: rewrite `P ↔ Q` premises into `(P→Q) ∧ (Q→P)`
383        // (the kernel has no `Iff` type), so either direction is usable by the
384        // existing implication + modus-ponens machinery after conjunction split.
385        split_conjuncts(&expand_iff(&abstracted), &mut flat_premises);
386    }
387    let abstracted_goal = engine_for_abstraction.abstract_all_events(goal);
388
389    // Register predicates and constants referenced by premises and goal.
390    let mut collector = SymbolCollector::new();
391    for premise in &flat_premises {
392        collector.collect(premise);
393    }
394    collector.collect(&abstracted_goal);
395    // Definiens bodies reference atomic predicates (and constants) that must be
396    // registered opaquely too, or kernel type-checking of the definition body
397    // fails. The definiendum itself is already a global (registered above), so
398    // `register_predicate` skips it.
399    for def in definitions {
400        collector.collect(&def.definiens);
401    }
402    // Inductive-domain predicates first (the induction motive needs `P : Ind → Prop`,
403    // e.g. `Nat → Prop` or `List → Prop`); the generic `Entity` registration below is
404    // idempotent and skips them.
405    for (name, ind) in collector.inductive_predicates() {
406        if kernel_ctx.get_global(name).is_none() {
407            kernel_ctx.add_declaration(
408                name,
409                Term::Pi {
410                    param: "_".to_string(),
411                    param_type: Box::new(Term::Global(ind.to_string())),
412                    body_type: Box::new(Term::Sort(Universe::Prop)),
413                },
414            );
415        }
416    }
417    for (name, arity) in collector.predicates() {
418        register_predicate(&mut kernel_ctx, name, arity);
419    }
420    for (name, arity) in collector.functions() {
421        register_function(&mut kernel_ctx, name, arity);
422    }
423    // Temporal modalities `Op : Prop → Prop` (`Past`, `Future`, …), so a tensed
424    // proposition `Past(P)` type-checks as a distinct proposition from `P`.
425    for name in collector.modalities() {
426        register_modality(&mut kernel_ctx, name);
427    }
428    // Atoms in arithmetic/comparison positions are `Int`, declared before the
429    // generic `Entity` constants so `register_constant` skips them.
430    for name in collector.int_atoms() {
431        if kernel_ctx.get_global(name).is_none() {
432            kernel_ctx.add_declaration(name, Term::Global("Int".to_string()));
433        }
434    }
435    // Definition parameters are λ-bound in the registered body, not constants;
436    // skip them so the definiens does not leak a spurious `p : Entity` global.
437    let param_names: HashSet<&str> = definitions
438        .iter()
439        .flat_map(|d| d.params.iter().map(String::as_str))
440        .collect();
441    for name in collector.constants() {
442        if param_names.contains(name.as_str()) {
443            continue;
444        }
445        register_constant(&mut kernel_ctx, name);
446    }
447
448    for (i, premise) in flat_premises.iter().enumerate() {
449        if let Ok(hyp_type) = proof_expr_to_type_ctx(premise, &kernel_ctx) {
450            let hyp_name = format!("h{}", i + 1);
451            kernel_ctx.add_declaration(&hyp_name, hyp_type);
452        }
453    }
454
455    // Also register each original CONJUNCTIVE premise as a WHOLE hypothesis, in
456    // addition to its split conjuncts. A derivation that references the conjunction
457    // itself — e.g. a tactic `cases`/`ConjunctionElim` projecting `A` out of `A ∧ B`
458    // — then resolves, while the search keeps using the flat conjuncts it prefers.
459    for (i, premise) in premises.iter().enumerate() {
460        let whole = expand_iff(&engine_for_abstraction.abstract_all_events(premise));
461        if matches!(whole, ProofExpr::And(_, _)) {
462            if let Ok(hyp_type) = proof_expr_to_type(&whole) {
463                kernel_ctx.add_declaration(&format!("hw{}", i + 1), hyp_type);
464            }
465        }
466    }
467
468    (kernel_ctx, flat_premises, abstracted_goal)
469}
470
471/// Certify `derivation` and require its inferred kernel type to be the goal type —
472/// the trust core shared by the prove and check entries.
473/// Run `f` on a thread with a large stack. Proof search AND certification both
474/// recurse to a depth proportional to the proof: a long chain re-derived from the
475/// axioms is legitimately deep, so the default ~8 MB native stack is not enough.
476/// The `max_depth` bound still terminates search and the loop detector still prunes
477/// regress — this only removes the *native*-stack ceiling so a deep, finite proof
478/// completes instead of aborting (the standard recursive-descent remedy).
479pub(crate) fn on_big_stack<T: Send>(f: impl FnOnce() -> T + Send) -> T {
480    // wasm has no threads, so `spawn_scoped` would return `Unsupported` and abort every in-browser
481    // proof. Run inline there — the `max_depth` bound and loop detector still terminate search; only
482    // the *native* ~8 MB stack ceiling needs the worker thread, and wasm sizes its stack at build
483    // time instead. The browser links this crate, so this path must never spawn.
484    #[cfg(target_arch = "wasm32")]
485    {
486        f()
487    }
488    #[cfg(not(target_arch = "wasm32"))]
489    {
490        std::thread::scope(|s| {
491            std::thread::Builder::new()
492                .stack_size(32 * 1024 * 1024)
493                .spawn_scoped(s, f)
494                .expect("spawn proof thread")
495                .join()
496                .expect("proof thread panicked")
497        })
498    }
499}
500
501fn finish_check(
502    kernel_ctx: Context,
503    abstracted_goal: &ProofExpr,
504    derivation: DerivationTree,
505) -> VerifiedProof {
506    let trace = std::env::var("LOGOS_TRACE").is_ok();
507    // === Certify ===
508    let t_cert = trace.then(std::time::Instant::now);
509    let proof_term = {
510        let cert_ctx = CertificationContext::new(&kernel_ctx);
511        match certify(&derivation, &cert_ctx) {
512            Ok(t) => t,
513            Err(e) => {
514                return VerifiedProof {
515                    derivation: Some(derivation),
516                    proof_term: None,
517                    kernel_ctx,
518                    verified: false,
519                    verification_error: Some(format!("Certification failed: {}", e)),
520                };
521            }
522        }
523    };
524    if let Some(t_cert) = t_cert {
525        eprintln!(
526            "[cert] certify(build) {:.2?} → {} kernel-term nodes",
527            t_cert.elapsed(),
528            count_term_nodes(&proof_term)
529        );
530    }
531
532    // === Kernel type-check ===
533    // The term must not merely be well-typed — its type must be the goal.
534    // Otherwise a certifier that produced a well-formed proof of the *wrong*
535    // proposition would be wrongly accepted. We compute the goal's kernel type
536    // and require the inferred type to match it (up to definitional equality).
537    let t_infer = trace.then(std::time::Instant::now);
538    let inferred = match infer_type(&kernel_ctx, &proof_term) {
539        Ok(t) => t,
540        Err(e) => {
541            return VerifiedProof {
542                derivation: Some(derivation),
543                proof_term: None,
544                kernel_ctx,
545                verified: false,
546                verification_error: Some(format!("Type check failed: {}", e)),
547            };
548        }
549    };
550    if let Some(t_infer) = t_infer {
551        eprintln!("[cert] infer_type(check) {:.2?}", t_infer.elapsed());
552    }
553
554    let goal_type = match proof_expr_to_type_ctx(abstracted_goal, &kernel_ctx) {
555        Ok(t) => t,
556        Err(e) => {
557            return VerifiedProof {
558                derivation: Some(derivation),
559                proof_term: None,
560                kernel_ctx,
561                verified: false,
562                verification_error: Some(format!(
563                    "Cannot express the goal as a kernel type: {}",
564                    e
565                )),
566            };
567        }
568    };
569
570    if !is_subtype(&kernel_ctx, &inferred, &goal_type) {
571        return VerifiedProof {
572            derivation: Some(derivation),
573            proof_term: None,
574            kernel_ctx,
575            verified: false,
576            verification_error: Some(format!(
577                "Proof term proves a different proposition: inferred {:?}, goal {:?}",
578                inferred, goal_type
579            )),
580        };
581    }
582
583    // === Independent re-check (R1, the de Bruijn criterion) ===
584    // A second, independently-written kernel (a de Bruijn checker, distinct from
585    // `infer_type` above) must concur. `Agreed` is two-kernel verification; an honest
586    // `Incomplete` (a fragment the re-checker does not cover, e.g. a δ-conversion it keeps
587    // opaque) leaves the main kernel authoritative. Only a genuine `Disagree` — one kernel
588    // accepts, the other rejects, or they infer different types — rejects the proof: that
589    // is exactly the soundness alarm a second kernel exists to raise.
590    if let DoubleCheck::Disagree(why) = double_check(&kernel_ctx, &proof_term) {
591        return VerifiedProof {
592            derivation: Some(derivation),
593            proof_term: None,
594            kernel_ctx,
595            verified: false,
596            verification_error: Some(format!("Independent re-checker disagreement: {}", why)),
597        };
598    }
599
600    VerifiedProof {
601        derivation: Some(derivation),
602        proof_term: Some(proof_term),
603        kernel_ctx,
604        verified: true,
605        verification_error: None,
606    }
607}
608
609/// The result of checking a rule set for an internal contradiction.
610///
611/// `inconsistent == true` is never a bare verdict: it is backed by `proof_term`,
612/// a kernel term of type `False` that re-checks under [`infer_type`] in
613/// `kernel_ctx`. `conflicting_premises` names the input premises (by index) that
614/// the proof of ⊥ actually used — the rules that clash.
615pub struct ConflictReport {
616    /// True iff the premises are jointly inconsistent *and* a kernel-checked
617    /// proof of `False` was produced.
618    pub inconsistent: bool,
619    /// The kernel proof of `False`, present only when `inconsistent`.
620    pub proof_term: Option<Term>,
621    /// The kernel context the proof term checks in.
622    pub kernel_ctx: Context,
623    /// Indices (into the input `premises`) of the premises the proof used.
624    pub conflicting_premises: Vec<usize>,
625    /// Why detection failed to certify, if a contradiction was sketched but not
626    /// kernel-checked (or none was found).
627    pub error: Option<String>,
628}
629
630/// Detect whether `premises` are jointly inconsistent, returning a
631/// kernel-checked proof of `False` and the indices of the clashing premises.
632///
633/// This is verified conflict detection: where an SMT-only tool returns "unsat",
634/// this returns a certificate anyone can re-check, plus *which* rules conflict.
635/// A consistent rule set yields `inconsistent == false` with no proof — no false
636/// alarms.
637pub fn detect_conflict(premises: &[ProofExpr]) -> ConflictReport {
638    let falsum = ProofExpr::Atom("⊥".to_string());
639    let outcome = prove_certify_check(premises, &falsum);
640
641    if !outcome.verified {
642        return ConflictReport {
643            inconsistent: false,
644            proof_term: None,
645            kernel_ctx: outcome.kernel_ctx,
646            conflicting_premises: Vec::new(),
647            error: outcome.verification_error,
648        };
649    }
650
651    // Collect which premises the derivation actually referenced. Every premise
652    // enters the proof as a `PremiseMatch` leaf (directly, or as the source of a
653    // `UniversalInst`); we match those leaf conclusions back to the inputs.
654    let mut used: Vec<usize> = Vec::new();
655    if let Some(derivation) = &outcome.derivation {
656        let mut leaves: Vec<&ProofExpr> = Vec::new();
657        collect_premise_leaves(derivation, &mut leaves);
658        for (i, premise) in premises.iter().enumerate() {
659            if leaves.iter().any(|leaf| *leaf == premise) && !used.contains(&i) {
660                used.push(i);
661            }
662        }
663    }
664
665    ConflictReport {
666        inconsistent: true,
667        proof_term: outcome.proof_term,
668        kernel_ctx: outcome.kernel_ctx,
669        conflicting_premises: used,
670        error: None,
671    }
672}
673
674/// Collect the conclusions of every `PremiseMatch` leaf in a derivation — the
675/// premises (and instantiation sources) the proof draws on.
676fn collect_premise_leaves<'a>(tree: &'a DerivationTree, out: &mut Vec<&'a ProofExpr>) {
677    if matches!(tree.rule, crate::InferenceRule::PremiseMatch) {
678        out.push(&tree.conclusion);
679    }
680    for premise in &tree.premises {
681        collect_premise_leaves(premise, out);
682    }
683}
684
685// =============================================================================
686// Symbol collection & registration (proof + kernel only — no language dep)
687// =============================================================================
688
689/// Collects predicate and constant names from a `ProofExpr`. Predicates are
690/// keyed to their arity (the max seen) so they register with the right type
691/// `Entity → … → Entity → Prop`, which is what lets relations like `shaves(a,b)`
692/// type-check.
693struct SymbolCollector {
694    predicates: HashMap<String, usize>,
695    functions: HashMap<String, usize>,
696    constants: HashSet<String>,
697    /// Lowercase `Constant` names — candidate individual constants. The pipeline
698    /// capitalizes proper names, but a definite description ("the butler") yields
699    /// a lowercase `Constant("butler")`. Such a name IS an individual constant
700    /// unless the SAME name is also used as a genuine variable (a quantifier
701    /// binder or a `Variable` occurrence), in which case the lowercase `Constant`
702    /// is a stray variable spelling and must stay unregistered.
703    weak_constants: HashSet<String>,
704    /// Names that occur as a genuine variable — a quantifier binder or a
705    /// `ProofTerm::Variable`. A `weak_constants` candidate colliding with one of
706    /// these is NOT registered as a constant.
707    variables: HashSet<String>,
708    /// Atoms that occur in an arithmetic/comparison argument position, so must be
709    /// typed `Int` (not the `Entity` default) — `le`/`lt`/… need `Int` operands.
710    int_atoms: HashSet<String>,
711    /// Unary predicates applied to an inductive-type constructor (`Zero`/`Succ` →
712    /// `Nat`, `Nil`/`Cons` → `List`), mapped to that inductive type's name, so they
713    /// are typed `<Ind> → Prop` (not the `Entity` default) — the motive of a
714    /// structural-`induction` proof is `λx:Ind. P(x)`, so `P` must accept `Ind`.
715    inductive_predicates: HashMap<String, &'static str>,
716    /// Temporal modality operators (`Past`, `Future`, …) seen wrapping a
717    /// proposition, each registered as an opaque `Op : Prop → Prop`.
718    modalities: HashSet<String>,
719}
720
721impl SymbolCollector {
722    fn new() -> Self {
723        SymbolCollector {
724            predicates: HashMap::new(),
725            functions: HashMap::new(),
726            constants: HashSet::new(),
727            weak_constants: HashSet::new(),
728            variables: HashSet::new(),
729            int_atoms: HashSet::new(),
730            inductive_predicates: HashMap::new(),
731            modalities: HashSet::new(),
732        }
733    }
734
735    /// Mark a term that sits in an Int position. Atoms become `Int`; nested
736    /// arithmetic propagates the Int-ness to its own operands.
737    fn mark_int(&mut self, term: &ProofTerm) {
738        match term {
739            // Numeric literals lower to `Lit(Int)` (certifier) — no declaration.
740            ProofTerm::Constant(s) if s.parse::<i64>().is_ok() => {}
741            ProofTerm::Constant(s) | ProofTerm::Variable(s) | ProofTerm::BoundVarRef(s) => {
742                self.int_atoms.insert(s.clone());
743            }
744            ProofTerm::Function(name, args)
745                if matches!(name.as_str(), "add" | "sub" | "mul" | "div" | "mod") =>
746            {
747                for a in args {
748                    self.mark_int(a);
749                }
750            }
751            _ => {}
752        }
753    }
754
755    fn note_predicate(&mut self, name: &str, arity: usize) {
756        self.predicates
757            .entry(name.to_string())
758            .and_modify(|a| *a = (*a).max(arity))
759            .or_insert(arity);
760    }
761
762    /// A `ProofTerm::Function` is Entity-valued (a function on entities), never a
763    /// proposition — only `ProofExpr::Predicate` is propositional. Registering it
764    /// as `Entity → … → Entity` (not `… → Prop`) lets `F(a)` appear inside terms
765    /// like `Eq Entity (F a) (F b)`, the basis for congruence.
766    fn note_function(&mut self, name: &str, arity: usize) {
767        self.functions
768            .entry(name.to_string())
769            .and_modify(|a| *a = (*a).max(arity))
770            .or_insert(arity);
771    }
772
773    fn collect(&mut self, expr: &ProofExpr) {
774        match expr {
775            ProofExpr::Predicate { name, args, .. } => {
776                self.note_predicate(name, args.len());
777                // A unary predicate applied to an inductive-type constructor is
778                // `<Ind> → Prop`, so it can be the motive of a structural induction.
779                if args.len() == 1 {
780                    if let Some(ind) = constructor_domain(&args[0]) {
781                        self.inductive_predicates.insert(name.clone(), ind);
782                    }
783                }
784                for arg in args {
785                    self.collect_term(arg);
786                }
787            }
788            ProofExpr::And(l, r)
789            | ProofExpr::Or(l, r)
790            | ProofExpr::Implies(l, r)
791            | ProofExpr::Iff(l, r) => {
792                self.collect(l);
793                self.collect(r);
794            }
795            ProofExpr::Not(inner) => self.collect(inner),
796            ProofExpr::ForAll { variable, body } | ProofExpr::Exists { variable, body } => {
797                self.variables.insert(variable.clone());
798                self.collect(body);
799            }
800            // A temporal modality `Op(P)`: note the operator (registered as
801            // `Op : Prop → Prop`) and recurse so `P`'s own predicates register.
802            ProofExpr::Temporal { operator, body } => {
803                self.modalities.insert(operator.clone());
804                self.collect(body);
805            }
806            ProofExpr::Identity(l, r) => {
807                self.collect_term(l);
808                self.collect_term(r);
809            }
810            // Atoms are propositional constants, not FOL predicates.
811            _ => {}
812        }
813    }
814
815    fn collect_term(&mut self, term: &ProofTerm) {
816        match term {
817            ProofTerm::Constant(name) => {
818                // Proper names (capitalized) and numeric labels (a year like `2004`)
819                // are unambiguously Entity constants. A lowercase name — a definite
820                // description's referent ("the butler" → `Constant("butler")`) — is a
821                // constant TOO, but only if it is never used as a genuine variable
822                // (a binder or `Variable` occurrence); that collision is resolved in
823                // `constants()`, once every symbol has been seen.
824                if name
825                    .chars()
826                    .next()
827                    .map(|c| c.is_uppercase() || c.is_ascii_digit())
828                    .unwrap_or(false)
829                {
830                    self.constants.insert(name.clone());
831                } else {
832                    self.weak_constants.insert(name.clone());
833                }
834            }
835            ProofTerm::Variable(name) | ProofTerm::BoundVarRef(name) => {
836                self.variables.insert(name.clone());
837            }
838            ProofTerm::Function(name, args) => {
839                // Arithmetic / comparison builtins are prelude globals (typed
840                // `Int → … → Int`/`Bool`); don't re-declare them, but type their
841                // operands as `Int`. Other functions are uninterpreted (Entity).
842                if matches!(
843                    name.as_str(),
844                    "le" | "lt" | "ge" | "gt" | "add" | "sub" | "mul" | "div" | "mod"
845                ) {
846                    for arg in args {
847                        self.mark_int(arg);
848                    }
849                } else {
850                    self.note_function(name, args.len());
851                }
852                for arg in args {
853                    self.collect_term(arg);
854                }
855            }
856            _ => {}
857        }
858    }
859
860    fn predicates(&self) -> impl Iterator<Item = (&String, usize)> {
861        self.predicates.iter().map(|(n, a)| (n, *a))
862    }
863
864    fn functions(&self) -> impl Iterator<Item = (&String, usize)> {
865        self.functions.iter().map(|(n, a)| (n, *a))
866    }
867
868    fn int_atoms(&self) -> impl Iterator<Item = &String> {
869        self.int_atoms.iter()
870    }
871
872    fn inductive_predicates(&self) -> impl Iterator<Item = (&String, &'static str)> {
873        self.inductive_predicates.iter().map(|(n, d)| (n, *d))
874    }
875
876    fn constants(&self) -> Vec<&String> {
877        // Capitalized/numeric constants always register; a lowercase candidate
878        // registers only when the name is never a genuine variable.
879        self.constants
880            .iter()
881            .chain(
882                self.weak_constants
883                    .iter()
884                    .filter(|n| !self.variables.contains(*n)),
885            )
886            .collect()
887    }
888
889    fn modalities(&self) -> impl Iterator<Item = &String> {
890        self.modalities.iter()
891    }
892}
893
894/// The inductive type whose constructor `t` is — `Zero`/`Succ` ⇒ `Nat`,
895/// `ENil`/`ECons` ⇒ `EList` — or `None` if `t` is not a recognized prelude
896/// constructor. The signal that a unary predicate applied to `t` ranges over that
897/// inductive type, not `Entity`, so it can serve as a structural-induction motive
898/// `λx:Ind. P(x)`. (The `E`-prefixed list names are the prelude's monomorphic
899/// `EList`, distinct from a user program's parametric `List`/`Nil`/`Cons`.)
900fn constructor_domain(t: &ProofTerm) -> Option<&'static str> {
901    let name = match t {
902        ProofTerm::Constant(s) => s.as_str(),
903        ProofTerm::Function(n, _) => n.as_str(),
904        _ => return None,
905    };
906    match name {
907        "Zero" | "Succ" => Some("Nat"),
908        "ENil" | "ECons" => Some("EList"),
909        _ => None,
910    }
911}
912
913/// Register a predicate `P : Entity → … → Entity → Prop` of the given arity
914/// (idempotent). Arity 0 registers a propositional constant `P : Prop`.
915#[doc(hidden)]
916fn count_term_nodes(t: &Term) -> usize {
917    match t {
918        Term::App(f, a) => 1 + count_term_nodes(f) + count_term_nodes(a),
919        Term::Pi { param_type, body_type, .. } => 1 + count_term_nodes(param_type) + count_term_nodes(body_type),
920        Term::Lambda { param_type, body, .. } => 1 + count_term_nodes(param_type) + count_term_nodes(body),
921        Term::Match { discriminant, motive, cases } => {
922            1 + count_term_nodes(discriminant) + count_term_nodes(motive)
923                + cases.iter().map(count_term_nodes).sum::<usize>()
924        }
925        Term::Fix { body, .. } => 1 + count_term_nodes(body),
926        _ => 1,
927    }
928}
929
930fn register_predicate(ctx: &mut Context, name: &str, arity: usize) {
931    if ctx.get_global(name).is_some() {
932        return;
933    }
934    let mut ty = Term::Sort(Universe::Prop);
935    for _ in 0..arity {
936        ty = Term::Pi {
937            param: "_".to_string(),
938            param_type: Box::new(Term::Global("Entity".to_string())),
939            body_type: Box::new(ty),
940        };
941    }
942    ctx.add_declaration(name, ty);
943}
944
945/// Register a function symbol `f : Entity → … → Entity → Entity` of the given arity
946/// (idempotent). Unlike a predicate, a function is Entity-valued, so `f(a)` is a
947/// term that can be compared by equality and rewritten under congruence.
948fn register_function(ctx: &mut Context, name: &str, arity: usize) {
949    if ctx.get_global(name).is_some() {
950        return;
951    }
952    let mut ty = Term::Global("Entity".to_string());
953    for _ in 0..arity {
954        ty = Term::Pi {
955            param: "_".to_string(),
956            param_type: Box::new(Term::Global("Entity".to_string())),
957            body_type: Box::new(ty),
958        };
959    }
960    ctx.add_declaration(name, ty);
961}
962
963/// Register a constant `c : Entity` (idempotent).
964fn register_constant(ctx: &mut Context, name: &str) {
965    if ctx.get_global(name).is_some() {
966        return;
967    }
968    ctx.add_declaration(name, Term::Global("Entity".to_string()));
969}
970
971/// Register a temporal modality `Op : Prop → Prop` (idempotent). A tensed
972/// proposition `Past(P)` lowers to `(Op P)`, distinct from `P`, so a modus-tollens
973/// chain over tensed premises certifies.
974fn register_modality(ctx: &mut Context, name: &str) {
975    if ctx.get_global(name).is_some() {
976        return;
977    }
978    ctx.add_declaration(
979        name,
980        Term::Pi {
981            param: "_".to_string(),
982            param_type: Box::new(Term::Sort(Universe::Prop)),
983            body_type: Box::new(Term::Sort(Universe::Prop)),
984        },
985    );
986}
987
988/// A failed-verification result carrying a definition-level error message.
989fn definition_error(message: String) -> VerifiedProof {
990    VerifiedProof {
991        derivation: None,
992        proof_term: None,
993        kernel_ctx: Context::new(),
994        verified: false,
995        verification_error: Some(message),
996    }
997}
998
999/// Abstract neo-Davidsonian events in each definiens into first-order form — the
1000/// SAME transformation premises and goals get in [`prepare_ctx_with_defs`]. A
1001/// definiens over a verb (`someone admires x` ⇒ a `NeoEvent`) becomes an ordinary
1002/// predicate the kernel can type and the engine can match against the theorem's
1003/// (also-abstracted) events.
1004fn abstract_definitions(definitions: &[Definition]) -> Vec<Definition> {
1005    let abstractor = BackwardChainer::new();
1006    definitions
1007        .iter()
1008        .map(|d| Definition {
1009            name: d.name.clone(),
1010            params: d.params.clone(),
1011            definiens: abstractor.abstract_all_events(&d.definiens),
1012        })
1013        .collect()
1014}
1015
1016/// Reject definitions that cannot be soundly registered (Rung 0a guards):
1017/// self-recursive definienda (δ-unfolding would not terminate) and definiens
1018/// bodies the kernel lowering cannot type. Surfacing these up front gives a
1019/// clear message instead of a silent opaque fallback or a fuel-capped failure.
1020///
1021/// Mutual recursion *across* definitions is a Rung 0b (library DAG) concern and
1022/// is not yet detected here; the kernel's normalize fuel cap is the backstop —
1023/// an unguarded cycle fails the proof, it never hangs or returns a false proof.
1024fn validate_definitions(definitions: &[Definition]) -> Result<(), String> {
1025    if definitions.is_empty() {
1026        return Ok(());
1027    }
1028    for def in definitions {
1029        if let Err(e) = proof_expr_to_type(&def.definiens) {
1030            return Err(format!("cannot lower definition `{}`: {:?}", def.name, e));
1031        }
1032    }
1033    // δ-unfolding a cycle (self or mutual) would not terminate. The def→def graph
1034    // catches both; `find_definition_cycle` returns the offending names.
1035    if let Some(cycle) = find_definition_cycle(definitions) {
1036        return Err(format!(
1037            "circular definition among {{{}}}: a definition may not recursively refer \
1038             to itself, directly or transitively",
1039            cycle.join(", ")
1040        ));
1041    }
1042    Ok(())
1043}
1044
1045/// Register one definition as a δ-unfoldable kernel definition:
1046/// `name : Entity → … → Prop := λ(p₁:Entity)…λ(pₙ:Entity). <definiens>`.
1047///
1048/// The definiens lowers its parameters to `Global(p)` (constants), but a kernel
1049/// `Lambda` binds `Var(p)` — so we rewrite `Global(p) → Var(p)` for each
1050/// parameter before abstracting. Assumes [`validate_definitions`] has passed.
1051fn register_definition(ctx: &mut Context, def: &Definition) {
1052    if ctx.get_global(&def.name).is_some() {
1053        return;
1054    }
1055    let mut body = match proof_expr_to_type(&def.definiens) {
1056        Ok(b) => b,
1057        Err(_) => {
1058            // Defensive: validation should have caught this. Keep the context
1059            // well-formed by registering the definiendum as an opaque predicate.
1060            register_predicate(ctx, &def.name, def.params.len());
1061            return;
1062        }
1063    };
1064    for p in &def.params {
1065        body = subst_global_to_var(body, p);
1066    }
1067    // Abstract innermost-last so parameters bind left-to-right.
1068    for p in def.params.iter().rev() {
1069        body = Term::Lambda {
1070            param: p.clone(),
1071            param_type: Box::new(Term::Global("Entity".to_string())),
1072            body: Box::new(body),
1073        };
1074    }
1075    let mut ty = Term::Sort(Universe::Prop);
1076    for _ in &def.params {
1077        ty = Term::Pi {
1078            param: "_".to_string(),
1079            param_type: Box::new(Term::Global("Entity".to_string())),
1080            body_type: Box::new(ty),
1081        };
1082    }
1083    ctx.add_definition(def.name.clone(), ty, body);
1084}
1085
1086/// Rewrite every free `Global(name)` in `term` to `Var(name)` — used to turn a
1087/// definiens parameter (lowered as a global constant) into a bindable variable
1088/// before λ-abstraction.
1089fn subst_global_to_var(term: Term, name: &str) -> Term {
1090    match term {
1091        Term::Global(n) => {
1092            if n == name {
1093                Term::Var(n)
1094            } else {
1095                Term::Global(n)
1096            }
1097        }
1098        Term::App(f, a) => Term::App(
1099            Box::new(subst_global_to_var(*f, name)),
1100            Box::new(subst_global_to_var(*a, name)),
1101        ),
1102        Term::Pi { param, param_type, body_type } => Term::Pi {
1103            param,
1104            param_type: Box::new(subst_global_to_var(*param_type, name)),
1105            body_type: Box::new(subst_global_to_var(*body_type, name)),
1106        },
1107        Term::Lambda { param, param_type, body } => Term::Lambda {
1108            param,
1109            param_type: Box::new(subst_global_to_var(*param_type, name)),
1110            body: Box::new(subst_global_to_var(*body, name)),
1111        },
1112        Term::Match { discriminant, motive, cases } => Term::Match {
1113            discriminant: Box::new(subst_global_to_var(*discriminant, name)),
1114            motive: Box::new(subst_global_to_var(*motive, name)),
1115            cases: cases.into_iter().map(|c| subst_global_to_var(c, name)).collect(),
1116        },
1117        Term::Fix { name: fix_name, body } => Term::Fix {
1118            name: fix_name,
1119            body: Box::new(subst_global_to_var(*body, name)),
1120        },
1121        other => other,
1122    }
1123}
1124
1125/// Collect, into `out`, every predicate name in `expr` that is one of the
1126/// `defined` names — i.e. the definitions this expression directly *uses*. Walks
1127/// the propositional structure (predicates are δ-expanded only in predicate
1128/// position, so that is exactly what we scan). Deduplicated, insertion-ordered.
1129fn collect_defined_predicates(expr: &ProofExpr, defined: &HashSet<&str>, out: &mut Vec<String>) {
1130    match expr {
1131        ProofExpr::Predicate { name, .. } => {
1132            if defined.contains(name.as_str()) && !out.iter().any(|n| n == name) {
1133                out.push(name.clone());
1134            }
1135        }
1136        ProofExpr::And(l, r)
1137        | ProofExpr::Or(l, r)
1138        | ProofExpr::Implies(l, r)
1139        | ProofExpr::Iff(l, r) => {
1140            collect_defined_predicates(l, defined, out);
1141            collect_defined_predicates(r, defined, out);
1142        }
1143        ProofExpr::Not(p) => collect_defined_predicates(p, defined, out),
1144        ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
1145            collect_defined_predicates(body, defined, out)
1146        }
1147        _ => {}
1148    }
1149}
1150
1151/// The direct def→def `uses` edges: for each definition, the OTHER definitions
1152/// its definiens references. O(total definiens size); membership is O(1).
1153fn def_edges(definitions: &[Definition]) -> Vec<(String, Vec<String>)> {
1154    let defined: HashSet<&str> = definitions.iter().map(|d| d.name.as_str()).collect();
1155    definitions
1156        .iter()
1157        .map(|d| {
1158            let mut uses = Vec::new();
1159            collect_defined_predicates(&d.definiens, &defined, &mut uses);
1160            (d.name.clone(), uses)
1161        })
1162        .collect()
1163}
1164
1165/// Detect a cycle (self-loop or mutual) in the def→def `uses` graph via Kahn's
1166/// topological elimination: if not every node can be peeled to zero in-degree,
1167/// the residual nodes form/feed cycles. Returns those names (sorted), or `None`
1168/// for a DAG. O(V + E).
1169fn find_definition_cycle(definitions: &[Definition]) -> Option<Vec<String>> {
1170    let edges = def_edges(definitions);
1171    let adj: HashMap<&str, &[String]> =
1172        edges.iter().map(|(n, u)| (n.as_str(), u.as_slice())).collect();
1173
1174    let mut indeg: HashMap<&str, usize> = adj.keys().map(|n| (*n, 0usize)).collect();
1175    for deps in adj.values() {
1176        for d in deps.iter() {
1177            if let Some(c) = indeg.get_mut(d.as_str()) {
1178                *c += 1;
1179            }
1180        }
1181    }
1182
1183    let mut queue: Vec<&str> = indeg
1184        .iter()
1185        .filter(|(_, &c)| c == 0)
1186        .map(|(n, _)| *n)
1187        .collect();
1188    let mut removed = 0usize;
1189    while let Some(n) = queue.pop() {
1190        removed += 1;
1191        if let Some(deps) = adj.get(n) {
1192            for d in deps.iter() {
1193                if let Some(c) = indeg.get_mut(d.as_str()) {
1194                    *c -= 1;
1195                    if *c == 0 {
1196                        queue.push(d.as_str());
1197                    }
1198                }
1199            }
1200        }
1201    }
1202
1203    if removed == adj.len() {
1204        None
1205    } else {
1206        let mut cyclic: Vec<String> = indeg
1207            .iter()
1208            .filter(|(_, &c)| c > 0)
1209            .map(|(n, _)| n.to_string())
1210            .collect();
1211        cyclic.sort();
1212        Some(cyclic)
1213    }
1214}
1215
1216/// The `uses` dependency graph among definitions and from a theorem to its
1217/// definitions — the Rung 0b graph seed (each node a definition/theorem, each
1218/// edge a `uses`). Direct edges only; transitive use is a query over this. This
1219/// is the structure a `mathscrapes` node/edge compiles into.
1220#[derive(Debug, Clone, Default)]
1221pub struct DependencyGraph {
1222    /// definiendum name → the defined names its definiens directly uses.
1223    pub def_uses: Vec<(String, Vec<String>)>,
1224    /// the defined names the theorem's premises + goal directly use.
1225    pub theorem_uses: Vec<String>,
1226}
1227
1228/// Build the [`DependencyGraph`] for a set of definitions plus a theorem
1229/// (premises + goal), recording which definitions each definiens uses and which
1230/// the theorem uses. Pure, allocation-light, O(total expression size).
1231pub fn dependency_graph(
1232    definitions: &[Definition],
1233    premises: &[ProofExpr],
1234    goal: &ProofExpr,
1235) -> DependencyGraph {
1236    let defined: HashSet<&str> = definitions.iter().map(|d| d.name.as_str()).collect();
1237    let def_uses = def_edges(definitions);
1238    let mut theorem_uses = Vec::new();
1239    for p in premises {
1240        collect_defined_predicates(p, &defined, &mut theorem_uses);
1241    }
1242    collect_defined_predicates(goal, &defined, &mut theorem_uses);
1243    DependencyGraph {
1244        def_uses,
1245        theorem_uses,
1246    }
1247}
1248
1249/// Maximum δ-expansion depth for expand-for-search — a backstop against an
1250/// accidental cross-definition cycle (self-recursion is already rejected by
1251/// [`validate_definitions`]; cross-def cycle detection is a Rung 0b concern).
1252const MAX_EXPANSION_DEPTH: usize = 64;
1253
1254/// δ-expand every defined predicate in `expr`: replace `def(args)` with its
1255/// definiens (parameters substituted by `args`), recursively, so the result is
1256/// stated purely in terms of undefined predicates the backward chainer can
1257/// search over. Non-defined predicates and all other nodes are walked
1258/// structurally. The identity when `defs` is empty.
1259fn expand_defs_in_expr(
1260    expr: &ProofExpr,
1261    defs: &HashMap<&str, &Definition>,
1262    depth: usize,
1263) -> ProofExpr {
1264    if depth >= MAX_EXPANSION_DEPTH {
1265        return expr.clone();
1266    }
1267    match expr {
1268        ProofExpr::Predicate { name, args, .. } => {
1269            if let Some(def) = defs.get(name.as_str()) {
1270                if def.params.len() == args.len() {
1271                    let substituted = substitute_params(&def.definiens, &def.params, args);
1272                    return expand_defs_in_expr(&substituted, defs, depth + 1);
1273                }
1274            }
1275            expr.clone()
1276        }
1277        ProofExpr::And(l, r) => ProofExpr::And(
1278            Box::new(expand_defs_in_expr(l, defs, depth)),
1279            Box::new(expand_defs_in_expr(r, defs, depth)),
1280        ),
1281        ProofExpr::Or(l, r) => ProofExpr::Or(
1282            Box::new(expand_defs_in_expr(l, defs, depth)),
1283            Box::new(expand_defs_in_expr(r, defs, depth)),
1284        ),
1285        ProofExpr::Implies(l, r) => ProofExpr::Implies(
1286            Box::new(expand_defs_in_expr(l, defs, depth)),
1287            Box::new(expand_defs_in_expr(r, defs, depth)),
1288        ),
1289        ProofExpr::Iff(l, r) => ProofExpr::Iff(
1290            Box::new(expand_defs_in_expr(l, defs, depth)),
1291            Box::new(expand_defs_in_expr(r, defs, depth)),
1292        ),
1293        ProofExpr::Not(p) => ProofExpr::Not(Box::new(expand_defs_in_expr(p, defs, depth))),
1294        ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
1295            variable: variable.clone(),
1296            body: Box::new(expand_defs_in_expr(body, defs, depth)),
1297        },
1298        ProofExpr::Exists { variable, body } => ProofExpr::Exists {
1299            variable: variable.clone(),
1300            body: Box::new(expand_defs_in_expr(body, defs, depth)),
1301        },
1302        other => other.clone(),
1303    }
1304}
1305
1306/// Substitute a definition's parameters with the actual arguments throughout its
1307/// definiens, when δ-expanding a definition occurrence.
1308fn substitute_params(expr: &ProofExpr, params: &[String], args: &[ProofTerm]) -> ProofExpr {
1309    match expr {
1310        ProofExpr::Predicate { name, args: pargs, world } => ProofExpr::Predicate {
1311            name: name.clone(),
1312            args: pargs.iter().map(|t| subst_term(t, params, args)).collect(),
1313            world: world.clone(),
1314        },
1315        ProofExpr::And(l, r) => ProofExpr::And(
1316            Box::new(substitute_params(l, params, args)),
1317            Box::new(substitute_params(r, params, args)),
1318        ),
1319        ProofExpr::Or(l, r) => ProofExpr::Or(
1320            Box::new(substitute_params(l, params, args)),
1321            Box::new(substitute_params(r, params, args)),
1322        ),
1323        ProofExpr::Implies(l, r) => ProofExpr::Implies(
1324            Box::new(substitute_params(l, params, args)),
1325            Box::new(substitute_params(r, params, args)),
1326        ),
1327        ProofExpr::Iff(l, r) => ProofExpr::Iff(
1328            Box::new(substitute_params(l, params, args)),
1329            Box::new(substitute_params(r, params, args)),
1330        ),
1331        ProofExpr::Not(p) => ProofExpr::Not(Box::new(substitute_params(p, params, args))),
1332        ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
1333            variable: variable.clone(),
1334            body: Box::new(substitute_params(body, params, args)),
1335        },
1336        ProofExpr::Exists { variable, body } => ProofExpr::Exists {
1337            variable: variable.clone(),
1338            body: Box::new(substitute_params(body, params, args)),
1339        },
1340        ProofExpr::Identity(l, r) => {
1341            ProofExpr::Identity(subst_term(l, params, args), subst_term(r, params, args))
1342        }
1343        other => other.clone(),
1344    }
1345}
1346
1347/// Rewrite biconditionals into their two implications: `P ↔ Q` becomes
1348/// `(P → Q) ∧ (Q → P)`. Applied to premises so biconditional elimination falls
1349/// out of the existing implication + conjunction machinery (the kernel has no
1350/// `Iff` type). Recurses through the propositional structure.
1351fn expand_iff(expr: &ProofExpr) -> ProofExpr {
1352    match expr {
1353        ProofExpr::Iff(p, q) => {
1354            let p = expand_iff(p);
1355            let q = expand_iff(q);
1356            ProofExpr::And(
1357                Box::new(ProofExpr::Implies(Box::new(p.clone()), Box::new(q.clone()))),
1358                Box::new(ProofExpr::Implies(Box::new(q), Box::new(p))),
1359            )
1360        }
1361        ProofExpr::And(l, r) => {
1362            ProofExpr::And(Box::new(expand_iff(l)), Box::new(expand_iff(r)))
1363        }
1364        ProofExpr::Or(l, r) => ProofExpr::Or(Box::new(expand_iff(l)), Box::new(expand_iff(r))),
1365        ProofExpr::Implies(l, r) => {
1366            ProofExpr::Implies(Box::new(expand_iff(l)), Box::new(expand_iff(r)))
1367        }
1368        ProofExpr::Not(p) => ProofExpr::Not(Box::new(expand_iff(p))),
1369        ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
1370            variable: variable.clone(),
1371            body: Box::new(expand_iff(body)),
1372        },
1373        ProofExpr::Exists { variable, body } => ProofExpr::Exists {
1374            variable: variable.clone(),
1375            body: Box::new(expand_iff(body)),
1376        },
1377        other => other.clone(),
1378    }
1379}
1380
1381/// Substitute a parameter with its argument inside a single term. Parameters lower
1382/// to `Constant`s and quantifier-bound variables are `Variable`/`BoundVarRef`, so
1383/// substituting ONLY `Constant`s can never capture a bound variable — even when a
1384/// definiens' quantifier reuses a parameter's name (`∀x … admire(x, x_param)`).
1385fn subst_term(term: &ProofTerm, params: &[String], args: &[ProofTerm]) -> ProofTerm {
1386    match term {
1387        ProofTerm::Constant(n) => match params.iter().position(|p| p == n) {
1388            Some(i) => args[i].clone(),
1389            None => term.clone(),
1390        },
1391        ProofTerm::Function(name, fargs) => ProofTerm::Function(
1392            name.clone(),
1393            fargs.iter().map(|t| subst_term(t, params, args)).collect(),
1394        ),
1395        ProofTerm::Group(ts) => {
1396            ProofTerm::Group(ts.iter().map(|t| subst_term(t, params, args)).collect())
1397        }
1398        // Bound variables (∀/∃) are never parameters — leave them untouched.
1399        ProofTerm::Variable(_) | ProofTerm::BoundVarRef(_) => term.clone(),
1400    }
1401}