[proof-new] Fixing SMT post-processor's handling of assumptions (#6277)
authorHaniel Barbosa <hanielbbarbosa@gmail.com>
Wed, 7 Apr 2021 21:17:33 +0000 (18:17 -0300)
committerGitHub <noreply@github.com>
Wed, 7 Apr 2021 21:17:33 +0000 (21:17 +0000)
Previously the SMT post-processor would update any assumption as long as it
had a proof for it. This can be a problem when one as assumption introduced in a
scope that should not be expanded. This commit fixes the issue by adding the
option of configuring a proof node updater to track scopes and the assumptions
they introduce, which can be used to determine the prood nodes which should be
updated. It also changes the SMT post-processor to only update assumptions that
have not been introduced in some scope.

This commit fixes an issue found by @Lachnitt during the integration of CVC4 and Isabelle.

src/expr/proof_node_updater.cpp
src/expr/proof_node_updater.h
src/options/proof_options.toml
src/prop/proof_post_processor.cpp
src/prop/proof_post_processor.h
src/smt/proof_manager.cpp
src/smt/proof_post_processor.cpp
src/smt/proof_post_processor.h
test/regress/CMakeLists.txt
test/regress/regress0/proofs/scope.smt2 [new file with mode: 0644]

index 933e5b99931b400c4b55754918e42f4935e38bee..cbd457cd79994dcf50c93edcb2e6def4ceb910f7 100644 (file)
@@ -125,28 +125,21 @@ void ProofNodeUpdater::processInternal(
       }
       traversing.push_back(cur);
       visit.push_back(cur);
-      if (d_mergeSubproofs)
+      // If we are not the top-level proof, we were a scope, or became a scope
+      // after updating, we do a separate recursive call to this method. This
+      // allows us to properly track the assumptions in scope, which is
+      // important for example to merge or to determine updates based on free
+      // assumptions.
+      if (cur->getRule() == PfRule::SCOPE && cur != pf)
       {
-        // If we are not the top-level proof, we were a scope, or became a
-        // scope after updating, we need to make a separate recursive call to
-        // this method. This is not necessary if we are not merging subproofs.
-        if (cur->getRule() == PfRule::SCOPE && cur != pf)
-        {
-          std::vector<Node> nfa;
-          // if we are debugging free assumptions, update the set
-          if (d_debugFreeAssumps)
-          {
-            nfa.insert(nfa.end(), fa.begin(), fa.end());
-            const std::vector<Node>& args = cur->getArguments();
-            nfa.insert(nfa.end(), args.begin(), args.end());
-            Trace("pfnu-debug2")
-                << "Process new scope with " << args << std::endl;
-          }
-          // Process in new call separately, since we should not cache
-          // the results of proofs that have a different scope.
-          processInternal(cur, nfa, traversing);
-          continue;
-        }
+        std::vector<Node> nfa;
+        nfa.insert(nfa.end(), fa.begin(), fa.end());
+        const std::vector<Node>& args = cur->getArguments();
+        nfa.insert(nfa.end(), args.begin(), args.end());
+        Trace("pfnu-debug2") << "Process new scope with " << args << std::endl;
+        // Process in new call separately
+        processInternal(cur, nfa, traversing);
+        continue;
       }
       const std::vector<std::shared_ptr<ProofNode>>& ccp = cur->getChildren();
       // now, process children
