Skip to main content

alkahest_cas/real/
cad.rs

1//! Cylindrical Algebraic Decomposition scaffolding and univariate quantifier
2//! elimination (V2-9).
3//!
4//! This module provides:
5//!
6//! - [`cad_project`] — Brown-style projection eliminating one variable via
7//!   discriminants (\(`\mathrm{res}(f, \partial_x f)`\)) and pairwise resultants.
8//! - [`cad_lift`] — produce isolating intervals for a squarefree algebraic
9//!   core built from polynomials in `main_var` (CAD lift stage along one axis).
10//! - [`decide`] — decides closed prenex formulas with **one quantifier block**
11//!   (`\exists`, `\forall`), where the quantifier-free body is purely polynomial in
12//!   the sole quantified symbol with rational/integer literals (parameters are only
13//!   integers).
14//!
15//! Multivariate QE and nested alternating quantifiers are left for future passes.
16
17use crate::diff::{diff, DiffError};
18use crate::errors::AlkahestError;
19use crate::kernel::expr::PredicateKind;
20use crate::kernel::Domain;
21use crate::kernel::{ExprId, ExprPool};
22use crate::logic::{formula_from_expr, Formula, LogicError};
23use crate::poly::resultant::{self, resultant};
24use crate::poly::{
25    poly_normal, real_roots, ConversionError, RealRootError, ResultantError, RootInterval, UniPoly,
26};
27use std::collections::{BTreeSet, HashMap};
28use std::fmt;
29
30// ---------------------------------------------------------------------------
31// Errors and result wrapper
32// ---------------------------------------------------------------------------
33
34/// Errors from CAD helpers and [`decide`].
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum CadError {
37    NotPolynomial(ConversionError),
38    Diff(DiffError),
39    Resultant(ResultantError),
40    RealRoots(RealRootError),
41    Logic(LogicError),
42    /// Feature gap (nested quantifiers, parametric polynomials, transcendental atoms, …).
43    Unsupported(&'static str),
44}
45
46impl fmt::Display for CadError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            CadError::NotPolynomial(e) => write!(f, "{e}"),
50            CadError::Diff(e) => write!(f, "{e}"),
51            CadError::Resultant(e) => write!(f, "{e}"),
52            CadError::RealRoots(e) => write!(f, "{e}"),
53            CadError::Logic(e) => write!(f, "{e}"),
54            CadError::Unsupported(s) => write!(f, "CAD: {s}"),
55        }
56    }
57}
58
59impl std::error::Error for CadError {}
60
61impl AlkahestError for CadError {
62    fn code(&self) -> &'static str {
63        match self {
64            CadError::NotPolynomial(e) => e.code(),
65            CadError::Diff(e) => e.code(),
66            CadError::Resultant(e) => e.code(),
67            CadError::RealRoots(e) => e.code(),
68            CadError::Logic(e) => e.code(),
69            CadError::Unsupported(_) => "E-CAD-001",
70        }
71    }
72
73    fn remediation(&self) -> Option<&'static str> {
74        match self {
75            CadError::NotPolynomial(e) => e.remediation(),
76            CadError::Diff(e) => e.remediation(),
77            CadError::Resultant(e) => e.remediation(),
78            CadError::RealRoots(e) => e.remediation(),
79            CadError::Logic(e) => e.remediation(),
80            CadError::Unsupported(_) => Some(
81                "use a purely polynomial constraint in one real variable without nested quantifiers; multivariate QE is incremental",
82            ),
83        }
84    }
85}
86
87impl From<ConversionError> for CadError {
88    fn from(value: ConversionError) -> Self {
89        CadError::NotPolynomial(value)
90    }
91}
92
93impl From<DiffError> for CadError {
94    fn from(value: DiffError) -> Self {
95        CadError::Diff(value)
96    }
97}
98
99impl From<ResultantError> for CadError {
100    fn from(value: ResultantError) -> Self {
101        CadError::Resultant(value)
102    }
103}
104
105impl From<RealRootError> for CadError {
106    fn from(value: RealRootError) -> Self {
107        CadError::RealRoots(value)
108    }
109}
110
111impl From<LogicError> for CadError {
112    fn from(value: LogicError) -> Self {
113        CadError::Logic(value)
114    }
115}
116
117/// Outcome of real QE [`decide`].
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct QeResult {
120    pub truth: bool,
121    pub witness: Option<HashMap<ExprId, rug::Rational>>,
122}
123
124// ---------------------------------------------------------------------------
125// NNF helpers (localized copy of [`crate::logic`] helpers)
126// ---------------------------------------------------------------------------
127
128fn dual_kind(kind: PredicateKind) -> PredicateKind {
129    use PredicateKind::{Eq, Ge, Gt, Le, Lt, Ne};
130    match kind {
131        Lt => Ge,
132        Le => Gt,
133        Gt => Le,
134        Ge => Lt,
135        Eq => Ne,
136        Ne => Eq,
137        other => other,
138    }
139}
140
141fn is_rel(kind: &PredicateKind) -> bool {
142    use PredicateKind::*;
143    matches!(kind, Lt | Le | Gt | Ge | Eq | Ne)
144}
145
146fn simplify_formula_constants(f: Formula) -> Formula {
147    match f {
148        Formula::And(a, b) => {
149            let la = simplify_formula_constants(*a);
150            let lb = simplify_formula_constants(*b);
151            match (&la, &lb) {
152                (Formula::False, _) | (_, Formula::False) => Formula::False,
153                (Formula::True, x) => x.clone(),
154                (x, Formula::True) => x.clone(),
155                _ => Formula::and(la, lb),
156            }
157        }
158        Formula::Or(a, b) => {
159            let la = simplify_formula_constants(*a);
160            let lb = simplify_formula_constants(*b);
161            match (&la, &lb) {
162                (Formula::True, _) | (_, Formula::True) => Formula::True,
163                (Formula::False, x) => x.clone(),
164                (x, Formula::False) => x.clone(),
165                _ => Formula::or(la, lb),
166            }
167        }
168        Formula::Not(x) => Formula::not(simplify_formula_constants(*x)),
169        Formula::Forall { var, body } => Formula::Forall {
170            var,
171            body: Box::new(simplify_formula_constants(*body)),
172        },
173        Formula::Exists { var, body } => Formula::Exists {
174            var,
175            body: Box::new(simplify_formula_constants(*body)),
176        },
177        other => other,
178    }
179}
180
181fn nnf_formula(f: Formula) -> Formula {
182    match f {
183        Formula::Not(inner) => match *inner {
184            Formula::True => Formula::False,
185            Formula::False => Formula::True,
186            Formula::Not(g) => nnf_formula(*g),
187            Formula::And(a, b) => nnf_formula(Formula::or(Formula::not(*a), Formula::not(*b))),
188            Formula::Or(a, b) => nnf_formula(Formula::and(Formula::not(*a), Formula::not(*b))),
189            Formula::Forall { var, body } => nnf_formula(Formula::Exists {
190                var,
191                body: Box::new(Formula::not(*body)),
192            }),
193            Formula::Exists { var, body } => nnf_formula(Formula::Forall {
194                var,
195                body: Box::new(Formula::not(*body)),
196            }),
197            Formula::Atom {
198                kind: PredicateKind::True,
199                ..
200            } => Formula::False,
201            Formula::Atom {
202                kind: PredicateKind::False,
203                ..
204            } => Formula::True,
205            Formula::Atom { kind, args } if is_rel(&kind) => Formula::Atom {
206                kind: dual_kind(kind),
207                args,
208            },
209            inner => Formula::Not(Box::new(inner)),
210        },
211        Formula::And(a, b) => Formula::and(nnf_formula(*a), nnf_formula(*b)),
212        Formula::Or(a, b) => Formula::or(nnf_formula(*a), nnf_formula(*b)),
213        Formula::Forall { var, body } => Formula::Forall {
214            var,
215            body: Box::new(nnf_formula(*body)),
216        },
217        Formula::Exists { var, body } => Formula::Exists {
218            var,
219            body: Box::new(nnf_formula(*body)),
220        },
221        other => other,
222    }
223}
224
225// ---------------------------------------------------------------------------
226// Variable sets
227// ---------------------------------------------------------------------------
228
229fn insert_formula_vars(pool: &ExprPool, expr: ExprId, out: &mut BTreeSet<ExprId>) {
230    for v in resultant::collect_free_vars(expr, pool) {
231        out.insert(v);
232    }
233}
234
235fn free_vars_formula(f: &Formula, pool: &ExprPool) -> BTreeSet<ExprId> {
236    match f {
237        Formula::Atom { args, .. } => {
238            let mut s = BTreeSet::new();
239            for &a in args {
240                insert_formula_vars(pool, a, &mut s);
241            }
242            s
243        }
244        Formula::And(a, b) | Formula::Or(a, b) => {
245            let mut s = free_vars_formula(a, pool);
246            s.extend(free_vars_formula(b, pool));
247            s
248        }
249        Formula::Not(x) => free_vars_formula(x, pool),
250        Formula::Exists { var, body } => {
251            let mut s = free_vars_formula(body, pool);
252            s.insert(*var);
253            s
254        }
255        Formula::Forall { var, body } => {
256            let mut s = free_vars_formula(body, pool);
257            s.insert(*var);
258            s
259        }
260        Formula::True | Formula::False => BTreeSet::new(),
261    }
262}
263
264fn contains_quantifier(f: &Formula) -> bool {
265    match f {
266        Formula::Exists { .. } | Formula::Forall { .. } => true,
267        Formula::And(a, b) | Formula::Or(a, b) => contains_quantifier(a) || contains_quantifier(b),
268        Formula::Not(x) => contains_quantifier(x),
269        Formula::True | Formula::False | Formula::Atom { .. } => false,
270    }
271}
272
273fn is_quantifier_free(f: &Formula) -> bool {
274    !contains_quantifier(f)
275}
276
277fn free_vars_subset_of_binding(pool: &ExprPool, f: &Formula, allowed: &BTreeSet<ExprId>) -> bool {
278    free_vars_formula(f, pool).is_subset(allowed)
279}
280
281fn poly_exprs_from_atom(
282    pool: &ExprPool,
283    kind: &PredicateKind,
284    args: &[ExprId],
285    _quant_var: ExprId,
286) -> Result<Vec<ExprId>, CadError> {
287    use PredicateKind::{False, True};
288    if matches!(kind, True | False) {
289        return Ok(vec![]);
290    }
291    if !is_rel(kind) {
292        return Err(CadError::Unsupported(
293            "only relation atoms are supported in CAD QE",
294        ));
295    }
296    if args.len() != 2 {
297        return Err(CadError::Logic(LogicError::UnsupportedExpr(
298            "relational predicate arity must be 2",
299        )));
300    }
301    let lhs = args[0];
302    let rhs = args[1];
303    let lhs_mrhs = poly_diff(pool, lhs, rhs)?;
304    Ok(vec![lhs_mrhs])
305}
306
307fn poly_exprs_from_formula(
308    pool: &ExprPool,
309    f: &Formula,
310    var: ExprId,
311) -> Result<Vec<ExprId>, CadError> {
312    match f {
313        Formula::True | Formula::False => Ok(vec![]),
314        Formula::Atom { kind, args } => poly_exprs_from_atom(pool, kind, args, var),
315        Formula::Not(inner) => {
316            if let Formula::Atom { kind, args } = inner.as_ref() {
317                if is_rel(kind) {
318                    poly_exprs_from_atom(pool, kind, args, var)
319                } else {
320                    Err(CadError::Unsupported(
321                        "NOT is only supported on relation atoms",
322                    ))
323                }
324            } else {
325                Err(CadError::Unsupported(
326                    "`Not` expects a relational atom underneath in this CAD fragment",
327                ))
328            }
329        }
330        Formula::And(a, b) | Formula::Or(a, b) => {
331            let mut v = poly_exprs_from_formula(pool, a, var)?;
332            v.extend(poly_exprs_from_formula(pool, b, var)?);
333            Ok(v)
334        }
335        _ => Err(CadError::Unsupported(
336            "expected quantifier-free Boolean combination of polynomials",
337        )),
338    }
339}
340
341fn eq_polynomials_for_sampling(
342    pool: &ExprPool,
343    f: &Formula,
344    var: ExprId,
345) -> Result<Vec<UniPoly>, CadError> {
346    fn rec(
347        pool: &ExprPool,
348        f: &Formula,
349        var: ExprId,
350        out: &mut Vec<UniPoly>,
351    ) -> Result<(), CadError> {
352        match f {
353            Formula::Atom {
354                kind: PredicateKind::Eq,
355                args,
356            } => {
357                if args.len() != 2 {
358                    return Err(CadError::Logic(LogicError::UnsupportedExpr(
359                        "Eq arity must be 2",
360                    )));
361                }
362                let d = UniPoly::from_symbolic(poly_diff(pool, args[0], args[1])?, var, pool)?;
363                if !d.is_zero() {
364                    out.push(d);
365                }
366                Ok(())
367            }
368            Formula::And(a, b) | Formula::Or(a, b) => {
369                rec(pool, a, var, out)?;
370                rec(pool, b, var, out)
371            }
372            Formula::Not(x) => {
373                if let Formula::Atom {
374                    kind: PredicateKind::Eq,
375                    args,
376                } = x.as_ref()
377                {
378                    // Sampling roots of Eq is irrelevant inside Ne — skip specialized roots.
379                    let _ = args;
380                    Ok(())
381                } else {
382                    Err(CadError::Unsupported(
383                        "NOT over non-Eq unsupported for sampling roots",
384                    ))
385                }
386            }
387            _ => Ok(()),
388        }
389    }
390
391    let mut out = Vec::new();
392    rec(pool, f, var, &mut out)?;
393    Ok(out)
394}
395
396// ---------------------------------------------------------------------------
397// Polynomial utilities
398// ---------------------------------------------------------------------------
399
400fn poly_diff(pool: &ExprPool, lhs: ExprId, rhs: ExprId) -> Result<ExprId, CadError> {
401    let minus_one = pool.integer(-1_i32);
402    let neg_rhs = pool.mul(vec![minus_one, rhs]);
403    Ok(pool.add(vec![lhs, neg_rhs]))
404}
405
406fn combine_algebraic_master(main_var: ExprId, polys: &[UniPoly]) -> UniPoly {
407    let mut nz: Vec<UniPoly> = polys
408        .iter()
409        .filter(|p| !p.is_zero())
410        .map(|p| p.squarefree_part())
411        .collect();
412    if nz.is_empty() {
413        UniPoly::constant(main_var, 1)
414    } else {
415        let mut m = nz.swap_remove(0);
416        for q in nz {
417            m = UniPoly::lcm_poly(&m, &q);
418        }
419        m.squarefree_part()
420    }
421}
422
423fn cauchy_bound(p: &UniPoly) -> rug::Rational {
424    let coeffs = p.coefficients();
425    if coeffs.is_empty() || p.degree() <= 0 {
426        return rug::Rational::from((1_u32, 1_u32));
427    }
428    let n = coeffs.len() - 1;
429    let lead = coeffs[n].clone().abs();
430    if lead.is_zero() {
431        return rug::Rational::from((1_u32, 1_u32));
432    }
433    let mut num = rug::Integer::from(0);
434    for c in coeffs.iter().take(n) {
435        num += c.clone().abs();
436    }
437    let frac = rug::Rational::from((num, lead)) + rug::Rational::from(1);
438    frac + rug::Rational::from(1)
439}
440
441fn iv_midpoint(lo: &rug::Rational, hi: &rug::Rational) -> rug::Rational {
442    (lo.clone() + hi.clone()) / rug::Rational::from((2_u32, 1_u32))
443}
444
445// ---------------------------------------------------------------------------
446// Sign evaluation at a rational sample
447// ---------------------------------------------------------------------------
448
449fn cmp_atom(
450    pool: &ExprPool,
451    kind: &PredicateKind,
452    args: &[ExprId],
453    var: ExprId,
454    pt: &rug::Rational,
455) -> Result<bool, CadError> {
456    use PredicateKind::{Eq, False, Ge, Gt, Le, Lt, Ne, True};
457    if matches!(kind, True) {
458        return Ok(true);
459    }
460    if matches!(kind, False) {
461        return Ok(false);
462    }
463    let diff = UniPoly::from_symbolic(poly_diff(pool, args[0], args[1])?, var, pool)?;
464    let v = diff.eval_rational(pt);
465    let z = rug::Rational::from(0);
466    Ok(match kind {
467        Eq => v == z,
468        Ne => v != z,
469        Lt => v < z,
470        Le => v <= z,
471        Gt => v > z,
472        Ge => v >= z,
473        _ => {
474            return Err(CadError::Unsupported("non-relational predicate in atom"));
475        }
476    })
477}
478
479fn eval_qf_formula(
480    pool: &ExprPool,
481    var: ExprId,
482    f: &Formula,
483    pt: &rug::Rational,
484) -> Result<bool, CadError> {
485    match f {
486        Formula::True => Ok(true),
487        Formula::False => Ok(false),
488        Formula::Atom { kind, args } => cmp_atom(pool, kind, args.as_slice(), var, pt),
489        Formula::And(a, b) => {
490            Ok(eval_qf_formula(pool, var, a, pt)? && eval_qf_formula(pool, var, b, pt)?)
491        }
492        Formula::Or(a, b) => {
493            Ok(eval_qf_formula(pool, var, a, pt)? || eval_qf_formula(pool, var, b, pt)?)
494        }
495        Formula::Not(x) => Ok(!eval_qf_formula(pool, var, x, pt)?),
496        _ => Err(CadError::Unsupported(
497            "quantifiers not allowed inside QF eval",
498        )),
499    }
500}
501
502fn intervals_overlap(a: &RootInterval, b: &RootInterval) -> bool {
503    !(a.hi < b.lo || b.hi < a.lo)
504}
505
506/// Some root of `g` lies in the closure of `iv` (overlap with an isolating interval of `squarefree(g)`).
507fn gcd_interval_shares_root_iv(g: &UniPoly, iv: &RootInterval) -> Result<bool, CadError> {
508    if g.is_zero() || g.degree() <= 0 {
509        return Ok(false);
510    }
511    let sg = g.squarefree_part();
512    let roots_g = real_roots(&sg)?;
513    Ok(roots_g.into_iter().any(|rj| intervals_overlap(iv, &rj)))
514}
515
516fn eval_qf_formula_on_iv(
517    pool: &ExprPool,
518    var: ExprId,
519    phi: &Formula,
520    iv: &RootInterval,
521    focus_sf: &UniPoly,
522) -> Result<bool, CadError> {
523    match phi {
524        Formula::True => Ok(true),
525        Formula::False => Ok(false),
526        Formula::Atom { kind, args } => {
527            use PredicateKind::{Eq, False, Ne, True};
528            if matches!(kind, True) {
529                return Ok(true);
530            }
531            if matches!(kind, False) {
532                return Ok(false);
533            }
534            let d_poly = UniPoly::from_symbolic(poly_diff(pool, args[0], args[1])?, var, pool)?;
535            if matches!(kind, Eq) {
536                let gx = focus_sf.gcd(&d_poly).unwrap_or_else(|| UniPoly::zero(var));
537                return gcd_interval_shares_root_iv(&gx, iv);
538            }
539            if matches!(kind, Ne) {
540                let gx = focus_sf.gcd(&d_poly).unwrap_or_else(|| UniPoly::zero(var));
541                return Ok(!gcd_interval_shares_root_iv(&gx, iv)?);
542            }
543            let mid = iv_midpoint(&iv.lo, &iv.hi);
544            eval_qf_formula(pool, var, phi, &mid)
545        }
546        Formula::And(a, b) => Ok(eval_qf_formula_on_iv(pool, var, a, iv, focus_sf)?
547            && eval_qf_formula_on_iv(pool, var, b, iv, focus_sf)?),
548        Formula::Or(a, b) => Ok(eval_qf_formula_on_iv(pool, var, a, iv, focus_sf)?
549            || eval_qf_formula_on_iv(pool, var, b, iv, focus_sf)?),
550        Formula::Not(x) => Ok(!eval_qf_formula_on_iv(pool, var, x, iv, focus_sf)?),
551        _ => Err(CadError::Unsupported(
552            "unexpected quantifier during CAD sample refinement",
553        )),
554    }
555}
556
557// ---------------------------------------------------------------------------
558// One-quantifier elimination (univariate)
559// ---------------------------------------------------------------------------
560
561fn decide_exists_univariate(
562    pool: &ExprPool,
563    var: ExprId,
564    phi: Formula,
565) -> Result<QeResult, CadError> {
566    let allowed: BTreeSet<ExprId> = [var].into_iter().collect();
567    if !free_vars_subset_of_binding(pool, &phi, &allowed) {
568        return Err(CadError::Unsupported(
569            "quantifier-free body may only reference the bound variable (constants allowed)",
570        ));
571    }
572
573    let poly_exprs = poly_exprs_from_formula(pool, &phi, var)?;
574    let mut polys_uni = Vec::<UniPoly>::new();
575    for e in poly_exprs.iter().copied() {
576        match UniPoly::from_symbolic(e, var, pool) {
577            Ok(p) => {
578                if !p.is_zero() {
579                    polys_uni.push(p.clone());
580                }
581            }
582            Err(err) => return Err(CadError::NotPolynomial(err)),
583        }
584    }
585
586    let mut candidates: BTreeSet<rug::Rational> = BTreeSet::new();
587    let master = combine_algebraic_master(var, &polys_uni);
588    let br = cauchy_bound(&master);
589    let roots_iv = real_roots(&master)?;
590
591    let mut breakpoints: Vec<rug::Rational> = Vec::new();
592    breakpoints.push(-br.clone());
593    for iv in roots_iv.iter() {
594        breakpoints.push(iv.lo.clone());
595        breakpoints.push(iv.hi.clone());
596    }
597    breakpoints.push(br.clone());
598
599    breakpoints.sort();
600    breakpoints.dedup_by(|a, b| *a == *b);
601    // midpoints between consecutive breakpoints strictly inside intervals
602    for w in breakpoints.windows(2) {
603        let lo = &w[0];
604        let hi = &w[1];
605        if lo < hi {
606            candidates.insert(iv_midpoint(lo, hi));
607        }
608    }
609
610    for p in eq_polynomials_for_sampling(pool, &phi, var)? {
611        let sf = p.squarefree_part();
612        let riv = real_roots(&sf)?;
613        for iv in riv {
614            candidates.insert(iv_midpoint(&iv.lo, &iv.hi));
615        }
616    }
617
618    for pt in candidates {
619        if eval_qf_formula(pool, var, &phi, &pt)? {
620            let mut wm = HashMap::new();
621            wm.insert(var, pt.clone());
622            return Ok(QeResult {
623                truth: true,
624                witness: Some(wm),
625            });
626        }
627    }
628
629    // Algebraic equality literals are rarely satisfied exactly at purely rational samples;
630    // use isolating intervals of squarefree Eq-polynomial factors with gcd-based Eq checks.
631    for p_focus in eq_polynomials_for_sampling(pool, &phi, var)? {
632        let sf = p_focus.squarefree_part();
633        if sf.is_zero() {
634            continue;
635        }
636        for iv in real_roots(&sf)? {
637            if eval_qf_formula_on_iv(pool, var, &phi, &iv, &sf)? {
638                let mid = iv_midpoint(&iv.lo, &iv.hi);
639                let mut wm = HashMap::new();
640                wm.insert(var, mid);
641                return Ok(QeResult {
642                    truth: true,
643                    witness: Some(wm),
644                });
645            }
646        }
647    }
648
649    Ok(QeResult {
650        truth: false,
651        witness: None,
652    })
653}
654
655fn decide_closed_qf(pool: &ExprPool, phi: Formula) -> Result<QeResult, CadError> {
656    if !free_vars_formula(&phi, pool).is_empty() {
657        return Err(CadError::Unsupported(
658            "closed formula unexpectedly contains free symbols",
659        ));
660    }
661    let zero = rug::Rational::from(0);
662    let dummy = pool.symbol("__cad_iv_local", Domain::Real);
663    Ok(QeResult {
664        truth: eval_qf_formula(pool, dummy, &phi, &zero)?,
665        witness: None,
666    })
667}
668
669fn decide_formula_inner(pool: &ExprPool, phi: Formula) -> Result<QeResult, CadError> {
670    let phi = simplify_formula_constants(nnf_formula(phi));
671    if is_quantifier_free(&phi) {
672        return decide_closed_qf(pool, phi);
673    }
674    match phi {
675        Formula::Exists { var, body } => {
676            if contains_quantifier(&body) {
677                return Err(CadError::Unsupported(
678                    "nested quantifiers are not implemented",
679                ));
680            }
681            decide_exists_univariate(pool, var, *body)
682        }
683        Formula::Forall { var, body } => {
684            if contains_quantifier(&body) {
685                return Err(CadError::Unsupported(
686                    "nested quantifiers are not implemented",
687                ));
688            }
689            let neg_body = nnf_formula(Formula::Not(body));
690            let inner = decide_exists_univariate(pool, var, neg_body)?;
691            Ok(QeResult {
692                truth: !inner.truth,
693                witness: None,
694            })
695        }
696        Formula::True => Ok(QeResult {
697            truth: true,
698            witness: None,
699        }),
700        Formula::False => Ok(QeResult {
701            truth: false,
702            witness: None,
703        }),
704        _ => Err(CadError::Unsupported(
705            "sentence must begin with forall/exists after quantifiers are outermost",
706        )),
707    }
708}
709
710/// Decide a closed first-order polynomial sentence (`forall` / `exists` prefix,
711/// optionally empty), built from Boolean combinations of polynomial relations.
712pub fn decide(formula: &Formula, pool: &ExprPool) -> Result<QeResult, CadError> {
713    decide_formula_inner(pool, formula.clone())
714}
715
716/// Decide from a predicate / quantified [`ExprId`], via [`formula_from_expr`].
717pub fn decide_expr(expr: ExprId, pool: &ExprPool) -> Result<QeResult, CadError> {
718    let fm = formula_from_expr(expr, pool)?;
719    decide(&fm, pool)
720}
721
722/// Brown-style projection polynomials for elimination of `elim_var`.
723///
724/// The returned polynomials are canonically rewritten with [`poly_normal`] in the
725/// union of remaining variables (+ constants only).
726///
727/// Projection set:
728/// resultant(`f`,`∂ f/ ∂ elim`,`elim`), all distinct pairwise resultants (`f`,`g`).
729pub fn cad_project(
730    polynomials: &[ExprId],
731    elim_var: ExprId,
732    pool: &ExprPool,
733) -> Result<Vec<ExprId>, CadError> {
734    if polynomials.is_empty() {
735        return Ok(Vec::new());
736    }
737    let mut all_vars = BTreeSet::new();
738    all_vars.insert(elim_var);
739    for &p in polynomials {
740        all_vars.extend(resultant::collect_free_vars(p, pool));
741    }
742    let vars_no_elim: Vec<ExprId> = all_vars
743        .iter()
744        .copied()
745        .filter(|&v| v != elim_var)
746        .collect();
747
748    let mut uniq: Vec<ExprId> = Vec::new();
749    let mut seen: BTreeSet<ExprId> = BTreeSet::new();
750
751    for i in 0..polynomials.len() {
752        let f_expr = polynomials[i];
753        let df = diff(f_expr, elim_var, pool)?.value;
754
755        let is_zero_f = UniPoly::from_symbolic(f_expr, elim_var, pool)
756            .map(|u| u.is_zero())
757            .unwrap_or(false);
758        let is_zero_df = UniPoly::from_symbolic(df, elim_var, pool)
759            .map(|u| u.is_zero())
760            .unwrap_or(true);
761
762        // Discriminant / projection coefficient via resultant with ∂f.
763        if !is_zero_f && !is_zero_df {
764            let rp = resultant(f_expr, df, elim_var, pool)?.value;
765            if seen.insert(rp) {
766                uniq.push(rp);
767            }
768        }
769
770        // Pairwise resultants don't require ∂f to be non-zero (Brown projection).
771        for &g_expr in polynomials.iter().skip(i + 1) {
772            let is_zero_g = UniPoly::from_symbolic(g_expr, elim_var, pool)
773                .map(|u| u.is_zero())
774                .unwrap_or(false);
775            if is_zero_f || is_zero_g {
776                continue;
777            }
778            let r = resultant(f_expr, g_expr, elim_var, pool)?.value;
779            if seen.insert(r) {
780                uniq.push(r);
781            }
782        }
783    }
784
785    let mut normed = Vec::<ExprId>::new();
786    for e in uniq {
787        let simplified = if vars_no_elim.is_empty() {
788            e
789        } else {
790            poly_normal(e, vars_no_elim.clone(), pool)?
791        };
792        normed.push(simplified);
793    }
794
795    normed.sort_unstable();
796    normed.dedup();
797    Ok(normed)
798}
799
800/// CAD lifting along `main_var`: isolate real roots of a squarefree amalgam built
801/// from projections of the listed polynomial expressions when viewed in `main_var`.
802pub fn cad_lift(
803    polynomials: &[ExprId],
804    main_var: ExprId,
805    pool: &ExprPool,
806) -> Result<Vec<RootInterval>, CadError> {
807    let mut polys_uni = Vec::new();
808    for &e in polynomials {
809        match UniPoly::from_symbolic(e, main_var, pool) {
810            Ok(u) => {
811                if !u.is_zero() {
812                    polys_uni.push(u);
813                }
814            }
815            Err(e) => return Err(CadError::NotPolynomial(e)),
816        }
817    }
818    let m = combine_algebraic_master(main_var, &polys_uni);
819    Ok(real_roots(&m)?)
820}
821
822// ---------------------------------------------------------------------------
823// Tests
824// ---------------------------------------------------------------------------
825
826#[cfg(test)]
827mod tests {
828    use super::*;
829    use crate::kernel::Domain;
830
831    #[test]
832    fn forall_x_squared_plus_one_positive() {
833        let p = ExprPool::new();
834        let x = p.symbol("x", Domain::Real);
835        let one = p.integer(1_i32);
836        let x_sq = p.pow(x, p.integer(2_i32));
837        let body = p.pred_gt(p.add(vec![x_sq, one]), p.integer(0_i32));
838
839        let f = Formula::Forall {
840            var: x,
841            body: Box::new(formula_from_expr(body, &p).unwrap()),
842        };
843        let r = decide(&f, &p).unwrap();
844        assert!(r.truth);
845        assert!(r.witness.is_none());
846    }
847
848    #[test]
849    fn exists_roots_x_squared_minus_two() {
850        let p = ExprPool::new();
851        let x = p.symbol("x", Domain::Real);
852        let two = p.integer(2_i32);
853        let xs = p.pow(x, p.integer(2_i32));
854        let body = p.pred_eq(xs, two);
855        let f = Formula::Exists {
856            var: x,
857            body: Box::new(formula_from_expr(body, &p).unwrap()),
858        };
859        let r = decide(&f, &p).unwrap();
860        assert!(r.truth);
861        assert!(r.witness.is_some());
862    }
863
864    #[test]
865    fn cad_lift_univariate_quadratic() {
866        let p = ExprPool::new();
867        let x = p.symbol("x", Domain::Real);
868        let xs = p.add(vec![p.pow(x, p.integer(2_i32)), p.integer(-2_i32)]);
869        let ivs = cad_lift(&[xs], x, &p).unwrap();
870        assert_eq!(ivs.len(), 2);
871        assert!(ivs.iter().all(|iv| iv.lo <= iv.hi));
872    }
873
874    #[test]
875    fn cad_project_circle_eliminates_y() {
876        let p = ExprPool::new();
877        let x = p.symbol("x", Domain::Real);
878        let y = p.symbol("y", Domain::Real);
879        let circle = p.add(vec![
880            p.pow(x, p.integer(2_i32)),
881            p.pow(y, p.integer(2_i32)),
882            p.integer(-1_i32),
883        ]);
884        let line = p.add(vec![y, pool_neg_x(&p, x)]); // y - x
885        let pr = cad_project(&[circle, line], y, &p).unwrap();
886        assert!(!pr.is_empty());
887    }
888
889    fn pool_neg_x(pool: &ExprPool, x: ExprId) -> ExprId {
890        pool.mul(vec![pool.integer(-1_i32), x])
891    }
892
893    #[test]
894    fn unipoly_eval_rational_zero() {
895        let p = ExprPool::new();
896        let x = p.symbol("x", Domain::Real);
897        let qp = UniPoly::from_symbolic(p.add(vec![x, p.integer(2_i32)]), x, &p).unwrap();
898        let z = qp.eval_rational(&rug::Rational::from(-2));
899        assert_eq!(z, 0);
900    }
901}