From 8dec6b068073e2439d02d2929eb0c2a19c10fc38 Mon Sep 17 00:00:00 2001 From: chriseth Date: Mon, 4 Jan 2021 17:04:04 +0100 Subject: [PATCH] Boolean LP Solver. --- libsolutil/BooleanLP.cpp | 713 ++++++++++++++++++++++++++++++++++ libsolutil/BooleanLP.h | 148 +++++++ libsolutil/CMakeLists.txt | 2 + test/CMakeLists.txt | 1 + test/libsolutil/BooleanLP.cpp | 322 +++++++++++++++ 5 files changed, 1186 insertions(+) create mode 100644 libsolutil/BooleanLP.cpp create mode 100644 libsolutil/BooleanLP.h create mode 100644 test/libsolutil/BooleanLP.cpp diff --git a/libsolutil/BooleanLP.cpp b/libsolutil/BooleanLP.cpp new file mode 100644 index 000000000..ba1541fa9 --- /dev/null +++ b/libsolutil/BooleanLP.cpp @@ -0,0 +1,713 @@ +/* + 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 '$' + 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 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::Unknown: + case LPResult::Unbounded: + case LPResult::Feasible: + result = CheckResult::UNKNOWN; + } + } + 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); +} diff --git a/libsolutil/BooleanLP.h b/libsolutil/BooleanLP.h new file mode 100644 index 000000000..04c6a5153 --- /dev/null +++ b/libsolutil/BooleanLP.h @@ -0,0 +1,148 @@ +/* + 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 +#pragma once + +#include + +#include + +#include + +#include +#include +#include +#include + +namespace solidity::util +{ + +/** + * A literal of a (potentially negated) boolean variable or an inactive constraint. + */ +struct Literal +{ + enum { PositiveVariable, NegativeVariable, Constraint } kind; + // Either points to a boolean variable or to a constraint. + size_t index{0}; +}; + +/** + * A clause is a disjunction of literals. + */ +struct Clause +{ + std::vector literals; +}; + + +struct State +{ + bool infeasible = false; + std::map variables; + std::vector isBooleanVariable; + // Potential constraints, referenced through clauses + std::map constraints; + // Unconditional bounds on variables + std::map>, 2>> bounds; + + std::vector clauses; +}; + +struct DPLL +{ + /// Try to simplify the set of clauses without branching. + /// @returns false if the set of clauses is unsatisfiable (without considering the constraints). + std::pair> simplify(); + size_t findUnassignedVariable() const; + bool setVariable(size_t _index, bool _value); + /// Sets variables and removes clauses or literals from clauses. + /// @returns false if the clauses are unsatisfiable. + bool setVariables(std::map const& _assignments, bool _removeSingleConstraintClauses = false); + + std::vector clauses; + std::vector constraints; +}; + + +class BooleanLPSolver: public smtutil::SolverInterface +{ +public: + void reset() override; + void push() override; + void pop() override; + + void declareVariable(std::string const& _name, smtutil::SortPointer const& _sort) override; + + void addAssertion(smtutil::Expression const& _expr) override; + + std::pair> + check(std::vector const& _expressionsToEvaluate) override; + + std::pair>> check(); + + std::string toString() const; + +private: + using rational = boost::rational; + + smtutil::Expression declareInternalBoolean(); + void declareVariable(std::string const& _name, bool _boolean); + + std::optional parseLiteral(smtutil::Expression const& _expr); + Literal negate(Literal const& _lit); + + Literal parseLiteralOrReturnEqualBoolean(smtutil::Expression const& _expr); + + /// Parses the expression and expects a linear sum of variables. + /// Returns a vector with the first element being the constant and the + /// other elements the factors for the respective variables. + /// If the expression cannot be properly parsed or is not linear, + /// returns an empty vector. + std::optional> parseLinearSum(smtutil::Expression const& _expression) const; + std::optional> parseProduct(smtutil::Expression const& _expression) const; + std::optional> parseFactor(smtutil::Expression const& _expression) const; + + bool tryAddDirectBounds(Constraint const& _constraint); + void addUpperBound(size_t _index, rational _value); + void addLowerBound(size_t _index, rational _value); + + size_t addConstraint(Constraint _constraint); + + void addBooleanEquality(Literal const& _left, smtutil::Expression const& _right); + + std::pair> runDPLL(SolvingState& _solvingState, DPLL _dpll); + std::string toString(DPLL const& _dpll) const; + std::string toString(std::vector>, 2>> const& _bounds) const; + std::string toString(Clause const& _clause) const; + + Constraint const& constraint(size_t _index) const; + + std::string variableName(size_t _index) const; + + bool isBooleanVariable(std::string const& _name) const; + bool isBooleanVariable(size_t _index) const; + + size_t m_constraintCounter = 0; + size_t m_internalVariableCounter = 0; + /// Stack of state, to allow for push()/pop(). + std::vector m_state{{State{}}}; + LPSolver m_lpSolver; +}; + + +} diff --git a/libsolutil/CMakeLists.txt b/libsolutil/CMakeLists.txt index a1d915bb3..be2900223 100644 --- a/libsolutil/CMakeLists.txt +++ b/libsolutil/CMakeLists.txt @@ -2,6 +2,8 @@ set(sources Algorithms.h AnsiColorized.h Assertions.h + BooleanLP.cpp + BooleanLP.h Common.h CommonData.cpp CommonData.h diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 73a3fef4f..d1f333fab 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -31,6 +31,7 @@ set(contracts_sources detect_stray_source_files("${contracts_sources}" "contracts/") set(libsolutil_sources + libsolutil/BooleanLP.cpp libsolutil/Checksum.cpp libsolutil/CommonData.cpp libsolutil/CommonIO.cpp diff --git a/test/libsolutil/BooleanLP.cpp b/test/libsolutil/BooleanLP.cpp new file mode 100644 index 000000000..a1de4ec07 --- /dev/null +++ b/test/libsolutil/BooleanLP.cpp @@ -0,0 +1,322 @@ +/* + 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 + +using namespace std; +using namespace solidity::smtutil; +using namespace solidity::util; + + +namespace solidity::util::test +{ + + +class BooleanLPTestFramework +{ +protected: + BooleanLPSolver solver; + + Expression variable(string const& _name) + { + return solver.newVariable(_name, smtutil::SortProvider::sintSort); + } + + Expression booleanVariable(string const& _name) + { + return solver.newVariable(_name, smtutil::SortProvider::boolSort); + } + + void addAssertion(Expression const& _expr) { solver.addAssertion(_expr); } + + void feasible(vector> const& _solution) + { + vector variables; + vector values; + for (auto const& [var, val]: _solution) + { + variables.emplace_back(var); + values.emplace_back(val); + } + auto [result, model] = solver.check(variables); + // TODO it actually never returns "satisfiable". + BOOST_CHECK(result == smtutil::CheckResult::SATISFIABLE); + BOOST_CHECK_EQUAL(joinHumanReadable(model), joinHumanReadable(values)); + } + + void infeasible() + { + auto [result, model] = solver.check({}); + BOOST_CHECK(result == smtutil::CheckResult::UNSATISFIABLE); + } +}; + + + +BOOST_FIXTURE_TEST_SUITE(BooleanLP, BooleanLPTestFramework, *boost::unit_test::label("nooptions")) + +BOOST_AUTO_TEST_CASE(lower_bound) +{ + Expression x = variable("x"); + Expression y = variable("y"); + addAssertion(y >= 1); + addAssertion(x <= 10); + addAssertion(2 * x + y <= 2); + feasible({{x, "0"}, {y, "2"}}); +} + +BOOST_AUTO_TEST_CASE(check_infeasible) +{ + Expression x = variable("x"); + addAssertion(x <= 3 && x >= 5); + infeasible(); +} + +BOOST_AUTO_TEST_CASE(unbounded) +{ + Expression x = variable("x"); + addAssertion(x >= 2); + feasible({{x, "2"}}); +} + +BOOST_AUTO_TEST_CASE(unbounded_two) +{ + Expression x = variable("x"); + Expression y = variable("y"); + addAssertion(x + y >= 2); + addAssertion(x <= 10); + feasible({{x, "10"}, {y, "0"}}); +} + +BOOST_AUTO_TEST_CASE(equal) +{ + Expression x = variable("x"); + Expression y = variable("y"); + solver.addAssertion(x == y + 10); + solver.addAssertion(x <= 20); + feasible({{x, "20"}, {y, "10"}}); +} + +BOOST_AUTO_TEST_CASE(push_pop) +{ + Expression x = variable("x"); + Expression y = variable("y"); + solver.addAssertion(x + y <= 20); + feasible({{x, "20"}, {y, "0"}}); + + solver.push(); + solver.addAssertion(x <= 5); + solver.addAssertion(y <= 5); + feasible({{x, "5"}, {y, "5"}}); + + solver.push(); + solver.addAssertion(x >= 7); + infeasible(); + solver.pop(); + + feasible({{x, "5"}, {y, "5"}}); + solver.pop(); + + feasible({{x, "20"}, {y, "0"}}); +} + +BOOST_AUTO_TEST_CASE(less_than) +{ + Expression x = variable("x"); + Expression y = variable("y"); + solver.addAssertion(x == y + 1); + solver.push(); + solver.addAssertion(y < x); + feasible({{x, "1"}, {y, "0"}}); + solver.pop(); + solver.push(); + solver.addAssertion(y > x); + infeasible(); + solver.pop(); +} + +BOOST_AUTO_TEST_CASE(equal_constant) +{ + Expression x = variable("x"); + Expression y = variable("y"); + solver.addAssertion(x < y); + solver.addAssertion(y == 5); + feasible({{x, "4"}, {y, "5"}}); +} + +BOOST_AUTO_TEST_CASE(chained_less_than) +{ + Expression x = variable("x"); + Expression y = variable("y"); + Expression z = variable("z"); + solver.addAssertion(x < y && y < z); + + solver.push(); + solver.addAssertion(z == 0); + infeasible(); + solver.pop(); + + solver.push(); + solver.addAssertion(z == 1); + infeasible(); + solver.pop(); + + solver.push(); + solver.addAssertion(z == 2); + feasible({{x, "0"}, {y, "1"}, {z, "2"}}); + solver.pop(); +} + +BOOST_AUTO_TEST_CASE(splittable) +{ + Expression x = variable("x"); + Expression y = variable("y"); + Expression z = variable("z"); + Expression w = variable("w"); + solver.addAssertion(x < y); + solver.addAssertion(x < y - 2); + solver.addAssertion(z + w == 28); + + solver.push(); + solver.addAssertion(z >= 30); + infeasible(); + solver.pop(); + + solver.addAssertion(z >= 2); + feasible({{x, "0"}, {y, "3"}, {z, "2"}, {w, "26"}}); + solver.push(); + solver.addAssertion(z >= 4); + feasible({{x, "0"}, {y, "3"}, {z, "4"}, {w, "24"}}); + + solver.push(); + solver.addAssertion(z < 4); + infeasible(); + solver.pop(); + + // z >= 4 is still active + solver.addAssertion(z >= 3); + feasible({{x, "0"}, {y, "3"}, {z, "4"}, {w, "24"}}); +} + + +BOOST_AUTO_TEST_CASE(boolean) +{ + Expression x = variable("x"); + Expression y = variable("y"); + Expression z = variable("z"); + solver.addAssertion(x <= 5); + solver.addAssertion(y <= 2); + solver.push(); + solver.addAssertion(x < y && x > y); + infeasible(); + solver.pop(); + Expression w = booleanVariable("w"); + solver.addAssertion(w == (x < y)); + solver.addAssertion(w || x > y); + feasible({{x, "0"}, {y, "3"}, {z, "2"}, {w, "26"}}); +} + +BOOST_AUTO_TEST_CASE(boolean_complex) +{ + Expression x = variable("x"); + Expression y = variable("y"); + Expression a = booleanVariable("a"); + Expression b = booleanVariable("b"); + solver.addAssertion(x <= 5); + solver.addAssertion(y <= 2); + solver.addAssertion(a == (x >= 2)); + solver.addAssertion(a || b); + solver.addAssertion(b == !a); + solver.addAssertion(b == (x < 2)); + feasible({{a, "1"}, {b, "0"}, {x, "5"}, {y, "2"}}); + solver.addAssertion(a && b); + infeasible(); +} + +BOOST_AUTO_TEST_CASE(magic_square) +{ + vector vars; + for (size_t i = 0; i < 9; i++) + vars.push_back(variable(string{static_cast('a' + i)})); + for (Expression const& var: vars) + solver.addAssertion(1 <= var && var <= 9); + // If we assert all to be mutually distinct, the problems gets too large. + for (size_t i = 0; i < 9; i++) + for (size_t j = i + 7; j < 9; j++) + solver.addAssertion(vars[i] != vars[j]); + for (size_t i = 0; i < 4; i++) + solver.addAssertion(vars[i] != vars[i + 1]); + for (size_t i = 0; i < 3; i++) + solver.addAssertion(vars[i] + vars[i + 3] + vars[i + 6] == 15); + for (size_t i = 0; i < 9; i += 3) + solver.addAssertion(vars[i] + vars[i + 1] + vars[i + 2] == 15); + feasible({ + {vars[0], "1"}, {vars[1], "0"}, {vars[2], "5"}, + {vars[3], "1"}, {vars[4], "0"}, {vars[5], "5"}, + {vars[6], "1"}, {vars[7], "0"}, {vars[8], "5"} + }); +} + +BOOST_AUTO_TEST_CASE(boolean_complex_2) +{ + Expression x = variable("x"); + Expression y = variable("y"); + Expression a = booleanVariable("a"); + Expression b = booleanVariable("b"); + solver.addAssertion(x != 20); + feasible({{x, "19"}}); + solver.addAssertion(x <= 5 || (x > 7 && x != 8)); + solver.addAssertion(a = (x == 9)); + feasible({{a, "1"}, {b, "0"}, {x, "5"}}); +// solver.addAssertion(!a || (x == 10)); +// solver.addAssertion(b == !a); +// solver.addAssertion(b == (x < 200)); +// feasible({{a, "1"}, {b, "0"}, {x, "5"}}); +// solver.addAssertion(a && b); +// infeasible(); +} + + +BOOST_AUTO_TEST_CASE(pure_boolean) +{ + Expression a = booleanVariable("a"); + Expression b = booleanVariable("b"); + Expression c = booleanVariable("c"); + Expression d = booleanVariable("d"); + Expression e = booleanVariable("e"); + Expression f = booleanVariable("f"); + solver.addAssertion(a && !b); + solver.addAssertion(b || c); + solver.addAssertion(c == (d || c)); + solver.addAssertion(f == (b && !c)); + solver.addAssertion(!f || e); + solver.addAssertion(c || d); + feasible({}); + solver.addAssertion(a && b); + infeasible(); +} + +BOOST_AUTO_TEST_SUITE_END() + +}