Normal form equality conflicts and uniqueness check (#4497)
authorAndrew Reynolds <andrew.j.reynolds@gmail.com>
Wed, 20 May 2020 15:20:34 +0000 (10:20 -0500)
committerGitHub <noreply@github.com>
Wed, 20 May 2020 15:20:34 +0000 (10:20 -0500)
This adds a new inference schema to strings that was discovered by the internal proof checker. It says that we are in conflict when an equality between the normal forms of two terms in the same equivalence class rewrites to false.

It also improves the efficiency of processing normal forms by only considering normal forms that are unique up to rewriting.

src/theory/strings/core_solver.cpp
src/theory/strings/core_solver.h
src/theory/strings/infer_info.cpp
src/theory/strings/infer_info.h
test/regress/CMakeLists.txt
test/regress/regress0/strings/quad-028-2-2-unsat.smt2 [new file with mode: 0644]

index 9989c89f456b1bd3107a10e6a7c9cb3b899e187f..aca43e8c828a43493e492d35f57fcf65051a4293 100644 (file)
@@ -618,7 +618,7 @@ void CoreSolver::normalizeEquivalenceClass(Node eqc, TypeNode stype)
       return;
     }
     // process the normal forms
-    processNEqc(normal_forms, stype);
+    processNEqc(eqc, normal_forms, stype);
     if (d_im.hasProcessed())
     {
       return;
@@ -879,81 +879,151 @@ void CoreSolver::getNormalForms(Node eqc,
           }
         }
         Trace("strings-solve") << std::endl;
-        
       }
     } else {
       Trace("strings-solve") << "--- Single normal form for equivalence class " << eqc << std::endl;
     }
-    
-    //if equivalence class is constant, approximate as containment, infer conflicts
-    Node c = d_bsolver.getConstantEqc( eqc );
-    if( !c.isNull() ){
-      Trace("strings-solve") << "Eqc is constant " << c << std::endl;
-      for (unsigned i = 0, size = normal_forms.size(); i < size; i++)
+  }
+}
+
+void CoreSolver::processNEqc(Node eqc,
+                             std::vector<NormalForm>& normal_forms,
+                             TypeNode stype)
+{
+  if (normal_forms.size() <= 1)
+  {
+    return;
+  }
+  // if equivalence class is constant, approximate as containment, infer
+  // conflicts
+  Node c = d_bsolver.getConstantEqc(eqc);
+  // the possible inferences
+  std::vector<CoreInferInfo> pinfer;
+  // compute normal forms that are effectively unique
+  std::unordered_map<Node, size_t, NodeHashFunction> nfCache;
+  std::vector<size_t> nfIndices;
+  bool hasConstIndex = false;
+  for (size_t i = 0, nnforms = normal_forms.size(); i < nnforms; i++)
+  {
+    NormalForm& nfi = normal_forms[i];
+    Node ni = utils::mkNConcat(nfi.d_nf, stype);
+    if (nfCache.find(ni) == nfCache.end())
+    {
+      // If the equivalence class is entailed to be constant, check
+      // if the normal form cannot be contained in that constant.
+      if (!c.isNull())
       {
-        NormalForm& nf = normal_forms[i];
         int firstc, lastc;
-        if (!StringsEntail::canConstantContainList(c, nf.d_nf, firstc, lastc))
+        if (!StringsEntail::canConstantContainList(c, nfi.d_nf, firstc, lastc))
         {
-          Node n = nf.d_base;
+          Node n = nfi.d_base;
           //conflict
           Trace("strings-solve") << "Normal form for " << n << " cannot be contained in constant " << c << std::endl;
-          //conflict, explanation is n = base ^ base = c ^ relevant porition of ( n = N[n] )
+          // conflict, explanation is:
+          //  n = base ^ base = c ^ relevant porition of ( n = N[n] )
           std::vector< Node > exp;
           d_bsolver.explainConstantEqc(n,eqc,exp);
           // Notice although not implemented, this can be minimized based on
           // firstc/lastc, normal_forms_exp_depend.
-          exp.insert(exp.end(), nf.d_exp.begin(), nf.d_exp.end());
-          Node conc = d_false;
-          d_im.sendInference(exp, conc, Inference::N_NCTN);
+          exp.insert(exp.end(), nfi.d_exp.begin(), nfi.d_exp.end());
+          d_im.sendInference(exp, d_false, Inference::N_NCTN);
+          return;
         }
       }
+
+      nfCache[ni] = i;
+      if (ni.isConst())
+      {
+        hasConstIndex = true;
+        nfIndices.insert(nfIndices.begin(), i);
+      }
+      else
+      {
+        nfIndices.push_back(i);
+      }
     }
   }
-}
+  size_t nnfs = nfIndices.size();
+  Trace("strings-solve") << "CoreSolver::processNEqc: " << nnfs << "/"
+                         << normal_forms.size() << " normal forms are unique."
+                         << std::endl;
 
