1use lasso::Spur;
18use oxiz_core::ast::{TermId, TermKind, TermManager};
19use oxiz_core::sort::SortId;
20use rustc_hash::{FxHashMap, FxHashSet};
21use smallvec::SmallVec;
22
23#[derive(Debug, Clone)]
25pub struct QuantifiedFormula {
26 pub term: TermId,
28 pub bound_vars: SmallVec<[(Spur, SortId); 2]>,
30 pub body: TermId,
32 pub universal: bool,
34 pub instantiation_count: usize,
36 pub max_instantiations: usize,
38}
39
40impl QuantifiedFormula {
41 pub fn new(
43 term: TermId,
44 bound_vars: SmallVec<[(Spur, SortId); 2]>,
45 body: TermId,
46 universal: bool,
47 ) -> Self {
48 Self {
49 term,
50 bound_vars,
51 body,
52 universal,
53 instantiation_count: 0,
54 max_instantiations: 100,
55 }
56 }
57
58 pub fn can_instantiate(&self) -> bool {
60 self.instantiation_count < self.max_instantiations
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct Instantiation {
67 pub quantifier: TermId,
69 pub substitution: FxHashMap<Spur, TermId>,
71 pub result: TermId,
73}
74
75#[derive(Debug, Clone)]
77pub enum MBQIResult {
78 NoQuantifiers,
80 Satisfied,
82 NewInstantiations(Vec<Instantiation>),
84 Conflict(Vec<TermId>),
86 InstantiationLimit,
88}
89
90#[derive(Debug)]
92pub struct MBQISolver {
93 quantifiers: Vec<QuantifiedFormula>,
95 generated_instantiations: FxHashSet<(TermId, Vec<(Spur, TermId)>)>,
97 candidates_by_sort: FxHashMap<SortId, Vec<TermId>>,
99 max_total_instantiations: usize,
101 total_instantiation_count: usize,
103 enabled: bool,
105}
106
107impl Default for MBQISolver {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl MBQISolver {
114 pub fn new() -> Self {
116 Self {
117 quantifiers: Vec::new(),
118 generated_instantiations: FxHashSet::default(),
119 candidates_by_sort: FxHashMap::default(),
120 max_total_instantiations: 10000,
121 total_instantiation_count: 0,
122 enabled: true,
123 }
124 }
125
126 pub fn with_limit(max_total: usize) -> Self {
128 let mut solver = Self::new();
129 solver.max_total_instantiations = max_total;
130 solver
131 }
132
133 pub fn set_enabled(&mut self, enabled: bool) {
135 self.enabled = enabled;
136 }
137
138 pub fn is_enabled(&self) -> bool {
140 self.enabled
141 }
142
143 pub fn clear(&mut self) {
145 self.quantifiers.clear();
146 self.generated_instantiations.clear();
147 self.candidates_by_sort.clear();
148 self.total_instantiation_count = 0;
149 }
150
151 pub fn add_quantifier(&mut self, term: TermId, manager: &TermManager) {
153 let Some(t) = manager.get(term) else {
154 return;
155 };
156
157 match &t.kind {
158 TermKind::Forall { vars, body, .. } => {
159 self.quantifiers
160 .push(QuantifiedFormula::new(term, vars.clone(), *body, true));
161 }
162 TermKind::Exists { vars, body, .. } => {
163 self.quantifiers
167 .push(QuantifiedFormula::new(term, vars.clone(), *body, false));
168 }
169 _ => {}
170 }
171 }
172
173 pub fn add_candidate(&mut self, term: TermId, sort: SortId) {
175 self.candidates_by_sort.entry(sort).or_default().push(term);
176 }
177
178 pub fn collect_ground_terms(&mut self, term: TermId, manager: &TermManager) {
180 self.collect_ground_terms_rec(term, manager, &mut FxHashSet::default());
181 }
182
183 fn collect_ground_terms_rec(
184 &mut self,
185 term: TermId,
186 manager: &TermManager,
187 visited: &mut FxHashSet<TermId>,
188 ) {
189 if visited.contains(&term) {
190 return;
191 }
192 visited.insert(term);
193
194 let Some(t) = manager.get(term) else {
195 return;
196 };
197
198 match &t.kind {
201 TermKind::Var(_) => {
202 self.add_candidate(term, t.sort);
204 }
205 TermKind::IntConst(_) | TermKind::RealConst(_) | TermKind::BitVecConst { .. } => {
206 self.add_candidate(term, t.sort);
207 }
208 TermKind::Apply { args, .. } => {
209 self.add_candidate(term, t.sort);
211 for &arg in args {
212 self.collect_ground_terms_rec(arg, manager, visited);
213 }
214 }
215 TermKind::Add(args)
216 | TermKind::Mul(args)
217 | TermKind::And(args)
218 | TermKind::Or(args) => {
219 for &arg in args {
220 self.collect_ground_terms_rec(arg, manager, visited);
221 }
222 }
223 TermKind::Sub(lhs, rhs)
224 | TermKind::Div(lhs, rhs)
225 | TermKind::Mod(lhs, rhs)
226 | TermKind::Eq(lhs, rhs)
227 | TermKind::Lt(lhs, rhs)
228 | TermKind::Le(lhs, rhs)
229 | TermKind::Gt(lhs, rhs)
230 | TermKind::Ge(lhs, rhs) => {
231 self.collect_ground_terms_rec(*lhs, manager, visited);
232 self.collect_ground_terms_rec(*rhs, manager, visited);
233 }
234 TermKind::Not(arg) | TermKind::Neg(arg) => {
235 self.collect_ground_terms_rec(*arg, manager, visited);
236 }
237 TermKind::Ite(cond, then_br, else_br) => {
238 self.collect_ground_terms_rec(*cond, manager, visited);
239 self.collect_ground_terms_rec(*then_br, manager, visited);
240 self.collect_ground_terms_rec(*else_br, manager, visited);
241 }
242 TermKind::Forall { body, .. } | TermKind::Exists { body, .. } => {
243 let _ = body;
246 }
247 _ => {}
248 }
249 }
250
251 pub fn get_candidates(&self, sort: SortId) -> &[TermId] {
253 self.candidates_by_sort
254 .get(&sort)
255 .map_or(&[], |v| v.as_slice())
256 }
257
258 pub fn at_limit(&self) -> bool {
260 self.total_instantiation_count >= self.max_total_instantiations
261 }
262
263 pub fn check_with_model(
269 &mut self,
270 model: &FxHashMap<TermId, TermId>,
271 manager: &mut TermManager,
272 ) -> MBQIResult {
273 if !self.enabled {
274 return MBQIResult::NoQuantifiers;
275 }
276
277 if self.quantifiers.is_empty() {
278 return MBQIResult::NoQuantifiers;
279 }
280
281 if self.at_limit() {
282 return MBQIResult::InstantiationLimit;
283 }
284
285 let mut new_instantiations = Vec::new();
286
287 for i in 0..self.quantifiers.len() {
289 if !self.quantifiers[i].can_instantiate() {
290 continue;
291 }
292
293 if !self.quantifiers[i].universal {
294 continue;
297 }
298
299 let instantiations = self.find_counterexamples(i, model, manager);
301
302 for inst in instantiations {
303 if self.at_limit() {
304 break;
305 }
306
307 let mut key_vec: Vec<_> = inst.substitution.iter().map(|(&k, &v)| (k, v)).collect();
309 key_vec.sort_by_key(|(k, _)| *k);
310 let key = (inst.quantifier, key_vec);
311
312 if self.generated_instantiations.contains(&key) {
313 continue;
314 }
315
316 self.generated_instantiations.insert(key);
317 self.quantifiers[i].instantiation_count += 1;
318 self.total_instantiation_count += 1;
319 new_instantiations.push(inst);
320 }
321 }
322
323 if new_instantiations.is_empty() {
324 MBQIResult::Satisfied
325 } else {
326 MBQIResult::NewInstantiations(new_instantiations)
327 }
328 }
329
330 fn find_counterexamples(
332 &self,
333 quantifier_idx: usize,
334 model: &FxHashMap<TermId, TermId>,
335 manager: &mut TermManager,
336 ) -> Vec<Instantiation> {
337 let quant = &self.quantifiers[quantifier_idx];
338 let mut results = Vec::new();
339
340 let candidates = self.build_candidate_lists(&quant.bound_vars, manager);
342
343 let combinations = self.enumerate_combinations(&candidates, 10); for combo in combinations {
347 let mut subst: FxHashMap<Spur, TermId> = FxHashMap::default();
349 for (i, &candidate) in combo.iter().enumerate() {
350 if i < quant.bound_vars.len() {
351 subst.insert(quant.bound_vars[i].0, candidate);
352 }
353 }
354
355 let ground_body = self.apply_substitution(quant.body, &subst, manager);
357
358 let evaluated = self.evaluate_under_model(ground_body, model, manager);
360
361 if let Some(t) = manager.get(evaluated)
363 && matches!(t.kind, TermKind::False)
364 {
365 results.push(Instantiation {
366 quantifier: quant.term,
367 substitution: subst,
368 result: ground_body,
369 });
370
371 if results.len() >= 5 {
373 break;
374 }
375 }
376 }
377
378 if results.is_empty() {
380 let model_instantiation = self.instantiate_from_model(quantifier_idx, model, manager);
382 if let Some(inst) = model_instantiation {
383 results.push(inst);
384 }
385 }
386
387 results
388 }
389
390 fn build_candidate_lists(
392 &self,
393 bound_vars: &[(Spur, SortId)],
394 manager: &mut TermManager,
395 ) -> Vec<Vec<TermId>> {
396 let mut result = Vec::new();
397
398 for &(_name, sort) in bound_vars {
399 let mut candidates = Vec::new();
400
401 if let Some(pool) = self.candidates_by_sort.get(&sort) {
403 candidates.extend(pool.iter().copied().take(10)); }
405
406 if sort == manager.sorts.int_sort {
408 for i in 0..3 {
410 let int_id = manager.mk_int(i);
411 if !candidates.contains(&int_id) {
412 candidates.push(int_id);
413 }
414 }
415 } else if sort == manager.sorts.bool_sort {
416 let true_id = manager.mk_true();
417 let false_id = manager.mk_false();
418 if !candidates.contains(&true_id) {
419 candidates.push(true_id);
420 }
421 if !candidates.contains(&false_id) {
422 candidates.push(false_id);
423 }
424 }
425
426 result.push(candidates);
427 }
428
429 result
430 }
431
432 fn enumerate_combinations(
434 &self,
435 candidates: &[Vec<TermId>],
436 max_per_dim: usize,
437 ) -> Vec<Vec<TermId>> {
438 if candidates.is_empty() {
439 return vec![vec![]];
440 }
441
442 let mut results = Vec::new();
443 let mut indices = vec![0usize; candidates.len()];
444
445 loop {
446 let combo: Vec<TermId> = indices
448 .iter()
449 .enumerate()
450 .filter_map(|(i, &idx)| candidates.get(i).and_then(|c| c.get(idx).copied()))
451 .collect();
452
453 if combo.len() == candidates.len() {
454 results.push(combo);
455 }
456
457 if results.len() >= 100 {
459 break;
460 }
461
462 let mut carry = true;
464 for (i, idx) in indices.iter_mut().enumerate() {
465 if carry {
466 *idx += 1;
467 let limit = candidates.get(i).map_or(1, |c| c.len().min(max_per_dim));
468 if *idx >= limit {
469 *idx = 0;
470 } else {
471 carry = false;
472 }
473 }
474 }
475
476 if carry {
477 break;
479 }
480 }
481
482 results
483 }
484
485 #[allow(clippy::only_used_in_recursion)]
487 fn apply_substitution(
488 &self,
489 term: TermId,
490 subst: &FxHashMap<Spur, TermId>,
491 manager: &mut TermManager,
492 ) -> TermId {
493 let Some(t) = manager.get(term).cloned() else {
496 return term;
497 };
498
499 match &t.kind {
500 TermKind::Var(name) => {
501 if let Some(&replacement) = subst.get(name) {
503 replacement
504 } else {
505 term
506 }
507 }
508 TermKind::Not(arg) => {
509 let new_arg = self.apply_substitution(*arg, subst, manager);
510 manager.mk_not(new_arg)
511 }
512 TermKind::And(args) => {
513 let new_args: Vec<_> = args
514 .iter()
515 .map(|&a| self.apply_substitution(a, subst, manager))
516 .collect();
517 manager.mk_and(new_args)
518 }
519 TermKind::Or(args) => {
520 let new_args: Vec<_> = args
521 .iter()
522 .map(|&a| self.apply_substitution(a, subst, manager))
523 .collect();
524 manager.mk_or(new_args)
525 }
526 TermKind::Eq(lhs, rhs) => {
527 let new_lhs = self.apply_substitution(*lhs, subst, manager);
528 let new_rhs = self.apply_substitution(*rhs, subst, manager);
529 manager.mk_eq(new_lhs, new_rhs)
530 }
531 TermKind::Lt(lhs, rhs) => {
532 let new_lhs = self.apply_substitution(*lhs, subst, manager);
533 let new_rhs = self.apply_substitution(*rhs, subst, manager);
534 manager.mk_lt(new_lhs, new_rhs)
535 }
536 TermKind::Le(lhs, rhs) => {
537 let new_lhs = self.apply_substitution(*lhs, subst, manager);
538 let new_rhs = self.apply_substitution(*rhs, subst, manager);
539 manager.mk_le(new_lhs, new_rhs)
540 }
541 TermKind::Gt(lhs, rhs) => {
542 let new_lhs = self.apply_substitution(*lhs, subst, manager);
543 let new_rhs = self.apply_substitution(*rhs, subst, manager);
544 manager.mk_gt(new_lhs, new_rhs)
545 }
546 TermKind::Ge(lhs, rhs) => {
547 let new_lhs = self.apply_substitution(*lhs, subst, manager);
548 let new_rhs = self.apply_substitution(*rhs, subst, manager);
549 manager.mk_ge(new_lhs, new_rhs)
550 }
551 TermKind::Add(args) => {
552 let new_args: SmallVec<[TermId; 4]> = args
553 .iter()
554 .map(|&a| self.apply_substitution(a, subst, manager))
555 .collect();
556 manager.mk_add(new_args)
557 }
558 TermKind::Sub(lhs, rhs) => {
559 let new_lhs = self.apply_substitution(*lhs, subst, manager);
560 let new_rhs = self.apply_substitution(*rhs, subst, manager);
561 manager.mk_sub(new_lhs, new_rhs)
562 }
563 TermKind::Mul(args) => {
564 let new_args: SmallVec<[TermId; 4]> = args
565 .iter()
566 .map(|&a| self.apply_substitution(a, subst, manager))
567 .collect();
568 manager.mk_mul(new_args)
569 }
570 TermKind::Neg(arg) => {
571 let new_arg = self.apply_substitution(*arg, subst, manager);
572 manager.mk_neg(new_arg)
573 }
574 TermKind::Implies(lhs, rhs) => {
575 let new_lhs = self.apply_substitution(*lhs, subst, manager);
576 let new_rhs = self.apply_substitution(*rhs, subst, manager);
577 manager.mk_implies(new_lhs, new_rhs)
578 }
579 TermKind::Ite(cond, then_br, else_br) => {
580 let new_cond = self.apply_substitution(*cond, subst, manager);
581 let new_then = self.apply_substitution(*then_br, subst, manager);
582 let new_else = self.apply_substitution(*else_br, subst, manager);
583 manager.mk_ite(new_cond, new_then, new_else)
584 }
585 TermKind::Apply { func, args } => {
586 let func_name = manager.resolve_str(*func).to_string();
587 let new_args: SmallVec<[TermId; 4]> = args
588 .iter()
589 .map(|&a| self.apply_substitution(a, subst, manager))
590 .collect();
591 manager.mk_apply(&func_name, new_args, t.sort)
592 }
593 _ => term,
595 }
596 }
597
598 #[allow(clippy::only_used_in_recursion)]
600 fn evaluate_under_model(
601 &self,
602 term: TermId,
603 model: &FxHashMap<TermId, TermId>,
604 manager: &mut TermManager,
605 ) -> TermId {
606 if let Some(&val) = model.get(&term) {
608 return val;
609 }
610
611 let Some(t) = manager.get(term).cloned() else {
612 return term;
613 };
614
615 match &t.kind {
616 TermKind::True | TermKind::False | TermKind::IntConst(_) | TermKind::RealConst(_) => {
617 term
619 }
620 TermKind::Var(_) => {
621 model.get(&term).copied().unwrap_or(term)
623 }
624 TermKind::Not(arg) => {
625 let eval_arg = self.evaluate_under_model(*arg, model, manager);
626 if let Some(arg_t) = manager.get(eval_arg) {
627 match arg_t.kind {
628 TermKind::True => return manager.mk_false(),
629 TermKind::False => return manager.mk_true(),
630 _ => {}
631 }
632 }
633 manager.mk_not(eval_arg)
634 }
635 TermKind::And(args) => {
636 let mut all_true = true;
637 for &arg in args {
638 let eval_arg = self.evaluate_under_model(arg, model, manager);
639 if let Some(arg_t) = manager.get(eval_arg) {
640 match arg_t.kind {
641 TermKind::False => return manager.mk_false(),
642 TermKind::True => continue,
643 _ => all_true = false,
644 }
645 } else {
646 all_true = false;
647 }
648 }
649 if all_true { manager.mk_true() } else { term }
650 }
651 TermKind::Or(args) => {
652 let mut all_false = true;
653 for &arg in args {
654 let eval_arg = self.evaluate_under_model(arg, model, manager);
655 if let Some(arg_t) = manager.get(eval_arg) {
656 match arg_t.kind {
657 TermKind::True => return manager.mk_true(),
658 TermKind::False => continue,
659 _ => all_false = false,
660 }
661 } else {
662 all_false = false;
663 }
664 }
665 if all_false { manager.mk_false() } else { term }
666 }
667 TermKind::Eq(lhs, rhs) => {
668 let eval_lhs = self.evaluate_under_model(*lhs, model, manager);
669 let eval_rhs = self.evaluate_under_model(*rhs, model, manager);
670
671 if eval_lhs == eval_rhs {
673 return manager.mk_true();
674 }
675
676 let lhs_t = manager.get(eval_lhs).cloned();
678 let rhs_t = manager.get(eval_rhs).cloned();
679
680 if let (Some(l), Some(r)) = (lhs_t, rhs_t) {
681 match (&l.kind, &r.kind) {
682 (TermKind::IntConst(a), TermKind::IntConst(b)) => {
683 if a == b {
684 return manager.mk_true();
685 } else {
686 return manager.mk_false();
687 }
688 }
689 (TermKind::True, TermKind::True) | (TermKind::False, TermKind::False) => {
690 return manager.mk_true();
691 }
692 (TermKind::True, TermKind::False) | (TermKind::False, TermKind::True) => {
693 return manager.mk_false();
694 }
695 _ => {}
696 }
697 }
698
699 term
700 }
701 TermKind::Lt(lhs, rhs) => {
702 let eval_lhs = self.evaluate_under_model(*lhs, model, manager);
703 let eval_rhs = self.evaluate_under_model(*rhs, model, manager);
704
705 let lhs_t = manager.get(eval_lhs).cloned();
706 let rhs_t = manager.get(eval_rhs).cloned();
707
708 if let (Some(l), Some(r)) = (lhs_t, rhs_t)
709 && let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind)
710 {
711 if a < b {
712 return manager.mk_true();
713 } else {
714 return manager.mk_false();
715 }
716 }
717
718 term
719 }
720 TermKind::Le(lhs, rhs) => {
721 let eval_lhs = self.evaluate_under_model(*lhs, model, manager);
722 let eval_rhs = self.evaluate_under_model(*rhs, model, manager);
723
724 let lhs_t = manager.get(eval_lhs).cloned();
725 let rhs_t = manager.get(eval_rhs).cloned();
726
727 if let (Some(l), Some(r)) = (lhs_t, rhs_t)
728 && let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind)
729 {
730 if a <= b {
731 return manager.mk_true();
732 } else {
733 return manager.mk_false();
734 }
735 }
736
737 term
738 }
739 TermKind::Gt(lhs, rhs) => {
740 let eval_lhs = self.evaluate_under_model(*lhs, model, manager);
741 let eval_rhs = self.evaluate_under_model(*rhs, model, manager);
742
743 let lhs_t = manager.get(eval_lhs).cloned();
744 let rhs_t = manager.get(eval_rhs).cloned();
745
746 if let (Some(l), Some(r)) = (lhs_t, rhs_t)
747 && let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind)
748 {
749 if a > b {
750 return manager.mk_true();
751 } else {
752 return manager.mk_false();
753 }
754 }
755
756 term
757 }
758 TermKind::Ge(lhs, rhs) => {
759 let eval_lhs = self.evaluate_under_model(*lhs, model, manager);
760 let eval_rhs = self.evaluate_under_model(*rhs, model, manager);
761
762 let lhs_t = manager.get(eval_lhs).cloned();
763 let rhs_t = manager.get(eval_rhs).cloned();
764
765 if let (Some(l), Some(r)) = (lhs_t, rhs_t)
766 && let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind)
767 {
768 if a >= b {
769 return manager.mk_true();
770 } else {
771 return manager.mk_false();
772 }
773 }
774
775 term
776 }
777 _ => {
779 manager.simplify(term)
781 }
782 }
783 }
784
785 fn instantiate_from_model(
787 &self,
788 quantifier_idx: usize,
789 model: &FxHashMap<TermId, TermId>,
790 manager: &mut TermManager,
791 ) -> Option<Instantiation> {
792 let quant = &self.quantifiers[quantifier_idx];
793 let mut subst: FxHashMap<Spur, TermId> = FxHashMap::default();
794
795 for &(name, sort) in &quant.bound_vars {
797 let mut found = None;
799 for (&term, &_value) in model {
800 if let Some(t) = manager.get(term)
801 && t.sort == sort
802 {
803 found = Some(term);
804 break;
805 }
806 }
807
808 let candidate = match found {
810 Some(t) => t,
811 None => {
812 if sort == manager.sorts.int_sort {
813 manager.mk_int(0)
814 } else if sort == manager.sorts.bool_sort {
815 manager.mk_true()
816 } else {
817 manager.mk_true()
819 }
820 }
821 };
822
823 subst.insert(name, candidate);
824 }
825
826 let ground_body = self.apply_substitution(quant.body, &subst, manager);
828
829 Some(Instantiation {
830 quantifier: quant.term,
831 substitution: subst,
832 result: ground_body,
833 })
834 }
835
836 pub fn stats(&self) -> MBQIStats {
838 MBQIStats {
839 num_quantifiers: self.quantifiers.len(),
840 total_instantiations: self.total_instantiation_count,
841 max_instantiations: self.max_total_instantiations,
842 unique_instantiations: self.generated_instantiations.len(),
843 }
844 }
845}
846
847#[derive(Debug, Clone)]
849pub struct MBQIStats {
850 pub num_quantifiers: usize,
852 pub total_instantiations: usize,
854 pub max_instantiations: usize,
856 pub unique_instantiations: usize,
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 #[test]
865 fn test_mbqi_new() {
866 let mbqi = MBQISolver::new();
867 assert!(mbqi.is_enabled());
868 assert_eq!(mbqi.quantifiers.len(), 0);
869 }
870
871 #[test]
872 fn test_mbqi_disable() {
873 let mut mbqi = MBQISolver::new();
874 mbqi.set_enabled(false);
875 assert!(!mbqi.is_enabled());
876
877 let model = FxHashMap::default();
878 let mut manager = TermManager::new();
879 let result = mbqi.check_with_model(&model, &mut manager);
880 assert!(matches!(result, MBQIResult::NoQuantifiers));
881 }
882
883 #[test]
884 fn test_mbqi_no_quantifiers() {
885 let mut mbqi = MBQISolver::new();
886 let model = FxHashMap::default();
887 let mut manager = TermManager::new();
888
889 let result = mbqi.check_with_model(&model, &mut manager);
890 assert!(matches!(result, MBQIResult::NoQuantifiers));
891 }
892
893 #[test]
894 fn test_mbqi_add_candidate() {
895 let mut mbqi = MBQISolver::new();
896 let manager = TermManager::new();
897
898 let sort = manager.sorts.int_sort;
899 mbqi.add_candidate(TermId::new(1), sort);
900 mbqi.add_candidate(TermId::new(2), sort);
901
902 let candidates = mbqi.get_candidates(sort);
903 assert_eq!(candidates.len(), 2);
904 }
905
906 #[test]
907 fn test_mbqi_stats() {
908 let mbqi = MBQISolver::new();
909 let stats = mbqi.stats();
910
911 assert_eq!(stats.num_quantifiers, 0);
912 assert_eq!(stats.total_instantiations, 0);
913 }
914
915 #[test]
916 fn test_mbqi_clear() {
917 let mut mbqi = MBQISolver::new();
918 let manager = TermManager::new();
919
920 mbqi.add_candidate(TermId::new(1), manager.sorts.int_sort);
921 mbqi.total_instantiation_count = 5;
922
923 mbqi.clear();
924
925 assert_eq!(mbqi.quantifiers.len(), 0);
926 assert_eq!(mbqi.total_instantiation_count, 0);
927 }
928
929 #[test]
930 fn test_mbqi_with_limit() {
931 let mbqi = MBQISolver::with_limit(100);
932 assert_eq!(mbqi.max_total_instantiations, 100);
933 }
934
935 #[test]
936 fn test_enumerate_combinations_empty() {
937 let mbqi = MBQISolver::new();
938 let candidates: Vec<Vec<TermId>> = vec![];
939 let combos = mbqi.enumerate_combinations(&candidates, 10);
940 assert_eq!(combos.len(), 1);
941 assert!(combos[0].is_empty());
942 }
943
944 #[test]
945 fn test_enumerate_combinations_single() {
946 let mbqi = MBQISolver::new();
947 let candidates = vec![vec![TermId::new(1), TermId::new(2)]];
948 let combos = mbqi.enumerate_combinations(&candidates, 10);
949 assert_eq!(combos.len(), 2);
950 }
951
952 #[test]
953 fn test_enumerate_combinations_multiple() {
954 let mbqi = MBQISolver::new();
955 let candidates = vec![
956 vec![TermId::new(1), TermId::new(2)],
957 vec![TermId::new(3), TermId::new(4)],
958 ];
959 let combos = mbqi.enumerate_combinations(&candidates, 10);
960 assert_eq!(combos.len(), 4);
962 }
963}