Modify lemma vs fact policy for datatype equalities (#5115)
authorAndrew Reynolds <andrew.j.reynolds@gmail.com>
Thu, 24 Sep 2020 03:13:35 +0000 (22:13 -0500)
committerGitHub <noreply@github.com>
Thu, 24 Sep 2020 03:13:35 +0000 (22:13 -0500)
This changes the lemma vs fact policy for datatype equalities. Previously, datatype equalities were sent as lemmas unless they were over datatypes that were composed of datatypes only. This is now changed so that equalities that do not involve direct subterms with finite non-datatype types are kept internal.

The primary type of equality that this targets are "Instantiate" equalities, e.g. the conclusion of:
(is-cons x) => x = (cons (head x) (tail x))
These equalities have been observed to generate large amounts of new terms for many benchmarks.

With this PR, the the challenging Facebook benchmark goes from 2 min 45 sec -> 29 sec. If the instantiate rule is disabled altogether, it still correctly solves, and is faster (~14 seconds), which however is not correct in general.

This change triggered two other issues:
(1) A relations benchmark involving transitive closure now times out. This has been a common issue for the relations solver and should be revisited.
(2) A potential issue with doPendingLemmas in InferenceManagerBuffer was uncovered. In rare cases, we can be re-entrant into this method since OutputChannel::lemma may trigger further preregistration of terms, which can trigger a recursive call to doPendingLemmas in the case of datatypes, which causes a segfault due to corrupting an iterator. This PR adds a simple guard for this method.

This PR also fixes some existing issues in computing cardinality for parametric datatypes.

src/expr/dtype_cons.cpp
src/expr/dtype_cons.h
src/theory/datatypes/inference_manager.cpp
src/theory/datatypes/inference_manager.h
src/theory/datatypes/theory_datatypes.cpp
src/theory/inference_manager_buffered.cpp
src/theory/inference_manager_buffered.h
test/regress/CMakeLists.txt
test/regress/regress0/datatypes/dt-different-params.smt2 [new file with mode: 0644]
test/regress/regress0/datatypes/list-bool.smt2 [new file with mode: 0644]

index 7eec52b19ba0b9bcf737d33fab77f83f9d06c7b4..8e86ba49db5523294278f7d321e5d9cac8651b8a 100644 (file)
@@ -153,60 +153,39 @@ Cardinality DTypeConstructor::getCardinality(TypeNode t) const
 
 bool DTypeConstructor::isFinite(TypeNode t) const
 {
-  Assert(isResolved());
-
-  TNode self = d_constructor;
-  // is this already in the cache ?
-  if (self.getAttribute(DTypeFiniteComputedAttr()))
-  {
-    return self.getAttribute(DTypeFiniteAttr());
-  }
-  std::vector<TypeNode> instTypes;
-  std::vector<TypeNode> paramTypes;
-  bool isParam = t.isParametricDatatype();
-  if (isParam)
-  {
-    paramTypes = t.getDType().getParameters();
-    instTypes = TypeNode(t).getParamTypes();
-  }
-  for (size_t i = 0, nargs = getNumArgs(); i < nargs; i++)
-  {
-    TypeNode tc = getArgType(i);
-    if (isParam)
-    {
-      tc = tc.substitute(paramTypes.begin(),
-                         paramTypes.end(),
-                         instTypes.begin(),
-                         instTypes.end());
-    }
-    if (!tc.isFinite())
-    {
-      self.setAttribute(DTypeFiniteComputedAttr(), true);
-      self.setAttribute(DTypeFiniteAttr(), false);
-      return false;
-    }
-  }
-  self.setAttribute(DTypeFiniteComputedAttr(), true);
-  self.setAttribute(DTypeFiniteAttr(), true);
-  return true;
+  std::pair<CardinalityType, bool> cinfo = computeCardinalityInfo(t);
+  return cinfo.first == CardinalityType::FINITE;
 }
 
 bool DTypeConstructor::isInterpretedFinite(TypeNode t) const
 {
-  Assert(isResolved());
-  TNode self = d_constructor;
-  // is this already in the cache ?
-  if (self.getAttribute(DTypeUFiniteComputedAttr()))
+  std::pair<CardinalityType, bool> cinfo = computeCardinalityInfo(t);
+  return cinfo.first != CardinalityType::INFINITE;
+}
+
+bool DTypeConstructor::hasFiniteExternalArgType(TypeNode t) const
+{
+  std::pair<CardinalityType, bool> cinfo = computeCardinalityInfo(t);
+  return cinfo.second;
+}
+
+std::pair<DTypeConstructor::CardinalityType, bool>
+DTypeConstructor::computeCardinalityInfo(TypeNode t) const
+{
+  std::map<TypeNode, std::pair<CardinalityType, bool> >::iterator it =
+      d_cardInfo.find(t);
+  if (it != d_cardInfo.end())
   {
-    return self.getAttribute(DTypeUFiniteAttr());
+    return it->second;
   }
+  std::pair<CardinalityType, bool> ret(CardinalityType::FINITE, false);
   std::vector<TypeNode> instTypes;
   std::vector<TypeNode> paramTypes;
   bool isParam = t.isParametricDatatype();
   if (isParam)
   {
     paramTypes = t.getDType().getParameters();
-    instTypes = TypeNode(t).getParamTypes();
+    instTypes = t.getParamTypes();
   }
   for (unsigned i = 0, nargs = getNumArgs(); i < nargs; i++)
   {
@@ -218,16 +197,30 @@ bool DTypeConstructor::isInterpretedFinite(TypeNode t) const
                          instTypes.begin(),
                          instTypes.end());
     }
-    if (!tc.isInterpretedFinite())
+    if (tc.isFinite())
+    {
+      // do nothing
+    }
+    else if (tc.isInterpretedFinite())
+    {
+      if (ret.first == CardinalityType::FINITE)
+      {
+        // not simply finite, it depends on uninterpreted sorts being finite
+        ret.first = CardinalityType::INTERPRETED_FINITE;
+      }
+    }
+    else
     {
-      self.setAttribute(DTypeUFiniteComputedAttr(), true);
-      self.setAttribute(DTypeUFiniteAttr(), false);
-      return false;
+      // infinite implies the constructor is infinite cardinality
+      ret.first = CardinalityType::INFINITE;
+      continue;
     }
+    // if the argument is (interpreted) finite and external, set the flag
+    // for indicating it has a finite external argument
+    ret.second = ret.second || !tc.isDatatype();
   }
