Solver Wrappers
A solver wrapper provides the glue code between the core components of Murxla and the solver under test. The interface for solver wrappers is defined at src/solver/solver.hpp.
The wrapper’s interface defines abstract classes for sorts
(murxla::AbsSort
), terms (murxla::AbsTerm
) and the
solver itself (murxla::Solver
).
Solver Wrapper Implementation
A solver wrapper derives solver-specific classes from the three abstract classes of the interface and must at least override the member functions required to be overriden. Additionally, for each class, there exists a set of member functions with default implementations that should be overriden (if supported) to test the solver (see the list of required and optional overrides below).
Random Number Generator (RNG)
Murxla’s generic solver API as implemented in the solver wrapper interface
aims at providing an interface for the solver under test that allows to plug
in any variants of API functions for specific SMT-LIB features.
For example, murxla::Solver::mk_value()
provides one interface
for creating bit-vector values
(murxla::Solver::mk_value(Sort sort, const std::string& value, Base base)
),
but most solvers provide API functions that take either a string or integer
representation for the value as argument.
For cases like this, it is useful to be able to randomly decide, for each call,
which API function of the solver under test to use.
In order to be able to deterministically replay a given trace, even when
minimized, it is necessary to decouple non-deterministic choices
in the solver wrapper from Murxla’s main RNG. Thus, the solver wrapper base
class maintains an independent RNG, which is seeded with a random seed at the
beginning of each call to the execution function of an action (naming
convention: <Action>::run(<args>)
). This seed is traced, and since each
call to run()
must first trace the Action’s execution via macro
MURXLA_TRACE
, we automatically seed the solver wrapper’s RNG
there and prepend the seed to the trace line:
#define MURXLA_TRACE \
d_solver.get_rng().reseed(d_sng.seed()), \
OstreamVoider() \
& Action::TraceStream(d_smgr).stream() \
<< std::setw(5) << d_sng.seed() << " "
The solver wrapper’s RNG can be directly accessed as the protected member
murxla::Solver::d_rng
, or via
murxla::Solver::get_rng()
.
Global Sort and Term Handling
Murxla defines types
Sort
and Term
for sort and term representations
which are shared pointers
(std::shared_ptr)
to AbsSort
and AbsTerm
.
We only deal with Sort
and
Term
objects in Murxla core components
and at the interface between Murxla and the solver wrapper.
Solver-Specific Extensions
Murxla supports various extensions with solver-specific features, i.e., features that cannot be plugged in via the generic solver wrapper API.
Operator Kinds
Murxla defines a base set of operator kinds as static const members of type
murxla::Op::Kind
of class murxla::Op
.
Solver-specific operator kinds are (by convention) defined as members of the
solver wrapper implementation of murxla::AbsTerm
.
By convention, we prefix solver-specific operator kinds with the solver’s
(short) name.
For example, the solver wrapper for Bitwuzla defines a bit-vector decrement
operator as member of murxla::BzlaTerm
as
inline static const Op::Kind OP_BV_DEC = "bzla-OP_BV_DEC";
Solver-specific operator kinds are added to the
operator kind manager via
overriding murxla::Solver::configure_opmgr()
, e.g.,
opmgr->add_op_kind(BzlaTerm::OP_BV_DEC, 1, 0, SORT_BV, {SORT_BV}, THEORY_BV);
Special Value Kinds
Murxla introduces the notion of murxla::AbsTerm::SpecialValueKind
for values that can be considered a special value in a theory, e.g.,
floating-point NaN (of a given floating-point format), or the minimum signed
bit-vector value (of a given bit-width).
Terms representing special values are created via
murxla::Solver::mk_special_value()
. A list of all special value
kinds defined in murxla::AbsTerm
is provided below:
As with solver-specific operator kinds, solver-specific special value kinds are
(by convention) defined as a static const member of type
murxla::AbsTerm::SpecialValueKind
of the solver wrapper
implementation of murxla::AbsTerm
.
And again, by convention, we prefix solver-specific special value kinds with
the solver’s (short) name.
Solver wrappers can extend the pre-defined list of special value kinds with
solver-specific kinds via
murxla::Solver::add_special_value()
.
Actions
Murxla defines a base API model implementing SMT-LIB semantics
(as a finite state machine, see FSM) with a
base set of actions.
This base API model can be extended with solver-specific features that cannot
be immediately plugged into the base API Model by adding transitions with
associated solver-specific actions to the FSM via overriding
murxla::Solver::configure_fsm()
.
Solver-specific actions directly interact with the API of the solver under test
(see FSM: Actions).
They are added to an existing (or new, solver-specific)
state while defining its priority and (optionally) a next state via
murxla::State::add_action()
(see FSM Configuration).
Features Unsupported By The Solver Under Test
Murxla requires to define a solver profile (see Solver Profiles) to
define the solver test configuration. The solver profile allows to define
which theories to consider and to disable unsupported sort and operator kinds.
Unsupported actions can be disabled via overriding
murxla::Solver::disable_unsupported_actions()
.
Solver Options
To enable option fuzzing, a solver wrapper can advertise a set of options
to Murxla by overriding murxla::Solver::configure_options()
.
Options can be added via the murxla::SolverManager::add_option()
of the solver manager object passed to
murxla::Solver::configure_options()
.
Murxla distinguishes three different option types:
Boolean options (
murxla::SolverOptionBool
)Numeric options (
murxla::SolverOptionNum
)Options with multiple string values (
murxla::SolverOptionList
)
For example, Bitwuzla supports querying all available options via its API.
This makes adding options via overriding
murxla::Solver::configure_options()
very easy since it allows to add options in an automated way:
void
BzlaSolver::configure_options(SolverManager* smgr) const
{
Bitwuzla* bzla = bitwuzla_new();
BitwuzlaOptionInfo info;
for (int32_t i = 0; i < BITWUZLA_OPT_NUM_OPTS; ++i)
{
BitwuzlaOption opt = static_cast<BitwuzlaOption>(i);
bitwuzla_get_option_info(bzla, opt, &info);
if (info.is_numeric)
{
smgr->add_option(new SolverOptionNum<uint32_t>(info.lng,
info.numeric.min_val,
info.numeric.max_val,
info.numeric.def_val));
}
else
{
std::vector<std::string> values;
for (size_t j = 0; j < info.string.num_values; ++j)
{
values.emplace_back(info.string.values[j]);
}
smgr->add_option(
new SolverOptionList(info.lng, values, info.string.def_val));
}
}
bitwuzla_delete(bzla);
}