@@ -180,7 +173,7 @@ bool ProofNodeUpdater::runUpdate(std::shared_ptr<ProofNode> cur,
                                  bool& continueUpdate)
 {
   // should it be updated?
-  if (!d_cb.shouldUpdate(cur, continueUpdate))
+  if (!d_cb.shouldUpdate(cur, fa, continueUpdate))
   {
     return false;
   }
@@ -196,9 +189,9 @@ bool ProofNodeUpdater::runUpdate(std::shared_ptr<ProofNode> cur,
     // store in the proof
     cpf.addProof(cp);
   }
-  Trace("pf-process-debug")
-      << "Updating (" << cur->getRule() << ")..." << std::endl;
   Node res = cur->getResult();
+  Trace("pf-process-debug")
+      << "Updating (" << cur->getRule() << "): " << res << std::endl;
   // only if the callback updated the node
   if (d_cb.update(res, id, ccn, cur->getArguments(), &cpf, continueUpdate))
   {
index 8e30f14d2e05f7770f9c6d38b1b85938ddc8063f..268fcda5e791cad155a0af632eab09c0d0b69378 100644 (file)
@@ -41,10 +41,12 @@ class ProofNodeUpdaterCallback
   /** Should proof pn be updated?
    *
    * @param pn the proof node that maybe should be updated
+   * @param fa the assumptions in scope
    * @param continueUpdate whether we should continue recursively updating pn
    * @return whether we should run the update method on pn
    */
   virtual bool shouldUpdate(std::shared_ptr<ProofNode> pn,
+                            const std::vector<Node>& fa,
                             bool& continueUpdate) = 0;
   /**
    * Update the proof rule application, store steps in cdp. Return true if
index ff03e70f54d7fca75c415bed151fb5dc9aefd0f9..f6a39e0fe30b5f6cad0af35708198c53d997294f 100644 (file)
@@ -16,7 +16,10 @@ header = "options/proof_options.h"
 [[option.mode.DOT]]
   name       = "dot"
   help       = "Output DOT proof"
-  
+[[option.mode.VERIT]]
+  name       = "verit"
+  help       = "Output veriT proof"
+
 [[option]]
   name       = "proofPrintConclusion"
   category   = "regular"
index 6b31e60535855ef0c2a15492802c41f3d5a26144..10bf53aa1b6febd463baa5b5fed29ef1d5f008cf 100644 (file)
@@ -29,6 +29,7 @@ ProofPostprocessCallback::ProofPostprocessCallback(
 void ProofPostprocessCallback::initializeUpdate() { d_assumpToProof.clear(); }
 
 bool ProofPostprocessCallback::shouldUpdate(std::shared_ptr<ProofNode> pn,
+                                            const std::vector<Node>& fa,
                                             bool& continueUpdate)
 {
   bool result = pn->getRule() == PfRule::ASSUME
index 363970117793fcc2d30ec2f4dbc6010b6c90160d..906faaafd85b079498d868970529bd77c1e4597b 100644 (file)
@@ -54,6 +54,7 @@ class ProofPostprocessCallback : public ProofNodeUpdaterCallback
    * cancelled, i.e., continueUpdate is set to false.
    */
   bool shouldUpdate(std::shared_ptr<ProofNode> pn,
+                    const std::vector<Node>& fa,
                     bool& continueUpdate) override;
   /** Update the proof rule application.
    *
index b651e390c411e30a5765121f77da41f3451adccf..846df7e41a5b6f375442c76f4682af6f1fd95640 100644 (file)
@@ -34,7 +34,27 @@ PfManager::PfManager(context::UserContext* u, SmtEngine* smte)
       d_pnm(new ProofNodeManager(d_pchecker.get())),
       d_pppg(new PreprocessProofGenerator(
           d_pnm.get(), u, "smt::PreprocessProofGenerator")),
-      d_pfpp(new ProofPostproccess(d_pnm.get(), smte, d_pppg.get())),
+      d_pfpp(new ProofPostproccess(
+          d_pnm.get(),
+          smte,
+          d_pppg.get(),
+          // by default the post-processor will update all assumptions, which
+          // can lead to SCOPE subproofs of the form
+          //   A
+          //  ...
+          //   B1    B2
+          //  ...   ...
+          // ------------
+          //      C
+          // ------------- SCOPE [B1, B2]
+          // B1 ^ B2 => C
+          //
+          // where A is an available assumption from outside the scope (note
+          // that B1 was an assumption of this SCOPE subproof but since it could
+          // be inferred from A, it was updated). This shape is problematic for
+          // the veriT reconstruction, so we disable the update of scoped
+          // assumptions (which would disable the update of B1 in this case).
+          options::proofFormatMode() != options::ProofFormatMode::VERIT)),
       d_finalProof(nullptr)
 {
   // add rules to eliminate here
@@ -131,7 +151,7 @@ void PfManager::printProof(std::ostream& out,
   }
   // TODO (proj #37) according to the proof format, post process the proof node
   // TODO (proj #37) according to the proof format, print the proof node
-  
+
   if (options::proofFormatMode() == options::ProofFormatMode::DOT)
   {
     proof::DotPrinter::print(out, fp.get());
index dedb686c33a4bc689ddefcb3abb767509dad68d9..a27a5697ebb5158522a93b87d96cdc1b676c085b 100644 (file)
@@ -33,12 +33,15 @@ namespace smt {
 
 ProofPostprocessCallback::ProofPostprocessCallback(ProofNodeManager* pnm,
                                                    SmtEngine* smte,
-                                                   ProofGenerator* pppg)
-    : d_pnm(pnm), d_smte(smte), d_pppg(pppg), d_wfpm(pnm)
+                                                   ProofGenerator* pppg,
+                                                   bool updateScopedAssumptions)
+    : d_pnm(pnm),
+      d_smte(smte),
+      d_pppg(pppg),
+      d_wfpm(pnm),
+      d_updateScopedAssumptions(updateScopedAssumptions)
 {
   d_true = NodeManager::currentNM()->mkConst(true);
-  // always check whether to update ASSUME
-  d_elimRules.insert(PfRule::ASSUME);
 }
 
 void ProofPostprocessCallback::initializeUpdate()
@@ -53,9 +56,26 @@ void ProofPostprocessCallback::setEliminateRule(PfRule rule)
 }
 
 bool ProofPostprocessCallback::shouldUpdate(std::shared_ptr<ProofNode> pn,
+                                            const std::vector<Node>& fa,
                                             bool& continueUpdate)
 {
-  return d_elimRules.find(pn->getRule()) != d_elimRules.end();
+  PfRule id = pn->getRule();
+  if (d_elimRules.find(id) != d_elimRules.end())
+  {
+    return true;
+  }
+  // other than elimination rules, we always update assumptions as long as
+  // d_updateScopedAssumptions is true or they are *not* in scope, i.e., not in
+  // fa
+  if (id != PfRule::ASSUME
+      || (!d_updateScopedAssumptions
+          && std::find(fa.begin(), fa.end(), pn->getResult()) != fa.end()))
+  {
+    Trace("smt-proof-pp-debug")
+        << "... not updating in-scope assumption " << pn->getResult() << "\n";
+    return false;
+  }
+  return true;
 }
 
 bool ProofPostprocessCallback::update(Node res,
@@ -1108,6 +1128,7 @@ void ProofPostprocessFinalCallback::initializeUpdate()
 }
 
 bool ProofPostprocessFinalCallback::shouldUpdate(std::shared_ptr<ProofNode> pn,
+                                                 const std::vector<Node>& fa,
                                                  bool& continueUpdate)
 {
   PfRule r = pn->getRule();
@@ -1146,9 +1167,10 @@ bool ProofPostprocessFinalCallback::wasPedanticFailure(std::ostream& out) const
 
 ProofPostproccess::ProofPostproccess(ProofNodeManager* pnm,
                                      SmtEngine* smte,
-                                     ProofGenerator* pppg)
+                                     ProofGenerator* pppg,
+                                     bool updateScopedAssumptions)
     : d_pnm(pnm),
-      d_cb(pnm, smte, pppg),
+      d_cb(pnm, smte, pppg, updateScopedAssumptions),
       // the update merges subproofs
       d_updater(d_pnm, d_cb, true),
       d_finalCb(pnm),
index 9f3e7e55b2af8eb3adad048669f2aabfc005dd72..4111e83dee358418ec29d0e68837bbe0f9857d1f 100644 (file)
@@ -40,7 +40,8 @@ class ProofPostprocessCallback : public ProofNodeUpdaterCallback
  public:
   ProofPostprocessCallback(ProofNodeManager* pnm,
                            SmtEngine* smte,
-                           ProofGenerator* pppg);
+                           ProofGenerator* pppg,
+                           bool updateScopedAssumptions);
   ~ProofPostprocessCallback() {}
   /**
    * Initialize, called once for each new ProofNode to process. This initializes
@@ -56,6 +57,7 @@ class ProofPostprocessCallback : public ProofNodeUpdaterCallback
   void setEliminateRule(PfRule rule);
   /** Should proof pn be updated? */
   bool shouldUpdate(std::shared_ptr<ProofNode> pn,
+                    const std::vector<Node>& fa,
                     bool& continueUpdate) override;
   /** Update the proof rule application. */
   bool update(Node res,
@@ -80,6 +82,8 @@ class ProofPostprocessCallback : public ProofNodeUpdaterCallback
   std::vector<Node> d_wfAssumptions;
   /** Kinds of proof rules we are eliminating */
   std::unordered_set<PfRule, PfRuleHashFunction> d_elimRules;
+  /** Whether we post-process assumptions in scope. */
+  bool d_updateScopedAssumptions;
   //---------------------------------reset at the begining of each update
   /** Mapping assumptions to their proof from preprocessing */
   std::map<Node, std::shared_ptr<ProofNode> > d_assumpToProof;
@@ -244,6 +248,7 @@ class ProofPostprocessFinalCallback : public ProofNodeUpdaterCallback
   void initializeUpdate();
   /** Should proof pn be updated? Returns false, adds to stats. */
   bool shouldUpdate(std::shared_ptr<ProofNode> pn,
+                    const std::vector<Node>& fa,
                     bool& continueUpdate) override;
   /** was pedantic failure */
   bool wasPedanticFailure(std::ostream& out) const;
@@ -274,9 +279,18 @@ class ProofPostprocessFinalCallback : public ProofNodeUpdaterCallback
 class ProofPostproccess
 {
  public:
+  /**
+   * @param pnm The proof node manager we are using
+   * @param smte The SMT engine whose proofs are being post-processed
+   * @param pppg The proof generator for pre-processing proofs
+   * @param updateScopedAssumptions Whether we post-process assumptions in
+   * scope. Since doing so is sound and only problematic depending on who is
+   * consuming the proof, it's true by default.
+   */
   ProofPostproccess(ProofNodeManager* pnm,
                     SmtEngine* smte,
-                    ProofGenerator* pppg);
+                    ProofGenerator* pppg,
+                    bool updateScopedAssumptions = true);
   ~ProofPostproccess();
   /** post-process */
   void process(std::shared_ptr<ProofNode> pf);
index 967b027de6082ee1243c7ea5fd9de68667f2ba3e..f3f31edbf5fb1512d7a547f44d2cfaf100f87700 100644 (file)
@@ -774,6 +774,7 @@ set(regress_0_tests
   regress0/printer/let_shadowing.smt2
   regress0/printer/symbol_starting_w_digit.smt2
   regress0/printer/tuples_and_records.cvc
+  regress0/proofs/scope.smt2
   regress0/push-pop/boolean/fuzz_12.smt2
   regress0/push-pop/boolean/fuzz_13.smt2
   regress0/push-pop/boolean/fuzz_14.smt2
diff --git a/test/regress/regress0/proofs/scope.smt2 b/test/regress/regress0/proofs/scope.smt2
new file mode 100644 (file)
index 0000000..7a32495
--- /dev/null
@@ -0,0 +1,54 @@
+; COMMAND-LINE: --produce-proofs
+; EXIT: 0
+; SCRUBBER: grep -v -E '.*'
+(set-logic AUFLIA)
+(declare-sort A$ 0)
+(declare-sort B$ 0)
+(declare-sort C$ 0)
+(declare-sort D$ 0)
+(declare-sort E$ 0)
+(declare-sort F$ 0)
+(declare-sort G$ 0)
+(declare-sort H$ 0)
+(declare-sort I$ 0)
+(declare-sort J$ 0)
+(declare-sort K$ 0)
+(declare-sort L$ 0)
+(declare-sort M$ 0)
+(declare-sort N$ 0)
+(declare-sort O$ 0)
+(declare-sort P$ 0)
+(declare-fun f1$ (M$ J$) L$)
+(declare-fun f2$ (N$ C$) L$)
+(declare-fun f3$ (C$ A$) B$)
+(declare-fun f4$ (D$ E$) B$)
+(declare-fun f5$ (F$ G$) D$)
+(declare-fun f6$ (A$) F$)
+(declare-fun f7$ (A$) G$)
+(declare-fun f8$ (A$) E$)
+(declare-fun f9$ (A$) E$)
+(declare-fun x1$ () M$)
+(declare-fun x2$ () N$)
+(declare-fun x3$ () C$)
+(declare-fun x4$ () C$)
+(declare-fun x5$ () D$)
+(declare-fun x6$ () J$)
+(declare-fun x7$ () J$)
+(declare-fun x8$ () M$)
+(declare-fun x9$ () O$)
+(declare-fun f10$ (H$ A$) K$)
+(declare-fun f3a$ (E$ H$) I$)
+(declare-fun f3b$ (J$ K$) I$)
+(declare-fun f5a$ (O$ P$) N$)
+(declare-fun x10$ () P$)
+(assert (! (forall ((?v0 A$)) (not (= (f3$ x3$ ?v0) (f4$ (f5$ (f6$ ?v0) (f7$ ?v0)) (f8$ ?v0))))) :named a0))
+(assert (! (forall ((?v0 A$)) (not (= (f3$ x4$ ?v0) (f4$ x5$ (f9$ ?v0))))) :named a1))
+(assert (! (forall ((?v0 A$) (?v1 H$)) (not (= (f3a$ (f8$ ?v0) ?v1) (f3b$ x6$ (f10$ ?v1 ?v0))))) :named a2))
+(assert (! (forall ((?v0 A$) (?v1 H$)) (not (= (f3a$ (f9$ ?v0) ?v1) (f3b$ x7$ (f10$ ?v1 ?v0))))) :named a3))
+(assert (! (not (= (f1$ x8$ x6$) (f2$ (f5a$ x9$ x10$) x3$))) :named a4))
+(assert (! (= (f1$ x1$ x7$) (f1$ x8$ x6$)) :named a5))
+(assert (! (= (f1$ x1$ x7$) (f2$ x2$ x4$)) :named a6))
+(assert (! (= (f2$ x2$ x4$) (f2$ (f5a$ x9$ x10$) x3$)) :named a7))
+(assert (! (not false) :named a8))
+(check-sat)
+(get-proof)
\ No newline at end of file