-  self.setAttribute(DTypeUFiniteComputedAttr(), true);
-  self.setAttribute(DTypeUFiniteAttr(), true);
-  return true;
+  d_cardInfo[t] = ret;
+  return ret;
 }
 
 bool DTypeConstructor::isResolved() const { return !d_tester.isNull(); }
index fc414c756da9a72f465a277641935466e274b12e..2dba895e92d74e2dfdbc6d8ab86942da020ac77d 100644 (file)
@@ -158,6 +158,13 @@ class DTypeConstructor
    * only be called for resolved constructors.
    */
   bool isInterpretedFinite(TypeNode t) const;
+  /**
+   * Has finite external argument type. This returns true if this constructor
+   * has an argument type that is not a datatype and is interpreted as a
+   * finite type. This function can only be called for resolved constructors.
+   *
+   */
+  bool hasFiniteExternalArgType(TypeNode t) const;
 
   /**
    * Returns true iff this constructor has already been
@@ -229,6 +236,17 @@ class DTypeConstructor
   void toStream(std::ostream& out) const;
 
  private:
+  /** Constructor cardinality type */
+  enum class CardinalityType
+  {
+    // the constructor is finite
+    FINITE,
+    // the constructor is interpreted-finite (finite under the assumption that
+    // uninterpreted sorts are finite)
+    INTERPRETED_FINITE,
+    // the constructor is infinte
+    INFINITE
+  };
   /** resolve
    *
    * This resolves (initializes) the constructor. For details
@@ -286,6 +304,13 @@ class DTypeConstructor
                          std::vector<TypeNode>& processing,
                          std::map<TypeNode, Node>& gt,
                          bool isValue) const;
+  /**
+   * Compute cardinality info, returns a pair where its first component is
+   * an identifier indicating the cardinality type of this constructor for
+   * type t, and a Boolean indicating whether the constructor has any arguments
+   * that have finite external type.
+   */
+  std::pair<CardinalityType, bool> computeCardinalityInfo(TypeNode t) const;
   /** compute shared selectors
    * This computes the maps d_sharedSelectors and d_sharedSelectorIndex.
    */
