Skip to main content

logicaffeine_proof/
grid_solver.rs

1//! A fast, certified finite-domain (logic-grid) solver.
2//!
3//! Logic-grid puzzles ground to a quantifier-free clause/rule set; a 4×4 grid is a
4//! small SAT(+equality) problem that a determined-CSP solver closes in milliseconds.
5//! The wall the previous engine hit was speed: `engine::cert_saturate` re-saturates
6//! `O(facts²)` at every search node. This module is the incremental replacement —
7//! trail-based watched-literal unit propagation + DPLL — that finds the answer fast.
8//!
9//! Crucially, certification is DECOUPLED from search (see [`crate::verify`]): the
10//! solver finds the model/refutation however it likes, then a post-pass emits a
11//! [`DerivationTree`] from the recorded reasons, reusing the certified inference
12//! rules. [`crate::verify::check_derivation`] re-checks that tree in the kernel, so
13//! the solver sits OUTSIDE the trusted base — a wrong tree yields `verified == false`,
14//! never a false claim.
15//!
16//! Build order (see plan): Phase A de-risks the emitter (this file's tests prove the
17//! certified tree SHAPES the solver will produce actually certify), before any search
18//! machinery exists.
19
20#![allow(dead_code)]
21
22use crate::{DerivationTree, InferenceRule, ProofExpr};
23
24// ── Tree constructors (the exact certified shapes the emitter produces) ──────────
25
26fn leaf(e: ProofExpr) -> DerivationTree {
27    DerivationTree::leaf(e, InferenceRule::PremiseMatch)
28}
29
30fn neg(e: &ProofExpr) -> ProofExpr {
31    match e {
32        ProofExpr::Not(inner) => (**inner).clone(),
33        _ => ProofExpr::Not(Box::new(e.clone())),
34    }
35}
36
37fn falsum() -> ProofExpr {
38    ProofExpr::Atom("⊥".into())
39}
40
41/// ModusPonens: from `impl_tree : A → C` and `ante_tree : A`, conclude `C`.
42fn modus_ponens(c: ProofExpr, impl_tree: DerivationTree, ante_tree: DerivationTree) -> DerivationTree {
43    DerivationTree::new(c, InferenceRule::ModusPonens, vec![impl_tree, ante_tree])
44}
45
46/// Disjunctive syllogism: from `or_tree : A ∨ B` and `neg_tree : ¬A`, conclude `B`.
47fn disjunction_elim(survivor: ProofExpr, or_tree: DerivationTree, neg_tree: DerivationTree) -> DerivationTree {
48    DerivationTree::new(survivor, InferenceRule::DisjunctionElim, vec![or_tree, neg_tree])
49}
50
51/// Conjunction introduction: from `l : A` and `r : B`, conclude `A ∧ B`.
52fn conj_intro(a: ProofExpr, b: ProofExpr, l: DerivationTree, r: DerivationTree) -> DerivationTree {
53    DerivationTree::new(ProofExpr::And(Box::new(a), Box::new(b)), InferenceRule::ConjunctionIntro, vec![l, r])
54}
55
56/// Contradiction: from `P` and `¬P`, conclude `⊥` (the certifier finds the polarity).
57fn contradiction(p: DerivationTree, np: DerivationTree) -> DerivationTree {
58    DerivationTree::new(falsum(), InferenceRule::Contradiction, vec![p, np])
59}
60
61/// Reductio: assume `assumed`, derive `⊥` (`falsum_tree`), conclude `¬assumed`.
62fn reductio(assumed: ProofExpr, falsum_tree: DerivationTree) -> DerivationTree {
63    let concl = ProofExpr::Not(Box::new(assumed.clone()));
64    DerivationTree::new(concl, InferenceRule::ReductioAdAbsurdum, vec![leaf(assumed), falsum_tree])
65}
66
67/// Equality symmetry: from `a = b`, conclude `b = a`.
68fn eq_sym(a: crate::ProofTerm, b: crate::ProofTerm, tree: DerivationTree) -> DerivationTree {
69    DerivationTree::new(ProofExpr::Identity(b, a), InferenceRule::EqualitySymmetry, vec![tree])
70}
71
72/// Disjunction case analysis to a common conclusion: from `A ∨ B` and a proof of the
73/// conclusion in each branch (with the disjunct bound as a local hypothesis), conclude
74/// the common goal.
75fn disjunction_cases(goal: ProofExpr, or_tree: DerivationTree, left: DerivationTree, right: DerivationTree) -> DerivationTree {
76    DerivationTree::new(goal, InferenceRule::DisjunctionCases, vec![or_tree, left, right])
77}
78
79/// Conjunction elimination: from `A ∧ B`, project the named `conjunct`.
80fn conj_elim(conjunct: ProofExpr, and_tree: DerivationTree) -> DerivationTree {
81    DerivationTree::new(conjunct, InferenceRule::ConjunctionElim, vec![and_tree])
82}
83
84// ── Atoms & literals ─────────────────────────────────────────────────────────────
85
86use std::collections::HashMap;
87use crate::ProofTerm;
88
89type Var = usize;
90
91#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
92struct Lit {
93    var: Var,
94    pos: bool,
95}
96
97impl Lit {
98    fn negate(self) -> Lit {
99        Lit { var: self.var, pos: !self.pos }
100    }
101}
102
103fn term_key(t: &ProofTerm) -> String {
104    t.to_string()
105}
106
107/// Canonical key for a POSITIVE atom. `Identity` is normalized to an unordered pair so
108/// `a = b` and `b = a` intern to the SAME variable — they are the same proposition.
109fn atom_key(e: &ProofExpr) -> Option<String> {
110    match e {
111        ProofExpr::Predicate { name, args, .. } => {
112            let parts: Vec<String> = args.iter().map(term_key).collect();
113            Some(format!("P:{}({})", name, parts.join(",")))
114        }
115        ProofExpr::Identity(a, b) => {
116            let (ka, kb) = (term_key(a), term_key(b));
117            let (lo, hi) = if ka <= kb { (ka, kb) } else { (kb, ka) };
118            Some(format!("=:{}|{}", lo, hi))
119        }
120        ProofExpr::Atom(s) => Some(format!("A:{}", s)),
121        _ => None,
122    }
123}
124
125fn is_atomish(e: &ProofExpr) -> bool {
126    matches!(
127        e,
128        ProofExpr::Predicate { .. } | ProofExpr::Identity(..) | ProofExpr::Atom(_)
129    )
130}
131
132fn is_literal(e: &ProofExpr) -> bool {
133    match e {
134        ProofExpr::Not(inner) => is_atomish(inner),
135        _ => is_atomish(e),
136    }
137}
138
139/// A clause disjunct the solver can decide on: a literal, or a conjunction whose every
140/// leaf is a literal (an of-pair disjunct). A disjunct carrying an implication or
141/// quantifier (e.g. a grounded `∃`-existence clause) is rejected, so `compile` skips it.
142fn is_clause_disjunct(e: &ProofExpr) -> bool {
143    match e {
144        ProofExpr::And(l, r) | ProofExpr::Or(l, r) => is_clause_disjunct(l) && is_clause_disjunct(r),
145        _ => is_literal(e),
146    }
147}
148
149fn flatten_and(e: &ProofExpr, out: &mut Vec<ProofExpr>) {
150    match e {
151        ProofExpr::And(l, r) => {
152            flatten_and(l, out);
153            flatten_and(r, out);
154        }
155        _ => out.push(e.clone()),
156    }
157}
158
159fn flatten_or(e: &ProofExpr, out: &mut Vec<ProofExpr>) {
160    match e {
161        ProofExpr::Or(l, r) => {
162            flatten_or(l, out);
163            flatten_or(r, out);
164        }
165        _ => out.push(e.clone()),
166    }
167}
168
169/// Intern every atom occurring in `e` (so a decision clause's compound disjuncts get
170/// variables even though they never appear as standalone literals).
171fn intern_all(table: &mut AtomTable, e: &ProofExpr) {
172    match e {
173        ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) | ProofExpr::Iff(l, r) => {
174            intern_all(table, l);
175            intern_all(table, r);
176        }
177        ProofExpr::Not(inner) => intern_all(table, inner),
178        _ if is_atomish(e) => {
179            table.intern(e);
180        }
181        _ => {}
182    }
183}
184
185#[derive(Default)]
186struct AtomTable {
187    by_key: HashMap<String, Var>,
188    atoms: Vec<ProofExpr>,
189}
190
191impl AtomTable {
192    fn intern(&mut self, atom: &ProofExpr) -> Option<Var> {
193        let k = atom_key(atom)?;
194        if let Some(&v) = self.by_key.get(&k) {
195            return Some(v);
196        }
197        let v = self.atoms.len();
198        self.atoms.push(atom.clone());
199        self.by_key.insert(k, v);
200        Some(v)
201    }
202
203    fn lit(&mut self, e: &ProofExpr) -> Option<Lit> {
204        match e {
205            ProofExpr::Not(inner) => Some(Lit { var: self.intern(inner)?, pos: false }),
206            _ => Some(Lit { var: self.intern(e)?, pos: true }),
207        }
208    }
209}
210
211// ── Clauses & rules ──────────────────────────────────────────────────────────────
212
213enum ClauseSrc {
214    /// A bare disjunction premise; the exact `Or(...)` expression (for emit).
215    Bare(ProofExpr),
216    /// Materialized when the disjunctive-consequent rule `usize` fires.
217    FromRule(usize),
218    /// Added inside a decision branch as the assumed disjunct of an enclosing
219    /// case-split (resolves to the bound conjunct hypothesis at emit time).
220    Assumed(ProofExpr),
221}
222
223/// A disjunction. Its disjuncts may be literals (enabling unit propagation, `lits`
224/// `Some`) or compound conjunctions (decision clauses, `lits` `None`). `or_expr`
225/// preserves the exact `Or` nesting the emitter must follow.
226struct Clause {
227    or_expr: ProofExpr,
228    disjuncts: Vec<ProofExpr>,
229    lits: Option<Vec<Lit>>,
230    src: ClauseSrc,
231}
232
233enum RuleCons {
234    Lit(Lit),
235    Disj(Vec<Lit>),
236}
237
238/// Three-valued status of a disjunct (or a whole conjunction) under the trail.
239#[derive(Clone, Copy, PartialEq, Eq)]
240enum Tri {
241    True,
242    False,
243    Live,
244}
245
246struct Rule {
247    ante: Vec<Lit>,
248    cons: RuleCons,
249    /// The exact `Implies(...)` premise (cited by ModusPonens emit).
250    src: ProofExpr,
251}
252
253/// How a variable came to be assigned — enough for the emitter to rebuild the
254/// certified sub-proof.
255#[derive(Clone)]
256enum Reason {
257    /// A unit premise; the exact premise expression.
258    Premise(ProofExpr),
259    /// ModusPonens: rule `usize` fired (all antecedents true) yielding this literal.
260    Rule(usize),
261    /// Exclusion (ModusTollens shape): rule `usize`'s consequent is violated and all
262    /// antecedents but this one are true, so this antecedent is forced false.
263    Exclude(usize),
264    /// Unit propagation: this literal is the survivor of clause `usize`.
265    Clause(usize),
266    /// Disjunctive syllogism over a COMPOUND clause: clause `clause` has a single live
267    /// disjunct (index `disjunct`); every other disjunct is refuted by a false conjunct.
268    /// This literal is one of that surviving disjunct's conjuncts — proved by peeling the
269    /// refuted disjuncts (`DisjunctionElim`) then projecting the conjunct (`ConjunctionElim`).
270    /// This is what lets an of-pair clue contribute a LINEAR deduction instead of a search.
271    CompoundClause { clause: usize, disjunct: usize },
272    /// A search decision / assumption (justified by a bound hypothesis at emit time).
273    Assumption,
274}
275
276// ── The solver ───────────────────────────────────────────────────────────────────
277
278struct Solver {
279    table: AtomTable,
280    clauses: Vec<Clause>,
281    rules: Vec<Rule>,
282    assign: Vec<Option<bool>>,
283    reason: Vec<Option<Reason>>,
284    trail: Vec<Var>,
285    /// Per-variable assignment order (index into `trail`). Lets the emitter refute a
286    /// disjunct using the conjunct that became false EARLIEST, keeping the reconstructed
287    /// proof's recursion strictly backward in trail order (acyclic).
288    pos: Vec<usize>,
289    fired_disj: Vec<bool>,
290    /// Clauses currently mid-case-split (so the recursive search does not re-pick a
291    /// clause whose chosen disjunct still has a pending sub-disjunction).
292    deciding: std::collections::HashSet<usize>,
293    /// Whether propagation is running at decision level 0 (the root fixpoint, never
294    /// backtracked). Compound-clause unit propagation fires ONLY here: a root-forced
295    /// literal is permanent, so its reason chain is monotone in trail order and the
296    /// linear emit is acyclic. Under a backtrackable assumption (search branch), an
297    /// of-pair clause's XOR-coupled disjuncts can force each other circularly, so there
298    /// the clause is left to the case-split fallback instead.
299    at_root: bool,
300}
301
302enum Conflict {
303    /// Rule `usize`'s antecedents are all true but its consequent is violated.
304    Rule(usize),
305    /// Clause `usize` has every literal violated.
306    Clause(usize),
307}
308
309/// Classify each ground premise into the solver's clause/rule/unit forms. Returns
310/// `None` on anything it cannot classify (e.g. a surviving quantifier), so the caller
311/// can fall back to the general engine.
312fn compile(premises: &[ProofExpr]) -> Option<Solver> {
313    let mut table = AtomTable::default();
314    let mut clauses = Vec::new();
315    let mut rules = Vec::new();
316    let mut units: Vec<(Lit, ProofExpr)> = Vec::new();
317
318    // Grounding conjoins each universal's instances into one top-level `And`; split
319    // those back into independent facts (a conjunction asserts each conjunct).
320    let mut flat: Vec<ProofExpr> = Vec::new();
321    for p in premises {
322        flatten_and(p, &mut flat);
323    }
324
325    // Classify each fact; a premise that does not fit the clause/rule/unit grammar is
326    // SKIPPED, not fatal. Skipping is sound — `check_derivation` re-checks every emitted
327    // tree against the FULL premise set, so a dropped (e.g. redundant grounded `∃`
328    // existence) premise can only make the solver fail to prove, never prove falsely.
329    for p in &flat {
330        match p {
331            ProofExpr::Or(..) => {
332                let mut ds = Vec::new();
333                flatten_or(p, &mut ds);
334                if !ds.iter().all(is_clause_disjunct) {
335                    continue;
336                }
337                intern_all(&mut table, p);
338                let lits = if ds.iter().all(is_literal) {
339                    ds.iter().map(|d| table.lit(d)).collect::<Option<Vec<Lit>>>()
340                } else {
341                    None
342                };
343                clauses.push(Clause {
344                    or_expr: p.clone(),
345                    disjuncts: ds,
346                    lits,
347                    src: ClauseSrc::Bare(p.clone()),
348                });
349            }
350            ProofExpr::Implies(ante, cons) => {
351                let mut as_ = Vec::new();
352                flatten_and(ante, &mut as_);
353                if !as_.iter().all(is_literal) {
354                    continue;
355                }
356                let cons_form = if is_literal(cons) {
357                    intern_all(&mut table, p);
358                    RuleCons::Lit(table.lit(cons).unwrap())
359                } else if matches!(cons.as_ref(), ProofExpr::Or(..)) {
360                    let mut ds = Vec::new();
361                    flatten_or(cons, &mut ds);
362                    if !ds.iter().all(is_literal) {
363                        continue;
364                    }
365                    intern_all(&mut table, p);
366                    RuleCons::Disj(ds.iter().map(|d| table.lit(d).unwrap()).collect())
367                } else {
368                    continue;
369                };
370                let ante_lits = as_.iter().map(|a| table.lit(a).unwrap()).collect();
371                rules.push(Rule { ante: ante_lits, cons: cons_form, src: p.clone() });
372            }
373            _ if is_literal(p) => {
374                intern_all(&mut table, p);
375                units.push((table.lit(p).unwrap(), p.clone()));
376            }
377            _ => continue,
378        }
379    }
380
381    let n = table.atoms.len();
382    let fired_disj = vec![false; rules.len()];
383    let mut solver = Solver {
384        table,
385        clauses,
386        rules,
387        assign: vec![None; n],
388        reason: vec![None; n],
389        trail: Vec::new(),
390        pos: vec![usize::MAX; n],
391        fired_disj,
392        deciding: std::collections::HashSet::new(),
393        at_root: false,
394    };
395    for (l, expr) in units {
396        let _ = solver.set(l, Reason::Premise(expr));
397    }
398    Some(solver)
399}
400
401impl Solver {
402    /// `Some(true)` if `l` holds, `Some(false)` if `l` is violated, `None` if unknown.
403    fn value(&self, l: Lit) -> Option<bool> {
404        self.assign[l.var].map(|b| b == l.pos)
405    }
406
407    fn var_of_atom(&self, atom: &ProofExpr) -> Option<Var> {
408        self.table.by_key.get(&atom_key(atom)?).copied()
409    }
410
411    fn lit_of(&self, e: &ProofExpr) -> Option<Lit> {
412        match e {
413            ProofExpr::Not(inner) => Some(Lit { var: self.var_of_atom(inner)?, pos: false }),
414            _ => Some(Lit { var: self.var_of_atom(e)?, pos: true }),
415        }
416    }
417
418    fn lit_expr(&self, l: Lit) -> ProofExpr {
419        let atom = self.table.atoms[l.var].clone();
420        if l.pos { atom } else { ProofExpr::Not(Box::new(atom)) }
421    }
422
423    fn set(&mut self, l: Lit, r: Reason) -> Result<(), ()> {
424        match self.assign[l.var] {
425            Some(b) => {
426                if b == l.pos {
427                    Ok(())
428                } else {
429                    Err(())
430                }
431            }
432            None => {
433                self.assign[l.var] = Some(l.pos);
434                self.reason[l.var] = Some(r);
435                self.pos[l.var] = self.trail.len();
436                self.trail.push(l.var);
437                Ok(())
438            }
439        }
440    }
441
442    /// Trail-based fixpoint propagation: forward ModusPonens, backward exclusion
443    /// (ModusTollens), disjunctive-consequent materialization, and unit-clause
444    /// propagation. Returns the conflict if one is reached.
445    fn propagate(&mut self) -> Result<(), Conflict> {
446        loop {
447            let mut changed = false;
448
449            for ri in 0..self.rules.len() {
450                // Tally the antecedents.
451                let mut n_nonsat = 0usize;
452                let mut the_nonsat: Option<Lit> = None;
453                let mut any_false = false;
454                for i in 0..self.rules[ri].ante.len() {
455                    let a = self.rules[ri].ante[i];
456                    match self.value(a) {
457                        Some(true) => {}
458                        Some(false) => {
459                            any_false = true;
460                            n_nonsat += 1;
461                            the_nonsat = Some(a);
462                        }
463                        None => {
464                            n_nonsat += 1;
465                            the_nonsat = Some(a);
466                        }
467                    }
468                }
469
470                // A false antecedent makes the rule vacuously true.
471                if any_false {
472                    continue;
473                }
474
475                if n_nonsat == 0 {
476                    // All antecedents hold → fire forward.
477                    match &self.rules[ri].cons {
478                        RuleCons::Lit(l) => {
479                            let l = *l;
480                            match self.value(l) {
481                                Some(true) => {}
482                                Some(false) => return Err(Conflict::Rule(ri)),
483                                None => {
484                                    let _ = self.set(l, Reason::Rule(ri));
485                                    changed = true;
486                                }
487                            }
488                        }
489                        RuleCons::Disj(lits) => {
490                            if !self.fired_disj[ri] {
491                                let lits = lits.clone();
492                                self.fired_disj[ri] = true;
493                                let (_, cons_form) = Self::split_implies(&self.rules[ri].src);
494                                let mut ds = Vec::new();
495                                flatten_or(&cons_form, &mut ds);
496                                self.clauses.push(Clause {
497                                    or_expr: cons_form,
498                                    disjuncts: ds,
499                                    lits: Some(lits),
500                                    src: ClauseSrc::FromRule(ri),
501                                });
502                                changed = true;
503                            }
504                        }
505                    }
506                } else if n_nonsat == 1 {
507                    // Exactly one antecedent open. If the consequent is a literal that is
508                    // already violated, that antecedent must be false (ModusTollens).
509                    if let RuleCons::Lit(l) = &self.rules[ri].cons {
510                        if self.value(*l) == Some(false) {
511                            let open = the_nonsat.unwrap();
512                            if self.value(open).is_none() {
513                                let _ = self.set(open.negate(), Reason::Exclude(ri));
514                                changed = true;
515                            }
516                        }
517                    }
518                }
519            }
520
521            for ci in 0..self.clauses.len() {
522                let (has_true, live) = self.clause_status(ci);
523                if has_true {
524                    continue;
525                }
526                if live.is_empty() {
527                    return Err(Conflict::Clause(ci));
528                }
529                // Unit propagation on a literal clause: the lone live literal is forced.
530                if self.clauses[ci].lits.is_some() && live.len() == 1 {
531                    if let Some(l) = self.lit_of(&self.clauses[ci].disjuncts[live[0]]) {
532                        if self.set(l, Reason::Clause(ci)).is_ok() {
533                            changed = true;
534                        }
535                    }
536                }
537                // Compound (of-pair / either-or) clause collapsed to a SINGLE live
538                // disjunct: disjunctive syllogism forces that disjunct, so assert each of
539                // its literal conjuncts. This is the deduction that lets the whole grid
540                // be forced by propagation (no search) → linear proofs. Gated to the root
541                // fixpoint: under a backtrackable assumption the survivor's conjuncts can
542                // be the only thing refuting a sibling (shared anchor), making the reason
543                // chain cyclic — those clauses are left to the case-split fallback.
544                if self.at_root && self.clauses[ci].lits.is_none() && live.len() == 1 {
545                    let di = live[0];
546                    let mut conjs = Vec::new();
547                    flatten_and(&self.clauses[ci].disjuncts[di].clone(), &mut conjs);
548                    for conj in &conjs {
549                        // Inner disjunction conjuncts (a nested either-or) are left to the
550                        // search; only the literal conjuncts are forced here.
551                        if !is_literal(conj) {
552                            continue;
553                        }
554                        let Some(l) = self.lit_of(conj) else { continue };
555                        match self.value(l) {
556                            Some(true) => {}
557                            Some(false) => return Err(Conflict::Clause(ci)),
558                            None => {
559                                let _ = self.set(l, Reason::CompoundClause { clause: ci, disjunct: di });
560                                changed = true;
561                            }
562                        }
563                    }
564                }
565            }
566
567            if !changed {
568                break;
569            }
570        }
571        Ok(())
572    }
573
574    // ── Emit: turn the trail into a certified DerivationTree ─────────────────────
575
576    /// Prove the literal assigned to variable `v` (in its assigned polarity).
577    fn prove_var(&self, v: Var) -> DerivationTree {
578        let val = self.assign[v].expect("prove_var on unassigned variable");
579        match self.reason[v].as_ref().expect("assigned variable lacks a reason") {
580            Reason::Premise(expr) => leaf(expr.clone()),
581            Reason::Assumption => leaf(self.lit_expr(Lit { var: v, pos: val })),
582            Reason::Rule(ri) => self.emit_rule_forward(*ri),
583            Reason::Exclude(ri) => self.emit_rule_exclude(*ri, v),
584            Reason::Clause(ci) => self.emit_clause_survivor(*ci, Lit { var: v, pos: val }),
585            Reason::CompoundClause { clause, disjunct } => {
586                self.emit_compound_clause(*clause, *disjunct, Lit { var: v, pos: val })
587            }
588        }
589    }
590
591    /// Emit a literal forced as a conjunct of a compound clause's single surviving
592    /// disjunct: peel the refuted disjuncts (disjunctive syllogism) to prove the
593    /// surviving conjunction, then ∧-eliminate down to the wanted literal. The
594    /// disjunction's `Or`-type appears ONCE here, not multiplied by a search tree.
595    fn emit_compound_clause(&self, ci: usize, di: usize, want: Lit) -> DerivationTree {
596        let or_expr = self.clauses[ci].or_expr.clone();
597        let or_tree = self.materialize_clause(ci);
598        let survivor = self.clauses[ci].disjuncts[di].clone();
599        let before = self.pos[want.var];
600        let surv_tree = self.extract_survivor(&or_expr, or_tree, &survivor, before);
601        let proj = self.project_conjunct(&survivor, surv_tree, want);
602        align_to(proj, &self.lit_expr(want))
603    }
604
605    /// From a proof of a (possibly nested) conjunction, ∧-eliminate down to the conjunct
606    /// whose literal is `want`.
607    fn project_conjunct(&self, conj: &ProofExpr, conj_tree: DerivationTree, want: Lit) -> DerivationTree {
608        match conj {
609            ProofExpr::And(l, r) => {
610                if self.conjunct_has(l, want) {
611                    let lt = conj_elim((**l).clone(), conj_tree);
612                    self.project_conjunct(l, lt, want)
613                } else {
614                    let rt = conj_elim((**r).clone(), conj_tree);
615                    self.project_conjunct(r, rt, want)
616                }
617            }
618            _ => conj_tree,
619        }
620    }
621
622    /// Does the conjunction `e` contain a literal conjunct equal to `want`?
623    fn conjunct_has(&self, e: &ProofExpr, want: Lit) -> bool {
624        match e {
625            ProofExpr::And(l, r) => self.conjunct_has(l, want) || self.conjunct_has(r, want),
626            _ => self.lit_of(e) == Some(want),
627        }
628    }
629
630    /// Prove a literal expression `want` exactly (aligning Identity orientation).
631    fn prove_lit_expr(&self, want: &ProofExpr) -> DerivationTree {
632        let lit = self.lit_of(want).expect("prove_lit_expr on an un-interned atom");
633        let base = self.prove_var(lit.var);
634        align_to(base, want)
635    }
636
637    fn split_implies(src: &ProofExpr) -> (ProofExpr, ProofExpr) {
638        match src {
639            ProofExpr::Implies(a, c) => ((**a).clone(), (**c).clone()),
640            _ => unreachable!("rule src is always an implication"),
641        }
642    }
643
644    /// Prove a conjunction/literal antecedent form. `forced` (if any) is proved by a
645    /// bound hypothesis leaf instead of recursing (it is the reductio assumption).
646    fn prove_ante(&self, form: &ProofExpr, forced: Option<Var>) -> DerivationTree {
647        match form {
648            ProofExpr::And(l, r) => conj_intro(
649                (**l).clone(),
650                (**r).clone(),
651                self.prove_ante(l, forced),
652                self.prove_ante(r, forced),
653            ),
654            _ => {
655                if let (Some(fv), Some(lit)) = (forced, self.lit_of(form)) {
656                    if lit.var == fv {
657                        return leaf(form.clone());
658                    }
659                }
660                self.prove_lit_expr(form)
661            }
662        }
663    }
664
665    fn emit_rule_forward(&self, ri: usize) -> DerivationTree {
666        let src = self.rules[ri].src.clone();
667        let (ante_form, cons_form) = Self::split_implies(&src);
668        let ante_proof = self.prove_ante(&ante_form, None);
669        modus_ponens(cons_form, leaf(src), ante_proof)
670    }
671
672    fn emit_rule_exclude(&self, ri: usize, forced_v: Var) -> DerivationTree {
673        let src = self.rules[ri].src.clone();
674        let (ante_form, cons_form) = Self::split_implies(&src);
675        let assumed = self.table.atoms[forced_v].clone();
676        let ante_proof = self.prove_ante(&ante_form, Some(forced_v));
677        let cons_tree = modus_ponens(cons_form.clone(), leaf(src), ante_proof);
678        let cons_lit = self.lit_of(&cons_form).expect("exclusion consequent must be a literal");
679        let neg_cons = self.prove_var(cons_lit.var);
680        // The consequent may be negative (a functionality rule `Li → ¬Lj`), so the
681        // contradiction's polarity order is not fixed — let `make_contradiction` sort it.
682        let bot = make_contradiction(cons_tree, neg_cons);
683        reductio(assumed, bot)
684    }
685
686    fn materialize_clause(&self, ci: usize) -> DerivationTree {
687        match &self.clauses[ci].src {
688            ClauseSrc::Bare(or_expr) | ClauseSrc::Assumed(or_expr) => leaf(or_expr.clone()),
689            ClauseSrc::FromRule(ri) => {
690                let src = self.rules[*ri].src.clone();
691                let (ante_form, cons_form) = Self::split_implies(&src);
692                let ante_proof = self.prove_ante(&ante_form, None);
693                modus_ponens(cons_form, leaf(src), ante_proof)
694            }
695        }
696    }
697
698    fn emit_clause_survivor(&self, ci: usize, survivor: Lit) -> DerivationTree {
699        let or_tree = self.materialize_clause(ci);
700        let or_expr = or_tree.conclusion.clone();
701        let survivor_expr = self.lit_expr(survivor);
702        let before = self.pos[survivor.var];
703        self.extract_survivor(&or_expr, or_tree, &survivor_expr, before)
704    }
705
706    /// Extract the surviving disjunct `target` from a (left-nested) disjunction whose
707    /// every other disjunct is violated, by iterated disjunctive syllogism. `before` caps
708    /// the trail position of the facts used to refute the other disjuncts (acyclicity).
709    fn extract_survivor(&self, or_expr: &ProofExpr, or_tree: DerivationTree, target: &ProofExpr, before: usize) -> DerivationTree {
710        match or_expr {
711            ProofExpr::Or(l, r) => {
712                if r.as_ref() == target {
713                    let lneg = self.prove_neg_disjunct(l, before);
714                    disjunction_elim(target.clone(), or_tree, lneg)
715                } else {
716                    let rneg = self.prove_neg_disjunct(r, before);
717                    let l_tree = disjunction_elim((**l).clone(), or_tree, rneg);
718                    self.extract_survivor(l, l_tree, target, before)
719                }
720            }
721            _ => or_tree,
722        }
723    }
724
725    /// Prove `¬e` where `e` is a violated disjunct: a single literal (directly) or a
726    /// sub-disjunction (by reductio whose branches close to ⊥). `before` caps the
727    /// refuting facts' trail positions.
728    fn prove_neg_disjunct(&self, e: &ProofExpr, before: usize) -> DerivationTree {
729        if is_literal(e) {
730            let lit = self.lit_of(e).expect("disjunct atom must be interned");
731            let proof = self.prove_var(lit.var);
732            align_to(proof, &neg_of(e))
733        } else {
734            let bot = self.falsum_from_disj(e, leaf(e.clone()), before);
735            reductio(e.clone(), bot)
736        }
737    }
738
739    /// Given `e_tree : e` (a disjunction, conjunction, or literal, all violated), derive ⊥,
740    /// refuting using only facts assigned strictly before `before` where possible.
741    fn falsum_from_disj(&self, e: &ProofExpr, e_tree: DerivationTree, before: usize) -> DerivationTree {
742        match e {
743            ProofExpr::Or(l, r) => {
744                let lb = self.falsum_from_disj(l, leaf((**l).clone()), before);
745                let rb = self.falsum_from_disj(r, leaf((**r).clone()), before);
746                disjunction_cases(falsum(), e_tree, lb, rb)
747            }
748            ProofExpr::And(l, r) => {
749                // A false conjunction disjunct: project the conjunct that became false
750                // EARLIEST — preferring one false strictly BEFORE the survivor was forced
751                // (`before`), else the globally earliest. Picking by trail order (not
752                // structural order, and capped at `before`) keeps the emit's recursion
753                // strictly backward, so the reconstructed proof is acyclic even when a
754                // later exclusion would otherwise refer back through the forcing clause.
755                let key = |x: &ProofExpr| {
756                    self.false_at(x, before)
757                        .map(|p| (0u8, p))
758                        .or_else(|| self.false_at(x, usize::MAX).map(|p| (1u8, p)))
759                };
760                let pick_left = match (key(l), key(r)) {
761                    (Some(a), Some(b)) => a <= b,
762                    (Some(_), None) => true,
763                    (None, Some(_)) => false,
764                    (None, None) => true,
765                };
766                let (conjunct, proj) = if pick_left {
767                    ((**l).clone(), conj_elim((**l).clone(), e_tree))
768                } else {
769                    ((**r).clone(), conj_elim((**r).clone(), e_tree))
770                };
771                self.falsum_from_disj(&conjunct, proj, before)
772            }
773            _ => {
774                let lit = self.lit_of(e).expect("disjunct atom must be interned");
775                let neg = self.prove_var(lit.var);
776                contra_align(e_tree, neg)
777            }
778        }
779    }
780
781    // ── Decision search (DPLL) ───────────────────────────────────────────────────
782
783    /// The trail position by which `e` is FALSE using only assignments strictly BEFORE
784    /// `before` (or `None`). A conjunction is false as soon as ANY conjunct is (earliest),
785    /// a disjunction only once ALL disjuncts are (latest), a literal at its own position.
786    /// The `before` cap keeps the reconstructed proof's recursion strictly backward.
787    fn false_at(&self, e: &ProofExpr, before: usize) -> Option<usize> {
788        match e {
789            ProofExpr::And(l, r) => match (self.false_at(l, before), self.false_at(r, before)) {
790                (Some(a), Some(b)) => Some(a.min(b)),
791                (a, b) => a.or(b),
792            },
793            ProofExpr::Or(l, r) => match (self.false_at(l, before), self.false_at(r, before)) {
794                (Some(a), Some(b)) => Some(a.max(b)),
795                _ => None,
796            },
797            _ => match self.lit_of(e) {
798                Some(l) if self.value(l) == Some(false) && self.pos[l.var] < before => Some(self.pos[l.var]),
799                _ => None,
800            },
801        }
802    }
803
804    /// Three-valued evaluation of a disjunct (or any conjunction/disjunction/literal)
805    /// under the current trail.
806    fn eval_disjunct(&self, e: &ProofExpr) -> Tri {
807        match e {
808            ProofExpr::And(l, r) => match (self.eval_disjunct(l), self.eval_disjunct(r)) {
809                (Tri::False, _) | (_, Tri::False) => Tri::False,
810                (Tri::True, Tri::True) => Tri::True,
811                _ => Tri::Live,
812            },
813            ProofExpr::Or(l, r) => match (self.eval_disjunct(l), self.eval_disjunct(r)) {
814                (Tri::True, _) | (_, Tri::True) => Tri::True,
815                (Tri::False, Tri::False) => Tri::False,
816                _ => Tri::Live,
817            },
818            _ => match self.lit_of(e).and_then(|l| self.value(l)) {
819                Some(true) => Tri::True,
820                Some(false) => Tri::False,
821                None => Tri::Live,
822            },
823        }
824    }
825
826    /// `(has_true, indices of live disjuncts)` for clause `ci`.
827    fn clause_status(&self, ci: usize) -> (bool, Vec<usize>) {
828        let mut has_true = false;
829        let mut live = Vec::new();
830        for (i, d) in self.clauses[ci].disjuncts.iter().enumerate() {
831            match self.eval_disjunct(d) {
832                Tri::True => has_true = true,
833                Tri::Live => live.push(i),
834                Tri::False => {}
835            }
836        }
837        (has_true, live)
838    }
839
840    /// The first unsatisfied clause with a live disjunct that the search must split.
841    /// (After propagation, literal clauses with a single live disjunct are already
842    /// unit-propagated, so only genuine decisions remain.)
843    fn pick_split(&self) -> Option<usize> {
844        // Split the MOST-CONSTRAINED open clause — the one with the fewest live disjuncts
845        // (minimum-remaining-values). A 2-live of-pair branches two ways and each branch
846        // re-propagates to a near-forced state; a 12-live one fans out twelve deep. Picking
847        // the smallest branching first keeps the case-analysis — and therefore the certified
848        // proof — shallow, the search analogue of the min-live closure selection.
849        (0..self.clauses.len())
850            .filter(|&ci| {
851                if self.deciding.contains(&ci) {
852                    return false;
853                }
854                let (has_true, live) = self.clause_status(ci);
855                if has_true || live.is_empty() {
856                    return false;
857                }
858                // A literal clause with a single live disjunct is already unit-propagated.
859                !(self.clauses[ci].lits.is_some() && live.len() < 2)
860            })
861            .min_by_key(|&ci| self.clause_status(ci).1.len())
862    }
863
864    /// Derive ⊥ from the current assumptions: propagate, and on a stall, case-split an
865    /// unresolved clause and refute every branch. `None` if a branch is satisfiable.
866    fn derive_falsum(&mut self, depth: usize) -> Option<DerivationTree> {
867        if depth > MAX_DEPTH {
868            return None;
869        }
870        if let Err(c) = self.propagate() {
871            return Some(self.emit_conflict(c));
872        }
873        let ci = self.pick_split()?;
874        let or_expr = self.clauses[ci].or_expr.clone();
875        let or_tree = self.materialize_clause(ci);
876        self.deciding.insert(ci);
877        let result = self.split_to_falsum(&or_expr, or_tree, depth);
878        self.deciding.remove(&ci);
879        result
880    }
881
882    fn split_to_falsum(&mut self, or_expr: &ProofExpr, or_tree: DerivationTree, depth: usize) -> Option<DerivationTree> {
883        match or_expr {
884            ProofExpr::Or(l, r) => {
885                let lb = self.branch(l, depth)?;
886                let rb = self.branch(r, depth)?;
887                Some(disjunction_cases(falsum(), or_tree, lb, rb))
888            }
889            single => self.branch(single, depth),
890        }
891    }
892
893    /// Assume `disjunct` (bound as a hypothesis by the enclosing case-split), refute it
894    /// to ⊥, then undo the assumption.
895    fn branch(&mut self, disjunct: &ProofExpr, depth: usize) -> Option<DerivationTree> {
896        let trail_snap = self.trail.len();
897        let clause_snap = self.clauses.len();
898        let fired_snap = self.fired_disj.clone();
899        let result = match self.assume_disjunct(disjunct) {
900            Some(info) => Some(self.emit_conflict_info(info)),
901            None => self.derive_falsum(depth + 1),
902        };
903        self.backtrack(trail_snap, clause_snap, fired_snap);
904        result
905    }
906
907    /// Assert a disjunct: set its literal conjuncts as assumptions and add any
908    /// disjunction conjuncts as branch-local clauses. Returns the first conflict, if any.
909    fn assume_disjunct(&mut self, d: &ProofExpr) -> Option<ConflictInfo> {
910        match d {
911            ProofExpr::And(l, r) => self.assume_disjunct(l).or_else(|| self.assume_disjunct(r)),
912            ProofExpr::Or(..) => {
913                let mut ds = Vec::new();
914                flatten_or(d, &mut ds);
915                let lits = if ds.iter().all(is_literal) {
916                    ds.iter().map(|x| self.lit_of(x)).collect::<Option<Vec<Lit>>>()
917                } else {
918                    None
919                };
920                self.clauses.push(Clause {
921                    or_expr: d.clone(),
922                    disjuncts: ds,
923                    lits,
924                    src: ClauseSrc::Assumed(d.clone()),
925                });
926                None
927            }
928            _ => {
929                let lit = self.lit_of(d).expect("assumed atom must be interned");
930                if self.set(lit, Reason::Assumption).is_err() {
931                    Some(ConflictInfo::Clash { assumed: d.clone() })
932                } else {
933                    None
934                }
935            }
936        }
937    }
938
939    fn backtrack(&mut self, trail_snap: usize, clause_snap: usize, fired_snap: Vec<bool>) {
940        while self.trail.len() > trail_snap {
941            let v = self.trail.pop().unwrap();
942            self.assign[v] = None;
943            self.reason[v] = None;
944        }
945        self.clauses.truncate(clause_snap);
946        self.fired_disj = fired_snap;
947    }
948
949    fn emit_conflict(&self, c: Conflict) -> DerivationTree {
950        match c {
951            Conflict::Rule(ri) => {
952                let src = self.rules[ri].src.clone();
953                let (ante_form, cons_form) = Self::split_implies(&src);
954                let ante_proof = self.prove_ante(&ante_form, None);
955                let cons_tree = modus_ponens(cons_form.clone(), leaf(src), ante_proof);
956                let cons_lit = self.lit_of(&cons_form).expect("rule consequent is a literal");
957                let opp = self.prove_var(cons_lit.var);
958                make_contradiction(cons_tree, opp)
959            }
960            Conflict::Clause(ci) => {
961                let or_expr = self.clauses[ci].or_expr.clone();
962                let or_tree = self.materialize_clause(ci);
963                self.falsum_from_disj(&or_expr, or_tree, usize::MAX)
964            }
965        }
966    }
967
968    fn emit_conflict_info(&self, info: ConflictInfo) -> DerivationTree {
969        match info {
970            ConflictInfo::Clash { assumed } => {
971                let lit = self.lit_of(&assumed).expect("clashing atom must be interned");
972                let existing = self.prove_var(lit.var);
973                make_contradiction(leaf(assumed), existing)
974            }
975            ConflictInfo::Rule(ri) => self.emit_conflict(Conflict::Rule(ri)),
976            ConflictInfo::Clause(ci) => self.emit_conflict(Conflict::Clause(ci)),
977        }
978    }
979
980    // ── Positive-cell goal by closure elimination + search ───────────────────────
981
982    /// Prove a positive cell `goal` that propagation alone does not force: find the
983    /// closure clause it sits in (the row's value disjunction), refute every OTHER
984    /// value by DPLL search, then the goal survives by disjunctive syllogism.
985    fn prove_positive_by_closure(&mut self, goal: &ProofExpr) -> Option<DerivationTree> {
986        let goal_lit = self.lit_of(goal)?;
987        // The cell can sit in more than one closure clause — its row (the values it may
988        // take) AND, once column closures are added, its column (the rows that may take
989        // its value). Both prove it by refuting their OTHER disjuncts. Try them
990        // FEWEST-LIVE first (the rest already refuted on the trail ⇒ the search opens the
991        // fewest branches and the proof stays small), but FALL THROUGH to the next on
992        // failure: trying only one closure would lose completeness when that one's
993        // refutations exceed the depth bound while another's would have closed.
994        let mut candidates: Vec<usize> = (0..self.clauses.len())
995            .filter(|&ci| {
996                self.clauses[ci]
997                    .lits
998                    .as_ref()
999                    .map_or(false, |ls| ls.contains(&goal_lit) && ls.len() >= 2)
1000            })
1001            .collect();
1002        candidates.sort_by_key(|&ci| self.clause_status(ci).1.len());
1003        for ci in candidates {
1004            let or_expr = self.clauses[ci].or_expr.clone();
1005            let or_tree = self.materialize_clause(ci);
1006            if let Some(tree) = self.extract_by_search(&or_expr, or_tree, goal) {
1007                return Some(tree);
1008            }
1009        }
1010        None
1011    }
1012
1013    /// Like `extract_survivor`, but the non-surviving disjuncts are refuted by SEARCH
1014    /// (assume + derive ⊥ + reductio) rather than read from the trail.
1015    fn extract_by_search(&mut self, or_expr: &ProofExpr, or_tree: DerivationTree, target: &ProofExpr) -> Option<DerivationTree> {
1016        debug_assert!(&or_tree.conclusion == or_expr, "extract_by_search mismatch:\n tree={:?}\n expr={:?}", or_tree.conclusion, or_expr);
1017        match or_expr {
1018            ProofExpr::Or(l, r) => {
1019                if r.as_ref() == target {
1020                    let lneg = self.prove_neg_by_search(l)?;
1021                    Some(disjunction_elim(target.clone(), or_tree, lneg))
1022                } else {
1023                    let rneg = self.prove_neg_by_search(r)?;
1024                    let l_tree = disjunction_elim((**l).clone(), or_tree, rneg);
1025                    self.extract_by_search(l, l_tree, target)
1026                }
1027            }
1028            _ => Some(or_tree),
1029        }
1030    }
1031
1032    /// Prove `¬e` where `e` is a single cell literal OR a sub-disjunction. A literal:
1033    /// directly if already false on the trail, else by assuming it and refuting it (DPLL),
1034    /// discharged by reductio. A disjunction `A ∨ B` (e.g. the whole `CT ∨ FL ∨ KY` to the
1035    /// LEFT of `Maine` when the goal cell is the closure's RIGHTMOST disjunct): assume it
1036    /// and case-split each disjunct to ⊥. Without the disjunction case, a cell that is the
1037    /// last value of its closure could never be proved positively (its left subtree is not
1038    /// a single literal).
1039    fn prove_neg_by_search(&mut self, e: &ProofExpr) -> Option<DerivationTree> {
1040        if let ProofExpr::Or(..) = e {
1041            let bot = self.disj_to_falsum(e)?;
1042            return Some(reductio(e.clone(), bot));
1043        }
1044        let lit = self.lit_of(e)?;
1045        match self.value(lit) {
1046            Some(false) => Some(align_to(self.prove_var(lit.var), &neg_of(e))),
1047            Some(true) => None,
1048            None => {
1049                let trail_snap = self.trail.len();
1050                let clause_snap = self.clauses.len();
1051                let fired_snap = self.fired_disj.clone();
1052                let _ = self.set(lit, Reason::Assumption);
1053                let bot = self.derive_falsum(0);
1054                let result = bot.map(|b| reductio(e.clone(), b));
1055                self.backtrack(trail_snap, clause_snap, fired_snap);
1056                result
1057            }
1058        }
1059    }
1060
1061    /// Derive ⊥ from `e` ASSUMED true (bound as a hypothesis by an enclosing case-split):
1062    /// a disjunction recurses (case-split each arm); a literal contradicts its own searched
1063    /// refutation. Mirrors `falsum_from_disj`, but the disjuncts are refuted by SEARCH
1064    /// rather than read off the trail.
1065    fn disj_to_falsum(&mut self, e: &ProofExpr) -> Option<DerivationTree> {
1066        if let ProofExpr::Or(l, r) = e {
1067            let lb = self.disj_to_falsum(l)?;
1068            let rb = self.disj_to_falsum(r)?;
1069            return Some(disjunction_cases(falsum(), leaf(e.clone()), lb, rb));
1070        }
1071        // `e` is a literal assumed true here; its searched negation closes the branch.
1072        let neg = self.prove_neg_by_search(e)?;
1073        Some(make_contradiction(leaf(e.clone()), neg))
1074    }
1075}
1076
1077const MAX_DEPTH: usize = 64;
1078
1079enum ConflictInfo {
1080    /// An assumed literal clashes with the existing assignment of its variable.
1081    Clash { assumed: ProofExpr },
1082    Rule(usize),
1083    Clause(usize),
1084}
1085
1086/// `Contradiction` from two proofs of opposite polarity, in either order.
1087fn make_contradiction(a: DerivationTree, b: DerivationTree) -> DerivationTree {
1088    if matches!(a.conclusion, ProofExpr::Not(_)) {
1089        contra_align(b, a)
1090    } else {
1091        contra_align(a, b)
1092    }
1093}
1094
1095/// `¬e` (peeling a double negation).
1096fn neg_of(e: &ProofExpr) -> ProofExpr {
1097    match e {
1098        ProofExpr::Not(inner) => (**inner).clone(),
1099        _ => ProofExpr::Not(Box::new(e.clone())),
1100    }
1101}
1102
1103/// Re-orient `tree` (a literal proof) to conclude exactly `want`, inserting an equality
1104/// symmetry (or a symmetry-reductio under a negation) when only the Identity order
1105/// differs.
1106fn align_to(tree: DerivationTree, want: &ProofExpr) -> DerivationTree {
1107    if &tree.conclusion == want {
1108        return tree;
1109    }
1110    match (&tree.conclusion, want) {
1111        (ProofExpr::Identity(a, b), ProofExpr::Identity(c, d)) if a == d && b == c => {
1112            eq_sym(a.clone(), b.clone(), tree)
1113        }
1114        (ProofExpr::Not(ti), ProofExpr::Not(wi)) => match (ti.as_ref(), wi.as_ref()) {
1115            (ProofExpr::Identity(a, b), ProofExpr::Identity(c, d)) if a == d && b == c => {
1116                let assumed = ProofExpr::Identity(c.clone(), d.clone());
1117                let flipped = eq_sym(c.clone(), d.clone(), leaf(assumed.clone()));
1118                let bot = contradiction(flipped, tree);
1119                reductio(assumed, bot)
1120            }
1121            _ => tree,
1122        },
1123        _ => tree,
1124    }
1125}
1126
1127/// Build `Contradiction` from a positive proof and a negation proof, aligning the
1128/// positive side's Identity orientation to the negation's inner term.
1129fn contra_align(pos_tree: DerivationTree, neg_tree: DerivationTree) -> DerivationTree {
1130    let inner = match &neg_tree.conclusion {
1131        ProofExpr::Not(i) => (**i).clone(),
1132        other => other.clone(),
1133    };
1134    let aligned = align_to(pos_tree, &inner);
1135    contradiction(aligned, neg_tree)
1136}
1137
1138/// Solve a single grid cell by propagation and emit its certified derivation. The
1139/// premises must be ground (call `grounding::ground_problem` first). Returns `None`
1140/// when the cell is not forced by propagation alone (it needs search — Phase C) or the
1141/// premises do not classify as a grid.
1142pub fn grid_prove(premises: &[ProofExpr], goal: &ProofExpr) -> Option<DerivationTree> {
1143    let mut solver = compile(premises)?;
1144    // The root fixpoint runs at decision level 0: its assignments are permanent, so
1145    // compound-clause unit propagation is sound to emit linearly here. Every later
1146    // `propagate()` runs under a backtrackable assumption with `at_root` false.
1147    //
1148    solver.at_root = true;
1149    let root = solver.propagate();
1150    solver.at_root = false;
1151    if root.is_err() {
1152        return None;
1153    }
1154    // A goal already forced by propagation (the Phase-B positive-cell path).
1155    if let Some(lit) = solver.lit_of(goal) {
1156        if solver.value(lit) == Some(true) {
1157            return Some(align_to(solver.prove_var(lit.var), goal));
1158        }
1159    }
1160    // A positive cell not forced by propagation: prove it by closure elimination,
1161    // refuting the row's other values via DPLL search.
1162    if !matches!(goal, ProofExpr::Not(_)) && solver.lit_of(goal).is_some() {
1163        if let Some(tree) = solver.prove_positive_by_closure(goal) {
1164            return Some(align_to(tree, goal));
1165        }
1166    }
1167    // A negative goal: assume the atom and refute it by search (DPLL), then reductio.
1168    if let ProofExpr::Not(inner) = goal {
1169        let x_lit = solver.lit_of(inner)?;
1170        match solver.value(x_lit) {
1171            Some(false) => return Some(align_to(solver.prove_var(x_lit.var), goal)),
1172            Some(true) => return None,
1173            None => {
1174                let _ = solver.set(x_lit, Reason::Assumption);
1175                let bot = solver.derive_falsum(0)?;
1176                return Some(reductio((**inner).clone(), bot));
1177            }
1178        }
1179    }
1180    None
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185    use super::*;
1186    use crate::{ProofExpr, ProofTerm};
1187
1188    fn c(s: &str) -> ProofTerm {
1189        ProofTerm::Constant(s.to_string())
1190    }
1191    fn v(s: &str) -> ProofTerm {
1192        ProofTerm::Variable(s.to_string())
1193    }
1194    fn pred(name: &str, args: Vec<ProofTerm>) -> ProofExpr {
1195        ProofExpr::Predicate { name: name.to_string(), args, world: None }
1196    }
1197    fn imp(a: ProofExpr, b: ProofExpr) -> ProofExpr {
1198        ProofExpr::Implies(Box::new(a), Box::new(b))
1199    }
1200    fn or(a: ProofExpr, b: ProofExpr) -> ProofExpr {
1201        ProofExpr::Or(Box::new(a), Box::new(b))
1202    }
1203    fn and(a: ProofExpr, b: ProofExpr) -> ProofExpr {
1204        ProofExpr::And(Box::new(a), Box::new(b))
1205    }
1206    fn not(e: ProofExpr) -> ProofExpr {
1207        ProofExpr::Not(Box::new(e))
1208    }
1209    fn id(a: ProofTerm, b: ProofTerm) -> ProofExpr {
1210        ProofExpr::Identity(a, b)
1211    }
1212    fn forall(var: &str, body: ProofExpr) -> ProofExpr {
1213        ProofExpr::ForAll { variable: var.to_string(), body: Box::new(body) }
1214    }
1215    fn exists(var: &str, body: ProofExpr) -> ProofExpr {
1216        ProofExpr::Exists { variable: var.to_string(), body: Box::new(body) }
1217    }
1218
1219    fn verify(premises: &[ProofExpr], goal: &ProofExpr, tree: DerivationTree) -> crate::verify::VerifiedProof {
1220        crate::verify::check_derivation(premises, goal, tree)
1221    }
1222
1223    // ── PHASE A: prove the emitter's tree SHAPES certify, before any search ──────
1224
1225    /// A propagation step (ModusPonens from a rule's reason) certifies.
1226    #[test]
1227    fn modus_ponens_shape_certifies() {
1228        let p = pred("P", vec![c("Obj")]);
1229        let q = pred("Q", vec![c("Obj")]);
1230        let premises = vec![p.clone(), imp(p.clone(), q.clone())];
1231        let tree = modus_ponens(q.clone(), leaf(imp(p.clone(), q.clone())), leaf(p));
1232        let r = verify(&premises, &q, tree);
1233        assert!(r.verified, "ModusPonens shape must certify; err: {:?}", r.verification_error);
1234    }
1235
1236    /// A unit-clause propagation (disjunctive syllogism from the clause's reason) certifies.
1237    #[test]
1238    fn disjunction_elim_shape_certifies() {
1239        let p = pred("P", vec![c("Obj")]);
1240        let q = pred("Q", vec![c("Obj")]);
1241        let premises = vec![or(p.clone(), q.clone()), not(p.clone())];
1242        let tree = disjunction_elim(q.clone(), leaf(or(p.clone(), q.clone())), leaf(not(p)));
1243        let r = verify(&premises, &q, tree);
1244        assert!(r.verified, "DisjunctionElim shape must certify; err: {:?}", r.verification_error);
1245    }
1246
1247    /// THE real grid shape, hand-built end to end: the 2-value bijection cell
1248    /// `In(Beta, Maine)`, proved by closure (ModusPonens) + a reductio that fires the
1249    /// at-most-one rule and closes the equality contradiction. Every node is a rule
1250    /// the solver's emitter will produce — if this certifies, the emitter is sound.
1251    #[test]
1252    fn two_value_grid_cell_handbuilt_certifies() {
1253        let trip = |t: ProofTerm| pred("Trip", vec![t]);
1254        let in_ = |t: ProofTerm, s: ProofTerm| pred("In", vec![t, s]);
1255        let fl = || c("Florida");
1256        let me_ = || c("Maine");
1257
1258        // Grounded premises (what `ground_problem` yields, split into the instances
1259        // the certifier registers as hypotheses):
1260        let closure_beta = imp(trip(c("Beta")), or(in_(c("Beta"), fl()), in_(c("Beta"), me_())));
1261        // at-most-one instance for the pair (Beta, Alpha):
1262        //   ((Trip(Beta)∧In(Beta,FL)) ∧ (Trip(Alpha)∧In(Alpha,FL))) → Beta = Alpha
1263        let amo_beta_alpha = imp(
1264            and(and(trip(c("Beta")), in_(c("Beta"), fl())), and(trip(c("Alpha")), in_(c("Alpha"), fl()))),
1265            id(c("Beta"), c("Alpha")),
1266        );
1267        let premises = vec![
1268            trip(c("Alpha")),
1269            trip(c("Beta")),
1270            not(id(c("Alpha"), c("Beta"))),
1271            closure_beta.clone(),
1272            amo_beta_alpha.clone(),
1273            in_(c("Alpha"), fl()),
1274        ];
1275        let goal = in_(c("Beta"), me_());
1276
1277        // ¬In(Beta,FL): assume In(Beta,FL); fire the at-most-one rule to get
1278        // Beta=Alpha; symmetrise to Alpha=Beta; contradict ¬(Alpha=Beta).
1279        let ante = conj_intro(
1280            and(trip(c("Beta")), in_(c("Beta"), fl())),
1281            and(trip(c("Alpha")), in_(c("Alpha"), fl())),
1282            conj_intro(trip(c("Beta")), in_(c("Beta"), fl()), leaf(trip(c("Beta"))), leaf(in_(c("Beta"), fl()))),
1283            conj_intro(trip(c("Alpha")), in_(c("Alpha"), fl()), leaf(trip(c("Alpha"))), leaf(in_(c("Alpha"), fl()))),
1284        );
1285        let beta_eq_alpha = modus_ponens(id(c("Beta"), c("Alpha")), leaf(amo_beta_alpha), ante);
1286        let alpha_eq_beta = eq_sym(c("Beta"), c("Alpha"), beta_eq_alpha);
1287        let contra = contradiction(alpha_eq_beta, leaf(not(id(c("Alpha"), c("Beta")))));
1288        let neg_in_beta_fl = reductio(in_(c("Beta"), fl()), contra);
1289
1290        // closure: Trip(Beta) → In(Beta,FL)∨In(Beta,ME); MP with Trip(Beta).
1291        let beta_disj = modus_ponens(
1292            or(in_(c("Beta"), fl()), in_(c("Beta"), me_())),
1293            leaf(closure_beta),
1294            leaf(trip(c("Beta"))),
1295        );
1296        // Disjunctive syllogism: (In(Beta,FL)∨In(Beta,ME)), ¬In(Beta,FL) ⊢ In(Beta,ME).
1297        let tree = disjunction_elim(in_(c("Beta"), me_()), beta_disj, neg_in_beta_fl);
1298
1299        let r = verify(&premises, &goal, tree);
1300        assert!(r.verified, "hand-built 2-value grid cell must certify; err: {:?}", r.verification_error);
1301    }
1302
1303    // ── PHASE B: the solver PROPAGATES and EMITS the certified tree itself ───────
1304
1305    /// The 2-value bijection, solved end to end by the incremental solver: ground the
1306    /// universal premises, `grid_prove` propagates `¬In(Beta,FL)` (exclusion) then the
1307    /// closure forces `In(Beta,Maine)`, and the emitted derivation kernel-certifies.
1308    #[test]
1309    fn two_value_grid_propagates_and_certifies() {
1310        let trip = |t: ProofTerm| pred("Trip", vec![t]);
1311        let in_ = |t: ProofTerm, s: ProofTerm| pred("In", vec![t, s]);
1312        let fl = || c("Florida");
1313        let me_ = || c("Maine");
1314        let closure = forall("x", imp(trip(v("x")), or(in_(v("x"), fl()), in_(v("x"), me_()))));
1315        let exactly_one_fl = forall(
1316            "x",
1317            forall(
1318                "y",
1319                imp(
1320                    and(and(trip(v("x")), in_(v("x"), fl())), and(trip(v("y")), in_(v("y"), fl()))),
1321                    id(v("x"), v("y")),
1322                ),
1323            ),
1324        );
1325        let premises = vec![
1326            trip(c("Alpha")),
1327            trip(c("Beta")),
1328            not(id(c("Alpha"), c("Beta"))),
1329            closure,
1330            exactly_one_fl,
1331            in_(c("Alpha"), fl()),
1332        ];
1333        let goal = in_(c("Beta"), me_());
1334        let (gp, gg) = crate::grounding::ground_problem(&premises, &goal);
1335        let tree = grid_prove(&gp, &gg).expect("2-value cell must be forced by propagation");
1336        let r = verify(&gp, &gg, tree);
1337        assert!(r.verified, "solver-emitted 2-value proof must certify; err: {:?}", r.verification_error);
1338    }
1339
1340    /// A full-size multi-category grid (4 trips × 3 four-value categories), solved by
1341    /// pure propagation: three states pinned, the fourth (`Delta → Maine`) forced by
1342    /// exclusion over the state closure + at-most-one, with the irrelevant year/friend
1343    /// categories present as noise. The solver emits a certified derivation.
1344    #[test]
1345    fn multi_category_grid_propagates_and_certifies() {
1346        let trips = ["Alpha", "Beta", "Gamma", "Delta"];
1347        let neq = |a: &str, b: &str| not(id(c(a), c(b)));
1348        let mut premises: Vec<ProofExpr> = Vec::new();
1349        let category = |rel: &str, vals: &[&str], out: &mut Vec<ProofExpr>| {
1350            for t in trips {
1351                let mut it = vals.iter().map(|val| pred(rel, vec![c(t), c(val)]));
1352                let first = it.next().unwrap();
1353                out.push(it.fold(first, or));
1354            }
1355            for val in vals {
1356                for (i, t) in trips.iter().enumerate() {
1357                    for u in &trips[i + 1..] {
1358                        out.push(imp(
1359                            and(pred(rel, vec![c(t), c(val)]), pred(rel, vec![c(u), c(val)])),
1360                            id(c(t), c(u)),
1361                        ));
1362                    }
1363                }
1364            }
1365        };
1366        for (i, t) in trips.iter().enumerate() {
1367            for u in &trips[i + 1..] {
1368                premises.push(neq(t, u));
1369            }
1370        }
1371        category("In", &["2001", "2002", "2003", "2004"], &mut premises);
1372        category("In", &["CT", "FL", "KY", "ME"], &mut premises);
1373        category("With", &["Bill", "Lillie", "Neal", "Yvonne"], &mut premises);
1374        premises.push(pred("In", vec![c("Alpha"), c("FL")]));
1375        premises.push(pred("In", vec![c("Beta"), c("KY")]));
1376        premises.push(pred("In", vec![c("Gamma"), c("CT")]));
1377        let goal = pred("In", vec![c("Delta"), c("ME")]);
1378        let tree = grid_prove(&premises, &goal).expect("Delta→ME must be forced by propagation");
1379        let r = verify(&premises, &goal, tree);
1380        assert!(r.verified, "solver-emitted multi-category proof must certify; err: {:?}", r.verification_error);
1381    }
1382
1383    // ── PHASE C: DPLL decision search over compound (of-pair) clauses ────────────
1384
1385    /// An of-pair clue ((In(A,FL) ∧ With(B,Neal)) ∨ (In(B,FL) ∧ With(A,Neal))) forces a
1386    /// third trip OUT of Florida: assuming In(C,FL), the at-most-one rules exclude both
1387    /// In(A,FL) and In(B,FL), collapsing the of-pair clause to ⊥. The solver searches
1388    /// and emits a certified case-analysis derivation.
1389    #[test]
1390    fn of_pair_forces_cell_certifies() {
1391        let in_fl = |t: &str| pred("In", vec![c(t), c("FL")]);
1392        let amo = |t: &str, u: &str| imp(and(in_fl(t), in_fl(u)), id(c(t), c(u)));
1393        let of_pair = or(
1394            and(in_fl("A"), pred("With", vec![c("B"), c("Neal")])),
1395            and(in_fl("B"), pred("With", vec![c("A"), c("Neal")])),
1396        );
1397        let premises = vec![
1398            of_pair,
1399            amo("A", "C"),
1400            amo("B", "C"),
1401            not(id(c("A"), c("C"))),
1402            not(id(c("B"), c("C"))),
1403            not(id(c("A"), c("B"))),
1404        ];
1405        let goal = not(in_fl("C"));
1406        let tree = grid_prove(&premises, &goal).expect("of-pair must force ¬In(C,FL)");
1407        let r = verify(&premises, &goal, tree);
1408        assert!(r.verified, "solver-emitted of-pair proof must certify; err: {:?}", r.verification_error);
1409    }
1410
1411    /// The compound of-pair shape: `(A ∧ (P ∨ Q)) ∨ (Bad ∧ C)` with `¬Bad`, `P→¬R`,
1412    /// `Q→¬R`. Assuming `R`, the `(Bad ∧ C)` arm collapses on `¬Bad` and the `A∧(P∨Q)`
1413    /// arm case-splits the inner `P ∨ Q`, both arms contradicting `R`. The solver finds
1414    /// the nested case analysis and emits a certified ⊥, discharged by reductio to `¬R`.
1415    #[test]
1416    fn compound_of_pair_resolves_certifies() {
1417        let a = pred("A", vec![c("Obj")]);
1418        let p = pred("P", vec![c("Obj")]);
1419        let q = pred("Q", vec![c("Obj")]);
1420        let bad = pred("Bad", vec![c("Obj")]);
1421        let cc = pred("C", vec![c("Obj")]);
1422        let rr = pred("R", vec![c("Obj")]);
1423        let of_pair = or(and(a, or(p.clone(), q.clone())), and(bad.clone(), cc));
1424        let premises = vec![
1425            of_pair,
1426            not(bad),
1427            imp(p, not(rr.clone())),
1428            imp(q, not(rr.clone())),
1429        ];
1430        let goal = not(rr);
1431        let tree = grid_prove(&premises, &goal).expect("compound of-pair must force ¬R");
1432        let r = verify(&premises, &goal, tree);
1433        assert!(r.verified, "solver-emitted compound of-pair proof must certify; err: {:?}", r.verification_error);
1434    }
1435
1436    fn count_rule(t: &DerivationTree, rule_dbg: &str) -> usize {
1437        let here = usize::from(format!("{:?}", t.rule).starts_with(rule_dbg));
1438        here + t.premises.iter().map(|c| count_rule(c, rule_dbg)).sum::<usize>()
1439    }
1440
1441    /// WAVE 1: a compound (of-pair-shaped) clause `(P∧Q) ∨ (R∧S)` with `¬R` refuting the
1442    /// second disjunct — disjunctive syllogism forces the first, so `P` and `Q` are forced
1443    /// by PROPAGATION (no search). The emitted proof must be LINEAR: zero `DisjunctionCases`.
1444    #[test]
1445    fn compound_clause_unit_propagates_linearly() {
1446        let o = || c("Obj");
1447        let p = pred("P", vec![o()]);
1448        let q = pred("Q", vec![o()]);
1449        let r = pred("R", vec![o()]);
1450        let s = pred("S", vec![o()]);
1451        let clause = or(and(p.clone(), q.clone()), and(r.clone(), s.clone()));
1452        let premises = vec![clause, not(r)];
1453        let tree = grid_prove(&premises, &p).expect("P must be forced by compound-clause propagation");
1454        assert_eq!(
1455            count_rule(&tree, "DisjunctionCases"),
1456            0,
1457            "compound-clause propagation must emit a LINEAR proof (no DisjunctionCases)"
1458        );
1459        let res = verify(&premises, &p, tree);
1460        assert!(res.verified, "linear compound-clause proof must certify; err: {:?}", res.verification_error);
1461    }
1462
1463    // ── PHASE E (de-risk): the REAL studio Simon, through prepare_premises ───────
1464
1465    /// The studio `Simon` example (2 trips × 2 categories: year {2003,2004} + state
1466    /// {Florida,Maine}), run through the SAME preparation `ui_bridge::prepare_premises`
1467    /// uses — `at_most_one_lemmas` + sort-aware grounding + `discharge_unary_facts` —
1468    /// then solved and certified by `grid_solver`. The grounded `∃`-existence clauses
1469    /// are skipped by `compile`; closures + at-most-one + the pins force `Beta∈Maine`.
1470    #[test]
1471    fn studio_simon_two_category_certifies() {
1472        use crate::grounding::{
1473            at_most_one_lemmas, discharge_unary_facts, domain_constants, ground_sorted, sort_domains,
1474        };
1475        let trip = |t: ProofTerm| pred("Trip", vec![t]);
1476        let in_ = |t: ProofTerm, s: ProofTerm| pred("In", vec![t, s]);
1477        let exactly_one = |val: &str| {
1478            let phi = |t: ProofTerm| and(trip(t.clone()), in_(t, c(val)));
1479            exists(
1480                "x",
1481                and(phi(v("x")), forall("y", imp(phi(v("y")), id(v("y"), v("x"))))),
1482            )
1483        };
1484        let mut premises = vec![
1485            trip(c("Alpha")),
1486            trip(c("Beta")),
1487            not(id(c("Alpha"), c("Beta"))),
1488            forall("x", imp(trip(v("x")), or(in_(v("x"), c("2003")), in_(v("x"), c("2004"))))),
1489            forall("x", imp(trip(v("x")), or(in_(v("x"), c("Florida")), in_(v("x"), c("Maine"))))),
1490            exactly_one("2003"),
1491            exactly_one("Florida"),
1492            in_(c("Alpha"), c("2003")),
1493            in_(c("Alpha"), c("Florida")),
1494        ];
1495        let goal = in_(c("Beta"), c("Maine"));
1496        // prepare_premises (tense erasure is a no-op for present-tense "is in").
1497        premises.extend(at_most_one_lemmas(&premises));
1498        let mut all = premises.clone();
1499        all.push(goal.clone());
1500        let fallback = domain_constants(&all);
1501        let sorts = sort_domains(&premises);
1502        let grounded: Vec<ProofExpr> = premises.iter().map(|p| ground_sorted(p, &sorts, &fallback)).collect();
1503        let prepared = discharge_unary_facts(&grounded);
1504        let tree = grid_prove(&prepared, &goal).expect("studio Simon must solve by propagation");
1505        let r = verify(&prepared, &goal, tree);
1506        assert!(r.verified, "studio Simon (2-category) must certify via grid_solver; err: {:?}", r.verification_error);
1507    }
1508}