Skip to content

Commit

Permalink
Add generic macro for rewriting body of quantified formulas (cvc5#11391)
Browse files Browse the repository at this point in the history
The quantifiers rewriter applies a set of rewrites recursively to bodies
of quantified formulas.

This introduces a macro for this method and fine grained proof
reconstruction for it. It introduces 2 new RARE rewrites for this
purpose. FYI @Lachnitt

It also makes a few minor fixes and refactorings in the relevant
classes.

Fills 27/91 remaining quantifier rewrite holes on regressions.

Also fixes a bug in the Eunoia signature for QUANT_MERGE_PRENEX, which
wasn't dropping duplicate variables.
  • Loading branch information
ajreynol authored Dec 3, 2024
1 parent 4b1a47f commit 9dfecba
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 51 deletions.
16 changes: 16 additions & 0 deletions include/cvc5/cvc5_proof_rule.h
Original file line number Diff line number Diff line change
Expand Up @@ -2658,6 +2658,18 @@ enum ENUM(ProofRewriteRule)
* \endverbatim
*/
EVALUE(MACRO_QUANT_VAR_ELIM_INEQ),
/**
* \verbatim embed:rst:leading-asterisk
* **Quantifiers -- Macro quantifiers rewrite body**
*
* .. math::
* \forall X.\> F = \forall X.\> G
*
* where :math:`G` is semantically equivalent to :math:`F`.
*
* \endverbatim
*/
EVALUE(MACRO_QUANT_REWRITE_BODY),
/**
* \verbatim embed:rst:leading-asterisk
* **Datatypes -- Instantiation**
Expand Down Expand Up @@ -3085,6 +3097,8 @@ enum ENUM(ProofRewriteRule)
EVALUE(ARRAY_STORE_OVERWRITE),
/** Auto-generated from RARE rule array-store-self */
EVALUE(ARRAY_STORE_SELF),
/** Auto-generated from RARE rule array-read-over-write-split */
EVALUE(ARRAY_READ_OVER_WRITE_SPLIT),
/** Auto-generated from RARE rule bool-double-not-elim */
EVALUE(BOOL_DOUBLE_NOT_ELIM),
/** Auto-generated from RARE rule bool-not-true */
Expand Down Expand Up @@ -3853,6 +3867,8 @@ enum ENUM(ProofRewriteRule)
EVALUE(EQ_SYMM),
/** Auto-generated from RARE rule eq-cond-deq */
EVALUE(EQ_COND_DEQ),
/** Auto-generated from RARE rule eq-ite-lift */
EVALUE(EQ_ITE_LIFT),
/** Auto-generated from RARE rule distinct-binary-elim */
EVALUE(DISTINCT_BINARY_ELIM),
/** Auto-generated from RARE rule uf-bv2nat-int2bv */
Expand Down
8 changes: 8 additions & 0 deletions proofs/eo/cpc/rules/Rewrites.eo
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@
:args (t1 i1)
:conclusion (= (store t1 i1 (select t1 i1)) t1)
)
(declare-rule array-read-over-write-split ((@T0 Type) (@T1 Type) (@T2 Type) (@T3 Type) (@T4 Type) (t1 (Array @T0 @T1)) (i1 @T2) (e1 @T3) (j1 @T4))
:args (t1 i1 e1 j1)
:conclusion (= (select (store t1 j1 e1) i1) (ite (= i1 j1) e1 (select t1 i1)))
)
(declare-rule bool-double-not-elim ((t1 Bool))
:args (t1)
:conclusion (= (not (not t1)) t1)
Expand Down Expand Up @@ -1908,6 +1912,10 @@
:args (t1 s1 r1)
:conclusion (= (= (= t1 s1) (= t1 r1)) (and (not (= t1 s1)) (not (= t1 r1))))
)
(declare-rule eq-ite-lift ((@T0 Type) (@T1 Type) (@T2 Type) (C1 Bool) (t1 @T0) (s1 @T1) (r1 @T2))
:args (C1 t1 s1 r1)
:conclusion (= (= (ite C1 t1 s1) r1) (ite C1 (= t1 r1) (= s1 r1)))
)
(declare-rule distinct-binary-elim ((@T0 Type) (@T1 Type) (t1 @T0) (s1 @T1))
:args (t1 s1)
:conclusion (= (distinct t1 s1) (not (= t1 s1)))
Expand Down
2 changes: 2 additions & 0 deletions src/api/cpp/cvc5_proof_rule_template.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ const char* toString(cvc5::ProofRewriteRule rule)
case ProofRewriteRule::MACRO_QUANT_VAR_ELIM_INEQ:
return "macro-quant-var-elim-ineq";
case ProofRewriteRule::QUANT_VAR_ELIM_EQ: return "quant-var-elim-eq";
case ProofRewriteRule::MACRO_QUANT_REWRITE_BODY:
return "macro-quant-rewrite-body";
case ProofRewriteRule::DT_INST: return "dt-inst";
case ProofRewriteRule::DT_COLLAPSE_SELECTOR: return "dt-collapse-selector";
case ProofRewriteRule::DT_COLLAPSE_TESTER: return "dt-collapse-tester";
Expand Down
38 changes: 36 additions & 2 deletions src/rewriter/basic_rewrite_rcons.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ bool BasicRewriteRCons::prove(CDProof* cdp,
{
Node eq = a.eqNode(b);
Trace("trewrite-rcons") << "Reconstruct " << eq << std::endl;
Node lhs = eq[0];
Node rhs = eq[1];
// this probably should never happen
if (eq[0] == eq[1])
{
Expand Down Expand Up @@ -218,6 +216,12 @@ void BasicRewriteRCons::ensureProofForTheoryRewrite(
handledMacro = true;
}
break;
case ProofRewriteRule::MACRO_QUANT_REWRITE_BODY:
if (ensureProofMacroQuantRewriteBody(cdp, eq))
{
handledMacro = true;
}
break;
default: break;
}
if (handledMacro)
Expand Down Expand Up @@ -799,6 +803,14 @@ bool BasicRewriteRCons::ensureProofMacroQuantVarElimEq(CDProof* cdp,
for (size_t i = 0, nchild = body1r.getNumChildren(); i < nchild; i++)
{
Node eql = body1r[i].eqNode(body1re[i]);
// must ensure that this is indeed an equivalence, otherwise this trust
// step will be unsound. this is the case e.g. when
// a != (str.++ b x) is turned into x != (str.substr a (str.len b) ...)
// where the latter implies the former, but they are not equivalent
if (rewrite(body1r[i]) != rewrite(body1re[i]))
{
return false;
}
if (body1r[i] == body1re[i])
{
cdp->addStep(eql, ProofRule::REFL, {}, {eql[0]});
Expand Down Expand Up @@ -912,6 +924,28 @@ bool BasicRewriteRCons::ensureProofMacroQuantMiniscope(CDProof* cdp,
return true;
}

bool BasicRewriteRCons::ensureProofMacroQuantRewriteBody(CDProof* cdp,
const Node& eq)
{
Trace("brc-macro") << "Expand quant rewrite body " << eq[0] << " == " << eq[1]
<< std::endl;
// Call the utility again with proof tracking and construct the term
// conversion proof. This proof itself may have trust steps in it.
TConvProofGenerator tcpg(d_env, nullptr);
theory::quantifiers::QuantifiersRewriter qrew(
nodeManager(), d_env.getRewriter(), options());
Node qr = qrew.computeRewriteBody(eq[0], &tcpg);
if (qr != eq[1])
{
Assert(false) << "Failed to rewrite " << eq[0] << " to " << qr
<< " != " << eq[1];
return false;
}
std::shared_ptr<ProofNode> pfn = tcpg.getProofFor(eq);
cdp->addProof(pfn);
return true;
}

bool BasicRewriteRCons::ensureProofArithPolyNormRel(CDProof* cdp,
const Node& eq)
{
Expand Down
10 changes: 10 additions & 0 deletions src/rewriter/basic_rewrite_rcons.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,16 @@ class BasicRewriteRCons : protected EnvObj
* @return true if added a closed proof of eq to cdp.
*/
bool ensureProofMacroQuantMiniscope(CDProof* cdp, const Node& eq);
/**
* Elaborate a rewrite eq that was proven by
* ProofRewriteRule::MACRO_QUANT_REWRITE_BODY.
*
* @param cdp The proof to add to.
* @param eq The rewrite proven by
* ProofRewriteRule::MACRO_QUANT_REWRITE_BODY.
* @return true if added a closed proof of eq to cdp.
*/
bool ensureProofMacroQuantRewriteBody(CDProof* cdp, const Node& eq);
/**
* @param cdp The proof to add to.
* @param eq The rewrite that can be proven by ProofRule::ARITH_POLY_NORM_REL.
Expand Down
3 changes: 3 additions & 0 deletions src/theory/arrays/rewrites
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
(define-rule array-store-self ((t ?Array) (i ?))
(store t i (select t i))
t)

(define-rule array-read-over-write-split ((t ?Array) (i ?) (e ?) (j ?))
(select (store t j e) i) (ite (= i j) e (select t i)))
127 changes: 85 additions & 42 deletions src/theory/quantifiers/quantifiers_rewriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "expr/node_algorithm.h"
#include "expr/skolem_manager.h"
#include "options/quantifiers_options.h"
#include "proof/conv_proof_generator.h"
#include "theory/arith/arith_msum.h"
#include "theory/booleans/theory_bool_rewriter.h"
#include "theory/datatypes/theory_datatypes_utils.h"
Expand Down Expand Up @@ -118,6 +119,8 @@ QuantifiersRewriter::QuantifiersRewriter(NodeManager* nm,
TheoryRewriteCtx::PRE_DSL);
registerProofRewriteRule(ProofRewriteRule::MACRO_QUANT_VAR_ELIM_INEQ,
TheoryRewriteCtx::PRE_DSL);
registerProofRewriteRule(ProofRewriteRule::MACRO_QUANT_REWRITE_BODY,
TheoryRewriteCtx::PRE_DSL);
}

Node QuantifiersRewriter::rewriteViaRule(ProofRewriteRule id, const Node& n)
Expand Down Expand Up @@ -363,6 +366,19 @@ Node QuantifiersRewriter::rewriteViaRule(ProofRewriteRule id, const Node& n)
}
}
break;
case ProofRewriteRule::MACRO_QUANT_REWRITE_BODY:
{
if (n.getKind() != Kind::FORALL)
{
return Node::null();
}
Node nr = computeRewriteBody(n);
if (nr != n)
{
return nr;
}
}
break;
default: break;
}
return Node::null();
Expand Down Expand Up @@ -712,31 +728,25 @@ void QuantifiersRewriter::computeDtTesterIteSplit(
Node QuantifiersRewriter::computeProcessTerms(const Node& q,
const std::vector<Node>& args,
Node body,
QAttributes& qa) const
QAttributes& qa,
TConvProofGenerator* pg) const
{
options::IteLiftQuantMode iteLiftMode = options::IteLiftQuantMode::NONE;
if (qa.isStandard())
{
iteLiftMode = d_opts.quantifiers.iteLiftQuant;
}
std::vector<Node> new_conds;
std::map<Node, Node> cache;
Node n = computeProcessTerms2(q, args, body, cache, new_conds, iteLiftMode);
if (!new_conds.empty())
{
new_conds.push_back(n);
n = nodeManager()->mkNode(Kind::OR, new_conds);
}
return n;
return computeProcessTerms2(q, args, body, cache, iteLiftMode, pg);
}

Node QuantifiersRewriter::computeProcessTerms2(
const Node& q,
const std::vector<Node>& args,
Node body,
std::map<Node, Node>& cache,
std::vector<Node>& new_conds,
options::IteLiftQuantMode iteLiftMode) const
options::IteLiftQuantMode iteLiftMode,
TConvProofGenerator* pg) const
{
NodeManager* nm = nodeManager();
Trace("quantifiers-rewrite-term-debug2")
Expand All @@ -745,40 +755,12 @@ Node QuantifiersRewriter::computeProcessTerms2(
if( iti!=cache.end() ){
return iti->second;
}
if (body.isClosure())
{
// Ensure no shadowing. If this term is a closure quantifying a variable
// in args, then we introduce fresh variable(s) and replace this closure
// to be over the fresh variables instead.
std::vector<Node> oldVars;
std::vector<Node> newVars;
for (size_t i = 0, nvars = body[0].getNumChildren(); i < nvars; i++)
{
const Node& v = body[0][i];
if (std::find(args.begin(), args.end(), v) != args.end())
{
Trace("quantifiers-rewrite-unshadow")
<< "Found shadowed variable " << v << " in " << q << std::endl;
oldVars.push_back(v);
Node nv = ElimShadowNodeConverter::getElimShadowVar(q, body, i);
newVars.push_back(nv);
}
}
if (!oldVars.empty())
{
Assert(oldVars.size() == newVars.size());
Node sbody = body.substitute(
oldVars.begin(), oldVars.end(), newVars.begin(), newVars.end());
cache[body] = sbody;
return sbody;
}
}
bool changed = false;
std::vector<Node> children;
for (const Node& bc : body)
{
// do the recursive call on children
Node nn = computeProcessTerms2(q, args, bc, cache, new_conds, iteLiftMode);
Node nn = computeProcessTerms2(q, args, bc, cache, iteLiftMode, pg);
children.push_back(nn);
changed = changed || nn != bc;
}
Expand All @@ -798,11 +780,39 @@ Node QuantifiersRewriter::computeProcessTerms2(
ret = body;
}

Node retOrig = ret;
Trace("quantifiers-rewrite-term-debug2")
<< "Returning " << ret << " for " << body << std::endl;
// do context-independent rewriting
if (ret.getKind() == Kind::EQUAL
&& iteLiftMode != options::IteLiftQuantMode::NONE)
if (ret.isClosure())
{
// Ensure no shadowing. If this term is a closure quantifying a variable
// in args, then we introduce fresh variable(s) and replace this closure
// to be over the fresh variables instead.
std::vector<Node> oldVars;
std::vector<Node> newVars;
for (size_t i = 0, nvars = ret[0].getNumChildren(); i < nvars; i++)
{
const Node& v = ret[0][i];
if (std::find(args.begin(), args.end(), v) != args.end())
{
Trace("quantifiers-rewrite-unshadow")
<< "Found shadowed variable " << v << " in " << q << std::endl;
oldVars.push_back(v);
Node nv = ElimShadowNodeConverter::getElimShadowVar(q, ret, i);
newVars.push_back(nv);
}
}
if (!oldVars.empty())
{
Assert(oldVars.size() == newVars.size());
Node sbody = ret.substitute(
oldVars.begin(), oldVars.end(), newVars.begin(), newVars.end());
ret = sbody;
}
}
else if (ret.getKind() == Kind::EQUAL
&& iteLiftMode != options::IteLiftQuantMode::NONE)
{
for (size_t i = 0; i < 2; i++)
{
Expand Down Expand Up @@ -863,6 +873,17 @@ Node QuantifiersRewriter::computeProcessTerms2(
ret = fullApp;
}
}
if (pg != nullptr)
{
if (retOrig != ret)
{
pg->addRewriteStep(retOrig,
ret,
nullptr,
false,
TrustId::MACRO_THEORY_REWRITE_RCONS_SIMPLE);
}
}
cache[body] = ret;
return ret;
}
Expand Down Expand Up @@ -2154,6 +2175,28 @@ bool QuantifiersRewriter::isStandard(QAttributes& qa, const Options& opts)
return qa.isStandard() && !is_strict_trigger;
}

Node QuantifiersRewriter::computeRewriteBody(const Node& n,
TConvProofGenerator* pg) const
{
Assert(n.getKind() == Kind::FORALL);
QAttributes qa;
QuantAttributes::computeQuantAttributes(n, qa);
std::vector<Node> args(n[0].begin(), n[0].end());
Node body = computeProcessTerms(n, args, n[1], qa, pg);
if (body != n[1])
{
std::vector<Node> children;
children.push_back(n[0]);
children.push_back(body);
if (n.getNumChildren() == 3)
{
children.push_back(n[2]);
}
return d_nm->mkNode(Kind::FORALL, children);
}
return n;
}

bool QuantifiersRewriter::doOperation(Node q,
RewriteStep computeOption,
QAttributes& qa) const
Expand Down
Loading

0 comments on commit 9dfecba

Please sign in to comment.