Skip to content

Commit 2fccdf3

Browse files
Work around Z3 not producing models for some quantified expressions
Z3's solve_eqs preprocessor eliminates a Boolean symbol defined via (assert (= symbol <quantified-expr>)) by substituting the quantified expression, and then returns that quantified expression from get-value instead of a Boolean value, which CBMC cannot parse (see Z3Prover/z3#7743). Emit the equivalence as two implications, (=> symbol <expr>) and (=> <expr> symbol), which solve_eqs does not treat as an elimination opportunity, while preserving logical equivalence. A let-binding shares the quantified expression so that it is emitted only once. As this is a Z3-specific problem, the workaround is gated on the Z3 solver; other solvers keep the single equality assertion. Z3 >= 4.15.3 offers the opt-in option smt.solve_eqs.non_ground (default: true), which avoids this elimination when set to false, but Z3's default behaviour is unchanged and older versions reject the option. This reinstates regression/cbmc-primitives/exists_memory_checks/smt_missing_range_check as CORE: it was demoted to KNOWNBUG in b0eedde precisely because of this Z3 misbehaviour. Likewise, regression/cbmc/z3/Issue5977.desc again expects exactly "unknown", and Quantifiers-assertion, Quantifiers-initialisation2, and Quantifiers-two-dimension-array now pass with Z3 and are no longer tagged broken-z3-smt-backend. A unit test in unit/solvers/smt2/smt2_conv.cpp additionally pins the exact SMT2 output for both Z3 and other solvers. Quantifiers that reach Z3 via other definitions, such as an assignment to a non-Boolean symbol, are handled in the next commit. Runtime of `cbmc --z3` (Z3 4.8.12) is unchanged on the quantifier tests that already passed before. Tests that previously stopped with an error right after the first solver call now run to completion, e.g., Quantifiers-initialisation2 takes 11.9s (previously an error after 1.4s). Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
1 parent 4839902 commit 2fccdf3

7 files changed

Lines changed: 102 additions & 15 deletions

File tree

‎regression/cbmc-primitives/exists_memory_checks/smt_missing_range_check.desc‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
KNOWNBUG smt-backend no-new-smt
1+
CORE smt-backend no-new-smt
22
smt_missing_range_check.c
33
--no-malloc-may-fail --pointer-check -z3
44
^EXIT=10$
@@ -13,6 +13,3 @@ quantifier, for out of bounds memory access, when using the smt backend and
1313
the range of the index is unbound. Note that this test is not expected to work
1414
with the SAT backend at the time of writing, as the SAT backend does not support
1515
quantifiers in this form.
16-
17-
Neither Z3 nor CVC5 currently return complete models, and Bitwuzla does not
18-
complete in more than 5 minutes.

‎regression/cbmc/Quantifiers-assertion/test.desc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
CORE broken-z3-smt-backend no-new-smt
1+
CORE no-new-smt
22
main.c
33

44
^\*\* Results:$

‎regression/cbmc/Quantifiers-initialisation2/test.desc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
CORE broken-z3-smt-backend no-new-smt
1+
CORE no-new-smt
22
main.c
33

44
^\*\* Results:$

‎regression/cbmc/Quantifiers-two-dimension-array/test.desc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
CORE broken-z3-smt-backend no-new-smt
1+
CORE no-new-smt
22
main.c
33

44
^\*\* Results:$

‎regression/cbmc/z3/Issue5977.desc‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
CORE smt-backend broken-cprover-smt-backend no-new-smt
22
Issue5977.c
33
--z3
4-
^EXIT=(6|10)$
4+
^EXIT=6$
55
^SIGNAL=0$
6-
^SMT2 solver returned ("unknown"|non-constant value for variable B\d+)$
6+
^SMT2 solver returned "unknown"$
77
--
88
--
99
This tests that an "unknown" result from the SMT solver is reported.

‎src/solvers/smt2/smt2_conv.cpp‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -985,17 +985,42 @@ literalt smt2_convt::convert(const exprt &expr)
985985
// Note that here we are always converting, so we do not need to consider
986986
// other literal kinds, only "|B###|"
987987