@@ -324,6 +349,8 @@ class DTypeConstructor
    * its argument index for this constructor.
    */
   mutable std::map<TypeNode, std::map<Node, unsigned> > d_sharedSelectorIndex;
+  /**  A cache for computeCardinalityInfo. */
+  mutable std::map<TypeNode, std::pair<CardinalityType, bool> > d_cardInfo;
 }; /* class DTypeConstructor */
 
 /**
index 45406a9b0c2ddaacf826a273ec7b873f3bfc3488..f056b9c5d98bfde6cb7bc78ba2b86367bc2b8669 100644 (file)
@@ -42,16 +42,13 @@ bool DatatypesInference::mustCommunicateFact(Node n, Node exp)
   }
   else if (n.getKind() == EQUAL)
   {
+    // Note that equalities due to instantiate are forced as lemmas if
+    // necessary as they are created. This ensures that terms are shared with
+    // external theories when necessary. We send the lemma here only if
+    // the equality is not for datatype terms, which can happen for collapse
+    // selector / term size or unification.
     TypeNode tn = n[0].getType();
-    if (!tn.isDatatype())
-    {
-      addLemma = true;
-    }
-    else
-    {
-      const DType& dt = tn.getDType();
-      addLemma = dt.involvesExternalType();
-    }
+    addLemma = !tn.isDatatype();
   }
   else if (n.getKind() == LEQ || n.getKind() == OR)
   {
@@ -68,8 +65,10 @@ bool DatatypesInference::mustCommunicateFact(Node n, Node exp)
 
 bool DatatypesInference::process(TheoryInferenceManager* im, bool asLemma)
 {
-  // check to see if we have to communicate it to the rest of the system
-  if (mustCommunicateFact(d_conc, d_exp))
+  // Check to see if we have to communicate it to the rest of the system.
+  // The flag asLemma is true when the inference was marked that it must be
+  // sent as a lemma in addPendingInference below.
+  if (asLemma || mustCommunicateFact(d_conc, d_exp))
   {
     // send it as an (explained) lemma
     std::vector<Node> exp;
@@ -95,9 +94,17 @@ InferenceManager::InferenceManager(Theory& t,
 
 void InferenceManager::addPendingInference(Node conc,
                                            Node exp,
-                                           ProofGenerator* pg)
+                                           ProofGenerator* pg,
+                                           bool forceLemma)
 {
-  d_pendingFact.emplace_back(new DatatypesInference(conc, exp, pg));
+  if (forceLemma)
+  {
+    d_pendingLem.emplace_back(new DatatypesInference(conc, exp, pg));
+  }
+  else
+  {
+    d_pendingFact.emplace_back(new DatatypesInference(conc, exp, pg));
+  }
 }
 
 void InferenceManager::process()
index 91536baabacfbb05e8a49cb974918c92e1c8f7fe..06c6ff2b59846309ec05b58e397a750d43ea6f77 100644 (file)
@@ -72,8 +72,18 @@ class InferenceManager : public InferenceManagerBuffered
   /**
    * Add pending inference, which may be processed as either a fact or
    * a lemma based on mustCommunicateFact in DatatypesInference above.
+   *
+   * @param conc The conclusion of the inference
+   * @param exp The explanation of the inference
+   * @param pg The proof generator who can provide a proof of (conc => exp)
+   * @param forceLemma Whether this inference *must* be processed as a lemma.
+   * Otherwise, it may be processed as a fact or lemma based on
+   * mustCommunicateFact.
    */