-void CoreSolver::processNEqc(std::vector<NormalForm>& normal_forms,
-                             TypeNode stype)
-{
-  //the possible inferences
-  std::vector<CoreInferInfo> pinfer;
-  // loop over all pairs 
-  for(unsigned i=0; i<normal_forms.size()-1; i++) {
+  // loop over all pairs
+  for (unsigned i = 0; i < nnfs - 1; i++)
+  {
     //unify each normalform[j] with normal_forms[i]
-    for(unsigned j=i+1; j<normal_forms.size(); j++ ) {
-      NormalForm& nfi = normal_forms[i];
-      NormalForm& nfj = normal_forms[j];
+    for (unsigned j = i + 1; j < nnfs; j++)
+    {
+      NormalForm& nfi = normal_forms[nfIndices[i]];
+      NormalForm& nfj = normal_forms[nfIndices[j]];
       //ensure that normal_forms[i] and normal_forms[j] are the same modulo equality, add to pinfer if not
       Trace("strings-solve") << "Strings: Process normal form #" << i << " against #" << j << "..." << std::endl;
       if (isNormalFormPair(nfi.d_base, nfj.d_base))
       {
         Trace("strings-solve") << "Strings: Already cached." << std::endl;
-      }else{
-        //process the reverse direction first (check for easy conflicts and inferences)
-        unsigned rindex = 0;
-        nfi.reverse();
-        nfj.reverse();
-        processSimpleNEq(nfi, nfj, rindex, true, 0, pinfer, stype);
-        nfi.reverse();
-        nfj.reverse();
-        if (d_im.hasProcessed())
-        {
-          return;
-        }
-        //AJR: for less aggressive endpoint inference
-        //rindex = 0;
+        continue;
+      }
+      // process the reverse direction first (check for easy conflicts and
+      // inferences)
+      unsigned rindex = 0;
+      nfi.reverse();
+      nfj.reverse();
+      processSimpleNEq(nfi, nfj, rindex, true, 0, pinfer, stype);
+      nfi.reverse();
+      nfj.reverse();
+      if (d_im.hasProcessed())
+      {
+        break;
+      }
+      // AJR: for less aggressive endpoint inference
+      // rindex = 0;
 
-        unsigned index = 0;
-        processSimpleNEq(nfi, nfj, index, false, rindex, pinfer, stype);
-        if (d_im.hasProcessed())
-        {
-          return;
-        }
+      unsigned index = 0;
+      processSimpleNEq(nfi, nfj, index, false, rindex, pinfer, stype);
+      if (d_im.hasProcessed())
+      {
+        break;
+      }
+    }
+    if (hasConstIndex || d_im.hasProcessed())
+    {
+      break;
+    }
+  }
+  if (d_state.isInConflict())
+  {
+    // conflict, we are done
+    return;
+  }
+  // Go back and check for normal form equality conflicts. These take
+  // precedence over any facts and lemmas.
+  for (const std::pair<const Node, size_t>& ni : nfCache)
+  {
+    for (const std::pair<const Node, size_t>& nj : nfCache)
+    {
+      if (ni.first >= nj.first)
+      {
+        // avoid duplicate comparisons
+        continue;
+      }
+      Node eq = ni.first.eqNode(nj.first);
+      eq = Rewriter::rewrite(eq);
+      if (eq == d_false)
+      {
+        std::vector<Node> exp;
+        NormalForm& nfi = normal_forms[ni.second];
+        NormalForm& nfj = normal_forms[nj.second];
+        exp.insert(exp.end(), nfi.d_exp.begin(), nfi.d_exp.end());
+        exp.insert(exp.end(), nfj.d_exp.begin(), nfj.d_exp.end());
+        exp.push_back(nfi.d_base.eqNode(nfj.d_base));
+        d_im.sendInference(exp, d_false, Inference::N_EQ_CONF);
+        return;
       }
     }
   }
-  if (pinfer.empty())
+  if (d_im.hasProcessed() || pinfer.empty())
   {
+    // either already sent a lemma or fact, or there are no possible inferences
     return;
   }
   // now, determine which of the possible inferences we want to add
index 4328b84628f2fa220c19b34d06e2742bfdda96a5..029f1c8502ce3dca7e0e5ca74babfa5418423b5b 100644 (file)
@@ -275,7 +275,7 @@ class CoreSolver
                       TypeNode stype);
   /** process normalize equivalence class
    *
-   * This is called when an equivalence class contains a set of terms that
+   * This is called when an equivalence class eqc contains a set of terms that
    * have normal forms given by the argument normal_forms. It either
    * verifies that all normal forms in this vector are identical, or otherwise
    * adds a conflict, lemma, or inference via the sendInference method.
@@ -288,7 +288,9 @@ class CoreSolver
    *
    * stype is the string-like type of the equivalence class we are processing.
    */
-  void processNEqc(std::vector<NormalForm>& normal_forms, TypeNode stype);
+  void processNEqc(Node eqc,
+                   std::vector<NormalForm>& normal_forms,
+                   TypeNode stype);
   /** process simple normal equality
    *
    * This method is called when two equal terms have normal forms nfi and nfj.
index 9c4ebafa17a7a44c7c424da31df5b4833592d691..e26a49e4e67223ec931832230e93513eba3c9d3e 100644 (file)
@@ -35,6 +35,7 @@ const char* toString(Inference i)
     case Inference::F_ENDPOINT_EMP: return "F_ENDPOINT_EMP";
     case Inference::F_ENDPOINT_EQ: return "F_ENDPOINT_EQ";
     case Inference::F_NCTN: return "F_NCTN";
+    case Inference::N_EQ_CONF: return "N_EQ_CONF";
     case Inference::N_ENDPOINT_EMP: return "N_ENDPOINT_EMP";
     case Inference::N_UNIFY: return "N_UNIFY";
     case Inference::N_ENDPOINT_EQ: return "N_ENDPOINT_EQ";
index fca528d1ba097facf2d74c42b792039176f2e587..5aecf52cb610138f703708a33f6ff9cd2c860493 100644 (file)
@@ -92,6 +92,10 @@ enum class Inference : uint32_t
   // Flat form not contained
   // x = c ^ x = y => false when rewrite( contains( y, c ) ) = false
   F_NCTN,
+  // Normal form equality conflict
+  //   x = N[x] ^ y = N[y] ^ x=y => false
+  // where Rewriter::rewrite(N[x]=N[y]) = false.
+  N_EQ_CONF,
   // Given two normal forms, infers that the remainder one of them has to be
   // empty. For example:
   //    If x1 ++ x2 = y1 and x1 = y1, then x2 = ""
index d5174af5e5bdc7e5b68aafeaca34abe07f01caab..ea98cf8191db2d6580a82f0e40374785bcc2ee2a 100644 (file)
@@ -961,6 +961,7 @@ set(regress_0_tests
   regress0/strings/norn-31.smt2
   regress0/strings/norn-simp-rew.smt2
   regress0/strings/parser-syms.cvc
+  regress0/strings/quad-028-2-2-unsat.smt2
   regress0/strings/re.all.smt2
   regress0/strings/re-syntax.smt2
   regress0/strings/re_diff.smt2
diff --git a/test/regress/regress0/strings/quad-028-2-2-unsat.smt2 b/test/regress/regress0/strings/quad-028-2-2-unsat.smt2
new file mode 100644 (file)
index 0000000..2ffe8a7
--- /dev/null
@@ -0,0 +1,21 @@
+(set-info :smt-lib-version 2.6)
+(set-logic QF_SLIA)
+(set-info :source |
+Generated by: Quang Loc Le
+Generated on: 2018-10-22
+Application: Word equations in a decidable fragment
+Target solver: Kepler_22
+Publication: "A decision procedure for string logic with quadratic equations, regular expressions and length constraints" by Q.L. Le and M. He, APLAS 2018.
+|)
+(set-info :license "https://creativecommons.org/licenses/by/4.0/")
+(set-info :category "crafted")
+(set-info :status unsat)
+
+(declare-fun  x1 () String )
+(declare-fun  x2 () String )
+(declare-fun  z () String )
+(declare-fun  t () String )
+(assert ( =( str.++( str.++( str.++ x1  "abc"  )  x2  )  z  ) ( str.++( str.++( str.++ x2  "bab"  )  x1  )  t  )  ) )
+(check-sat)
+
+(exit)