Coverage Report

Created: 2025-09-08 08:10

/src/solidity/libsolidity/formal/Z3CHCSmtLib2Interface.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
  This file is part of solidity.
3
4
  solidity is free software: you can redistribute it and/or modify
5
  it under the terms of the GNU General Public License as published by
6
  the Free Software Foundation, either version 3 of the License, or
7
  (at your option) any later version.
8
9
  solidity is distributed in the hope that it will be useful,
10
  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
  GNU General Public License for more details.
13
14
  You should have received a copy of the GNU General Public License
15
  along with solidity.  If not, see <http://www.gnu.org/licenses/>.
16
*/
17
// SPDX-License-Identifier: GPL-3.0
18
19
#include <libsolidity/formal/Z3CHCSmtLib2Interface.h>
20
21
#include <libsolidity/interface/UniversalCallback.h>
22
23
#include <libsmtutil/SMTLib2Parser.h>
24
25
#include <boost/algorithm/string/predicate.hpp>
26
27
#include <stack>
28
29
#ifdef EMSCRIPTEN_BUILD
30
#include <z3++.h>
31
#endif
32
33
using namespace solidity::frontend::smt;
34
using namespace solidity::smtutil;
35
36
Z3CHCSmtLib2Interface::Z3CHCSmtLib2Interface(
37
  frontend::ReadCallback::Callback _smtCallback,
38
  std::optional<unsigned int> _queryTimeout,
39
  bool _computeInvariants
40
): CHCSmtLib2Interface({}, std::move(_smtCallback), _queryTimeout), m_computeInvariants(_computeInvariants)
41
0
{
42
#ifdef EMSCRIPTEN_BUILD
43
  constexpr int resourceLimit = 2000000;
44
  if (m_queryTimeout)
45
    z3::set_param("timeout", int(*m_queryTimeout));
46
  else
47
    z3::set_param("rlimit", resourceLimit);
48
  z3::set_param("rewriter.pull_cheap_ite", true);
49
  z3::set_param("fp.spacer.q3.use_qgen", true);
50
  z3::set_param("fp.spacer.mbqi", false);
51
  z3::set_param("fp.spacer.ground_pobs", false);
52
#endif
53
0
}
54
55
void Z3CHCSmtLib2Interface::setupSmtCallback(bool _enablePreprocessing)
56
0
{
57
0
  if (auto* universalCallback = m_smtCallback.target<frontend::UniversalCallback>())
58
0
    universalCallback->smtCommand().setZ3(m_queryTimeout, _enablePreprocessing, m_computeInvariants);
59
0
}
60
61
CHCSolverInterface::QueryResult Z3CHCSmtLib2Interface::query(smtutil::Expression const& _block)
62
0
{
63
0
  setupSmtCallback(true);
64
0
  std::string query = dumpQuery(_block);
65
0
  try
66
0
  {
67
#ifdef EMSCRIPTEN_BUILD
68
    z3::set_param("fp.xform.slice", true);
69
    z3::set_param("fp.xform.inline_linear", true);
70
    z3::set_param("fp.xform.inline_eager", true);
71
    std::string response = Z3_eval_smtlib2_string(z3::context{}, query.c_str());
72
#else
73
0
    std::string response = querySolver(query);
74
0
#endif
75
    // NOTE: Our internal semantics is UNSAT -> SAFE and SAT -> UNSAFE, which corresponds to usual SMT-based model checking
76
    // However, with CHC solvers, the meaning is flipped, UNSAT -> UNSAFE and SAT -> SAFE.
77
    // So we have to flip the answer.
78
0
    if (boost::starts_with(response, "unsat"))
79
0
    {
80
      // Repeat the query with preprocessing disabled, to get the full proof
81
0
      setupSmtCallback(false);
82
0
      query = "(set-option :produce-proofs true)" + query + "\n(get-proof)";
83
#ifdef EMSCRIPTEN_BUILD
84
      z3::set_param("fp.xform.slice", false);
85
      z3::set_param("fp.xform.inline_linear", false);
86
      z3::set_param("fp.xform.inline_eager", false);
87
      response = Z3_eval_smtlib2_string(z3::context{}, query.c_str());
88
#else
89
0
      response = querySolver(query);
90
0
#endif
91
0
      setupSmtCallback(true);
92
0
      if (!boost::starts_with(response, "unsat"))
93
0
        return {CheckResult::SATISFIABLE, {}, {}};
94
0
      return {CheckResult::SATISFIABLE, {}, graphFromZ3Answer(response)};
95
0
    }
96
97
0
    CheckResult result;
98
0
    if (boost::starts_with(response, "sat"))
99
0
      return {CheckResult::UNSATISFIABLE, invariantsFromSolverResponse(response), {}};
100
0
    if (boost::starts_with(response, "unknown"))
101
0
      result = CheckResult::UNKNOWN;
102
0
    else
103
0
      result = CheckResult::ERROR;
104
105
0
    return {result, {}, {}};
106
0
  }
107
0
  catch(smtutil::SMTSolverInteractionError const&)
108
0
  {
109
0
    return {CheckResult::ERROR, {}, {}};
110
0
  }
111
0
}
112
113
114
CHCSolverInterface::CexGraph Z3CHCSmtLib2Interface::graphFromZ3Answer(std::string const& _proof) const
115
0
{
116
0
  std::stringstream ss(_proof);
117
0
  std::string answer;
118
0
  ss >> answer;
119
0
  smtSolverInteractionRequire(answer == "unsat", "Proof must follow an unsat answer");
120
121
0
  SMTLib2Parser parser(ss);
122
0
  if (parser.isEOF()) // No proof from Z3
123
0
    return {};
124
  // For some reason Z3 outputs everything as a single s-expression
125
0
  SMTLib2Expression parsedOutput;
126
0
  try
127
0
  {
128
0
    parsedOutput = parser.parseExpression();
129
0
  }
130
0
  catch (SMTLib2Parser::ParsingException&)
131
0
  {
132
0
    smtSolverInteractionRequire(false, "Error during parsing Z3's proof");
133
0
  }
134
0
  smtSolverInteractionRequire(parser.isEOF(), "Error during parsing Z3's proof");
135
0
  smtSolverInteractionRequire(!isAtom(parsedOutput), "Encountered unexpected format of Z3's proof");
136
0
  auto& commands = asSubExpressions(parsedOutput);
137
0
  ScopedParser expressionParser(m_context);
138
0
  for (auto& command: commands)
139
0
  {
140
0
    if (isAtom(command))
141
0
      continue;
142
143
0
    auto const& args = asSubExpressions(command);
144
0
    smtSolverInteractionRequire(args.size() > 0, "Encountered unexpected format of Z3's proof");
145
0
    auto const& head = args[0];
146
0
    if (!isAtom(head))
147
0
      continue;
148
149
    // Z3 can introduce new helper predicates to be used in the proof
150
    // e.g., "(declare-fun query!0 (Bool Bool Bool Int Int Bool Bool Bool Bool Bool Bool Bool Int) Bool)"
151
0
    if (asAtom(head) == "declare-fun")
152
0
    {
153
0
      smtSolverInteractionRequire(args.size() == 4, "Encountered unexpected format of Z3's proof");
154
0
      auto const& name = args[1];
155
0
      auto const& domainSorts = args[2];
156
0
      auto const& codomainSort = args[3];
157
0
      smtSolverInteractionRequire(isAtom(name), "Encountered unexpected format of Z3's proof");
158
0
      smtSolverInteractionRequire(!isAtom(domainSorts), "Encountered unexpected format of Z3's proof");
159
0
      expressionParser.addVariableDeclaration(asAtom(name), expressionParser.toSort(codomainSort));
160
0
    }
161
    // The subexpression starting with "proof" contains the whole proof, which we need to transform to our internal
162
    // representation
163
0
    else if (asAtom(head) == "proof")
164
0
    {
165
0
      inlineLetExpressions(command);
166
0
      return graphFromSMTLib2Expression(command, expressionParser);
167
0
    }
168
0
  }
169
0
  return {};
170
0
}
171
172
CHCSolverInterface::CexGraph Z3CHCSmtLib2Interface::graphFromSMTLib2Expression(
173
  SMTLib2Expression const& _proof,
174
  ScopedParser& _context
175
)
176
0
{
177
0
  auto fact = [](SMTLib2Expression const& _node) -> SMTLib2Expression const& {
178
0
    if (isAtom(_node))
179
0
      return _node;
180
0
    smtSolverInteractionRequire(!asSubExpressions(_node).empty(), "Encountered unexpected format of Z3's proof");
181
0
    return asSubExpressions(_node).back();
182
0
  };
183
0
  smtSolverInteractionRequire(!isAtom(_proof), "Encountered unexpected format of Z3's proof");
184
0
  auto const& proofArgs = asSubExpressions(_proof);
185
0
  smtSolverInteractionRequire(proofArgs.size() == 2, "Encountered unexpected format of Z3's proof");
186
0
  smtSolverInteractionRequire(isAtom(proofArgs.at(0)) && asAtom(proofArgs.at(0)) == "proof", "Encountered unexpected format of Z3's proof");
187
0
  auto const& proofNode = proofArgs.at(1);
188
0
  auto const& derivedFact = fact(proofNode);
189
0
  if (isAtom(proofNode) || !isAtom(derivedFact) || asAtom(derivedFact) != "false")
190
0
    return {};
191
192
0
  CHCSolverInterface::CexGraph graph;
193
194
0
  std::stack<SMTLib2Expression const*> proofStack;
195
0
  proofStack.push(&asSubExpressions(proofNode).at(1));
196
197
0
  std::map<SMTLib2Expression const*, unsigned> visitedIds;
198
0
  unsigned nextId = 0;
199
200
0
  auto const* root = proofStack.top();
201
0
  auto const& derivedRootFact = fact(*root);
202
0
  visitedIds.emplace(root, nextId++);
203
0
  graph.nodes.emplace(visitedIds.at(root), _context.toSMTUtilExpression(derivedRootFact));
204
205
0
  auto isHyperRes = [](SMTLib2Expression const& expr) {
206
0
    if (isAtom(expr)) return false;
207
0
    auto const& subExprs = asSubExpressions(expr);
208
0
    smtSolverInteractionRequire(!subExprs.empty(), "Encountered unexpected format of Z3's proof");
209
0
    auto const& op = subExprs.at(0);
210
0
    if (isAtom(op)) return false;
211
0
    auto const& opExprs = asSubExpressions(op);
212
0
    if (opExprs.size() < 2) return false;
213
0
    auto const& ruleName = opExprs.at(1);
214
0
    return isAtom(ruleName) && asAtom(ruleName) == "hyper-res";
215
0
  };
216
217
0
  while (!proofStack.empty())
218
0
  {
219
0
    auto const* currentNode = proofStack.top();
220
0
    smtSolverInteractionRequire(visitedIds.find(currentNode) != visitedIds.end(), "Error in processing Z3's proof");
221
0
    auto id = visitedIds.at(currentNode);
222
0
    smtSolverInteractionRequire(graph.nodes.count(id), "Error in processing Z3's proof");
223
0
    proofStack.pop();
224
225
0
    if (isHyperRes(*currentNode))
226
0
    {
227
0
      auto const& args = asSubExpressions(*currentNode);
228
0
      smtSolverInteractionRequire(args.size() > 1, "Unexpected format of hyper-resolution rule in Z3's proof");
229
      // args[0] is the name of the rule
230
      // args[1] is the clause used
231
      // last argument is the derived fact
232
      // the arguments in the middle are the facts where we need to recurse
233
0
      for (unsigned i = 2; i < args.size() - 1; ++i)
234
0
      {
235
0
        auto const* child = &args[i];
236
0
        if (!visitedIds.count(child))
237
0
        {
238
0
          visitedIds.emplace(child, nextId++);
239
0
          proofStack.push(child);
240
0
        }
241
242
0
        auto childId = visitedIds.at(child);
243
0
        if (!graph.nodes.count(childId))
244
0
        {
245
0
          graph.nodes.emplace(childId, _context.toSMTUtilExpression(fact(*child)));
246
0
          graph.edges[childId] = {};
247
0
        }
248
249
0
        graph.edges[id].push_back(childId);
250
0
      }
251
0
    }
252
0
  }
253
0
  return graph;
254
0
}
255