-  void addPendingInference(Node conc, Node exp, ProofGenerator* pg = nullptr);
+  void addPendingInference(Node conc,
+                           Node exp,
+                           ProofGenerator* pg = nullptr,
+                           bool forceLemma = false);
   /**
    * Process the current lemmas and facts. This is a custom method that can
    * be seen as overriding the behavior of calling both doPendingLemmas and
index 9cecb6f276189789d649efcc0006b025055efc97..376dbb1dbd0749161bbf80275234f5c4160572a6 100644 (file)
@@ -1541,7 +1541,8 @@ void TheoryDatatypes::instantiate( EqcInfo* eqc, Node n ){
     exp = getLabel(n);
     tt = exp[0];
   }
-  const DType& dt = tt.getType().getDType();
+  TypeNode ttn = tt.getType();
+  const DType& dt = ttn.getDType();
   // instantiate this equivalence class
   eqc->d_inst = true;
   Node tt_cons = getInstantiateCons(tt, dt, index);
@@ -1551,10 +1552,17 @@ void TheoryDatatypes::instantiate( EqcInfo* eqc, Node n ){
     return;
   }
   eq = tt.eqNode(tt_cons);
-  Debug("datatypes-inst") << "DtInstantiate : " << eqc << " " << eq
-                          << std::endl;
-  d_im.addPendingInference(eq, exp);
-  Trace("datatypes-infer-debug") << "inst : " << eqc << " " << n << std::endl;
+  // Determine if the equality must be sent out as a lemma. Notice that
+  // we can keep new equalities from the instantiate rule internal as long as
+  // they are for datatype constructors that have no arguments that have
+  // finite external type. Such equalities must be sent because they introduce
+  // selector terms that may contribute to conflicts due to cardinality (good
+  // examples of this are regress0/datatypes/dt-param-card4-bool-sat.smt2 and
+  // regress0/datatypes/list-bool.smt2).
+  bool forceLemma = dt[index].hasFiniteExternalArgType(ttn);
+  Trace("datatypes-infer-debug") << "DtInstantiate : " << eqc << " " << eq
+                                 << " forceLemma = " << forceLemma << std::endl;
+  d_im.addPendingInference(eq, exp, nullptr, forceLemma);
   Trace("datatypes-infer") << "DtInfer : instantiate : " << eq << " by " << exp
                            << std::endl;
 }
index 1da81411631836acc75cc12d5b29d558ad1effdd..5699e75ad5d65e9c89f29e2f3b17341b4aba25db 100644 (file)
@@ -25,7 +25,7 @@ namespace theory {
 InferenceManagerBuffered::InferenceManagerBuffered(Theory& t,
                                                    TheoryState& state,
                                                    ProofNodeManager* pnm)
-    : TheoryInferenceManager(t, state, pnm)
+    : TheoryInferenceManager(t, state, pnm), d_processingPendingLemmas(false)
 {
 }
 
@@ -94,12 +94,19 @@ void InferenceManagerBuffered::doPendingFacts()
 
 void InferenceManagerBuffered::doPendingLemmas()
 {
+  if (d_processingPendingLemmas)
+  {
+    // already processing
+    return;
+  }
+  d_processingPendingLemmas = true;
   for (const std::unique_ptr<TheoryInference>& plem : d_pendingLem)
   {
     // process this lemma
     plem->process(this, true);
   }
   d_pendingLem.clear();
+  d_processingPendingLemmas = false;
 }
 
 void InferenceManagerBuffered::doPendingPhaseRequirements()
index 3d249ea80414bff194a0b99522972e1e09d123fb..74bbcc3754f35f7944c15a697ef3a646ca1f0fc7 100644 (file)
@@ -134,6 +134,12 @@ class InferenceManagerBuffered : public TheoryInferenceManager
   std::vector<std::unique_ptr<TheoryInference>> d_pendingFact;
   /** A map from literals to their pending phase requirement */
   std::map<Node, bool> d_pendingReqPhase;
