Fix issues with cyclic proofs due to double SYMM applications (#7083)
authorAndrew Reynolds <andrew.j.reynolds@gmail.com>
Wed, 1 Sep 2021 02:51:46 +0000 (21:51 -0500)
committerGitHub <noreply@github.com>
Wed, 1 Sep 2021 02:51:46 +0000 (02:51 +0000)
Our way of constructing proofs from the equality engine in very rare cases may cause cyclic proofs due to constructing double applications of SYMM, which are not recognized as assumptions in CDProof. This is due to an interplay between LazyProofChain using an underlying CDProof as its default proof generator, where the proof chain would use the proofs from the CDProof to form a cyclic proof.

This was encountered in 9 SMT-LIB benchmarks in QF_SLIA.

This makes us safer in several places related to double SYMM steps.

src/proof/lazy_proof.cpp
src/proof/proof.cpp
src/proof/proof_node_manager.cpp
src/proof/proof_node_manager.h
test/regress/CMakeLists.txt
test/regress/regress2/strings/proof-fail-083021-delta.smt2 [new file with mode: 0644]

index b16a8d758f493d9df933a1c9a70fc3b089bb8ecb..eab452d49398159bd6c4daf85b7a274deb83f28a 100644 (file)
@@ -100,7 +100,14 @@ std::shared_ptr<ProofNode> LazyCDProof::getProofFor(Node fact)
 
             if (isSym)
             {
-              d_manager->updateNode(cur, PfRule::SYMM, {pgc}, {});
+              if (pgc->getRule() == PfRule::SYMM)
+              {
+                d_manager->updateNode(cur, pgc->getChildren()[0].get());
+              }
+              else
+              {
+                d_manager->updateNode(cur, PfRule::SYMM, {pgc}, {});
+              }
             }
             else
             {
index 79f84135d83191cfc237b2d05cb60ffd2dc3d864..dc370a04dd9c6ab98f6c1fe8310b5353b48eb054 100644 (file)
@@ -97,8 +97,7 @@ std::shared_ptr<ProofNode> CDProof::getProofSymm(Node fact)
     if (pf == nullptr)
     {
       Trace("cdproof") << "...fresh make symm" << std::endl;
-      std::shared_ptr<ProofNode> psym =
-          d_manager->mkNode(PfRule::SYMM, pschild, args, fact);
+      std::shared_ptr<ProofNode> psym = d_manager->mkSymm(pfs, fact);
       Assert(psym != nullptr);
       d_nodes.insert(fact, psym);
       return psym;
@@ -411,13 +410,23 @@ bool CDProof::isAssumption(ProofNode* pn)
   {
     return true;
   }
-  else if (rule == PfRule::SYMM)
+  else if (rule != PfRule::SYMM)
   {
-    const std::vector<std::shared_ptr<ProofNode>>& pc = pn->getChildren();
-    Assert(pc.size() == 1);
-    return pc[0]->getRule() == PfRule::ASSUME;
+    return false;
   }
-  return false;
+  pn = ProofNodeManager::cancelDoubleSymm(pn);
+  rule = pn->getRule();
+  if (rule == PfRule::ASSUME)
+  {
+    return true;
+  }
+  else if (rule != PfRule::SYMM)
+  {
+    return false;
+  }
+  const std::vector<std::shared_ptr<ProofNode>>& pc = pn->getChildren();
+  Assert(pc.size() == 1);
+  return pc[0]->getRule() == PfRule::ASSUME;
 }
 
 bool CDProof::isSame(TNode f, TNode g)
index cf19eebdf91debd000d2eae2d53e225269d36a04..7e41a905788ae02c5b35920769f39eb937295577 100644 (file)
@@ -61,6 +61,18 @@ std::shared_ptr<ProofNode> ProofNodeManager::mkAssume(Node fact)
   return mkNode(PfRule::ASSUME, {}, {fact}, fact);
 }
 
+std::shared_ptr<ProofNode> ProofNodeManager::mkSymm(
+    std::shared_ptr<ProofNode> child, Node expected)
+{
+  if (child->getRule() == PfRule::SYMM)
+  {
+    Assert(expected.isNull()
+           || child->getChildren()[0]->getResult() == expected);
+    return child->getChildren()[0];
+  }
+  return mkNode(PfRule::SYMM, {child}, {}, expected);
+}
+
 std::shared_ptr<ProofNode> ProofNodeManager::mkTrans(
     const std::vector<std::shared_ptr<ProofNode>>& children, Node expected)
 {
@@ -173,7 +185,14 @@ std::shared_ptr<ProofNode> ProofNodeManager::mkScope(
         // use SYMM if possible
         if (aMatch == aeqSym)
         {
-          updateNode(pfs.get(), PfRule::SYMM, children, {});
+          if (pfaa->getRule() == PfRule::SYMM)
+          {
+            updateNode(pfs.get(), pfaa->getChildren()[0].get());
+          }
+          else
+          {
+            updateNode(pfs.get(), PfRule::SYMM, children, {});
+          }
         }
         else
         {
@@ -263,6 +282,11 @@ bool ProofNodeManager::updateNode(ProofNode* pn, ProofNode* pnr)
 {
   Assert(pn != nullptr);
   Assert(pnr != nullptr);
+  if (pn == pnr)
+  {
+    // same node, no update necessary
+    return true;
+  }
   if (pn->getResult() != pnr->getResult())
   {
     return false;
@@ -299,7 +323,7 @@ Node ProofNodeManager::checkInternal(
 ProofChecker* ProofNodeManager::getChecker() const { return d_checker; }
 
 std::shared_ptr<ProofNode> ProofNodeManager::clone(
-    std::shared_ptr<ProofNode> pn)
+    std::shared_ptr<ProofNode> pn) const
 {
   const ProofNode* orig = pn.get();
   std::unordered_map<const ProofNode*, std::shared_ptr<ProofNode>> visited;
@@ -333,7 +357,13 @@ std::shared_ptr<ProofNode> ProofNodeManager::clone(
       {
         it = visited.find(cp.get());
         Assert(it != visited.end());
-        Assert(it->second != nullptr);
+        // if we encounter nullptr here, then this child is currently being
+        // traversed at a higher level, hence this corresponds to a cyclic
+        // proof.
+        if (it->second == nullptr)
+        {
+          Unreachable() << "Cyclic proof encountered when cloning a proof node";
+        }
         cchildren.push_back(it->second);
       }
       cloned = std::make_shared<ProofNode>(
@@ -347,6 +377,23 @@ std::shared_ptr<ProofNode> ProofNodeManager::clone(
   return visited[orig];
 }
 
+ProofNode* ProofNodeManager::cancelDoubleSymm(ProofNode* pn)
+{
+  while (pn->getRule() == PfRule::SYMM)
+  {
+    std::shared_ptr<ProofNode> pnc = pn->getChildren()[0];
+    if (pnc->getRule() == PfRule::SYMM)
+    {
+      pn = pnc->getChildren()[0].get();
+    }
+    else
+    {
+      break;
+    }
+  }
+  return pn;
+}
+
 bool ProofNodeManager::updateNodeInternal(
     ProofNode* pn,
     PfRule id,
index 2fa7ed3e97c2ee9cf77f20a61957a04a59c557ec..541686a30643da0314932fe98f7ec003a796b9df 100644 (file)
@@ -84,6 +84,12 @@ class ProofNodeManager
    * @return The ASSUME proof of fact.
    */
   std::shared_ptr<ProofNode> mkAssume(Node fact);
+  /**
+   * Make symm, which accounts for whether the child is already a SYMM
+   * node, in which case we return its child.
+   */
+  std::shared_ptr<ProofNode> mkSymm(std::shared_ptr<ProofNode> child,
+                                    Node expected = Node::null());
   /**
    * Make transitivity proof, where children contains one or more proofs of
    * equalities that form an ordered chain. In other words, the vector children
@@ -166,7 +172,12 @@ class ProofNodeManager
    * @param pn The proof node to clone
    * @return the cloned proof node.
    */
-  std::shared_ptr<ProofNode> clone(std::shared_ptr<ProofNode> pn);
+  std::shared_ptr<ProofNode> clone(std::shared_ptr<ProofNode> pn) const;
+  /**
+   * Cancel double SYMM. Returns a proof node that is not a double application
+   * of SYMM, e.g. for (SYMM (SYMM (r P))), this returns (r P) where r != SYMM.
+   */
+  static ProofNode* cancelDoubleSymm(ProofNode* pn);
 
  private:
   /** The (optional) proof checker */
index 0d8ec19dcde219ea7d7a6deefd08f56fdfd8ca88..17585d36399fe98a639ef9fd016f80d368f47cfc 100644 (file)
@@ -2539,6 +2539,7 @@ set(regress_2_tests
   regress2/strings/issue6639-replace-re-all.smt2
   regress2/strings/issue918.smt2
   regress2/strings/non_termination_regular_expression6.smt2
+  regress2/strings/proof-fail-083021-delta.smt2
   regress2/strings/range-perf.smt2
   regress2/strings/repl-repl-i-no-push.smt2
   regress2/strings/repl-repl.smt2
diff --git a/test/regress/regress2/strings/proof-fail-083021-delta.smt2 b/test/regress/regress2/strings/proof-fail-083021-delta.smt2
new file mode 100644 (file)
index 0000000..981102b
--- /dev/null
@@ -0,0 +1,12 @@
+(set-logic QF_SLIA)
+(set-info :status unsat)
+(declare-const x Bool)
+(declare-const x1 Int)
+(declare-fun s () String)
+(assert (ite (str.prefixof "-" (str.substr s 0 1)) (= 0 (str.to_int (str.substr (str.substr s 0 (+ 1 1)) 1 (- (str.len (str.substr s 0 (+ 1 1))) 1)))) true))
+(assert (not (<= (- (str.len s) 1 (+ 1 1) 1) 3)))
+(assert (ite x true (> 1 (str.len (str.substr s (+ 1 1 1 1) (- (str.len s) (+ 1 1 1 1 1)))))))
+(assert (ite (str.prefixof "-" (str.substr s (+ 1 1) 1)) (= 0 (str.to_int (str.substr (str.substr s (+ 1 1) (+ 1 1 1)) 1 (- (str.len (str.substr s 1 x1)) 1)))) true))
+(assert (str.in_re s (re.+ (re.range "0" "9"))))
+(assert (not (<= (str.to_int (str.substr s 0 (+ 1 1))) 255)))
+(check-sat)