1use crate::diff::{diff, DiffError};
37use crate::errors::AlkahestError;
38use crate::kernel::expr::PredicateKind;
39use crate::kernel::subs;
40use crate::kernel::Domain;
41use crate::kernel::{ExprId, ExprPool};
42use crate::logic::{formula_from_expr, Formula, LogicError};
43use crate::poly::resultant::{self, resultant};
44use crate::poly::{
45 poly_normal, real_roots, ConversionError, RealRootError, ResultantError, RootInterval, UniPoly,
46};
47use std::collections::{BTreeSet, HashMap};
48use std::fmt;
49
50#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum CadError {
57 NotPolynomial(ConversionError),
58 Diff(DiffError),
59 Resultant(ResultantError),
60 RealRoots(RealRootError),
61 Logic(LogicError),
62 Unsupported(&'static str),
64}
65
66impl fmt::Display for CadError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 CadError::NotPolynomial(e) => write!(f, "{e}"),
70 CadError::Diff(e) => write!(f, "{e}"),
71 CadError::Resultant(e) => write!(f, "{e}"),
72 CadError::RealRoots(e) => write!(f, "{e}"),
73 CadError::Logic(e) => write!(f, "{e}"),
74 CadError::Unsupported(s) => write!(f, "CAD: {s}"),
75 }
76 }
77}
78
79impl std::error::Error for CadError {}
80
81impl AlkahestError for CadError {
82 fn code(&self) -> &'static str {
83 match self {
84 CadError::NotPolynomial(e) => e.code(),
85 CadError::Diff(e) => e.code(),
86 CadError::Resultant(e) => e.code(),
87 CadError::RealRoots(e) => e.code(),
88 CadError::Logic(e) => e.code(),
89 CadError::Unsupported(_) => "E-CAD-001",
90 }
91 }
92
93 fn remediation(&self) -> Option<&'static str> {
94 match self {
95 CadError::NotPolynomial(e) => e.remediation(),
96 CadError::Diff(e) => e.remediation(),
97 CadError::Resultant(e) => e.remediation(),
98 CadError::RealRoots(e) => e.remediation(),
99 CadError::Logic(e) => e.remediation(),
100 CadError::Unsupported(_) => Some(
101 "use a purely polynomial constraint in one or two real variables with at most \
102 a 2-quantifier prefix; deeper nesting and full multivariate QE are incremental",
103 ),
104 }
105 }
106}
107
108impl From<ConversionError> for CadError {
109 fn from(value: ConversionError) -> Self {
110 CadError::NotPolynomial(value)
111 }
112}
113
114impl From<DiffError> for CadError {
115 fn from(value: DiffError) -> Self {
116 CadError::Diff(value)
117 }
118}
119
120impl From<ResultantError> for CadError {
121 fn from(value: ResultantError) -> Self {
122 CadError::Resultant(value)
123 }
124}
125
126impl From<RealRootError> for CadError {
127 fn from(value: RealRootError) -> Self {
128 CadError::RealRoots(value)
129 }
130}
131
132impl From<LogicError> for CadError {
133 fn from(value: LogicError) -> Self {
134 CadError::Logic(value)
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct QeResult {
141 pub truth: bool,
142 pub witness: Option<HashMap<ExprId, rug::Rational>>,
143}
144
145fn dual_kind(kind: PredicateKind) -> PredicateKind {
150 use PredicateKind::{Eq, Ge, Gt, Le, Lt, Ne};
151 match kind {
152 Lt => Ge,
153 Le => Gt,
154 Gt => Le,
155 Ge => Lt,
156 Eq => Ne,
157 Ne => Eq,
158 other => other,
159 }
160}
161
162fn is_rel(kind: &PredicateKind) -> bool {
163 use PredicateKind::*;
164 matches!(kind, Lt | Le | Gt | Ge | Eq | Ne)
165}
166
167fn simplify_formula_constants(f: Formula) -> Formula {
168 match f {
169 Formula::And(a, b) => {
170 let la = simplify_formula_constants(*a);
171 let lb = simplify_formula_constants(*b);
172 match (&la, &lb) {
173 (Formula::False, _) | (_, Formula::False) => Formula::False,
174 (Formula::True, x) => x.clone(),
175 (x, Formula::True) => x.clone(),
176 _ => Formula::and(la, lb),
177 }
178 }
179 Formula::Or(a, b) => {
180 let la = simplify_formula_constants(*a);
181 let lb = simplify_formula_constants(*b);
182 match (&la, &lb) {
183 (Formula::True, _) | (_, Formula::True) => Formula::True,
184 (Formula::False, x) => x.clone(),
185 (x, Formula::False) => x.clone(),
186 _ => Formula::or(la, lb),
187 }
188 }
189 Formula::Not(x) => Formula::not(simplify_formula_constants(*x)),
190 Formula::Forall { var, body } => Formula::Forall {
191 var,
192 body: Box::new(simplify_formula_constants(*body)),
193 },
194 Formula::Exists { var, body } => Formula::Exists {
195 var,
196 body: Box::new(simplify_formula_constants(*body)),
197 },
198 other => other,
199 }
200}
201
202fn nnf_formula(f: Formula) -> Formula {
203 match f {
204 Formula::Not(inner) => match *inner {
205 Formula::True => Formula::False,
206 Formula::False => Formula::True,
207 Formula::Not(g) => nnf_formula(*g),
208 Formula::And(a, b) => nnf_formula(Formula::or(Formula::not(*a), Formula::not(*b))),
209 Formula::Or(a, b) => nnf_formula(Formula::and(Formula::not(*a), Formula::not(*b))),
210 Formula::Forall { var, body } => nnf_formula(Formula::Exists {
211 var,
212 body: Box::new(Formula::not(*body)),
213 }),
214 Formula::Exists { var, body } => nnf_formula(Formula::Forall {
215 var,
216 body: Box::new(Formula::not(*body)),
217 }),
218 Formula::Atom {
219 kind: PredicateKind::True,
220 ..
221 } => Formula::False,
222 Formula::Atom {
223 kind: PredicateKind::False,
224 ..
225 } => Formula::True,
226 Formula::Atom { kind, args } if is_rel(&kind) => Formula::Atom {
227 kind: dual_kind(kind),
228 args,
229 },
230 inner => Formula::Not(Box::new(inner)),
231 },
232 Formula::And(a, b) => Formula::and(nnf_formula(*a), nnf_formula(*b)),
233 Formula::Or(a, b) => Formula::or(nnf_formula(*a), nnf_formula(*b)),
234 Formula::Forall { var, body } => Formula::Forall {
235 var,
236 body: Box::new(nnf_formula(*body)),
237 },
238 Formula::Exists { var, body } => Formula::Exists {
239 var,
240 body: Box::new(nnf_formula(*body)),
241 },
242 other => other,
243 }
244}
245
246fn insert_formula_vars(pool: &ExprPool, expr: ExprId, out: &mut BTreeSet<ExprId>) {
251 for v in resultant::collect_free_vars(expr, pool) {
252 out.insert(v);
253 }
254}
255
256fn free_vars_formula(f: &Formula, pool: &ExprPool) -> BTreeSet<ExprId> {
257 match f {
258 Formula::Atom { args, .. } => {
259 let mut s = BTreeSet::new();
260 for &a in args {
261 insert_formula_vars(pool, a, &mut s);
262 }
263 s
264 }
265 Formula::And(a, b) | Formula::Or(a, b) => {
266 let mut s = free_vars_formula(a, pool);
267 s.extend(free_vars_formula(b, pool));
268 s
269 }
270 Formula::Not(x) => free_vars_formula(x, pool),
271 Formula::Exists { var, body } => {
272 let mut s = free_vars_formula(body, pool);
273 s.insert(*var);
274 s
275 }
276 Formula::Forall { var, body } => {
277 let mut s = free_vars_formula(body, pool);
278 s.insert(*var);
279 s
280 }
281 Formula::True | Formula::False => BTreeSet::new(),
282 }
283}
284
285fn contains_quantifier(f: &Formula) -> bool {
286 match f {
287 Formula::Exists { .. } | Formula::Forall { .. } => true,
288 Formula::And(a, b) | Formula::Or(a, b) => contains_quantifier(a) || contains_quantifier(b),
289 Formula::Not(x) => contains_quantifier(x),
290 Formula::True | Formula::False | Formula::Atom { .. } => false,
291 }
292}
293
294fn is_quantifier_free(f: &Formula) -> bool {
295 !contains_quantifier(f)
296}
297
298fn free_vars_subset_of_binding(pool: &ExprPool, f: &Formula, allowed: &BTreeSet<ExprId>) -> bool {
299 free_vars_formula(f, pool).is_subset(allowed)
300}
301
302fn poly_exprs_from_atom(
303 pool: &ExprPool,
304 kind: &PredicateKind,
305 args: &[ExprId],
306 _quant_var: ExprId,
307) -> Result<Vec<ExprId>, CadError> {
308 use PredicateKind::{False, True};
309 if matches!(kind, True | False) {
310 return Ok(vec![]);
311 }
312 if !is_rel(kind) {
313 return Err(CadError::Unsupported(
314 "only relation atoms are supported in CAD QE",
315 ));
316 }
317 if args.len() != 2 {
318 return Err(CadError::Logic(LogicError::UnsupportedExpr(
319 "relational predicate arity must be 2",
320 )));
321 }
322 let lhs = args[0];
323 let rhs = args[1];
324 let lhs_mrhs = poly_diff(pool, lhs, rhs)?;
325 Ok(vec![lhs_mrhs])
326}
327
328fn poly_exprs_from_formula(
329 pool: &ExprPool,
330 f: &Formula,
331 var: ExprId,
332) -> Result<Vec<ExprId>, CadError> {
333 match f {
334 Formula::True | Formula::False => Ok(vec![]),
335 Formula::Atom { kind, args } => poly_exprs_from_atom(pool, kind, args, var),
336 Formula::Not(inner) => {
337 if let Formula::Atom { kind, args } = inner.as_ref() {
338 if is_rel(kind) {
339 poly_exprs_from_atom(pool, kind, args, var)
340 } else {
341 Err(CadError::Unsupported(
342 "NOT is only supported on relation atoms",
343 ))
344 }
345 } else {
346 Err(CadError::Unsupported(
347 "`Not` expects a relational atom underneath in this CAD fragment",
348 ))
349 }
350 }
351 Formula::And(a, b) | Formula::Or(a, b) => {
352 let mut v = poly_exprs_from_formula(pool, a, var)?;
353 v.extend(poly_exprs_from_formula(pool, b, var)?);
354 Ok(v)
355 }
356 _ => Err(CadError::Unsupported(
357 "expected quantifier-free Boolean combination of polynomials",
358 )),
359 }
360}
361
362fn eq_polynomials_for_sampling(
363 pool: &ExprPool,
364 f: &Formula,
365 var: ExprId,
366) -> Result<Vec<UniPoly>, CadError> {
367 fn rec(
368 pool: &ExprPool,
369 f: &Formula,
370 var: ExprId,
371 out: &mut Vec<UniPoly>,
372 ) -> Result<(), CadError> {
373 match f {
374 Formula::Atom {
386 kind: PredicateKind::Eq | PredicateKind::Le | PredicateKind::Ge,
387 args,
388 } => {
389 if args.len() != 2 {
390 return Err(CadError::Logic(LogicError::UnsupportedExpr(
391 "comparison arity must be 2",
392 )));
393 }
394 let d = UniPoly::from_symbolic_clear_denoms(
395 poly_diff(pool, args[0], args[1])?,
396 var,
397 pool,
398 )?;
399 if !d.is_zero() {
400 out.push(d);
401 }
402 Ok(())
403 }
404 Formula::And(a, b) | Formula::Or(a, b) => {
405 rec(pool, a, var, out)?;
406 rec(pool, b, var, out)
407 }
408 Formula::Not(x) => {
409 if let Formula::Atom {
410 kind: PredicateKind::Eq | PredicateKind::Le | PredicateKind::Ge,
411 args,
412 } = x.as_ref()
413 {
414 let _ = args;
418 Ok(())
419 } else {
420 Err(CadError::Unsupported(
421 "NOT over strict comparison unsupported for sampling roots",
422 ))
423 }
424 }
425 _ => Ok(()),
426 }
427 }
428
429 let mut out = Vec::new();
430 rec(pool, f, var, &mut out)?;
431 Ok(out)
432}
433
434fn poly_diff(pool: &ExprPool, lhs: ExprId, rhs: ExprId) -> Result<ExprId, CadError> {
439 let minus_one = pool.integer(-1_i32);
440 let neg_rhs = pool.mul(vec![minus_one, rhs]);
441 Ok(pool.add(vec![lhs, neg_rhs]))
442}
443
444fn combine_algebraic_master(main_var: ExprId, polys: &[UniPoly]) -> UniPoly {
445 let mut nz: Vec<UniPoly> = polys
446 .iter()
447 .filter(|p| !p.is_zero())
448 .map(|p| p.squarefree_part())
449 .collect();
450 if nz.is_empty() {
451 UniPoly::constant(main_var, 1)
452 } else {
453 let mut m = nz.swap_remove(0);
454 for q in nz {
455 m = UniPoly::lcm_poly(&m, &q);
456 }
457 m.squarefree_part()
458 }
459}
460
461const ISOLATION_REFINEMENTS: u32 = 60;
466
467fn refine_isolating(master: &UniPoly, iv: &RootInterval) -> (rug::Rational, rug::Rational) {
480 let mut lo = iv.lo.clone();
481 let mut hi = iv.hi.clone();
482
483 let v_lo = master.eval_rational(&lo);
490 let v_hi = master.eval_rational(&hi);
491 let (anchor_positive, anchor_is_lo) = if v_lo != 0 {
492 (v_lo > 0, true)
493 } else if v_hi != 0 {
494 (v_hi > 0, false)
495 } else {
496 return (lo, hi);
499 };
500
501 for _ in 0..ISOLATION_REFINEMENTS {
502 let mid = iv_midpoint(&lo, &hi);
503 let v = master.eval_rational(&mid);
504 if v == 0 {
505 return (mid.clone(), mid);
506 }
507 if ((v > 0) == anchor_positive) == anchor_is_lo {
510 lo = mid;
511 } else {
512 hi = mid;
513 }
514 }
515 (lo, hi)
516}
517
518fn cauchy_bound(p: &UniPoly) -> rug::Rational {
519 let coeffs = p.coefficients();
520 if coeffs.is_empty() || p.degree() <= 0 {
521 return rug::Rational::from((1_u32, 1_u32));
522 }
523 let n = coeffs.len() - 1;
524 let lead = coeffs[n].clone().abs();
525 if lead.is_zero() {
526 return rug::Rational::from((1_u32, 1_u32));
527 }
528 let mut num = rug::Integer::from(0);
529 for c in coeffs.iter().take(n) {
530 num += c.clone().abs();
531 }
532 let frac = rug::Rational::from((num, lead)) + rug::Rational::from(1);
533 frac + rug::Rational::from(1)
534}
535
536fn iv_midpoint(lo: &rug::Rational, hi: &rug::Rational) -> rug::Rational {
537 (lo.clone() + hi.clone()) / rug::Rational::from((2_u32, 1_u32))
538}
539
540fn cmp_atom(
545 pool: &ExprPool,
546 kind: &PredicateKind,
547 args: &[ExprId],
548 var: ExprId,
549 pt: &rug::Rational,
550) -> Result<bool, CadError> {
551 use PredicateKind::{Eq, False, Ge, Gt, Le, Lt, Ne, True};
552 if matches!(kind, True) {
553 return Ok(true);
554 }
555 if matches!(kind, False) {
556 return Ok(false);
557 }
558 let diff = UniPoly::from_symbolic_clear_denoms(poly_diff(pool, args[0], args[1])?, var, pool)?;
559 let v = diff.eval_rational(pt);
560 let z = rug::Rational::from(0);
561 Ok(match kind {
562 Eq => v == z,
563 Ne => v != z,
564 Lt => v < z,
565 Le => v <= z,
566 Gt => v > z,
567 Ge => v >= z,
568 _ => {
569 return Err(CadError::Unsupported("non-relational predicate in atom"));
570 }
571 })
572}
573
574fn eval_qf_formula(
575 pool: &ExprPool,
576 var: ExprId,
577 f: &Formula,
578 pt: &rug::Rational,
579) -> Result<bool, CadError> {
580 match f {
581 Formula::True => Ok(true),
582 Formula::False => Ok(false),
583 Formula::Atom { kind, args } => cmp_atom(pool, kind, args.as_slice(), var, pt),
584 Formula::And(a, b) => {
585 Ok(eval_qf_formula(pool, var, a, pt)? && eval_qf_formula(pool, var, b, pt)?)
586 }
587 Formula::Or(a, b) => {
588 Ok(eval_qf_formula(pool, var, a, pt)? || eval_qf_formula(pool, var, b, pt)?)
589 }
590 Formula::Not(x) => Ok(!eval_qf_formula(pool, var, x, pt)?),
591 _ => Err(CadError::Unsupported(
592 "quantifiers not allowed inside QF eval",
593 )),
594 }
595}
596
597fn intervals_overlap(a: &RootInterval, b: &RootInterval) -> bool {
598 !(a.hi < b.lo || b.hi < a.lo)
599}
600
601fn gcd_interval_shares_root_iv(g: &UniPoly, iv: &RootInterval) -> Result<bool, CadError> {
603 if g.is_zero() || g.degree() <= 0 {
604 return Ok(false);
605 }
606 let sg = g.squarefree_part();
607 let roots_g = real_roots(&sg)?;
608 Ok(roots_g.into_iter().any(|rj| intervals_overlap(iv, &rj)))
609}
610
611fn eval_qf_formula_on_iv(
612 pool: &ExprPool,
613 var: ExprId,
614 phi: &Formula,
615 iv: &RootInterval,
616 focus_sf: &UniPoly,
617) -> Result<bool, CadError> {
618 match phi {
619 Formula::True => Ok(true),
620 Formula::False => Ok(false),
621 Formula::Atom { kind, args } => {
622 use PredicateKind::{Eq, False, Ne, True};
623 if matches!(kind, True) {
624 return Ok(true);
625 }
626 if matches!(kind, False) {
627 return Ok(false);
628 }
629 let d_poly =
630 UniPoly::from_symbolic_clear_denoms(poly_diff(pool, args[0], args[1])?, var, pool)?;
631 if matches!(kind, Eq) {
632 let gx = focus_sf.gcd(&d_poly).unwrap_or_else(|| UniPoly::zero(var));
633 return gcd_interval_shares_root_iv(&gx, iv);
634 }
635 if matches!(kind, Ne) {
636 let gx = focus_sf.gcd(&d_poly).unwrap_or_else(|| UniPoly::zero(var));
637 return Ok(!gcd_interval_shares_root_iv(&gx, iv)?);
638 }
639 let mid = iv_midpoint(&iv.lo, &iv.hi);
640 eval_qf_formula(pool, var, phi, &mid)
641 }
642 Formula::And(a, b) => Ok(eval_qf_formula_on_iv(pool, var, a, iv, focus_sf)?
643 && eval_qf_formula_on_iv(pool, var, b, iv, focus_sf)?),
644 Formula::Or(a, b) => Ok(eval_qf_formula_on_iv(pool, var, a, iv, focus_sf)?
645 || eval_qf_formula_on_iv(pool, var, b, iv, focus_sf)?),
646 Formula::Not(x) => Ok(!eval_qf_formula_on_iv(pool, var, x, iv, focus_sf)?),
647 _ => Err(CadError::Unsupported(
648 "unexpected quantifier during CAD sample refinement",
649 )),
650 }
651}
652
653fn decide_exists_univariate(
658 pool: &ExprPool,
659 var: ExprId,
660 phi: Formula,
661) -> Result<QeResult, CadError> {
662 let allowed: BTreeSet<ExprId> = [var].into_iter().collect();
663 if !free_vars_subset_of_binding(pool, &phi, &allowed) {
664 return Err(CadError::Unsupported(
665 "quantifier-free body may only reference the bound variable (constants allowed)",
666 ));
667 }
668
669 let poly_exprs = poly_exprs_from_formula(pool, &phi, var)?;
670 let mut polys_uni = Vec::<UniPoly>::new();
671 for e in poly_exprs.iter().copied() {
672 match UniPoly::from_symbolic_clear_denoms(e, var, pool) {
673 Ok(p) => {
674 if !p.is_zero() {
675 polys_uni.push(p.clone());
676 }
677 }
678 Err(err) => return Err(CadError::NotPolynomial(err)),
679 }
680 }
681
682 let mut candidates: BTreeSet<rug::Rational> = BTreeSet::new();
683 let master = combine_algebraic_master(var, &polys_uni);
684 let br = cauchy_bound(&master);
685 let roots_iv = real_roots(&master)?;
686
687 let mut breakpoints: Vec<rug::Rational> = Vec::new();
688 breakpoints.push(-br.clone());
689 for iv in roots_iv.iter() {
690 breakpoints.push(iv.lo.clone());
691 breakpoints.push(iv.hi.clone());
692 let (rlo, rhi) = refine_isolating(&master, iv);
701 breakpoints.push(rlo);
702 breakpoints.push(rhi);
703 }
704 breakpoints.push(br.clone());
705
706 breakpoints.sort();
707 breakpoints.dedup_by(|a, b| *a == *b);
708 for b in &breakpoints {
722 candidates.insert(b.clone());
723 }
724 for w in breakpoints.windows(2) {
725 let lo = &w[0];
726 let hi = &w[1];
727 if lo < hi {
728 candidates.insert(iv_midpoint(lo, hi));
729 }
730 }
731
732 for p in eq_polynomials_for_sampling(pool, &phi, var)? {
733 let sf = p.squarefree_part();
734 let riv = real_roots(&sf)?;
735 for iv in riv {
736 candidates.insert(iv_midpoint(&iv.lo, &iv.hi));
737 }
738 }
739
740 for pt in candidates {
741 if eval_qf_formula(pool, var, &phi, &pt)? {
742 let mut wm = HashMap::new();
743 wm.insert(var, pt.clone());
744 return Ok(QeResult {
745 truth: true,
746 witness: Some(wm),
747 });
748 }
749 }
750
751 let mut untested_algebraic_boundary = false;
754 for p_focus in eq_polynomials_for_sampling(pool, &phi, var)? {
755 let sf = p_focus.squarefree_part();
756 if sf.is_zero() {
757 continue;
758 }
759 for iv in real_roots(&sf)? {
760 if iv.lo != iv.hi {
761 untested_algebraic_boundary = true;
762 }
763 if eval_qf_formula_on_iv(pool, var, &phi, &iv, &sf)? {
764 let mid = iv_midpoint(&iv.lo, &iv.hi);
771 let witness = if eval_qf_formula(pool, var, &phi, &mid)? {
772 let mut wm = HashMap::new();
773 wm.insert(var, mid);
774 Some(wm)
775 } else {
776 None
777 };
778 return Ok(QeResult {
779 truth: true,
780 witness,
781 });
782 }
783 }
784 }
785
786 if untested_algebraic_boundary && body_has_boundary_atom(&phi) {
794 return Err(CadError::Unsupported(ALGEBRAIC_BOUNDARY_MSG));
795 }
796
797 Ok(QeResult {
798 truth: false,
799 witness: None,
800 })
801}
802
803const ALGEBRAIC_BOUNDARY_MSG: &str = "the formula has a non-strict atom (=, <=, >=) whose only \
804 possible solutions are roots of an irrational algebraic number; deciding it needs \
805 algebraic-number CAD lifting (full CAD). Refusing rather than reporting an unsatisfiability \
806 that was never checked at that point";
807
808fn decide_closed_qf(pool: &ExprPool, phi: Formula) -> Result<QeResult, CadError> {
809 if !free_vars_formula(&phi, pool).is_empty() {
810 return Err(CadError::Unsupported(
811 "closed formula unexpectedly contains free symbols",
812 ));
813 }
814 let zero = rug::Rational::from(0);
815 let dummy = pool.symbol("__cad_iv_local", Domain::Real);
816 Ok(QeResult {
817 truth: eval_qf_formula(pool, dummy, &phi, &zero)?,
818 witness: None,
819 })
820}
821
822#[derive(Debug, Clone, Copy, PartialEq, Eq)]
824enum Quant {
825 Exists,
826 Forall,
827}
828
829fn decide_formula_inner(pool: &ExprPool, phi: Formula) -> Result<QeResult, CadError> {
830 let phi = simplify_formula_constants(nnf_formula(phi));
831 if is_quantifier_free(&phi) {
832 return decide_closed_qf(pool, phi);
833 }
834 match phi {
835 Formula::Exists { var, body } => decide_quantified(pool, Quant::Exists, var, *body),
836 Formula::Forall { var, body } => decide_quantified(pool, Quant::Forall, var, *body),
837 Formula::True => Ok(QeResult {
838 truth: true,
839 witness: None,
840 }),
841 Formula::False => Ok(QeResult {
842 truth: false,
843 witness: None,
844 }),
845 _ => Err(CadError::Unsupported(
846 "sentence must begin with forall/exists after quantifiers are outermost",
847 )),
848 }
849}
850
851fn decide_quantified(
856 pool: &ExprPool,
857 outer_q: Quant,
858 outer_var: ExprId,
859 body: Formula,
860) -> Result<QeResult, CadError> {
861 if !contains_quantifier(&body) {
862 return decide_one_var(pool, outer_q, outer_var, body);
863 }
864 match body {
865 Formula::Exists {
866 var: inner_var,
867 body: inner_body,
868 } if !contains_quantifier(&inner_body) => decide_two_var(
869 pool,
870 outer_q,
871 outer_var,
872 Quant::Exists,
873 inner_var,
874 *inner_body,
875 ),
876 Formula::Forall {
877 var: inner_var,
878 body: inner_body,
879 } if !contains_quantifier(&inner_body) => decide_two_var(
880 pool,
881 outer_q,
882 outer_var,
883 Quant::Forall,
884 inner_var,
885 *inner_body,
886 ),
887 _ => Err(CadError::Unsupported(
888 "quantifier prefixes of length > 2 are not implemented",
889 )),
890 }
891}
892
893fn decide_one_var(
894 pool: &ExprPool,
895 q: Quant,
896 var: ExprId,
897 body: Formula,
898) -> Result<QeResult, CadError> {
899 match q {
900 Quant::Exists => decide_exists_univariate(pool, var, body),
901 Quant::Forall => {
902 let neg_body = nnf_formula(Formula::Not(Box::new(body)));
903 let inner = decide_exists_univariate(pool, var, neg_body)?;
904 Ok(QeResult {
905 truth: !inner.truth,
906 witness: None,
907 })
908 }
909 }
910}
911
912fn decide_two_var(
922 pool: &ExprPool,
923 outer_q: Quant,
924 outer_var: ExprId,
925 inner_q: Quant,
926 inner_var: ExprId,
927 body: Formula,
928) -> Result<QeResult, CadError> {
929 match (outer_q, inner_q) {
930 (Quant::Exists, Quant::Exists) => decide_exists_exists(pool, outer_var, inner_var, body),
931 (Quant::Exists, Quant::Forall) => decide_exists_forall(pool, outer_var, inner_var, body),
932 (Quant::Forall, Quant::Forall) => {
933 let neg = nnf_formula(Formula::Not(Box::new(body)));
935 let inner = decide_exists_exists(pool, outer_var, inner_var, neg)?;
936 Ok(QeResult {
937 truth: !inner.truth,
938 witness: None,
939 })
940 }
941 (Quant::Forall, Quant::Exists) => {
942 let neg = nnf_formula(Formula::Not(Box::new(body)));
944 let inner = decide_exists_forall(pool, outer_var, inner_var, neg)?;
945 Ok(QeResult {
946 truth: !inner.truth,
947 witness: None,
948 })
949 }
950 }
951}
952
953fn body_has_boundary_atom(f: &Formula) -> bool {
963 match f {
964 Formula::Atom { kind, .. } => matches!(
965 kind,
966 PredicateKind::Eq | PredicateKind::Le | PredicateKind::Ge
967 ),
968 Formula::And(a, b) | Formula::Or(a, b) => {
969 body_has_boundary_atom(a) || body_has_boundary_atom(b)
970 }
971 Formula::Not(x) => body_has_boundary_atom(x),
972 _ => false,
973 }
974}
975
976fn body_has_nonstrict_atom(f: &Formula) -> bool {
996 match f {
997 Formula::Atom { kind, .. } => matches!(
998 kind,
999 PredicateKind::Eq | PredicateKind::Ne | PredicateKind::Le | PredicateKind::Ge
1000 ),
1001 Formula::And(a, b) | Formula::Or(a, b) => {
1002 body_has_nonstrict_atom(a) || body_has_nonstrict_atom(b)
1003 }
1004 Formula::Not(x) => body_has_nonstrict_atom(x),
1005 _ => false,
1006 }
1007}
1008
1009fn rational_to_expr(pool: &ExprPool, r: &rug::Rational) -> ExprId {
1011 if *r.denom() == 1_u32 {
1012 pool.integer(r.numer().clone())
1013 } else {
1014 pool.rational(r.numer().clone(), r.denom().clone())
1015 }
1016}
1017
1018fn subst_body_var(
1022 pool: &ExprPool,
1023 body: &Formula,
1024 var: ExprId,
1025 value: ExprId,
1026) -> Result<Formula, CadError> {
1027 let expr = body.to_expr(pool);
1028 let mut map = HashMap::new();
1029 map.insert(var, value);
1030 let substituted = subs(expr, &map, pool);
1031 Ok(formula_from_expr(substituted, pool)?)
1032}
1033
1034struct XCells {
1039 candidates: Vec<rug::Rational>,
1040 ambiguous_irrational_root: bool,
1041}
1042
1043fn project_and_sample_x(
1053 pool: &ExprPool,
1054 x: ExprId,
1055 y: ExprId,
1056 body: &Formula,
1057) -> Result<XCells, CadError> {
1058 let allowed: BTreeSet<ExprId> = [x, y].into_iter().collect();
1059 if !free_vars_subset_of_binding(pool, body, &allowed) {
1060 return Err(CadError::Unsupported(
1061 "quantifier-free body may only reference the two bound variables (constants allowed)",
1062 ));
1063 }
1064
1065 let bivariate_polys = poly_exprs_from_formula(pool, body, y)?;
1066 let mut y_dep: Vec<ExprId> = Vec::new();
1067 let mut y_free: Vec<ExprId> = Vec::new();
1068 for e in bivariate_polys {
1069 if resultant::collect_free_vars(e, pool).contains(&y) {
1070 y_dep.push(e);
1071 } else {
1072 y_free.push(e);
1075 }
1076 }
1077
1078 let projected = cad_project(&y_dep, y, pool)?;
1079
1080 let mut polys_x_uni: Vec<UniPoly> = Vec::new();
1081 for e in projected.into_iter().chain(y_free) {
1082 match UniPoly::from_symbolic_clear_denoms(e, x, pool) {
1083 Ok(p) => {
1084 if !p.is_zero() {
1085 polys_x_uni.push(p);
1086 }
1087 }
1088 Err(err) => return Err(CadError::NotPolynomial(err)),
1089 }
1090 }
1091
1092 let master = combine_algebraic_master(x, &polys_x_uni);
1093 let br = cauchy_bound(&master);
1094 let roots_iv = real_roots(&master)?;
1095
1096 let mut breakpoints: Vec<rug::Rational> = vec![-br.clone()];
1097 for iv in &roots_iv {
1098 breakpoints.push(iv.lo.clone());
1099 breakpoints.push(iv.hi.clone());
1100 }
1101 breakpoints.push(br);
1102 breakpoints.sort();
1103 breakpoints.dedup_by(|a, b| *a == *b);
1104
1105 let mut candidates: BTreeSet<rug::Rational> = BTreeSet::new();
1106 for w in breakpoints.windows(2) {
1107 if w[0] < w[1] {
1108 candidates.insert(iv_midpoint(&w[0], &w[1]));
1109 }
1110 }
1111
1112 let mut ambiguous_irrational_root = false;
1113 for iv in &roots_iv {
1114 if iv.lo == iv.hi {
1115 candidates.insert(iv.lo.clone());
1116 } else {
1117 ambiguous_irrational_root = true;
1118 }
1119 }
1120
1121 Ok(XCells {
1122 candidates: candidates.into_iter().collect(),
1123 ambiguous_irrational_root,
1124 })
1125}
1126
1127const IRRATIONAL_ROOT_MSG: &str = "a non-strict atom (=, /=, <=, >=) combined with an irrational \
1128 projection root of the eliminated variable would require algebraic-number CAD lifting \
1129 (full CAD); refusing to guess rather than risk an unsound answer";
1130
1131fn decide_exists_exists(
1144 pool: &ExprPool,
1145 x: ExprId,
1146 y: ExprId,
1147 body: Formula,
1148) -> Result<QeResult, CadError> {
1149 let cells = project_and_sample_x(pool, x, y, &body)?;
1150 for x0 in &cells.candidates {
1151 let x_expr = rational_to_expr(pool, x0);
1152 let subst = subst_body_var(pool, &body, x, x_expr)?;
1153 let inner = decide_exists_univariate(pool, y, subst)?;
1154 if inner.truth {
1155 let mut wm = HashMap::new();
1156 wm.insert(x, x0.clone());
1157 if let Some(inner_w) = inner.witness {
1158 if let Some(yv) = inner_w.get(&y) {
1159 wm.insert(y, yv.clone());
1160 }
1161 }
1162 return Ok(QeResult {
1163 truth: true,
1164 witness: Some(wm),
1165 });
1166 }
1167 }
1168 if cells.ambiguous_irrational_root && body_has_nonstrict_atom(&body) {
1169 return Err(CadError::Unsupported(IRRATIONAL_ROOT_MSG));
1170 }
1171 Ok(QeResult {
1172 truth: false,
1173 witness: None,
1174 })
1175}
1176
1177fn decide_exists_forall(
1181 pool: &ExprPool,
1182 x: ExprId,
1183 y: ExprId,
1184 body: Formula,
1185) -> Result<QeResult, CadError> {
1186 let cells = project_and_sample_x(pool, x, y, &body)?;
1187 for x0 in &cells.candidates {
1188 let x_expr = rational_to_expr(pool, x0);
1189 let subst = subst_body_var(pool, &body, x, x_expr)?;
1190 let neg = nnf_formula(Formula::Not(Box::new(subst)));
1191 let inner = decide_exists_univariate(pool, y, neg)?;
1192 if !inner.truth {
1193 let mut wm = HashMap::new();
1194 wm.insert(x, x0.clone());
1195 return Ok(QeResult {
1196 truth: true,
1197 witness: Some(wm),
1198 });
1199 }
1200 }
1201 if cells.ambiguous_irrational_root && body_has_nonstrict_atom(&body) {
1202 return Err(CadError::Unsupported(IRRATIONAL_ROOT_MSG));
1203 }
1204 Ok(QeResult {
1205 truth: false,
1206 witness: None,
1207 })
1208}
1209
1210pub fn decide(formula: &Formula, pool: &ExprPool) -> Result<QeResult, CadError> {
1213 decide_formula_inner(pool, formula.clone())
1214}
1215
1216pub fn decide_expr(expr: ExprId, pool: &ExprPool) -> Result<QeResult, CadError> {
1218 let fm = formula_from_expr(expr, pool)?;
1219 decide(&fm, pool)
1220}
1221
1222pub fn cad_project(
1230 polynomials: &[ExprId],
1231 elim_var: ExprId,
1232 pool: &ExprPool,
1233) -> Result<Vec<ExprId>, CadError> {
1234 if polynomials.is_empty() {
1235 return Ok(Vec::new());
1236 }
1237 let mut all_vars = BTreeSet::new();
1238 all_vars.insert(elim_var);
1239 for &p in polynomials {
1240 all_vars.extend(resultant::collect_free_vars(p, pool));
1241 }
1242 let vars_no_elim: Vec<ExprId> = all_vars
1243 .iter()
1244 .copied()
1245 .filter(|&v| v != elim_var)
1246 .collect();
1247
1248 let mut uniq: Vec<ExprId> = Vec::new();
1249 let mut seen: BTreeSet<ExprId> = BTreeSet::new();
1250
1251 for i in 0..polynomials.len() {
1252 let f_expr = polynomials[i];
1253 let df = diff(f_expr, elim_var, pool)?.value;
1254
1255 let is_zero_f = UniPoly::from_symbolic(f_expr, elim_var, pool)
1256 .map(|u| u.is_zero())
1257 .unwrap_or(false);
1258 let is_zero_df = UniPoly::from_symbolic(df, elim_var, pool)
1259 .map(|u| u.is_zero())
1260 .unwrap_or(true);
1261
1262 if !is_zero_f && !is_zero_df {
1264 let rp = resultant(f_expr, df, elim_var, pool)?.value;
1265 if seen.insert(rp) {
1266 uniq.push(rp);
1267 }
1268 }
1269
1270 for &g_expr in polynomials.iter().skip(i + 1) {
1272 let is_zero_g = UniPoly::from_symbolic(g_expr, elim_var, pool)
1273 .map(|u| u.is_zero())
1274 .unwrap_or(false);
1275 if is_zero_f || is_zero_g {
1276 continue;
1277 }
1278 let r = resultant(f_expr, g_expr, elim_var, pool)?.value;
1279 if seen.insert(r) {
1280 uniq.push(r);
1281 }
1282 }
1283 }
1284
1285 let mut normed = Vec::<ExprId>::new();
1286 for e in uniq {
1287 let simplified = if vars_no_elim.is_empty() {
1288 e
1289 } else {
1290 poly_normal(e, vars_no_elim.clone(), pool)?
1291 };
1292 normed.push(simplified);
1293 }
1294
1295 normed.sort_unstable();
1296 normed.dedup();
1297 Ok(normed)
1298}
1299
1300pub fn cad_lift(
1303 polynomials: &[ExprId],
1304 main_var: ExprId,
1305 pool: &ExprPool,
1306) -> Result<Vec<RootInterval>, CadError> {
1307 let mut polys_uni = Vec::new();
1308 for &e in polynomials {
1309 match UniPoly::from_symbolic(e, main_var, pool) {
1310 Ok(u) => {
1311 if !u.is_zero() {
1312 polys_uni.push(u);
1313 }
1314 }
1315 Err(e) => return Err(CadError::NotPolynomial(e)),
1316 }
1317 }
1318 let m = combine_algebraic_master(main_var, &polys_uni);
1319 Ok(real_roots(&m)?)
1320}
1321
1322#[cfg(test)]
1327mod tests {
1328 use super::*;
1329 use crate::kernel::Domain;
1330
1331 #[test]
1332 fn forall_x_squared_plus_one_positive() {
1333 let p = ExprPool::new();
1334 let x = p.symbol("x", Domain::Real);
1335 let one = p.integer(1_i32);
1336 let x_sq = p.pow(x, p.integer(2_i32));
1337 let body = p.pred_gt(p.add(vec![x_sq, one]), p.integer(0_i32));
1338
1339 let f = Formula::Forall {
1340 var: x,
1341 body: Box::new(formula_from_expr(body, &p).unwrap()),
1342 };
1343 let r = decide(&f, &p).unwrap();
1344 assert!(r.truth);
1345 assert!(r.witness.is_none());
1346 }
1347
1348 #[test]
1349 fn exists_roots_x_squared_minus_two() {
1350 let p = ExprPool::new();
1351 let x = p.symbol("x", Domain::Real);
1352 let two = p.integer(2_i32);
1353 let xs = p.pow(x, p.integer(2_i32));
1354 let body = p.pred_eq(xs, two);
1355 let f = Formula::Exists {
1356 var: x,
1357 body: Box::new(formula_from_expr(body, &p).unwrap()),
1358 };
1359 let r = decide(&f, &p).unwrap();
1360 assert!(r.truth);
1361 assert!(r.witness.is_none());
1366 }
1367
1368 #[test]
1375 fn exists_witness_satisfies_the_equation() {
1376 let p = ExprPool::new();
1377 let x = p.symbol("x", Domain::Real);
1378 let lhs = p.add(vec![p.mul(vec![p.integer(3_i32), x]), p.integer(-2_i32)]);
1379 let body = p.pred_eq(lhs, p.integer(0_i32));
1380 let f = Formula::Exists {
1381 var: x,
1382 body: Box::new(formula_from_expr(body, &p).unwrap()),
1383 };
1384 let r = decide(&f, &p).unwrap();
1385 assert!(r.truth);
1386 let w = r
1387 .witness
1388 .expect("2/3 is rational, so a witness is reportable");
1389 assert_eq!(w[&x], rug::Rational::from((2, 3)));
1390 }
1391
1392 #[test]
1399 fn forall_square_positive_is_false_at_a_non_dyadic_root() {
1400 let p = ExprPool::new();
1401 let x = p.symbol("x", Domain::Real);
1402 let inner = p.add(vec![p.mul(vec![p.integer(3_i32), x]), p.integer(2_i32)]);
1403 let body = p.pred_gt(p.pow(inner, p.integer(2_i32)), p.integer(0_i32));
1404 let f = Formula::Forall {
1405 var: x,
1406 body: Box::new(formula_from_expr(body, &p).unwrap()),
1407 };
1408 assert!(!decide(&f, &p).unwrap().truth);
1409 }
1410
1411 #[test]
1415 fn forall_square_positive_refuses_at_an_irrational_root() {
1416 let p = ExprPool::new();
1417 let x = p.symbol("x", Domain::Real);
1418 let inner = p.add(vec![p.pow(x, p.integer(2_i32)), p.integer(-2_i32)]);
1419 let body = p.pred_gt(p.pow(inner, p.integer(2_i32)), p.integer(0_i32));
1420 let f = Formula::Forall {
1421 var: x,
1422 body: Box::new(formula_from_expr(body, &p).unwrap()),
1423 };
1424 assert!(matches!(
1425 decide(&f, &p),
1426 Err(CadError::Unsupported(ALGEBRAIC_BOUNDARY_MSG))
1427 ));
1428 }
1429
1430 #[test]
1431 fn cad_lift_univariate_quadratic() {
1432 let p = ExprPool::new();
1433 let x = p.symbol("x", Domain::Real);
1434 let xs = p.add(vec![p.pow(x, p.integer(2_i32)), p.integer(-2_i32)]);
1435 let ivs = cad_lift(&[xs], x, &p).unwrap();
1436 assert_eq!(ivs.len(), 2);
1437 assert!(ivs.iter().all(|iv| iv.lo <= iv.hi));
1438 }
1439
1440 #[test]
1441 fn cad_project_circle_eliminates_y() {
1442 let p = ExprPool::new();
1443 let x = p.symbol("x", Domain::Real);
1444 let y = p.symbol("y", Domain::Real);
1445 let circle = p.add(vec![
1446 p.pow(x, p.integer(2_i32)),
1447 p.pow(y, p.integer(2_i32)),
1448 p.integer(-1_i32),
1449 ]);
1450 let line = p.add(vec![y, pool_neg_x(&p, x)]); let pr = cad_project(&[circle, line], y, &p).unwrap();
1452 assert!(!pr.is_empty());
1453 }
1454
1455 fn pool_neg_x(pool: &ExprPool, x: ExprId) -> ExprId {
1456 pool.mul(vec![pool.integer(-1_i32), x])
1457 }
1458
1459 #[test]
1460 fn unipoly_eval_rational_zero() {
1461 let p = ExprPool::new();
1462 let x = p.symbol("x", Domain::Real);
1463 let qp = UniPoly::from_symbolic(p.add(vec![x, p.integer(2_i32)]), x, &p).unwrap();
1464 let z = qp.eval_rational(&rug::Rational::from(-2));
1465 assert_eq!(z, 0);
1466 }
1467
1468 fn xy_pool() -> (ExprPool, ExprId, ExprId) {
1473 let p = ExprPool::new();
1474 let x = p.symbol("x", Domain::Real);
1475 let y = p.symbol("y", Domain::Real);
1476 (p, x, y)
1477 }
1478
1479 #[test]
1480 fn exists_exists_circle_through_origin_true() {
1481 let (p, x, y) = xy_pool();
1483 let sum_sq = p.add(vec![p.pow(x, p.integer(2_i32)), p.pow(y, p.integer(2_i32))]);
1484 let body = p.pred_eq(sum_sq, p.integer(0_i32));
1485 let f = Formula::Exists {
1486 var: x,
1487 body: Box::new(Formula::Exists {
1488 var: y,
1489 body: Box::new(formula_from_expr(body, &p).unwrap()),
1490 }),
1491 };
1492 let r = decide(&f, &p).unwrap();
1493 assert!(r.truth);
1494 let wit = r.witness.expect("witness expected for true ∃∃");
1495 assert_eq!(wit.get(&x), Some(&rug::Rational::from(0)));
1496 assert_eq!(wit.get(&y), Some(&rug::Rational::from(0)));
1497 }
1498
1499 #[test]
1500 fn exists_exists_circle_plus_one_false() {
1501 let (p, x, y) = xy_pool();
1503 let sum_sq = p.add(vec![
1504 p.pow(x, p.integer(2_i32)),
1505 p.pow(y, p.integer(2_i32)),
1506 p.integer(1_i32),
1507 ]);
1508 let body = p.pred_eq(sum_sq, p.integer(0_i32));
1509 let f = Formula::Exists {
1510 var: x,
1511 body: Box::new(Formula::Exists {
1512 var: y,
1513 body: Box::new(formula_from_expr(body, &p).unwrap()),
1514 }),
1515 };
1516 let r = decide(&f, &p).unwrap();
1517 assert!(!r.truth);
1518 assert!(r.witness.is_none());
1519 }
1520
1521 #[test]
1522 fn forall_forall_sum_of_squares_nonneg_true() {
1523 let (p, x, y) = xy_pool();
1525 let sum_sq = p.add(vec![p.pow(x, p.integer(2_i32)), p.pow(y, p.integer(2_i32))]);
1526 let body = p.pred_ge(sum_sq, p.integer(0_i32));
1527 let f = Formula::Forall {
1528 var: x,
1529 body: Box::new(Formula::Forall {
1530 var: y,
1531 body: Box::new(formula_from_expr(body, &p).unwrap()),
1532 }),
1533 };
1534 let r = decide(&f, &p).unwrap();
1535 assert!(r.truth);
1536 assert!(r.witness.is_none());
1537 }
1538
1539 #[test]
1540 fn forall_forall_product_positive_false() {
1541 let (p, x, y) = xy_pool();
1543 let xy = p.mul(vec![x, y]);
1544 let body = p.pred_gt(xy, p.integer(0_i32));
1545 let f = Formula::Forall {
1546 var: x,
1547 body: Box::new(Formula::Forall {
1548 var: y,
1549 body: Box::new(formula_from_expr(body, &p).unwrap()),
1550 }),
1551 };
1552 let r = decide(&f, &p).unwrap();
1553 assert!(!r.truth);
1554 }
1555
1556 #[test]
1557 fn forall_exists_every_x_has_bigger_y_true() {
1558 let (p, x, y) = xy_pool();
1560 let body = p.pred_gt(y, x);
1561 let f = Formula::Forall {
1562 var: x,
1563 body: Box::new(Formula::Exists {
1564 var: y,
1565 body: Box::new(formula_from_expr(body, &p).unwrap()),
1566 }),
1567 };
1568 let r = decide(&f, &p).unwrap();
1569 assert!(r.truth);
1570 }
1571
1572 #[test]
1573 fn exists_forall_no_x_is_upper_bound_false() {
1574 let (p, x, y) = xy_pool();
1576 let body = p.pred_ge(x, y);
1577 let f = Formula::Exists {
1578 var: x,
1579 body: Box::new(Formula::Forall {
1580 var: y,
1581 body: Box::new(formula_from_expr(body, &p).unwrap()),
1582 }),
1583 };
1584 let r = decide(&f, &p).unwrap();
1585 assert!(!r.truth);
1586 }
1587
1588 #[test]
1589 fn three_variable_prefix_is_unsupported() {
1590 let p = ExprPool::new();
1592 let x = p.symbol("x", Domain::Real);
1593 let y = p.symbol("y", Domain::Real);
1594 let z = p.symbol("z", Domain::Real);
1595 let body = p.pred_eq(p.add(vec![x, y, z]), p.integer(0_i32));
1596 let f = Formula::Exists {
1597 var: x,
1598 body: Box::new(Formula::Exists {
1599 var: y,
1600 body: Box::new(Formula::Exists {
1601 var: z,
1602 body: Box::new(formula_from_expr(body, &p).unwrap()),
1603 }),
1604 }),
1605 };
1606 let err = decide(&f, &p).unwrap_err();
1607 assert_eq!(err.code(), "E-CAD-001");
1608 assert!(matches!(err, CadError::Unsupported(_)));
1609 }
1610
1611 #[test]
1612 fn univariate_regression_still_works_after_two_var_addition() {
1613 let p = ExprPool::new();
1616 let x = p.symbol("x", Domain::Real);
1617 let body = p.pred_gt(p.pow(x, p.integer(2_i32)), p.integer(4_i32));
1618 let f = Formula::Exists {
1619 var: x,
1620 body: Box::new(formula_from_expr(body, &p).unwrap()),
1621 };
1622 let r = decide(&f, &p).unwrap();
1623 assert!(r.truth);
1624 }
1625}
1626
1627#[cfg(test)]
1628mod sample_point_completeness_tests {
1629 use super::*;
1630 use crate::kernel::Domain;
1631
1632 fn poly(pool: &ExprPool, x: ExprId, coeffs: &[i64]) -> ExprId {
1634 let terms: Vec<ExprId> = coeffs
1635 .iter()
1636 .enumerate()
1637 .map(|(i, &c)| {
1638 let ci = pool.integer(c);
1639 if i == 0 {
1640 ci
1641 } else {
1642 pool.mul(vec![ci, pool.pow(x, pool.integer(i as i64))])
1643 }
1644 })
1645 .collect();
1646 pool.add(terms)
1647 }
1648
1649 fn forall(pool: &ExprPool, x: ExprId, body: Formula) -> Result<QeResult, CadError> {
1650 decide(
1651 &Formula::Forall {
1652 var: x,
1653 body: Box::new(body),
1654 },
1655 pool,
1656 )
1657 }
1658
1659 fn atom(kind: PredicateKind, lhs: ExprId, rhs: ExprId) -> Formula {
1660 Formula::Atom {
1661 kind,
1662 args: vec![lhs, rhs],
1663 }
1664 }
1665
1666 #[test]
1673 fn false_universals_are_not_proved() {
1674 let pool = ExprPool::new();
1675 let x = pool.symbol("x", Domain::Real);
1676 let zero = pool.integer(0_i32);
1677
1678 for (label, coeffs, kind) in [
1679 ("x^2 > 0", vec![0, 0, 1], PredicateKind::Gt),
1680 ("x^4 > 0", vec![0, 0, 0, 0, 1], PredicateKind::Gt),
1681 (
1682 "2x^4+x^3-4x^2+3 >= 0",
1683 vec![3, 0, -4, 1, 2],
1684 PredicateKind::Ge,
1685 ),
1686 ] {
1687 let p = poly(&pool, x, &coeffs);
1688 let got = forall(&pool, x, atom(kind, p, zero)).expect("decidable");
1689 assert!(
1690 !got.truth,
1691 "`forall x. {label}` is false but decide returned true"
1692 );
1693 }
1694 }
1695
1696 #[test]
1698 fn true_universals_still_hold() {
1699 let pool = ExprPool::new();
1700 let x = pool.symbol("x", Domain::Real);
1701 let zero = pool.integer(0_i32);
1702
1703 for (label, coeffs, kind) in [
1704 ("x^2 >= 0", vec![0, 0, 1], PredicateKind::Ge),
1705 ("x^2 + 1 > 0", vec![1, 0, 1], PredicateKind::Gt),
1706 ("(x-1)^2 >= 0", vec![1, -2, 1], PredicateKind::Ge),
1707 ] {
1708 let p = poly(&pool, x, &coeffs);
1709 let got = forall(&pool, x, atom(kind, p, zero)).expect("decidable");
1710 assert!(
1711 got.truth,
1712 "`forall x. {label}` is true but decide returned false"
1713 );
1714 }
1715 }
1716
1717 #[test]
1725 fn refinement_survives_a_root_valued_endpoint() {
1726 let pool = ExprPool::new();
1727 let x = pool.symbol("x", Domain::Real);
1728 let p = poly(&pool, x, &[3, 0, -4, 1, 2]);
1729 let up = UniPoly::from_symbolic_clear_denoms(p, x, &pool).expect("polynomial");
1730 let master = combine_algebraic_master(x, &[up]);
1731
1732 let iv = RootInterval::new(rug::Rational::from(-2), rug::Rational::from(-1));
1733 let (lo, hi) = refine_isolating(&master, &iv);
1734
1735 assert!(lo <= hi, "refined bracket is inverted");
1736 assert!(
1737 hi < -1,
1738 "bracket collapsed onto the endpoint root -1 instead of isolating its own root"
1739 );
1740 assert!(lo > -2, "bracket did not tighten at all");
1741 }
1742
1743 fn touching_at_sqrt_two(pool: &ExprPool, x: ExprId, y: ExprId) -> ExprId {
1754 let inner = pool.add(vec![pool.pow(x, pool.integer(2_i32)), pool.integer(-2_i32)]);
1755 pool.add(vec![
1756 pool.pow(inner, pool.integer(2_i32)),
1757 pool.pow(y, pool.integer(2_i32)),
1758 ])
1759 }
1760
1761 #[test]
1764 fn two_var_nonstrict_boundary_at_an_irrational_root_is_not_denied() {
1765 let pool = ExprPool::new();
1766 let x = pool.symbol("x", Domain::Real);
1767 let y = pool.symbol("y", Domain::Real);
1768 let lhs = touching_at_sqrt_two(&pool, x, y);
1769 let body = atom(PredicateKind::Le, lhs, pool.integer(0_i32));
1770 let f = Formula::Exists {
1771 var: x,
1772 body: Box::new(Formula::Exists {
1773 var: y,
1774 body: Box::new(body),
1775 }),
1776 };
1777 match decide(&f, &pool) {
1778 Ok(r) => assert!(
1779 r.truth,
1780 "`exists x exists y. (x^2-2)^2 + y^2 <= 0` is true at (sqrt 2, 0)"
1781 ),
1782 Err(e) => assert_eq!(e.code(), "E-CAD-001"),
1783 }
1784 }
1785
1786 #[test]
1789 fn two_var_universal_over_an_irrational_root_is_not_proved() {
1790 let pool = ExprPool::new();
1791 let x = pool.symbol("x", Domain::Real);
1792 let y = pool.symbol("y", Domain::Real);
1793 let lhs = touching_at_sqrt_two(&pool, x, y);
1794 let body = atom(PredicateKind::Gt, lhs, pool.integer(0_i32));
1795 let f = Formula::Forall {
1796 var: x,
1797 body: Box::new(Formula::Forall {
1798 var: y,
1799 body: Box::new(body),
1800 }),
1801 };
1802 match decide(&f, &pool) {
1803 Ok(r) => assert!(
1804 !r.truth,
1805 "`forall x forall y. (x^2-2)^2 + y^2 > 0` is false at (sqrt 2, 0)"
1806 ),
1807 Err(e) => assert_eq!(e.code(), "E-CAD-001"),
1808 }
1809 }
1810
1811 #[test]
1815 fn two_var_nonstrict_unsatisfiable_still_decides_false() {
1816 let pool = ExprPool::new();
1817 let x = pool.symbol("x", Domain::Real);
1818 let y = pool.symbol("y", Domain::Real);
1819 let lhs = pool.add(vec![touching_at_sqrt_two(&pool, x, y), pool.integer(1_i32)]);
1820 let body = atom(PredicateKind::Le, lhs, pool.integer(0_i32));
1821 let f = Formula::Exists {
1822 var: x,
1823 body: Box::new(Formula::Exists {
1824 var: y,
1825 body: Box::new(body),
1826 }),
1827 };
1828 let r = decide(&f, &pool).expect("two squares plus one is decidable");
1829 assert!(!r.truth, "two squares plus 1 is never <= 0");
1830 }
1831
1832 #[test]
1835 fn two_var_nonstrict_boundary_at_a_rational_root_is_found() {
1836 let pool = ExprPool::new();
1837 let x = pool.symbol("x", Domain::Real);
1838 let y = pool.symbol("y", Domain::Real);
1839 let inner = pool.add(vec![
1840 pool.mul(vec![pool.integer(3_i32), x]),
1841 pool.integer(-2_i32),
1842 ]);
1843 let lhs = pool.add(vec![
1844 pool.pow(inner, pool.integer(2_i32)),
1845 pool.pow(y, pool.integer(2_i32)),
1846 ]);
1847 let body = atom(PredicateKind::Le, lhs, pool.integer(0_i32));
1848 let f = Formula::Exists {
1849 var: x,
1850 body: Box::new(Formula::Exists {
1851 var: y,
1852 body: Box::new(body),
1853 }),
1854 };
1855 let r = decide(&f, &pool).expect("a rational boundary point is reachable");
1856 assert!(r.truth, "(3x-2)^2 + y^2 <= 0 holds at (2/3, 0)");
1857 }
1858}