mirror of
				https://github.com/Z3Prover/z3
				synced 2025-10-31 03:32:28 +00:00 
			
		
		
		
	Contract bit information to large unit-intervals
This commit is contained in:
		
							parent
							
								
									0dae2d40b5
								
							
						
					
					
						commit
						3f8edb9aac
					
				
					 8 changed files with 346 additions and 118 deletions
				
			
		|  | @ -234,7 +234,7 @@ namespace polysat { | |||
|         m_lemmas.push_back(&cl); | ||||
| 
 | ||||
|         SASSERT(!empty()); | ||||
|         logger().begin_conflict(); | ||||
|         logger().begin_conflict(cl.name()); | ||||
|     } | ||||
| 
 | ||||
|     void conflict::init_by_viable_interval(pvar v) { | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ namespace polysat { | |||
|         backtrack _backtrack(fi.side_cond); | ||||
| 
 | ||||
|         fi.coeff = 1; | ||||
|         fi.src = c; | ||||
|         fi.src.push_back(c); | ||||
| 
 | ||||
|         // eval(lhs) = a1*v + eval(e1) = a1*v + b1
 | ||||
|         // eval(rhs) = a2*v + eval(e2) = a2*v + b2
 | ||||
|  | @ -135,7 +135,7 @@ namespace polysat { | |||
|         backtrack _backtrack(fi.side_cond); | ||||
| 
 | ||||
|         fi.coeff = 1; | ||||
|         fi.src = c; | ||||
|         fi.src.push_back(c); | ||||
| 
 | ||||
|         struct show { | ||||
|             forbidden_intervals& f; | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ namespace polysat { | |||
|     struct fi_record { | ||||
|         eval_interval               interval; | ||||
|         vector<signed_constraint>   side_cond; | ||||
|         signed_constraint           src; | ||||
|         vector<signed_constraint>   src; // only units may have multiple src (as they can consist of contracted bit constraints)
 | ||||
|         rational                    coeff; | ||||
| 
 | ||||
|         /** Create invalid fi_record */ | ||||
|  |  | |||
|  | @ -275,7 +275,7 @@ namespace polysat { | |||
|         } | ||||
|         else { | ||||
|             // forward propagation
 | ||||
|             SASSERT(!(pv.is_val() && qv.is_val() && rv.is_val())); | ||||
|             /*SASSERT(!(pv.is_val() && qv.is_val() && rv.is_val()));
 | ||||
|             if (qv.is_val() && !rv.is_val()) { | ||||
|                 const rational& qr = qv.val(); | ||||
|                 if (qr >= m.power_of_2()) | ||||
|  | @ -285,7 +285,7 @@ namespace polysat { | |||
|                     const rational& pr = pv.val(); | ||||
|                     return s.mk_clause(~lshr, ~s.eq(p(), m.mk_val(pr)), ~s.eq(q(), m.mk_val(qr)), s.eq(r(), m.mk_val(machine_div(pr, rational::power_of_two(qr.get_unsigned())))), true); | ||||
|                 } | ||||
|             } | ||||
|             }*/ | ||||
|         } | ||||
|         return {}; | ||||
|     } | ||||
|  | @ -365,7 +365,7 @@ namespace polysat { | |||
|         } | ||||
|         else { | ||||
|             // forward propagation
 | ||||
|             SASSERT(!(pv.is_val() && qv.is_val() && rv.is_val())); | ||||
|             /*SASSERT(!(pv.is_val() && qv.is_val() && rv.is_val()));
 | ||||
|             if (qv.is_val() && !rv.is_val()) { | ||||
|                 const rational& qr = qv.val(); | ||||
|                 if (qr >= m.power_of_2()) | ||||
|  | @ -375,7 +375,7 @@ namespace polysat { | |||
|                     const rational& pr = pv.val(); | ||||
|                     return s.mk_clause(~shl, ~s.eq(p(), m.mk_val(pr)), ~s.eq(q(), m.mk_val(qr)), s.eq(r(), m.mk_val(rational::power_of_two(qr.get_unsigned()) * pr)), true); | ||||
|                 } | ||||
|             } | ||||
|             }*/ | ||||
|         } | ||||
|         return {}; | ||||
|     } | ||||
|  | @ -544,16 +544,25 @@ namespace polysat { | |||
|                 bool rb = rv.val().get_bit(i); | ||||
|                 if (rb == (pb && qb)) | ||||
|                     continue; | ||||
|                 if (pb && qb && !rb) | ||||
|                     return s.mk_clause(~andc, ~s.bit(p(), i), ~s.bit(q(), i), s.bit(r(), i), true); | ||||
|                 else if (!pb && rb) | ||||
|                     return s.mk_clause(~andc, s.bit(p(), i), ~s.bit(r(), i), true); | ||||
|                 else if (!qb && rb) | ||||
|                     return s.mk_clause(~andc, s.bit(q(), i), ~s.bit(r(), i), true); | ||||
|                 if (pb && qb && !rb) { | ||||
|                     verbose_stream() << "Conflict propagation " << pv << " (" << p() << ") & " << qv << " (" << q() << ") = " << bitwise_and(pv.val(), qv.val()) << " (" << r() << ")\n"; | ||||
|                     verbose_stream() << "1 & 1 = 0 bit " << i << "\n"; | ||||
|                     s.add_clause(s.mk_clause(~andc, ~s.bit(p(), i), ~s.bit(q(), i), s.bit(r(), i), true)); | ||||
|                 } | ||||
|                 else if (!pb && rb) { | ||||
|                     verbose_stream() << "Conflict propagation " << pv << " (" << p() << ") & " << qv << " (" << q() << ") = " << bitwise_and(pv.val(), qv.val()) << " (" << r() << ")\n"; | ||||
|                     verbose_stream() << "0 & ? = 1 bit " << i << "\n"; | ||||
|                     s.add_clause(s.mk_clause(~andc, s.bit(p(), i), ~s.bit(r(), i), true)); | ||||
|                 } | ||||
|                 else if (!qb && rb) { | ||||
|                     verbose_stream() << "Conflict propagation " << pv << " (" << p() << ") & " << qv << " (" << q() << ") = " << bitwise_and(pv.val(), qv.val()) << " (" << r() << ")\n"; | ||||
|                     verbose_stream() << "? & 0 = 1 bit " << i << "\n"; | ||||
|                     s.add_clause(s.mk_clause(~andc, s.bit(q(), i), ~s.bit(r(), i), true)); | ||||
|                 } | ||||
|                 else | ||||
|                     UNREACHABLE(); | ||||
|                 return {}; | ||||
|             } | ||||
|             return {}; | ||||
|         } | ||||
| 
 | ||||
|         // Propagate r if p or q are 0
 | ||||
|  | @ -562,8 +571,9 @@ namespace polysat { | |||
|         if (qv.is_zero() && !rv.is_zero())  // rv not necessarily fully evaluated
 | ||||
|             return s.mk_clause(~andc, s.ule(r(), q()), true); | ||||
|         // p = a && q = b ==> r = a & b
 | ||||
|         if (pv.is_val() && qv.is_val() && !rv.is_val()) | ||||
|         /*if (pv.is_val() && qv.is_val() && !rv.is_val()) {
 | ||||
|             return s.mk_clause(~andc, ~s.eq(p(), pv), ~s.eq(q(), qv), s.eq(r(), bitwise_and(pv.val(), qv.val())), true); | ||||
|         }*/ | ||||
| 
 | ||||
|         return {}; | ||||
|     } | ||||
|  | @ -652,8 +662,8 @@ namespace polysat { | |||
| 
 | ||||
|         // forward propagation: p assigned  ==>  r = pseudo_inverse(eval(p))
 | ||||
|         // TODO: (later) this should be propagated instead of adding a clause
 | ||||
|         if (pv.is_val() && !rv.is_val()) | ||||
|             return s.mk_clause(~invc, ~s.eq(p(), pv), s.eq(r(), pv.val().pseudo_inverse(m.power_of_2())), true); | ||||
|         /*if (pv.is_val() && !rv.is_val())
 | ||||
|             return s.mk_clause(~invc, ~s.eq(p(), pv), s.eq(r(), pv.val().pseudo_inverse(m.power_of_2())), true);*/ | ||||
| 
 | ||||
|         if (!pv.is_val() || !rv.is_val()) | ||||
|             return {}; | ||||
|  |  | |||
|  | @ -443,7 +443,9 @@ namespace polysat { | |||
| #if ENABLE_LINEAR_SOLVER | ||||
|         m_linear_solver.push(); | ||||
| #endif | ||||
| #if 0 | ||||
|         m_fixed_bits.push(); | ||||
| #endif | ||||
|     } | ||||
| 
 | ||||
|     void solver::pop_levels(unsigned num_levels) { | ||||
|  | @ -458,8 +460,9 @@ namespace polysat { | |||
| #if ENABLE_LINEAR_SOLVER | ||||
|         m_linear_solver.pop(num_levels); | ||||
| #endif | ||||
| #if 0 | ||||
|         m_fixed_bits.pop(); | ||||
|          | ||||
| #endif | ||||
|         while (num_levels > 0) { | ||||
|             switch (m_trail.back()) { | ||||
|             case trail_instr_t::qhead_i: { | ||||
|  |  | |||
|  | @ -78,6 +78,7 @@ namespace polysat { | |||
|         if (m_alloc.empty()) | ||||
|             return alloc(entry); | ||||
|         auto* e = m_alloc.back(); | ||||
|         e->src.reset(); | ||||
|         e->side_cond.reset(); | ||||
|         e->coeff = 1; | ||||
|         e->refined = nullptr; | ||||
|  | @ -311,8 +312,9 @@ namespace polysat { | |||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     bool viable::refine_viable(pvar v, rational const& val) { | ||||
|         return refine_equal_lin(v, val) && refine_disequal_lin(v, val); | ||||
|     template<bool FORWARD> | ||||
|     bool viable::refine_viable(pvar v, rational const& val, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications) { | ||||
|         return refine_bits<FORWARD>(v, val, fixed, justifications) && refine_equal_lin(v, val) && refine_disequal_lin(v, val); | ||||
|     } | ||||
| 
 | ||||
| namespace { | ||||
|  | @ -518,6 +520,35 @@ namespace { | |||
|     } | ||||
| } | ||||
| 
 | ||||
|     template<bool FORWARD> | ||||
|     bool viable::refine_bits(pvar v, rational const& val, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications) { | ||||
| 
 | ||||
|         pdd v_pdd = s.var(v); | ||||
| 
 | ||||
|         // TODO: We might also extend simultaneously up and downwards if we want the actual interval (however, this might make use of more fixed bits and is weaker - worse - therefore)
 | ||||
|         entry* ne = alloc_entry(); | ||||
|         rational new_val = extend_by_bits<FORWARD>(v_pdd, val, fixed, justifications, ne->src, ne->side_cond); | ||||
| 
 | ||||
|         if (new_val == val) { | ||||
|             m_alloc.push_back(ne); | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         ne->coeff = 1; | ||||
|         if (FORWARD) { | ||||
|             LOG("refine-bits for v" << v << " [" << val << ", " << new_val << "["); | ||||
|             ne->interval = eval_interval::proper(v_pdd.manager().mk_val(val), val, v_pdd.manager().mk_val(new_val), new_val); | ||||
|         } | ||||
|         else { | ||||
|             rational upper_bound = val == v_pdd.manager().max_value() ? rational::zero() : val + 1;; | ||||
|             LOG("refine-bits for v" << v << " [" << new_val << ", " << upper_bound << "["); | ||||
|             ne->interval = eval_interval::proper(v_pdd.manager().mk_val(new_val), new_val, v_pdd.manager().mk_val(upper_bound), upper_bound); | ||||
|         } | ||||
|         SASSERT(ne->interval.currently_contains(val)); | ||||
|         intersect(v, ne); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /**
 | ||||
|      * Traverse all interval constraints with coefficients to check whether current value 'val' for | ||||
|      * 'v' is feasible. If not, extract a (maximal) interval to block 'v' from being assigned val. | ||||
|  | @ -545,7 +576,11 @@ namespace { | |||
|         do { | ||||
|             rational coeff_val = mod(e->coeff * val, mod_value); | ||||
|             if (e->interval.currently_contains(coeff_val)) { | ||||
|                 LOG("refine-equal-lin for v" << v << " in src: " << lit_pp(s, e->src)); | ||||
|                 IF_LOGGING( | ||||
|                     verbose_stream() << "refine-equal-lin for v" << v << " in src: "; | ||||
|                     for (const auto& src : e->src) | ||||
|                         verbose_stream() << lit_pp(s, src) << "\n"; | ||||
|                 ); | ||||
|                 LOG("forbidden interval v" << v << " " << num_pp(s, v, val) << "    " << num_pp(s, v, e->coeff, true) << " * " << e->interval); | ||||
| 
 | ||||
|                 if (mod(e->interval.hi_val() + 1, mod_value) == e->interval.lo_val()) { | ||||
|  | @ -668,7 +703,11 @@ namespace { | |||
|         m_diseq_lin[v] = m_diseq_lin[v]->next(); | ||||
| 
 | ||||
|         do { | ||||
|             LOG("refine-disequal-lin for src: " << e->src); | ||||
|             IF_LOGGING( | ||||
|                     verbose_stream() << "refine-disequal-lin for v" << v << " in src: "; | ||||
|                     for (const auto& src : e->src) | ||||
|                         verbose_stream() << lit_pp(s, src) << "\n"; | ||||
|             ); | ||||
|             // We compute an interval if the concrete value 'val' violates the constraint:
 | ||||
|             //      p*val + q >  r*val + s  if e->src.is_positive()
 | ||||
|             //      p*val + q >= r*val + s  if e->src.is_negative()
 | ||||
|  | @ -684,7 +723,7 @@ namespace { | |||
|             rational const b = mod(r * val + s_, mod_value); | ||||
|             rational const np = mod_value - p; | ||||
|             rational const nr = mod_value - r; | ||||
|             int const corr = e->src.is_negative() ? 1 : 0; | ||||
|             int const corr = e->src[0].is_negative() ? 1 : 0; | ||||
| 
 | ||||
|             auto delta_l = [&](rational const& val) { | ||||
|                 rational num = a - b + corr; | ||||
|  | @ -717,7 +756,7 @@ namespace { | |||
|                 return std::min(max_value - val, dmax); | ||||
|             }; | ||||
| 
 | ||||
|             if (a > b || (e->src.is_negative() && a == b)) { | ||||
|             if (a > b || (e->src[0].is_negative() && a == b)) { | ||||
|                 rational lo = val - delta_l(val); | ||||
|                 rational hi = val + delta_u(val) + 1; | ||||
| 
 | ||||
|  | @ -742,72 +781,185 @@ namespace { | |||
|         while (e != first); | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     bool viable::quick_bit_check(pvar v) { | ||||
| #if 0 | ||||
|         return true; | ||||
| #endif | ||||
| 
 | ||||
|     // Skips all values that are not feasible w.r.t. fixed bits
 | ||||
|     template<bool FORWARD> | ||||
|     rational viable::extend_by_bits(const pdd& var, const rational& bound, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications, vector<signed_constraint>& src, vector<signed_constraint>& side_cond) const { | ||||
|         unsigned k = var.power_of_2(); | ||||
|         if (fixed.empty()) | ||||
|             return bound; | ||||
| 
 | ||||
|         SASSERT(k == fixed.size()); | ||||
| 
 | ||||
|         auto add_justification = [&](unsigned i) { | ||||
|             auto& to_add = justifications[i]; | ||||
|             SASSERT(!to_add.empty()); | ||||
|             for (auto& add : to_add) { | ||||
|                 // TODO: Check for duplicates; maybe we add the same src/side_cond over and over again
 | ||||
|                 for (auto& sc : add->side_cond) | ||||
|                     side_cond.push_back(sc); | ||||
|                 for (auto& c : add->src) | ||||
|                     src.push_back(c); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         unsigned firstFail; | ||||
|         for (firstFail = k; firstFail > 0; firstFail--) { | ||||
|             if (fixed[firstFail - 1] != l_undef) { | ||||
|                 lbool current = bound.get_bit(firstFail - 1) ? l_true : l_false; | ||||
|                 if (current != fixed[firstFail - 1]) | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|         if (firstFail == 0) | ||||
|             return bound; // the value is feasible according to fixed bits
 | ||||
| 
 | ||||
|         svector<lbool> new_bound(fixed.size()); | ||||
| 
 | ||||
|         for (unsigned i = 0; i < firstFail; i++) { | ||||
|             if (fixed[i] != l_undef) { | ||||
|                 SASSERT(fixed[i] == l_true || fixed[i] == l_false); | ||||
|                 new_bound[i] = fixed[i]; | ||||
|                 if (FORWARD != (fixed[i] == l_false)) | ||||
|                     add_justification(i); // Minimize number of responsible fixed bits; we only add those justifications we need for sure
 | ||||
|             } | ||||
|             else | ||||
|                 new_bound[i] = FORWARD ? l_false : l_true; | ||||
|         } | ||||
| 
 | ||||
|         bool carry = fixed[firstFail - 1] == (FORWARD ? l_false : l_true); | ||||
| 
 | ||||
|         for (unsigned i = firstFail; i < new_bound.size(); i++) { | ||||
|             if (fixed[i] == l_undef) { | ||||
|                 lbool current = bound.get_bit(i) ? l_true : l_false; | ||||
|                 if (carry) { | ||||
|                     if (FORWARD) { | ||||
|                         if (current == l_false) { | ||||
|                             new_bound[i] = l_true; | ||||
|                             carry = false; | ||||
|                         } | ||||
|                         else | ||||
|                             new_bound[i] = l_false; | ||||
|                     } | ||||
|                     else { | ||||
|                         if (current == l_true) { | ||||
|                             new_bound[i] = l_false; | ||||
|                             carry = false; | ||||
|                         } | ||||
|                         else | ||||
|                             new_bound[i] = l_true; | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                     new_bound[i] = current; | ||||
|             } | ||||
|             else { | ||||
|                 new_bound[i] = fixed[i]; | ||||
|                 if (carry) | ||||
|                     add_justification(i); // Again, we need this justification; if carry is false we don't need it
 | ||||
|             } | ||||
|         } | ||||
|         if (carry) { | ||||
|             // We covered everything
 | ||||
|             /*if (FORWARD)
 | ||||
|                 return rational::power_of_two(k); | ||||
|             else*/ | ||||
|                 return rational::zero(); | ||||
|         } | ||||
| 
 | ||||
|         // TODO: Directly convert new_bound in rational?
 | ||||
|         rational ret = rational::zero(); | ||||
|         for (unsigned i = new_bound.size(); i > 0; i--) { | ||||
|             ret *= 2; | ||||
|             SASSERT(new_bound[i - 1] != l_undef); | ||||
|             ret += new_bound[i - 1] == l_true ? 1 : 0; | ||||
|         } | ||||
|         if (!FORWARD) | ||||
|             return ret + 1; | ||||
|         return ret; | ||||
|     } | ||||
| 
 | ||||
|     // returns true iff no conflict was encountered
 | ||||
|     bool viable::collect_bit_information(pvar v, bool add_conflict, svector<lbool>& fixed, vector<ptr_vector<entry>>& justifications) { | ||||
| 
 | ||||
|         pdd p = s.var(v); | ||||
|         // maybe pass them as arguments rather than having them as fields...
 | ||||
|         fixed.clear(); | ||||
|         justifications.clear(); | ||||
|         fixed.resize(p.power_of_2(), l_undef); | ||||
|         justifications.resize(p.power_of_2(), ptr_vector<entry>()); | ||||
| 
 | ||||
|         auto* e = m_equal_lin[v]; | ||||
|         auto* first = e; | ||||
|         if (!e) | ||||
|             return true; | ||||
|          | ||||
|         pdd p = s.var(v); | ||||
| 
 | ||||
|         clause_builder builder(s, "bit check"); | ||||
|         svector<lbool> fixed(p.power_of_2(), l_undef); | ||||
|         vector<ptr_vector<entry>> justifications(p.power_of_2(), ptr_vector<entry>()); | ||||
|         vector<std::pair<entry*, trailing_bits>> postponed; | ||||
|          | ||||
| 
 | ||||
|         auto add_entry = [&builder](entry* e) { | ||||
|             for (const auto& sc : e->side_cond) { | ||||
|                 builder.insert_eval(~sc); | ||||
|                 LOG("Side cond: " << sc); | ||||
|             } | ||||
|             builder.insert_eval(~e->src); | ||||
|             LOG("Adding to core: " << e->src); | ||||
|             SASSERT(e->src.size() == 1); | ||||
|             for (const auto& src : e->src) { | ||||
|                 builder.insert_eval(~src); | ||||
|                 LOG("Adding to core: " << e->src); | ||||
|             } | ||||
|         }; | ||||
|          | ||||
|         auto add_entry_list = [add_entry](const ptr_vector<entry>& list) { | ||||
|             for (const auto& e : list)  | ||||
|             for (const auto& e : list) | ||||
|                 add_entry(e); | ||||
|         }; | ||||
|          | ||||
|         unsigned largest_mask = 0; | ||||
|          | ||||
|         do { | ||||
|             if (e->src.size() != 1) { | ||||
|                 e = e->next(); | ||||
|                 continue; | ||||
|             } | ||||
|             signed_constraint& src = e->src[0]; | ||||
|             single_bit bit; | ||||
|             trailing_bits mask; | ||||
|             if (e->src->is_ule() &&  | ||||
|                 simplify_clause::get_bit(s.subst(e->src->to_ule().lhs()), s.subst(e->src->to_ule().rhs()), p, bit, e->src.is_positive()) && p.is_var()) { | ||||
|             if (src->is_ule() && | ||||
|                 simplify_clause::get_bit(s.subst(src->to_ule().lhs()), s.subst(src->to_ule().rhs()), p, bit, src.is_positive()) && p.is_var()) { | ||||
|                  | ||||
|                 lbool prev = fixed[bit.position]; | ||||
|                 fixed[bit.position] = bit.positive ? l_true : l_false; | ||||
|                 //verbose_stream() << "Setting bit " << bit.position << " to " << bit.positive << " because of " << e->src << "\n"; 
 | ||||
|                 if (prev != l_undef && fixed[bit.position] != prev) { | ||||
|                     LOG("Bit conflicting " << e->src << " with " << justifications[bit.position][0]->src); | ||||
|                     add_entry_list(justifications[bit.position]); | ||||
|                     add_entry(e); | ||||
|                     s.set_conflict(*builder.build()); | ||||
|                     verbose_stream() << "Bit conflicting " << e->src << " with " << justifications[bit.position][0]->src << "\n"; | ||||
|                     if (add_conflict) { | ||||
|                         add_entry_list(justifications[bit.position]); | ||||
|                         add_entry(e); | ||||
|                         s.set_conflict(*builder.build()); | ||||
|                     } | ||||
|                     return false; | ||||
|                 } | ||||
|                 // just override; we prefer bit constraints over parity as those are easier for subsumption to remove
 | ||||
|                 // verbose_stream() << "Adding bit constraint: " <<  e->src[0] << " (" << bit.position << ")\n";
 | ||||
|                 justifications[bit.position].clear(); | ||||
|                 justifications[bit.position].push_back(e); | ||||
|             } | ||||
|             else if ((e->src->is_eq() || e->src.is_diseq()) &&  | ||||
|                 simplify_clause::get_trailing_mask(s.subst(e->src->to_ule().lhs()), s.subst(e->src->to_ule().rhs()), p, mask, e->src.is_positive()) && p.is_var()) { | ||||
|             else if ((src->is_eq() || src.is_diseq()) && | ||||
|                 simplify_clause::get_trailing_mask(s.subst(src->to_ule().lhs()), s.subst(src->to_ule().rhs()), p, mask, src.is_positive()) && p.is_var()) { | ||||
|                  | ||||
|                 if (e->src.is_positive()) { | ||||
|                 if (src.is_positive()) { | ||||
|                     for (unsigned i = 0; i < mask.length; i++) { | ||||
|                         lbool prev = fixed[i]; | ||||
|                         fixed[i] = mask.bits.get_bit(i) ? l_true : l_false; | ||||
|                         //verbose_stream() << "Setting bit " << i << " to " << mask.bits.get_bit(i) << " because of parity " << e->src << "\n";
 | ||||
|                         if (prev != l_undef) { | ||||
|                             if (fixed[i] != prev) { | ||||
|                                 LOG("Positive parity conflicting " << e->src << " with " << justifications[i][0]->src); | ||||
|                                 add_entry_list(justifications[i]); | ||||
|                                 add_entry(e); | ||||
|                                 s.set_conflict(*builder.build()); | ||||
|                                 verbose_stream() << "Positive parity conflicting " << e->src << " with " << justifications[i][0]->src << "\n"; | ||||
|                                 if (add_conflict) { | ||||
|                                     add_entry_list(justifications[i]); | ||||
|                                     add_entry(e); | ||||
|                                     s.set_conflict(*builder.build()); | ||||
|                                 } | ||||
|                                 return false; | ||||
|                             } | ||||
|                             else { | ||||
|  | @ -816,12 +968,14 @@ namespace { | |||
|                                     largest_mask = mask.length; | ||||
|                                     justifications[i].clear(); | ||||
|                                     justifications[i].push_back(e); | ||||
|                                     // verbose_stream() << "Adding parity constraint: " <<  e->src[0] << " (" << i << ")\n";
 | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         else { | ||||
|                             SASSERT(justifications[i].empty()); | ||||
|                             justifications[i].push_back(e); | ||||
|                             // verbose_stream() << "Adding parity constraint: " <<  e->src[0] << " (" << i << ")\n";
 | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | @ -859,11 +1013,13 @@ namespace { | |||
|                 if (i == neg.second.length) { | ||||
|                     if (indet == 0) { | ||||
|                         // Already false
 | ||||
|                         LOG("Found conflict with constraint " << neg.first->src); | ||||
|                         for (unsigned k = 0; k < neg.second.length; k++) | ||||
|                             add_entry_list(justifications[k]); | ||||
|                         add_entry(neg.first); | ||||
|                         s.set_conflict(*builder.build()); | ||||
|                         verbose_stream() << "Found conflict with constraint " << neg.first->src << "\n"; | ||||
|                         if (add_conflict) { | ||||
|                             for (unsigned k = 0; k < neg.second.length; k++) | ||||
|                                 add_entry_list(justifications[k]); | ||||
|                             add_entry(neg.first); | ||||
|                             s.set_conflict(*builder.build()); | ||||
|                         } | ||||
|                         return false; | ||||
|                     } | ||||
|                     else if (indet == 1) { | ||||
|  | @ -891,10 +1047,17 @@ namespace { | |||
|     } | ||||
| 
 | ||||
|     bool viable::has_viable(pvar v) { | ||||
| 
 | ||||
|         svector<lbool> fixed; | ||||
|         vector<ptr_vector<entry>> justifications; | ||||
| 
 | ||||
|         if (!collect_bit_information(v, false, fixed, justifications)) | ||||
|             return false; | ||||
| 
 | ||||
|         refined: | ||||
|         auto* e = m_units[v]; | ||||
| 
 | ||||
| #define CHECK_RETURN(val) { if (refine_viable(v, val)) return true; else goto refined; } | ||||
| #define CHECK_RETURN(val) { if (refine_viable<true>(v, val, fixed, justifications)) return true; else goto refined; } | ||||
| 
 | ||||
|         if (!e) | ||||
|             CHECK_RETURN(rational::zero()); | ||||
|  | @ -929,9 +1092,15 @@ namespace { | |||
|     } | ||||
| 
 | ||||
|     bool viable::is_viable(pvar v, rational const& val) { | ||||
| 
 | ||||
|         svector<lbool> fixed; | ||||
|         vector<ptr_vector<entry>> justifications; | ||||
| 
 | ||||
|         if (!collect_bit_information(v, false, fixed, justifications)) | ||||
|             return false; | ||||
|         auto* e = m_units[v]; | ||||
|         if (!e) | ||||
|             return refine_viable(v, val); | ||||
|             return refine_viable<true>(v, val, fixed, justifications); | ||||
|         entry* first = e; | ||||
|         entry* last = first->prev(); | ||||
|         if (last->interval.currently_contains(val)) | ||||
|  | @ -940,9 +1109,9 @@ namespace { | |||
|             if (e->interval.currently_contains(val)) | ||||
|                 return false; | ||||
|             if (val < e->interval.lo_val()) | ||||
|                 return refine_viable(v, val); | ||||
|                 return refine_viable<true>(v, val, fixed, justifications); | ||||
|         } | ||||
|         return refine_viable(v, val); | ||||
|         return refine_viable<true>(v, val, fixed, justifications); | ||||
|     } | ||||
| 
 | ||||
|     find_t viable::find_viable(pvar v, rational& lo) { | ||||
|  | @ -985,12 +1154,14 @@ namespace { | |||
|                     auto const& hi = e->interval.hi(); | ||||
|                     if (lo.is_val() && hi.is_val()) { | ||||
|                         if (out_c.empty() && lo.val() > hi.val()) { | ||||
|                             out_c.push_back(e->src); | ||||
|                             for (const auto& src : e->src) | ||||
|                                 out_c.push_back(src); | ||||
|                             out_hi = lo.val() - 1; | ||||
|                             found = true; | ||||
|                         } | ||||
|                         else if (!out_c.empty() && lo.val() <= out_hi && out_hi < hi.val()) { | ||||
|                             out_c.push_back(e->src); | ||||
|                             for (const auto& src : e->src) | ||||
|                                 out_c.push_back(src); | ||||
|                             out_hi = lo.val() - 1; | ||||
|                             found = true;                         | ||||
|                         } | ||||
|  | @ -1019,12 +1190,14 @@ namespace { | |||
|                     auto const& hi = e->interval.hi(); | ||||
|                     if (lo.is_val() && hi.is_val()) { | ||||
|                         if (out_c.empty() && hi.val() != 0 && (lo.val() == 0 || lo.val() > hi.val())) { | ||||
|                             out_c.push_back(e->src); | ||||
|                             for (const auto& src : e->src) | ||||
|                                 out_c.push_back(src); | ||||
|                             out_lo = hi.val(); | ||||
|                             found = true; | ||||
|                         } | ||||
|                         else if (!out_c.empty() && lo.val() <= out_lo && out_lo < hi.val()) { | ||||
|                             out_c.push_back(e->src); | ||||
|                             for (const auto& src : e->src) | ||||
|                                 out_c.push_back(src); | ||||
|                             out_lo = hi.val(); | ||||
|                             found = true; | ||||
|                         } | ||||
|  | @ -1048,14 +1221,17 @@ namespace { | |||
|         if (!e) | ||||
|             return false; | ||||
| 
 | ||||
|         bool found = false; | ||||
| 
 | ||||
|         do { | ||||
|             if (e->src == c)  | ||||
|             found = e->src.contains(c); | ||||
|             if (found) | ||||
|                 break; | ||||
|             e = e->next(); | ||||
|         } | ||||
|         while (e != first); | ||||
| 
 | ||||
|         if (e->src != c) | ||||
|         if (!found) | ||||
|             return false; | ||||
|         entry const* e0 = e; | ||||
|         // display_one(verbose_stream() << "selected e0 = ", v, e0) << "\n";
 | ||||
|  | @ -1101,7 +1277,8 @@ namespace { | |||
|                     out_c.push_back(sc); | ||||
|                 } | ||||
|                 // verbose_stream() << "E: " << lit_pp(s, e->src) << "\n";
 | ||||
|                 out_c.push_back(e->src); | ||||
|                 for (const auto& src : e->src) | ||||
|                     out_c.push_back(src); | ||||
|             } | ||||
|             e = n; | ||||
|         } | ||||
|  | @ -1117,8 +1294,13 @@ namespace { | |||
| 
 | ||||
|     template <query_t mode> | ||||
|     lbool viable::query(pvar v, typename query_result<mode>::result_t& result) { | ||||
|         if (!quick_bit_check(v)) | ||||
|             return l_false; | ||||
| 
 | ||||
|         svector<lbool> fixed; | ||||
|         vector<ptr_vector<entry>> justifications; | ||||
| 
 | ||||
|         if (!collect_bit_information(v, true, fixed, justifications)) | ||||
|             return l_false; // conflict already added
 | ||||
| 
 | ||||
|         // max number of interval refinements before falling back to the univariate solver
 | ||||
|         unsigned const refinement_budget = 1000; | ||||
|         unsigned refinements = refinement_budget; | ||||
|  | @ -1127,11 +1309,11 @@ namespace { | |||
|             lbool res = l_undef; | ||||
| 
 | ||||
|             if constexpr (mode == query_t::find_viable) | ||||
|                 res = query_find(v, result.first, result.second); | ||||
|                 res = query_find(v, result.first, result.second, fixed, justifications); | ||||
|             else if constexpr (mode == query_t::min_viable) | ||||
|                 res = query_min(v, result); | ||||
|                 res = query_min(v, result, fixed, justifications); | ||||
|             else if constexpr (mode == query_t::max_viable) | ||||
|                 res = query_max(v, result); | ||||
|                 res = query_max(v, result, fixed, justifications); | ||||
|             else if constexpr (mode == query_t::has_viable) { | ||||
|                 NOT_IMPLEMENTED_YET(); | ||||
|             } | ||||
|  | @ -1147,7 +1329,7 @@ namespace { | |||
|         return query_fallback<mode>(v, result); | ||||
|     } | ||||
|      | ||||
|     lbool viable::query_find(pvar v, rational& lo, rational& hi) { | ||||
|     lbool viable::query_find(pvar v, rational& lo, rational& hi, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications) { | ||||
|         auto const& max_value = s.var2pdd(v).max_value(); | ||||
|         lbool const refined = l_undef; | ||||
| 
 | ||||
|  | @ -1158,9 +1340,9 @@ namespace { | |||
|         hi = max_value; | ||||
|          | ||||
|         auto* e = m_units[v]; | ||||
|         if (!e && !refine_viable(v, lo)) | ||||
|         if (!e && !refine_viable<true>(v, lo, fixed, justifications)) | ||||
|             return refined; | ||||
|         if (!e && !refine_viable(v, hi)) | ||||
|         if (!e && !refine_viable<false>(v, hi, fixed, justifications)) | ||||
|             return refined; | ||||
|         if (!e) | ||||
|             return l_true; | ||||
|  | @ -1177,9 +1359,9 @@ namespace { | |||
|         if (last->interval.lo_val() < last->interval.hi_val() && | ||||
|             last->interval.hi_val() < max_value) { | ||||
|             lo = last->interval.hi_val(); | ||||
|             if (!refine_viable(v, lo)) | ||||
|             if (!refine_viable<true>(v, lo, fixed, justifications)) | ||||
|                 return refined; | ||||
|             if (!refine_viable(v, max_value)) | ||||
|             if (!refine_viable<false>(v, max_value, fixed, justifications)) | ||||
|                 return refined; | ||||
|             return l_true; | ||||
|         } | ||||
|  | @ -1211,18 +1393,18 @@ namespace { | |||
|         } | ||||
|         while (e != last); | ||||
| 
 | ||||
|         if (!refine_viable(v, lo)) | ||||
|         if (!refine_viable<true>(v, lo, fixed, justifications)) | ||||
|             return refined; | ||||
|         if (!refine_viable(v, hi)) | ||||
|         if (!refine_viable<false>(v, hi, fixed, justifications)) | ||||
|             return refined; | ||||
|         return l_true; | ||||
|     } | ||||
| 
 | ||||
|     lbool viable::query_min(pvar v, rational& lo) { | ||||
|     lbool viable::query_min(pvar v, rational& lo, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications) { | ||||
|         // TODO: should be able to deal with UNSAT case; since also min_viable has to deal with it due to fallback solver
 | ||||
|         lo = 0; | ||||
|         entry* e = m_units[v]; | ||||
|         if (!e && !refine_viable(v, lo)) | ||||
|         if (!e && !refine_viable<true>(v, lo, fixed, justifications)) | ||||
|             return l_undef; | ||||
|         if (!e) | ||||
|             return l_true; | ||||
|  | @ -1237,17 +1419,17 @@ namespace { | |||
|             e = e->next(); | ||||
|         } | ||||
|         while (e != first); | ||||
|         if (!refine_viable(v, lo)) | ||||
|         if (!refine_viable<true>(v, lo, fixed, justifications)) | ||||
|             return l_undef; | ||||
|         SASSERT(is_viable(v, lo)); | ||||
|         return l_true; | ||||
|     } | ||||
| 
 | ||||
|     lbool viable::query_max(pvar v, rational& hi) { | ||||
|     lbool viable::query_max(pvar v, rational& hi, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications) { | ||||
|         // TODO: should be able to deal with UNSAT case; since also max_viable has to deal with it due to fallback solver
 | ||||
|         hi = s.var2pdd(v).max_value(); | ||||
|         auto* e = m_units[v]; | ||||
|         if (!e && !refine_viable(v, hi)) | ||||
|         if (!e && !refine_viable<false>(v, hi, fixed, justifications)) | ||||
|             return l_undef; | ||||
|         if (!e) | ||||
|             return l_true; | ||||
|  | @ -1260,7 +1442,7 @@ namespace { | |||
|             e = e->prev(); | ||||
|         } | ||||
|         while (e != last); | ||||
|         if (!refine_viable(v, hi)) | ||||
|         if (!refine_viable<false>(v, hi, fixed, justifications)) | ||||
|             return l_undef; | ||||
|         SASSERT(is_viable(v, hi)); | ||||
|         return l_true; | ||||
|  | @ -1282,13 +1464,15 @@ namespace { | |||
|             entry const* origin = e; | ||||
|             while (origin->refined) | ||||
|                 origin = origin->refined; | ||||
|             signed_constraint const c = origin->src; | ||||
|             sat::literal const lit = c.blit(); | ||||
|             if (!added.contains(lit)) { | ||||
|                 added.insert(lit); | ||||
|                 LOG("Adding " << lit_pp(s, lit)); | ||||
|                 IF_VERBOSE(10, verbose_stream() << ";; " << lit_pp(s, lit) << "\n"); | ||||
|                 c.add_to_univariate_solver(v, s, *us, lit.to_uint()); | ||||
|             for (const auto& src : origin->src) { | ||||
|                 sat::literal const lit = src.blit(); | ||||
|                 if (!added.contains(lit)) { | ||||
|                     added.insert(lit); | ||||
|                     LOG("Adding " << lit_pp(s, lit)); | ||||
|                     IF_VERBOSE(10, verbose_stream() << ";; " << lit_pp(s, lit) << "\n"); | ||||
|                     verbose_stream() << ";; " << lit_pp(s, lit) << "\n"; | ||||
|                     src.add_to_univariate_solver(v, s, *us, lit.to_uint()); | ||||
|                 } | ||||
|             } | ||||
|             e = e->next(); | ||||
|         } | ||||
|  | @ -1391,9 +1575,23 @@ namespace { | |||
|         entry const* first = e; | ||||
|         SASSERT(e); | ||||
|         // If there is a full interval, all others would have been removed
 | ||||
|         SASSERT(!e->interval.is_full() || e->next() == e); | ||||
|         SASSERT(e->interval.is_full() || all_of(*e, [](entry const& f) { return !f.interval.is_full(); })); | ||||
|         clause_builder lemma(s); | ||||
|         if (first->interval.is_full()) { | ||||
|             SASSERT(first->next() == first); | ||||
|             for (auto sc : first->side_cond) | ||||
|                 lemma.insert_eval(~sc); | ||||
|             for (const auto& src : first->src) { | ||||
|                 lemma.insert(~src); | ||||
|                 core.insert(src); | ||||
|                 core.insert_vars(src); | ||||
|             } | ||||
|             core.add_lemma("viable", lemma.build()); | ||||
|             core.logger().log(inf_fi(*this, v)); | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         SASSERT(all_of(*first, [](entry const& f) { return !f.interval.is_full(); })); | ||||
| 
 | ||||
|         do { | ||||
|             // Build constraint: upper bound of each interval is not contained in the next interval,
 | ||||
|             // using the equivalence:  t \in [l;h[  <=>  t-l < h-l
 | ||||
|  | @ -1439,15 +1637,16 @@ namespace { | |||
| 
 | ||||
|             // verbose_stream() << e->interval << " " << e->side_cond << " " << e->src << ";\n";
 | ||||
| 
 | ||||
|             if (!e->interval.is_full()) { | ||||
|                 signed_constraint c = s.m_constraints.elem(e->interval.hi(), n->interval.symbolic()); | ||||
|                 lemma.insert_try_eval(~c); | ||||
|             } | ||||
|             signed_constraint c = s.m_constraints.elem(e->interval.hi(), n->interval.symbolic()); | ||||
|             lemma.insert_try_eval(~c); | ||||
| 
 | ||||
|             for (auto sc : e->side_cond) | ||||
|                 lemma.insert_eval(~sc); | ||||
|             lemma.insert(~e->src); | ||||
|             core.insert(e->src); | ||||
|             core.insert_vars(e->src); | ||||
|             for (const auto& src : e->src) { | ||||
|                 lemma.insert(~src); | ||||
|                 core.insert(src); | ||||
|                 core.insert_vars(src); | ||||
|             } | ||||
|             e = n; | ||||
|         } | ||||
|         while (e != first); | ||||
|  | @ -1471,7 +1670,12 @@ namespace { | |||
|             return; | ||||
|         entry* first = e; | ||||
|         do { | ||||
|             LOG("v" << v << ": " << e->interval << " " << e->side_cond << " " << e->src); | ||||
|             IF_LOGGING( | ||||
|                     verbose_stream() << "v" << v << ": " << e->interval << " " << e->side_cond << " "; | ||||
|                     for (const auto& src : e->src) | ||||
|                         verbose_stream() << src << " "; | ||||
|                     verbose_stream() << "\n"; | ||||
|             ); | ||||
|             e = e->next(); | ||||
|         } | ||||
|         while (e != first); | ||||
|  | @ -1495,7 +1699,7 @@ namespace { | |||
|             rational const& s_ = e->interval.hi().val(); | ||||
|             out << "[ "; | ||||
|             out << val_pp(m, p, true) << "*v" << v << " + " << val_pp(m, q_); | ||||
|             out << (e->src.is_positive() ? " > " : " >= "); | ||||
|             out << (e->src[0].is_positive() ? " > " : " >= "); | ||||
|             out << val_pp(m, r, true) << "*v" << v << " + " << val_pp(m, s_); | ||||
|             out << " ] "; | ||||
|         } | ||||
|  | @ -1503,7 +1707,9 @@ namespace { | |||
|             out << e->coeff << " * v" << v << " " << e->interval << " "; | ||||
|         else | ||||
|             out << e->interval << " "; | ||||
|         out << e->side_cond << " " << e->src << "; "; | ||||
|         out << e->side_cond << " "; | ||||
|         for (const auto& src : e->src) | ||||
|             out << src << "; "; | ||||
|         return out; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -94,13 +94,20 @@ namespace polysat { | |||
| 
 | ||||
|         bool intersect(pvar v, entry* e); | ||||
| 
 | ||||
|         bool refine_viable(pvar v, rational const& val); | ||||
|         template<bool FORWARD> | ||||
|         bool refine_viable(pvar v, rational const& val, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications); | ||||
| 
 | ||||
|         template<bool FORWARD> | ||||
|         bool refine_bits(pvar v, rational const& val, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications); | ||||
| 
 | ||||
|         bool refine_equal_lin(pvar v, rational const& val); | ||||
| 
 | ||||
|         bool refine_disequal_lin(pvar v, rational const& val); | ||||
|          | ||||
|         bool quick_bit_check(pvar v); | ||||
| 
 | ||||
|         template<bool FORWARD> | ||||
|         rational extend_by_bits(const pdd& var, const rational& bounds, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications, vector<signed_constraint>& src, vector<signed_constraint>& side_cond) const; | ||||
| 
 | ||||
|         bool collect_bit_information(pvar v, bool add_conflict, svector<lbool>& fixed, vector<ptr_vector<entry>>& justifications); | ||||
| 
 | ||||
|         std::ostream& display_one(std::ostream& out, pvar v, entry const* e) const; | ||||
|         std::ostream& display_all(std::ostream& out, pvar v, entry const* e, char const* delimiter = "") const; | ||||
|  | @ -113,9 +120,9 @@ namespace polysat { | |||
|          * Interval-based queries | ||||
|          * @return l_true on success, l_false on conflict, l_undef on refinement | ||||
|          */ | ||||
|         lbool query_min(pvar v, rational& out_lo); | ||||
|         lbool query_max(pvar v, rational& out_hi); | ||||
|         lbool query_find(pvar v, rational& out_lo, rational& out_hi); | ||||
|         lbool query_min(pvar v, rational& out_lo, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications); | ||||
|         lbool query_max(pvar v, rational& out_hi, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications); | ||||
|         lbool query_find(pvar v, rational& out_lo, rational& out_hi, const svector<lbool>& fixed, const vector<ptr_vector<entry>>& justifications); | ||||
| 
 | ||||
|         /**
 | ||||
|          * Bitblasting-based queries. | ||||
|  | @ -261,7 +268,7 @@ namespace polysat { | |||
|             } | ||||
| 
 | ||||
|             signed_constraint& operator*() { | ||||
|                 return idx < curr->side_cond.size() ? curr->side_cond[idx] : curr->src; | ||||
|                 return idx < curr->side_cond.size() ? curr->side_cond[idx] : curr->src[idx - curr->side_cond.size()]; | ||||
|             } | ||||
| 
 | ||||
|             bool operator==(iterator const& other) const { | ||||
|  |  | |||
|  | @ -678,8 +678,9 @@ namespace polysat { | |||
|             s.add_clause(c2, false); | ||||
|             s.m_viable.intersect(x.var(), c1); | ||||
|             s.m_viable.intersect(x.var(), c2); | ||||
|              | ||||
|             VERIFY(!s.m_viable.quick_bit_check(x.var())); | ||||
|             svector<lbool> fixed; | ||||
|             vector<ptr_vector<viable::entry>> justifications; | ||||
|             VERIFY(!s.m_viable.collect_bit_information(x.var(), false, fixed, justifications)); | ||||
|         } | ||||
|          | ||||
|         // parity(x) >= 3 and bit_1(x)
 | ||||
|  | @ -692,8 +693,9 @@ namespace polysat { | |||
|             s.add_clause(c2, false); | ||||
|             s.m_viable.intersect(x.var(), c1); | ||||
|             s.m_viable.intersect(x.var(), c2); | ||||
|              | ||||
|             VERIFY(!s.m_viable.quick_bit_check(x.var())); | ||||
|             svector<lbool> fixed; | ||||
|             vector<ptr_vector<viable::entry>> justifications; | ||||
|             VERIFY(!s.m_viable.collect_bit_information(x.var(), false, fixed, justifications)); | ||||
|         } | ||||
|          | ||||
|         // 8 * x + 3 == 0 or 8 * x + 5 == 0 is unsat
 | ||||
|  | @ -1490,9 +1492,9 @@ namespace polysat { | |||
|         } | ||||
| 
 | ||||
|         // x*y <= b & a <= x & !Omega(x*y) => a*y <= b
 | ||||
|         static void test_ineq_axiom4(unsigned bw = 32) { | ||||
|         static void test_ineq_axiom4(unsigned bw = 32, unsigned i = 0) { | ||||
|             auto const bound = rational::power_of_two(bw/2); | ||||
|             for (unsigned i = 0; i < 24; ++i) { | ||||
|             for (; i < 24; ++i) { | ||||
|                 scoped_solver s(concat(__func__, " bw=", bw, " perm=", i)); | ||||
|                 auto x = s.var(s.add_var(bw)); | ||||
|                 auto y = s.var(s.add_var(bw)); | ||||
|  | @ -1950,7 +1952,7 @@ static void STD_CALL polysat_on_ctrl_c(int) { | |||
| 
 | ||||
| void tst_polysat() { | ||||
|     using namespace polysat; | ||||
| 
 | ||||
|     test_polysat::test_ineq_axiom4(32, 7); | ||||
| #if 0  // Enable this block to run a single unit test with detailed output.
 | ||||
|     collect_test_records = false; | ||||
|     test_max_conflicts = 50; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue