/* This file is part of solidity. solidity is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. solidity is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with solidity. If not, see . */ // SPDX-License-Identifier: GPL-3.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace solidity; using namespace solidity::util; using namespace solidity::smtutil; using rational = boost::rational; pair> DPLL::simplify() { // TODO use vector? map assignments; while (!clauses.empty() && !ranges::all_of(clauses, [](Clause const& _c) { return _c.literals.size() > 1; })) { // TODO this assumes that no clause contains more than one constraint // TODO add an assertion for that. for (Clause const& c: clauses) if (c.literals.empty()) return {false, {}}; else if (c.literals.size() == 1) { Literal const& literal = c.literals.front(); if (literal.kind == Literal::Constraint) constraints.push_back(literal.index); else { bool value = literal.kind == Literal::PositiveVariable ? true : false; if (assignments.count(literal.index) && assignments.at(literal.index) != value) return {false, {}}; //cout << "Assigning" << variableName(literal.index) << " " << value << endl; assignments[literal.index] = value; } } if (!setVariables(assignments, true)) return {false, {}}; } return {true, move(assignments)}; } size_t DPLL::findUnassignedVariable() const { for (Clause const& c: clauses) for (Literal const& l: c.literals) if (l.kind != Literal::Constraint) return l.index; solAssert(false, ""); return 0; } bool DPLL::setVariable(size_t _index, bool _value) { return setVariables({{_index, _value}}); } bool DPLL::setVariables(map const& _assignments, bool _removeSingleConstraintClauses) { // TODO do this in a way so that we do not have to copy twice. vector prunedClauses; for (Clause const& c: clauses) { bool skipClause = false; vector newClause; if (_removeSingleConstraintClauses && c.literals.size() == 1 && c.literals.front().kind == Literal::Constraint) continue; for (Literal const& l: c.literals) if (l.kind != Literal::Constraint && _assignments.count(l.index)) { if (_assignments.at(l.index) == (l.kind == Literal::PositiveVariable)) { skipClause = true; break; } } else newClause.push_back(l); if (skipClause) continue; if (newClause.empty()) return false; prunedClauses.push_back(Clause{move(newClause)}); } clauses = move(prunedClauses); return true; } void BooleanLPSolver::reset() { m_state = vector{{State{}}}; m_internalVariableCounter = 0; m_constraintCounter = 0; // Do not reset the solver, it should retain its cache. } void BooleanLPSolver::push() { if (m_state.back().infeasible) { m_state.emplace_back(); m_state.back().infeasible = true; return; } map variables = m_state.back().variables; map>, 2>> bounds = m_state.back().bounds; vector isBooleanVariable = m_state.back().isBooleanVariable; m_state.emplace_back(); m_state.back().variables = move(variables); m_state.back().isBooleanVariable = move(isBooleanVariable); m_state.back().bounds = move(bounds); } void BooleanLPSolver::pop() { m_state.pop_back(); solAssert(!m_state.empty(), ""); } void BooleanLPSolver::declareVariable(string const& _name, SortPointer const& _sort) { // Internal variables are '$', so escape `$` to `$$`. string name = (_name.empty() || _name.at(0) != '$') ? _name : "$$" + _name; // TODO This will not be an integer variable in our model. // Introduce a new kind? solAssert(_sort && (_sort->kind == Kind::Int || _sort->kind == Kind::Bool), ""); solAssert(!m_state.back().variables.count(name), ""); declareVariable(name, _sort->kind == Kind::Bool); } void BooleanLPSolver::addAssertion(Expression const& _expr) { if (_expr.arguments.empty()) m_state.back().clauses.emplace_back(Clause{vector{*parseLiteral(_expr)}}); else if (_expr.name == "=") { // Try to see if both sides are linear. optional> left = parseLinearSum(_expr.arguments.at(0)); optional> right = parseLinearSum(_expr.arguments.at(1)); if (left && right) { vector data = *left - *right; data[0] *= -1; Constraint c{move(data), _expr.name == "="}; if (!tryAddDirectBounds(c)) { size_t index = addConstraint(move(c)); m_state.back().clauses.emplace_back(Clause{vector{Literal{Literal::Constraint, index}}}); } } else if (_expr.arguments.at(0).arguments.empty() && isBooleanVariable(_expr.arguments.at(0).name)) addBooleanEquality(*parseLiteral(_expr.arguments.at(0)), _expr.arguments.at(1)); else if (_expr.arguments.at(1).arguments.empty() && isBooleanVariable(_expr.arguments.at(1).name)) addBooleanEquality(*parseLiteral(_expr.arguments.at(1)), _expr.arguments.at(0)); else { Literal newBoolean = *parseLiteral(declareInternalBoolean()); addBooleanEquality(newBoolean, _expr.arguments.at(0)); addBooleanEquality(newBoolean, _expr.arguments.at(1)); } } else if (_expr.name == "and") { addAssertion(_expr.arguments.at(0)); addAssertion(_expr.arguments.at(1)); } else if (_expr.name == "or") { // We could try to parse a full clause here. Literal left = parseLiteralOrReturnEqualBoolean(_expr.arguments.at(0)); Literal right = parseLiteralOrReturnEqualBoolean(_expr.arguments.at(1)); if (left.kind == Literal::Constraint && right.kind == Literal::Constraint) { // We cannot have more than one constraint per clause. right = *parseLiteral(declareInternalBoolean()); addBooleanEquality(right, _expr.arguments.at(1)); } m_state.back().clauses.emplace_back(Clause{vector{left, right}}); } else if (_expr.name == "not") { Literal l = negate(parseLiteralOrReturnEqualBoolean(_expr.arguments.at(0))); m_state.back().clauses.emplace_back(Clause{vector{l}}); } else if (_expr.name == "<=") { optional> left = parseLinearSum(_expr.arguments.at(0)); optional> right = parseLinearSum(_expr.arguments.at(1)); if (!left || !right) return; vector data = *left - *right; data[0] *= -1; Constraint c{move(data), _expr.name == "="}; if (!tryAddDirectBounds(c)) { size_t index = addConstraint(move(c)); m_state.back().clauses.emplace_back(Clause{vector{Literal{Literal::Constraint, index}}}); } } else if (_expr.name == ">=") addAssertion(_expr.arguments.at(1) <= _expr.arguments.at(0)); else if (_expr.name == "<") addAssertion(_expr.arguments.at(0) <= _expr.arguments.at(1) - 1); else if (_expr.name == ">") addAssertion(_expr.arguments.at(1) < _expr.arguments.at(0)); } pair> BooleanLPSolver::check(vector const& _expressionsToEvaluate) { cout << "Solving boolean constraint system" << endl; cout << toString() << endl; if (m_state.back().infeasible) return make_pair(CheckResult::UNSATISFIABLE, vector{}); // TODO compress variables because we ignore boolean variables SolvingState state; for (auto&& [name, index]: m_state.back().variables) resizeAndSet(state.variableNames, index, name); for (auto&& [index, value]: m_state.back().bounds) resizeAndSet(state.bounds, index, value); std::vector clauses; for (State const& s: m_state) for (Clause const& clause: s.clauses) clauses.push_back(clause); auto&& [result, model] = runDPLL(state, DPLL{move(clauses), {}}); if (result == CheckResult::SATISFIABLE) { vector requestedModel; for (Expression const& e: _expressionsToEvaluate) requestedModel.emplace_back( e.arguments.empty() && model.count(e.name) ? ::toString(model[e.name], 0) : string("unknown") ); return {result, move(requestedModel)}; } else return {result, {}}; } string BooleanLPSolver::toString() const { string result; for (State const& s: m_state) for (Clause const& c: s.clauses) result += toString(c); result += "-- Bounds:\n"; for (State const& s: m_state) for (auto&& [index, bounds]: s.bounds) { if (!bounds[0] && !bounds[1]) continue; if (bounds[0]) result += ::toString(*bounds[0]) + " <= "; result += variableName(index); if (bounds[1]) result += " <= " + ::toString(*bounds[1]); result += "\n"; } return result; } Expression BooleanLPSolver::declareInternalBoolean() { string name = "$" + to_string(m_internalVariableCounter++); declareVariable(name, true); return smtutil::Expression(name, {}, SortProvider::boolSort); } void BooleanLPSolver::declareVariable(string const& _name, bool _boolean) { size_t index = m_state.back().variables.size() + 1; m_state.back().variables[_name] = index; resizeAndSet(m_state.back().isBooleanVariable, index, _boolean); } optional BooleanLPSolver::parseLiteral(smtutil::Expression const& _expr) { // TODO constanst true/false? if (_expr.arguments.empty()) { if (isBooleanVariable(_expr.name)) return Literal{ Literal::PositiveVariable, m_state.back().variables.at(_expr.name) }; } else if (_expr.name == "not") return negate(parseLiteralOrReturnEqualBoolean(_expr.arguments.at(0))); else if (_expr.name == "<=") { optional> left = parseLinearSum(_expr.arguments.at(0)); optional> right = parseLinearSum(_expr.arguments.at(1)); if (!left || !right) return {}; vector data = *left - *right; data[0] *= -1; return Literal{Literal::Constraint, addConstraint(Constraint{move(data), false})}; } else if (_expr.name == ">=") return parseLiteral(_expr.arguments.at(1) <= _expr.arguments.at(0)); else if (_expr.name == "<") return parseLiteral(_expr.arguments.at(0) <= _expr.arguments.at(1) - 1); else if (_expr.name == ">") return parseLiteral(_expr.arguments.at(1) < _expr.arguments.at(0)); return {}; } Literal BooleanLPSolver::negate(Literal const& _lit) { switch (_lit.kind) { case Literal::NegativeVariable: return Literal{Literal::PositiveVariable, _lit.index}; case Literal::PositiveVariable: return Literal{Literal::NegativeVariable, _lit.index}; default: break; } Constraint const& c = constraint(_lit.index); solAssert(!c.equality, ""); // X > b // -x < -b // -x <= -b - 1 Constraint negated = c; negated.data *= -1; negated.data[0] -= 1; return Literal{Literal::Constraint, addConstraint(move(negated))}; } Literal BooleanLPSolver::parseLiteralOrReturnEqualBoolean(Expression const& _expr) { if (optional literal = parseLiteral(_expr)) return *literal; else { Literal newBoolean = *parseLiteral(declareInternalBoolean()); addBooleanEquality(newBoolean, _expr); return newBoolean; } } optional> BooleanLPSolver::parseLinearSum(smtutil::Expression const& _expr) const { if (_expr.arguments.empty() || _expr.name == "*") return parseProduct(_expr); else if (_expr.name == "+" || _expr.name == "-") { optional> left = parseLinearSum(_expr.arguments.at(0)); optional> right = parseLinearSum(_expr.arguments.at(1)); if (!left || !right) return std::nullopt; return _expr.name == "+" ? add(*left, *right) : *left - *right; } else return std::nullopt; } optional> BooleanLPSolver::parseProduct(smtutil::Expression const& _expr) const { if (_expr.arguments.empty()) return parseFactor(_expr); else if (_expr.name == "*") // The multiplication ensures that only one of them can be a variable. return vectorProduct(parseFactor(_expr.arguments.at(0)), parseFactor(_expr.arguments.at(1))); else return std::nullopt; } optional> BooleanLPSolver::parseFactor(smtutil::Expression const& _expr) const { solAssert(_expr.arguments.empty(), ""); solAssert(!_expr.name.empty(), ""); if ('0' <= _expr.name[0] && _expr.name[0] <= '9') return vector{rational(bigint(_expr.name))}; else if (_expr.name == "true") return vector{rational(bigint(1))}; else if (_expr.name == "false") return vector{rational(bigint(0))}; size_t index = m_state.back().variables.at(_expr.name); solAssert(index > 0, ""); if (isBooleanVariable(index)) return nullopt; return factorForVariable(index, rational(bigint(1))); } bool BooleanLPSolver::tryAddDirectBounds(Constraint const& _constraint) { auto nonzero = _constraint.data | ranges::views::enumerate | ranges::views::tail | ranges::views::filter( [](std::pair const& _x) { return !!_x.second; } ); // TODO we can exit early on in the loop above. if (ranges::distance(nonzero) > 1) return false; //cout << "adding direct bound." << endl; if (ranges::distance(nonzero) == 0) { // 0 <= b or 0 = b if ( _constraint.data.front() < 0 || (_constraint.equality && _constraint.data.front() != 0) ) { // cout << "SETTING INF" << endl; m_state.back().infeasible = true; } } else { auto&& [varIndex, factor] = nonzero.front(); // a * x <= b rational bound = _constraint.data[0] / factor; if (factor > 0 || _constraint.equality) addUpperBound(varIndex, bound); if (factor < 0 || _constraint.equality) addLowerBound(varIndex, bound); } return true; } void BooleanLPSolver::addUpperBound(size_t _index, rational _value) { //cout << "adding " << variableName(_index) << " <= " << toString(_value) << endl; if (!m_state.back().bounds[_index][1] || _value < *m_state.back().bounds[_index][1]) m_state.back().bounds[_index][1] = move(_value); } void BooleanLPSolver::addLowerBound(size_t _index, rational _value) { // Lower bound must be at least zero. _value = max(_value, rational{}); //cout << "adding " << variableName(_index) << " >= " << toString(_value) << endl; if (!m_state.back().bounds[_index][0] || _value > *m_state.back().bounds[_index][0]) m_state.back().bounds[_index][0] = move(_value); } size_t BooleanLPSolver::addConstraint(Constraint _constraint) { size_t index = ++m_constraintCounter; m_state.back().constraints[index] = move(_constraint); return index; } void BooleanLPSolver::addBooleanEquality(Literal const& _left, smtutil::Expression const& _right) { if (optional right = parseLiteral(_right)) { // includes: not, <=, <, >=, >, boolean variables. // a = b <=> (-a \/ b) /\ (a \/ -b) Literal negLeft = negate(_left); Literal negRight = negate(*right); m_state.back().clauses.emplace_back(Clause{vector{negLeft, *right}}); m_state.back().clauses.emplace_back(Clause{vector{_left, negRight}}); } else if (_right.name == "=" && parseLinearSum(_right.arguments.at(0)) && parseLinearSum(_right.arguments.at(1))) // a = (x = y) <=> a = (x <= y && x >= y) addBooleanEquality( _left, _right.arguments.at(0) <= _right.arguments.at(1) && _right.arguments.at(1) <= _right.arguments.at(0) ); else { Literal a = parseLiteralOrReturnEqualBoolean(_right.arguments.at(0)); Literal b = parseLiteralOrReturnEqualBoolean(_right.arguments.at(1)); if (a.kind == Literal::Constraint && b.kind == Literal::Constraint) { // We cannot have more than one constraint per clause. b = *parseLiteral(declareInternalBoolean()); addBooleanEquality(b, _right.arguments.at(1)); } if (_right.name == "and") { // a = and(x, y) <=> (-a \/ x) /\ ( -a \/ y) /\ (a \/ -x \/ -y) m_state.back().clauses.emplace_back(Clause{vector{negate(_left), a}}); m_state.back().clauses.emplace_back(Clause{vector{negate(_left), b}}); m_state.back().clauses.emplace_back(Clause{vector{_left, negate(a), negate(b)}}); } else if (_right.name == "or") { // a = or(x, y) <=> (-a \/ x \/ y) /\ (a \/ -x) /\ (a \/ -y) m_state.back().clauses.emplace_back(Clause{vector{negate(_left), a, b}}); m_state.back().clauses.emplace_back(Clause{vector{_left, negate(a)}}); m_state.back().clauses.emplace_back(Clause{vector{_left, negate(b)}}); } else if (_right.name == "=") { // l = eq(a, b) <=> (-l or -a or b) and (-l or a or -b) and (l or -a or -b) and (l or a or b) m_state.back().clauses.emplace_back(Clause{vector{negate(_left), negate(a), b}}); m_state.back().clauses.emplace_back(Clause{vector{negate(_left), a, negate(b)}}); m_state.back().clauses.emplace_back(Clause{vector{_left, negate(a), negate(b)}}); m_state.back().clauses.emplace_back(Clause{vector{_left, a, b}}); } else solAssert(false, "Unsupported operation: " + _right.name); } } // TODO as input we do not need the full solving state, only the bounds pair> BooleanLPSolver::runDPLL(SolvingState& _solvingState, DPLL _dpll) { //cout << "Running dpll on" << endl << toString(_solvingState.bounds) << "\n" << toString(_dpll) << endl; auto&& [simplifyResult, booleanModel] = _dpll.simplify(); if (!simplifyResult) return {CheckResult::UNSATISFIABLE, {}}; cout << "Simplified to" << endl << toString(_solvingState.bounds) << "\n" << toString(_dpll) << endl; CheckResult result = CheckResult::UNKNOWN; map model; // TODO we really should do the below, and probably even before and after each boolean decision. // It is very likely that we have some complicated boolean condition in the program, but // the unconditional things are already unsatisfiable. // TODO could run this check already even though not all variables are assigned // and return unsat if it is already unsat. if (_dpll.clauses.empty()) { cout << "Invoking LP..." << endl; _solvingState.constraints.clear(); for (size_t c: _dpll.constraints) _solvingState.constraints.emplace_back(constraint(c)); LPResult lpResult; tie(lpResult, model) = m_lpSolver.check(_solvingState); // LP solver is a rational solver, not an integer solver, // so "feasible" does not mean there is an integer solution. // TODO we could check the model to see if all variables are integer....s // TODO if it is a pure boolean problem, we can actually answer "satisfiable" switch (lpResult) { case LPResult::Infeasible: result = CheckResult::UNSATISFIABLE; break; case LPResult::Feasible: // TODO this is actually wrong, but difficult to test otherwise. result = CheckResult::SATISFIABLE; break; case LPResult::Unknown: case LPResult::Unbounded: result = CheckResult::UNKNOWN; break; } } else { size_t varIndex = _dpll.findUnassignedVariable(); DPLL copy = _dpll; if (_dpll.setVariable(varIndex, true)) { booleanModel[varIndex] = true; cout << "Trying " << variableName(varIndex) << " = true\n"; tie(result, model) = runDPLL(_solvingState, move(_dpll)); // TODO actually we should also handle UNKNOWN here. } // TODO it will never be "satisfiable" if (result != CheckResult::SATISFIABLE) { cout << "Trying " << variableName(varIndex) << " = false\n"; if (!copy.setVariable(varIndex, false)) return {CheckResult::UNSATISFIABLE, {}}; booleanModel[varIndex] = false; /*auto&& [result, model] = */runDPLL(_solvingState, move(copy)); } } if (result == CheckResult::SATISFIABLE) { for (auto const& [index, value]: booleanModel) model[variableName(index)] = value ? 1 : 0; return {result, move(model)}; } else return {result, {}}; } string BooleanLPSolver::toString(DPLL const& _dpll) const { string result; for (size_t c: _dpll.constraints) result += toString(Clause{{Literal{Literal::Constraint, c}}}); for (Clause const& c: _dpll.clauses) result += toString(c); return result; } string BooleanLPSolver::toString(std::vector>, 2>> const& _bounds) const { string result; for (auto&& [index, bounds]: _bounds | ranges::views::enumerate) { if (!bounds[0] && !bounds[1]) continue; if (bounds[0]) result += ::toString(*bounds[0]) + " <= "; // TODO If the variables are compressed, this does no longer work. result += variableName(index); if (bounds[1]) result += " <= " + ::toString(*bounds[1]); result += "\n"; } return result; } string BooleanLPSolver::toString(Clause const& _clause) const { vector literals; for (Literal const& l: _clause.literals) if (l.kind == Literal::Constraint) { Constraint const& constr = constraint(l.index); vector line; for (auto&& [index, multiplier]: constr.data | ranges::views::enumerate) if (index > 0 && multiplier != 0) { string mult = multiplier == -1 ? "-" : multiplier == 1 ? "" : ::toString(multiplier) + " "; line.emplace_back(mult + variableName(index)); } literals.emplace_back(joinHumanReadable(line, " + ") + (constr.equality ? " = " : " <= ") + ::toString(constr.data.front())); } else literals.emplace_back((l.kind == Literal::NegativeVariable ? "!" : "") + variableName(l.index)); return joinHumanReadable(literals, " \\/ ") + "\n"; } Constraint const& BooleanLPSolver::constraint(size_t _index) const { for (State const& state: m_state) if (state.constraints.count(_index)) return state.constraints.at(_index); solAssert(false, ""); } string BooleanLPSolver::variableName(size_t _index) const { for (auto const& v: m_state.back().variables) if (v.second == _index) return v.first; return {}; } bool BooleanLPSolver::isBooleanVariable(string const& _name) const { if (!m_state.back().variables.count(_name)) return false; size_t index = m_state.back().variables.at(_name); solAssert(index > 0, ""); return isBooleanVariable(index); } bool BooleanLPSolver::isBooleanVariable(size_t _index) const { return _index < m_state.back().isBooleanVariable.size() && m_state.back().isBooleanVariable.at(_index); }