+  /**
+   * Whether we are currently processing pending lemmas. This flag ensures
+   * that we do not call pending lemmas recursively, which may lead to
+   * segfaults.
+   */
+  bool d_processingPendingLemmas;
 };
 
 }  // namespace theory
index c7bf666d657b84ed10e5cc4f414cf33bbbb7c0d0..fc2167a4a1354dabc818a22cc2496fb588ebc15a 100644 (file)
@@ -415,6 +415,7 @@ set(regress_0_tests
   regress0/datatypes/datatype3.cvc
   regress0/datatypes/datatype4.cvc
   regress0/datatypes/dt-2.6.smt2
+  regress0/datatypes/dt-different-params.smt2
   regress0/datatypes/dt-match-pat-param-2.6.smt2
   regress0/datatypes/dt-param-2.6.smt2
   regress0/datatypes/dt-param-2.6-print.smt2
@@ -426,6 +427,7 @@ set(regress_0_tests
   regress0/datatypes/issue1433.smt2
   regress0/datatypes/issue2838.cvc
   regress0/datatypes/jsat-2.6.smt2
+  regress0/datatypes/list-bool.smt2
   regress0/datatypes/model-subterms-min.smt2
   regress0/datatypes/mutually-recursive.cvc
   regress0/datatypes/pair-bool-bool.cvc
@@ -1676,7 +1678,6 @@ set(regress_1_tests
   regress1/rels/rel_pressure_0.cvc
   regress1/rels/rel_tc_10_1.cvc
   regress1/rels/rel_tc_4.cvc
-  regress1/rels/rel_tc_4_1.cvc
   regress1/rels/rel_tc_5_1.cvc
   regress1/rels/rel_tc_6.cvc
   regress1/rels/rel_tc_9_1.cvc
@@ -2519,6 +2520,8 @@ set(regression_disabled_tests
   # doing a coverage build with LFSC.
   regress1/quantifiers/set3.smt2
   regress1/rels/garbage_collect.cvc
+  # times out after dt fact update due to overly eager splitting for tclosure
+  regress1/rels/rel_tc_4_1.cvc
   regress1/sets/setofsets-disequal.smt2
   regress1/sets/finite-type/sets-card-neg-mem-union-2.smt2
   regress1/simple-rdl-definefun.smt2
diff --git a/test/regress/regress0/datatypes/dt-different-params.smt2 b/test/regress/regress0/datatypes/dt-different-params.smt2
new file mode 100644 (file)
index 0000000..f73d82d
--- /dev/null
@@ -0,0 +1,16 @@
+(set-logic QF_ALL_SUPPORTED)
+(set-info :status unsat)
+(declare-datatypes ((Data 1)) ((par (T) ((data (first T))))))
+
+(declare-fun q1 () (Data Int))
+(declare-fun q2 () (Data Int))
+(declare-fun q3 () (Data Int))
+
+(assert (distinct q1 q2 q3))
+
+(declare-fun p1 () (Data Bool))
+(declare-fun p2 () (Data Bool))
+(declare-fun p3 () (Data Bool))
+
+(assert (distinct p1 p2 p3))
+(check-sat)
diff --git a/test/regress/regress0/datatypes/list-bool.smt2 b/test/regress/regress0/datatypes/list-bool.smt2
new file mode 100644 (file)
index 0000000..adc7ad9
--- /dev/null
@@ -0,0 +1,13 @@
+(set-logic ALL)
+(set-info :status unsat)
+(declare-datatypes ((list 0)) (
+((cons (head Bool) (tail list)) (nil))
+))
+(declare-fun x1 () list)
+(declare-fun x2 () list)
+(declare-fun x3 () list)
+(assert (= (tail x1) nil))
+(assert (= (tail x2) nil))
+(assert (= (tail x3) nil))
+(assert (distinct x1 x2 x3 nil))
+(check-sat)