988-
// Z3 refuses get-value when a defined symbol contains a quantifier.
988+
// Z3 refuses get-value when a defined symbol contains a quantifier, so we
989+
// declare the symbol and constrain it separately rather than using
990+
// define-fun.
989991
if(has_quantifier(prepared_expr))
990992
{
991993
out << "(declare-fun ";
992994
convert_literal(l);
993995
out << " () Bool)\n";
994-
out << "(assert (= ";
995-
convert_literal(l);
996-
out << ' ';
997-
convert_expr(prepared_expr);
998-
out << "))\n";
996+
if(solver == solvert::Z3)
997+
{
998+
// A single (assert (= B <expr>)) does not help on Z3: its solve_eqs
999+
// preprocessor eliminates B by substituting the quantified <expr>, and
1000+
// then hands the eliminated form back through get-value (see
1001+
// Z3Prover/z3#7743). Asserting the equivalence as two implications
1002+
// instead prevents this; a let-binding shares the (possibly large)
1003+
// quantified expression so that it is emitted only once. Z3 >= 4.15.3
1004+
// offers the opt-in option smt.solve_eqs.non_ground (default: true),
1005+
// which avoids this elimination when set to false, but Z3's default
1006+
// behaviour is unchanged and older versions reject the option, so we
1007+
// keep this encoding for Z3.
1008+
out << "(assert (let ((?def ";
1009+
convert_expr(prepared_expr);
1010+
out << ")) (and (=> ";
1011+
convert_literal(l);
1012+
out << " ?def) (=> ?def ";
1013+
convert_literal(l);
1014+
out << "))))\n";
1015+
}
1016+
else
1017+
{
1018+
out << "(assert (= ";
1019+
convert_literal(l);
1020+
out << ' ';
1021+
convert_expr(prepared_expr);
1022+
out << "))\n";
1023+
}
9991024
}
10001025
else
10011026
{

‎unit/solvers/smt2/smt2_conv.cpp‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,3 +869,68 @@ TEST_CASE(
869869
REQUIRE(smt2_dec() == decision_proceduret::resultt::D_UNSATISFIABLE);
870870
}
871871
}
872+
873+
/// Helper: full SMT2 emitted by converting (handling) a Boolean expression
874+
static std::string convert_handle(const exprt &expr, smt2_convt::solvert solver)
875+
{
876+
symbol_tablet symbol_table;
877+
namespacet ns(symbol_table);
878+
std::ostringstream out;
879+
smt2_convt conv(ns, "test", "", "QF_BV", solver, out);
880+
conv.handle(expr);
881+
return out.str();
882+
}
883+
884+
TEST_CASE("smt2_convt quantifier definition encoding", "[core][solvers][smt2]")
885+
{
886+
// A Boolean handle whose definition contains a quantifier cannot be emitted
887+
// as a `define-fun` (Z3 rejects `get-value` on such a symbol), so it is
888+
// declared and constrained separately.
889+
const unsignedbv_typet bv8{8};
890+
const symbol_exprt i{"i", bv8};
891+
const exists_exprt quantified{i, equal_exprt{i, from_integer(0, bv8)}};
892+
const std::string quant = "(exists ((i (_ BitVec 8))) (= i (_ bv0 8)))";
893+
894+
GIVEN("a quantified Boolean expression and the Z3 solver")
895+
{
896+
const std::string out = convert_handle(quantified, smt2_convt::solvert::Z3);
897+
INFO("SMT2 output:\n" << out);
898+
899+
THEN("it is declared and constrained by a let-bound equivalence")
900+
{
901+
// A single `(assert (= B0 <quantifier>))` would be undone by Z3's
902+
// solve_eqs preprocessor (Z3Prover/z3#7743), so the equivalence is
903+
// emitted as two implications. A let-binding shares the quantified
904+
// expression so that it is written only once.
905+
const std::string declare = "(declare-fun B0 () Bool)";
906+
const std::string assertion =
907+
"(assert (let ((?def " + quant + ")) (and (=> B0 ?def) (=> ?def B0))))";
908+
909+
REQUIRE(out.find(declare) != std::string::npos);
910+
REQUIRE(out.find(assertion) != std::string::npos);
911+
// the quantified expression is emitted exactly once
912+
REQUIRE(out.find(quant) == out.rfind(quant));
913+
// no plain equality definition
914+
REQUIRE(out.find("(assert (= B0 ") == std::string::npos);
915+
}
916+
}
917+
918+
GIVEN("a quantified Boolean expression and a generic solver")
919+
{
920+
const std::string out =
921+
convert_handle(quantified, smt2_convt::solvert::GENERIC);
922+
INFO("SMT2 output:\n" << out);
923+
924+
THEN("it is declared and constrained by a single equality")
925+
{
926+
// The Z3-specific solve_eqs workaround is not applied to other solvers.
927+
const std::string declare = "(declare-fun B0 () Bool)";
928+
const std::string equality = "(assert (= B0 " + quant + "))";
929+
930+
REQUIRE(out.find(declare) != std::string::npos);
931+
REQUIRE(out.find(equality) != std::string::npos);
932+
// no implication-based workaround
933+
REQUIRE(out.find("(assert (=> ") == std::string::npos);
934+
}
935+
}
936+
}

0 commit comments

Comments
 (0)