3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-12 01:56:22 +00:00

Merge remote-tracking branch 'origin/master' into c3

This commit is contained in:
CEisenhofer 2026-07-01 17:18:21 +02:00
commit 706f62286e
199 changed files with 11004 additions and 4584 deletions

View file

@ -18,13 +18,13 @@
double ackr_helper::calculate_lemma_bound(fun2terms_map const& occs1, sel2terms_map const& occs2) {
double total = 0;
for (auto const& kv : occs1) {
total += n_choose_2_chk(kv.m_value->var_args.size());
total += kv.m_value->const_args.size() * kv.m_value->var_args.size();
for (auto const &[k, v] : occs1) {
total += n_choose_2_chk(v->var_args.size());
total += v->const_args.size() * v->var_args.size();
}
for (auto const& kv : occs2) {
total += n_choose_2_chk(kv.m_value->var_args.size());
total += kv.m_value->const_args.size() * kv.m_value->var_args.size();
for (auto const &[k, v] : occs2) {
total += n_choose_2_chk(v->var_args.size());
total += v->const_args.size() * v->var_args.size();
}
return total;
}

View file

@ -52,14 +52,26 @@ public:
return m_autil.is_select(a) && is_uninterp_const(a->get_arg(0));
}
void mark_non_select_rec(expr* t, expr_mark& visited, expr_mark& non_select) {
if (visited.is_marked(t))
return;
visited.mark(t, true);
non_select.mark(t, true);
if (is_app(t)) {
for (expr *arg : *to_app(t))
mark_non_select_rec(arg, visited,non_select);
}
}
void mark_non_select(app* a, expr_mark& non_select) {
if (m_autil.is_select(a)) {
bool first = true;
expr_mark visited;
for (expr* arg : *a) {
if (first)
first = false;
else
non_select.mark(arg, true);
mark_non_select_rec(arg, visited, non_select);
}
}
else {
@ -70,10 +82,10 @@ public:
void prune_non_select(obj_map<app, app_set*> & sels, expr_mark& non_select) {
ptr_vector<app> nons;
for (auto& kv : sels) {
if (non_select.is_marked(kv.m_key)) {
nons.push_back(kv.m_key);
dealloc(kv.m_value);
for (auto &[k, v] : sels) {
if (non_select.is_marked(k)) {
nons.push_back(k);
dealloc(v);
}
}
for (app* s : nons) {

View file

@ -149,9 +149,9 @@ void lackr::eager_enc() {
checkpoint();
ackr(v);
}
for (auto const& kv : m_sel2terms) {
for (auto const &[k, v] : m_sel2terms) {
checkpoint();
ackr(kv.get_value());
ackr(v);
}
}
@ -190,13 +190,13 @@ void lackr::abstract_fun(fun2terms_map const& apps) {
}
void lackr::abstract_sel(sel2terms_map const& apps) {
for (auto const& kv : apps) {
func_decl * fd = kv.m_key->get_decl();
for (app * t : kv.m_value->const_args) {
for (auto const &[k, v] : apps) {
func_decl * fd = k->get_decl();
for (app * t : v->const_args) {
app * fc = m.mk_fresh_const(fd->get_name(), t->get_sort());
m_info->set_abstr(t, fc);
}
for (app * t : kv.m_value->var_args) {
for (app * t : v->var_args) {
app * fc = m.mk_fresh_const(fd->get_name(), t->get_sort());
m_info->set_abstr(t, fc);
}

View file

@ -60,6 +60,7 @@
<Warn>4</Warn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<DocumentationFile>$(OutputPath)\Microsoft.Z3.xml</DocumentationFile>
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<!-- Compilation items -->

View file

@ -310,7 +310,7 @@ go run basic_example.go
## Memory Management
The Go bindings use `runtime.SetFinalizer` to automatically manage Z3 reference counts. You don't need to manually call inc_ref/dec_ref. However, be aware that finalizers run during garbage collection, so resources may not be freed immediately.
The Go bindings use `runtime.SetFinalizer` to automatically manage Z3 reference counts. You don't need to manually call inc_ref/dec_ref. However, be aware that finalizers run during garbage collection, so resources may not be freed immediately. The bindings enable `Z3_enable_concurrent_dec_ref` when creating contexts so finalizer-driven decref operations are safe with concurrent GC.
## Thread Safety

View file

@ -89,6 +89,7 @@ type Context struct {
// NewContext creates a new Z3 context with default configuration.
func NewContext() *Context {
ctx := &Context{ptr: C.Z3_mk_context_rc(C.Z3_mk_config())}
C.Z3_enable_concurrent_dec_ref(ctx.ptr)
runtime.SetFinalizer(ctx, func(c *Context) {
C.Z3_del_context(c.ptr)
})
@ -98,6 +99,7 @@ func NewContext() *Context {
// NewContextWithConfig creates a new Z3 context with the given configuration.
func NewContextWithConfig(cfg *Config) *Context {
ctx := &Context{ptr: C.Z3_mk_context_rc(cfg.ptr)}
C.Z3_enable_concurrent_dec_ref(ctx.ptr)
runtime.SetFinalizer(ctx, func(c *Context) {
C.Z3_del_context(c.ptr)
})

View file

@ -154,7 +154,7 @@ static void decide_eh(void* _p, Z3_solver_callback cb, Z3_ast _val, unsigned bit
info->jenv->CallVoidMethod(info->jobj, info->decide, (jlong)_val, bit, is_pos);
}
static jboolean on_binding_eh(void* _p, Z3_solver_callback cb, Z3_ast _q, Z3_ast _inst) {
[[maybe_unused]] static jboolean on_binding_eh(void* _p, Z3_solver_callback cb, Z3_ast _q, Z3_ast _inst) {
JavaInfo *info = static_cast<JavaInfo*>(_p);
ScopedCB scoped(info, cb);
return info->jenv->CallBooleanMethod(info->jobj, info->on_binding, (jlong)_q, (jlong)_inst);

View file

@ -5400,10 +5400,20 @@
"dev": true
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz",
"integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
@ -5501,15 +5511,25 @@
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
"integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"linkify-it": "^5.0.1",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"

View file

@ -1,3 +1,29 @@
[build-system]
requires = ["setuptools>=70"]
build-backend = "setuptools.build_meta"
# --- Pyodide / WebAssembly (PEP 783) build configuration ---------------------
# Consumed by pyodide-build (invoked directly or via `cibuildwheel --platform
# pyodide`). These flags are forwarded to the emscripten toolchain that compiles
# libz3 to wasm32. setup.py's IS_PYODIDE branch appends the same -fexceptions
# flags defensively, but declaring them here is what cibuildwheel relies on.
# Pyodide 314 / emscripten 5 builds its main module with *native wasm*
# exception handling and wasm longjmp (see Pyodide's Makefile.envs). Side
# modules like libz3.so MUST match that ABI: building with the legacy
# `-fexceptions` (JS-based EH) makes libz3 import `invoke_*` trampolines that the
# Pyodide runtime no longer provides -> "cannot resolve symbol invoke_vi" at the
# first Z3 call. WASM_BIGINT is required because the Z3 C API passes 64-bit ints
# across the ctypes/JS boundary.
[tool.pyodide.build]
cflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm"
cxxflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm"
ldflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_BIGINT -sSIDE_MODULE=1"
# --- cibuildwheel: produce a PyPI-publishable pyemscripten wheel -------------
[tool.cibuildwheel]
# Pyodide 314 ships CPython 3.14; match the single ABI it targets.
build = "cp314-*"
[tool.cibuildwheel.pyodide]
# z3test.py is the upstream smoke test; run it inside the Pyodide test venv.
test-command = "python {project}/z3test.py z3"

View file

@ -42,9 +42,16 @@ if RELEASE_DIR is None:
BUILD_PLATFORM = "emscripten"
BUILD_ARCH = "wasm32"
BUILD_OS_VERSION = os.environ['_PYTHON_HOST_PLATFORM'].split('_')[1:-1]
build_env['CFLAGS'] = build_env.get('CFLAGS', '') + " -fexceptions"
build_env['CXXFLAGS'] = build_env.get('CXXFLAGS', '') + " -fexceptions"
build_env['LDFLAGS'] = build_env.get('LDFLAGS', '') + " -fexceptions"
# Match Pyodide's native-wasm exception/longjmp ABI (see Makefile.envs in
# Pyodide). The legacy JS-based "-fexceptions" makes libz3.so import
# invoke_* trampolines that the modern Pyodide runtime does not export,
# which surfaces as "cannot resolve symbol invoke_vi" on the first Z3
# call. These mirror [tool.pyodide.build] in pyproject.toml so direct
# `pyodide build` invocations stay consistent with cibuildwheel.
_wasm_eh = " -fwasm-exceptions -sSUPPORT_LONGJMP=wasm"
build_env['CFLAGS'] = build_env.get('CFLAGS', '') + _wasm_eh
build_env['CXXFLAGS'] = build_env.get('CXXFLAGS', '') + _wasm_eh
build_env['LDFLAGS'] = build_env.get('LDFLAGS', '') + _wasm_eh + " -sWASM_BIGINT -sSIDE_MODULE=1"
IS_SINGLE_THREADED = True
ENABLE_LTO = False
# build with pthread doesn't work. The WASM bindings are also single threaded.
@ -305,6 +312,21 @@ class bdist_wheel(_bdist_wheel):
def finalize_options(self):
if BUILD_PLATFORM == "emscripten":
# Under pyodide-build / `cibuildwheel --platform pyodide`, the
# authoritative wheel platform tag is handed to us verbatim via
# _PYTHON_HOST_PLATFORM. For PEP 783 (Pyodide >= 0.28 / "314") this
# is e.g. "pyemscripten_2026_0_wasm32" -- a tag PyPI accepts. The
# reconstruction below instead produced "emscripten_<ver>_wasm32",
# which is locked to a Pyodide release and rejected by PyPI, so we
# defer to pyodide-build's tag when it is available.
host_platform = os.environ.get('_PYTHON_HOST_PLATFORM')
if host_platform:
self.plat_name = host_platform
else:
os_version_tag = '_'.join(BUILD_OS_VERSION) if BUILD_OS_VERSION else 'xxxxxx'
self.plat_name = f"emscripten_{os_version_tag}_wasm32"
return super().finalize_options()
if BUILD_ARCH is not None and BUILD_PLATFORM is not None:
os_version_tag = '_'.join(BUILD_OS_VERSION) if BUILD_OS_VERSION is not None else 'xxxxxx'
os_version_tag = self.remove_build_machine_os_version(BUILD_PLATFORM, os_version_tag)

View file

@ -621,7 +621,7 @@ struct z3_replayer::imp {
Z3_symbol get_symbol(unsigned pos) const {
check_arg(pos, SYMBOL);
return (Z3_symbol)m_args[pos].m_sym;
return (Z3_symbol)const_cast<void*>(m_args[pos].m_sym);
}
void * get_obj(unsigned pos) const {

View file

@ -2894,7 +2894,7 @@ proof * ast_manager::mk_transitivity(unsigned num_proofs, proof * const * proofs
}
});
ptr_buffer<expr> args;
args.append(num_proofs, (expr**) proofs);
args.append(num_proofs, (expr* const *) proofs);
args.push_back(mk_eq(n1,n2));
return mk_app(basic_family_id, PR_TRANSITIVITY_STAR, args.size(), args.data());
}
@ -2903,7 +2903,7 @@ proof * ast_manager::mk_monotonicity(func_decl * R, app * f1, app * f2, unsigned
SASSERT(f1->get_num_args() == f2->get_num_args());
SASSERT(f1->get_decl() == f2->get_decl());
ptr_buffer<expr> args;
args.append(num_proofs, (expr**) proofs);
args.append(num_proofs, (expr* const *) proofs);
args.push_back(mk_app(R, f1, f2));
proof* p = mk_app(basic_family_id, PR_MONOTONICITY, args.size(), args.data());
return p;
@ -2965,7 +2965,7 @@ proof * ast_manager::mk_rewrite_star(expr * s, expr * t, unsigned num_proofs, pr
if (proofs_disabled())
return nullptr;
ptr_buffer<expr> args;
args.append(num_proofs, (expr**) proofs);
args.append(num_proofs, (expr* const *) proofs);
args.push_back(mk_eq(s, t));
return mk_app(basic_family_id, PR_REWRITE_STAR, args.size(), args.data());
}
@ -3055,7 +3055,7 @@ proof * ast_manager::mk_unit_resolution(unsigned num_proofs, proof * const * pro
}
if (!found_complement) {
args.append(num_proofs, (expr**)proofs);
args.append(num_proofs, (expr* const *)proofs);
CTRACE(mk_unit_resolution_bug, !is_or(f1), tout << mk_ll_pp(f1, *this) << "\n";
for (unsigned i = 1; i < num_proofs; ++i)
tout << mk_pp(proofs[i], *this) << "\n";
@ -3125,7 +3125,7 @@ proof * ast_manager::mk_unit_resolution(unsigned num_proofs, proof * const * pro
tout << mk_pp(new_fact, *this) << "\n";);
ptr_buffer<expr> args;
args.append(num_proofs, (expr**) proofs);
args.append(num_proofs, (expr* const *) proofs);
args.push_back(new_fact);
#ifdef Z3DEBUG
expr * f1 = get_fact(proofs[0]);
@ -3191,7 +3191,7 @@ proof * ast_manager::mk_apply_defs(expr * n, expr * def, unsigned num_proofs, pr
if (proofs_disabled())
return nullptr;
ptr_buffer<expr> args;
args.append(num_proofs, (expr**) proofs);
args.append(num_proofs, (expr* const *) proofs);
args.push_back(mk_oeq(n, def));
return mk_app(basic_family_id, PR_APPLY_DEF, args.size(), args.data());
}
@ -3225,7 +3225,7 @@ proof * ast_manager::mk_nnf_pos(expr * s, expr * t, unsigned num_proofs, proof *
return nullptr;
check_nnf_proof_parents(num_proofs, proofs);
ptr_buffer<expr> args;
args.append(num_proofs, (expr**) proofs);
args.append(num_proofs, (expr* const *) proofs);
args.push_back(mk_oeq(s, t));
return mk_app(basic_family_id, PR_NNF_POS, args.size(), args.data());
}
@ -3235,7 +3235,7 @@ proof * ast_manager::mk_nnf_neg(expr * s, expr * t, unsigned num_proofs, proof *
return nullptr;
check_nnf_proof_parents(num_proofs, proofs);
ptr_buffer<expr> args;
args.append(num_proofs, (expr**) proofs);
args.append(num_proofs, (expr* const *) proofs);
args.push_back(mk_oeq(mk_not(s), t));
return mk_app(basic_family_id, PR_NNF_NEG, args.size(), args.data());
}
@ -3305,7 +3305,7 @@ proof * ast_manager::mk_redundant_del(expr* e) {
proof * ast_manager::mk_clause_trail(unsigned n, expr* const* ps) {
ptr_buffer<expr> args;
args.append(n, (expr**) ps);
args.append(n, (expr* const *) ps);
return mk_app(basic_family_id, PR_CLAUSE_TRAIL, 0, nullptr, args.size(), args.data());
}
@ -3323,7 +3323,7 @@ proof * ast_manager::mk_th_lemma(
for (unsigned i = 0; i < num_params; ++i)
parameters.push_back(params[i]);
ptr_buffer<expr> args;
args.append(num_proofs, (expr**) proofs);
args.append(num_proofs, (expr* const *) proofs);
args.push_back(fact);
return mk_app(basic_family_id, PR_TH_LEMMA, parameters.size(), parameters.data(), args.size(), args.data());
}

View file

@ -68,8 +68,8 @@ private:
inline ast_manager & m() const { return m_manager; }
// label for an expression
std::string label_of_expr(const expr * e) const {
expr_ref er((expr*)e, m());
std::string label_of_expr(const expr *e) const {
expr_ref er(const_cast<expr *>(e), m());
std::ostringstream out;
out << er << std::flush;
return escape_dot(out.str());

View file

@ -39,11 +39,16 @@ z3_add_component(rewriter
rewriter.cpp
seq_axioms.cpp
seq_eq_solver.cpp
seq_derive.cpp
seq_subset.cpp
seq_split.cpp
seq_derive.cpp
seq_range_collapse.cpp
seq_range_predicate.cpp
seq_rewriter.cpp
seq_regex_bisim.cpp
seq_skolem.cpp
term_enumeration.cpp
th_rewriter.cpp
value_sweep.cpp
var_subst.cpp

View file

@ -768,9 +768,10 @@ void bit_blaster_tpl<Cfg>::mk_smod(unsigned sz, expr * const * a_bits, expr * co
template<typename Cfg>
void bit_blaster_tpl<Cfg>::mk_eq(unsigned sz, expr * const * a_bits, expr * const * b_bits, expr_ref & out) {
expr_ref_vector out_bits(m());
out_bits.resize(sz);
for (unsigned i = 0; i < sz; ++i) {
mk_iff(a_bits[i], b_bits[i], out);
out_bits.push_back(out);
out_bits[i] = out;
}
mk_and(out_bits.size(), out_bits.data(), out);
}

View file

@ -1196,15 +1196,15 @@ bool bool_rewriter::decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref &
}
for (expr *e : subterms::ground(expr_ref(r, m()))) {
if (m().is_ite(e, cond, r1, r2)) {
expr_safe_replace rep1(m());
expr_safe_replace rep2(m());
rep1.insert(e, r1);
rep2.insert(e, r2);
m_rep1.reset();
m_rep2.reset();
m_rep1.insert(e, r1);
m_rep2.insert(e, r2);
c = cond;
th = r;
el = r;
rep1(th);
rep2(el);
m_rep1(th);
m_rep2(el);
return true;
}
}
@ -1212,6 +1212,4 @@ bool bool_rewriter::decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref &
}
template class rewriter_tpl<bool_rewriter_cfg>;
template class rewriter_tpl<bool_rewriter_cfg>;

View file

@ -20,6 +20,7 @@ Notes:
#include "ast/ast.h"
#include "ast/rewriter/rewriter.h"
#include "ast/rewriter/expr_safe_replace.h"
#include "util/params.h"
/**
@ -64,6 +65,7 @@ class bool_rewriter {
ptr_vector<expr> m_todo1, m_todo2;
unsigned_vector m_counts1, m_counts2;
expr_mark m_marked;
expr_safe_replace m_rep1, m_rep2;
br_status mk_flat_and_core(unsigned num_args, expr * const * args, expr_ref & result);
br_status mk_flat_or_core(unsigned num_args, expr * const * args, expr_ref & result);
@ -87,7 +89,7 @@ class bool_rewriter {
expr_ref simplify_eq_ite(expr* value, expr* ite);
public:
bool_rewriter(ast_manager & m, params_ref const & p = params_ref()):m_manager(m), m_local_ctx_cost(0) {
bool_rewriter(ast_manager & m, params_ref const & p = params_ref()):m_manager(m), m_local_ctx_cost(0), m_rep1(m), m_rep2(m) {
updt_params(p);
}
ast_manager & m() const { return m_manager; }
@ -243,7 +245,9 @@ public:
void mk_nor(expr * arg1, expr * arg2, expr_ref & result);
void mk_ge2(expr* a, expr* b, expr* c, expr_ref& result);
// If r is, or contains, an if-then-else, decompose it into a top-level
// ite by hoisting the (first) inner ite condition: returns c, th, el such
// that r is equivalent to (ite c th el). Returns false if r has no ite.
bool decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref &el);
};

View file

@ -452,6 +452,8 @@ namespace seq {
// |t| = 0 => |s| = 0 or indexof(t,s,offset) = -1
// ~contains(t,s) => indexof(t,s,offset) = -1
add_clause(mk_ge(i, -1));
add_clause(cnt, i_eq_m1);
add_clause(~t_eq_empty, s_eq_empty, i_eq_m1);
@ -638,8 +640,8 @@ namespace seq {
add_clause(~i_ge_0, i_ge_len_s, mk_eq(i, len_x));
}
add_clause(i_ge_0, mk_seq_eq(e, emp));
add_clause(~i_ge_len_s, mk_seq_eq(e, emp));
add_clause(i_ge_0, mk_eq(e, emp));
add_clause(~i_ge_len_s, mk_eq(e, emp));
add_clause(~i_ge_0, i_ge_len_s, mk_eq(one, len_e));
add_clause(mk_le(len_e, 1));
}
@ -1066,7 +1068,7 @@ namespace seq {
void axioms::replace_re_axiom(expr* e) {
expr* s = nullptr, *r = nullptr, *t = nullptr;
VERIFY(seq.str.is_replace_re(e, s, r, t));
throw default_exception("replace-re is not supported");
throw default_exception("no support for replace-re");
}
// A basic strategy for supporting replace_all and other
@ -1075,34 +1077,22 @@ namespace seq {
// using iterative deepening can be re-used.
//
// create recursive relation 'ra' with properties:
// ra(i, j, s, p, t, r) =
// if len(s) = i && len(r) = j then
// true
// else if len(s) > i = 0 && p = "" && r = t + s then
// true
// else if len(s) > i && p != "" &&
// s = extract(s, 0, i) + p + extract(s, i + len(p), len(s)) &&
// r = extract(r, 0, i) + t + extract(r, i + len(p), len(r)) && ra(i + len(p), j + len(t), s, p, t, r)
// else if ~prefix(p, extract(s, i, len(s)) && at(s,i) = at(r,j) then
// ra(i + 1, j + 1, s, p, t, r)
// else false
// ra(i, j, s, p, t, r) <- len(s) = i && len(r) = j
// ra(i, j, s, p, t, r) <- len(s) > i = 0 && p = "" && r = t + s
// ra(i, j, s, p, t, r) <- len(s) > i && p != "" && s = extract(s, 0, i) + p + extract(s, i + len(p), len(s)) && r = extract(r, 0, i) + t + extract(r, i + len(p), len(r)) && ra(i + len(p), j + len(t), s, p, t, r)
// ra(i, s, p, t, r) <- ~prefix(p, extract(s, i, len(s)) && at(s,i) = at(r,j) && ra(i + 1, j + 1, s, p, t, r)
// which amounts to:
//
//
// Then assert
// ra(s, p, t, replace_all(s, p, t))
//
// ra(s, p, t, r) is a recursive predicate:
// ra(s, p, t, r) iff replace_all(s, p, t) = r
//
// Base case, empty s or p: r = s
// Match case, prefix(p, s): s = p ++ s', r = t ++ r', ra(s', p, t, r')
// No-match case: r[0] = s[0], ra(s[1:], p, t, r[1:])
//
// Assert: ra(s, p, t, replace_all(s, p, t))
//
void axioms::replace_all_axiom(expr* r) {
expr* s = nullptr, *p = nullptr, *t = nullptr;
VERIFY(seq.str.is_replace_all(r, s, p, t));
recfun::util rec(m);
recfun::decl::plugin& plugin = rec.get_plugin();
recfun_replace replace(m);
sort* srt = s->get_sort();
sort* domain[4] = { srt, srt, srt, srt };
auto ra = rec.find_def_decl(symbol("ra"), 4, domain, m.mk_bool_sort(), true);
@ -1146,7 +1136,7 @@ namespace seq {
void axioms::replace_re_all_axiom(expr* e) {
expr* s = nullptr, *p = nullptr, *t = nullptr;
VERIFY(seq.str.is_replace_re_all(e, s, p, t));
throw default_exception("replace_re_all is not supported");
throw default_exception("no support for replace-re-all");
}
@ -1345,7 +1335,7 @@ namespace seq {
/**
* Consider the recursive definition of negated contains:
~contains(a, b) =
~contains(a, b) =
if |b| > |a| then true
else if |b| = |a| then a != b
else ~prefix(b, a) and ~contains(a[1:], b)
@ -1420,9 +1410,9 @@ namespace seq {
return bound_tracker;
}
// |u| != |v| OR
// |u| != |v| OR
// (u = w[a]u' AND v = w[b]v' AND a != b AND |u'| = |v'|)
void axioms::diseq_axiom(expr *u, expr *v) {
void axioms::diseq_axiom(expr *u, expr *v) {
expr_ref u_len(mk_len(u), m);
expr_ref v_len(mk_len(v), m);
expr_ref len_eq(mk_eq(u_len, v_len), m);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,266 @@
/*++
Copyright (c) 2026 Microsoft Corporation
Module Name:
seq_derive.h
Abstract:
Symbolic derivative computation for regular expressions.
Produces an ITE-tree (transition regex) representation where
the free variable is de Bruijn index 0 representing the input character.
Based on the theory of symbolic derivatives and transition regexes:
- Veanes et al., "On Symbolic Derivatives and Transition Regexes" (LPAR 2024)
- Varatalu, Veanes, Ernits, "RE#" (POPL 2025)
- Stanford, Veanes, Bjørner, "Symbolic Boolean Derivatives" (PLDI 2021)
Authors:
Nikolaj Bjorner (nbjorner) 2025-06-03
--*/
#pragma once
#include "ast/seq_decl_plugin.h"
#include "ast/arith_decl_plugin.h"
#include "ast/array_decl_plugin.h"
#include "ast/rewriter/bool_rewriter.h"
#include "util/obj_pair_hashtable.h"
#include "util/obj_triple_hashtable.h"
#include <functional>
class seq_rewriter;
namespace seq {
enum class derivative_kind { antimirov_t, brzozowski_t };
/**
* Symbolic derivative engine for regular expressions.
*
* Given a regex r, operator()(r) computes a symbolic derivative δ(r)
* represented as an ITE-tree over character predicates (using de Bruijn
* variable 0 for the character). Evaluating the ITE-tree for a concrete
* character 'a' yields the classical Brzozowski derivative δ_a(r).
*
* The ITE-tree structure implicitly defines minterms (equivalence classes
* of characters indistinguishable by the regex).
*
* Key properties:
* - Results are memoized for termination on cyclic derivative graphs
* - Union/intersection operands are sorted for ACI canonicalization
* - Depth-bounded to prevent stack overflow
*/
class derive {
ast_manager& m;
seq_util m_util;
arith_util m_autil;
bool_rewriter m_br;
seq_rewriter& m_re;
// Cache: maps (ele, regex) pair to its derivative
obj_pair_map<expr, expr, expr*> m_acache, m_bcache;
obj_pair_map<expr, expr, expr*> m_atop_cache, m_btop_cache; // post-simplify cache
expr_ref_vector m_trail; // pin cached results
// Op cache for ITE-hoisting operations (union, inter, concat, complement)
// Path-aware caches: key is (a, b, path_expr) for binary ops, (a, path_expr) for complement
obj_triple_map<expr, expr, expr, expr *> m_aunion_cache, m_bunion_cache, m_ainter_cache, m_binter_cache, m_axor_cache, m_bxor_cache;
obj_pair_map<expr, expr, expr*> m_aconcat_cache, m_bconcat_cache;
obj_pair_map<expr, expr, expr*> m_acomplement_cache, m_bcomplement_cache;
// Depth limiting
unsigned m_depth { 0 };
static const unsigned m_max_depth = 512;
seq_util::rex& re() { return m_util.re; }
seq_util& u() { return m_util; }
derivative_kind m_derivative_kind = derivative_kind::antimirov_t;
// The element (character) for the current derivative computation
expr_ref m_ele;
// Path state for inline pruning during mk_inter/mk_union/mk_complement
using intervals_t = svector<std::pair<unsigned, unsigned>>;
// Path: vector of signed atoms
svector<std::pair<expr*, bool>> m_path;
// Intervals: feasible character ranges under current path (append-only)
intervals_t m_intervals;
unsigned m_intervals_start { 0 };
// Stack of saved states for push/pop
struct path_save { unsigned path_sz; unsigned intervals_sz; unsigned intervals_start; expr* path_expr; };
svector<path_save> m_path_stack;
// Boolean expression encoding of current path (for cache keys)
expr_ref m_path_expr;
// Path interface
lbool push(expr* c, bool sign); // l_true: implied, l_undef: pushed (must pop), l_false: contradicts
void pop(); // restore state to matching push
expr* get_path_expr() { return m_path_expr; }
obj_pair_map<expr, expr, expr *> &cache() {
return m_derivative_kind == derivative_kind::antimirov_t ? m_acache : m_bcache;
}
obj_pair_map<expr, expr, expr *> &top_cache() {
return m_derivative_kind == derivative_kind::antimirov_t ? m_atop_cache : m_btop_cache;
}
obj_triple_map<expr, expr, expr, expr *> &union_cache() {
return m_derivative_kind == derivative_kind::antimirov_t ? m_aunion_cache : m_bunion_cache;
}
obj_triple_map<expr, expr, expr, expr *> &inter_cache() {
return m_derivative_kind == derivative_kind::antimirov_t ? m_ainter_cache : m_binter_cache;
}
obj_triple_map<expr, expr, expr, expr *> &xor_cache() {
return m_derivative_kind == derivative_kind::antimirov_t ? m_axor_cache : m_bxor_cache;
}
obj_pair_map<expr, expr, expr *> &concat_cache() {
return m_derivative_kind == derivative_kind::antimirov_t ? m_aconcat_cache : m_bconcat_cache;
}
obj_pair_map<expr, expr, expr *> &complement_cache() {
return m_derivative_kind == derivative_kind::antimirov_t ? m_acomplement_cache : m_bcomplement_cache;
}
// Hoist ITE: apply_op through ite(c, t, e) with path pruning
expr_ref apply_ite(expr* c, expr* t, expr* e, expr* r, std::function<expr_ref(expr*, expr*)> apply_op);
expr_ref apply_ite(expr* c, expr* t1, expr* e1, expr* t2, expr* e2, std::function<expr_ref(expr*, expr*)> apply_op);
expr_ref apply_ite(expr* c, expr* t, expr* e, std::function<expr_ref(expr*)> apply_op);
// Common ITE dispatch for binary ops (union/inter)
expr_ref hoist_ite(expr* a, expr* b, std::function<expr_ref(expr*, expr*)> apply_op);
// Evaluate a condition against the current path/intervals
lbool eval_path_cond(expr* c);
// Internal helpers for push
lbool push_path_atoms(expr* c, bool sign);
lbool push_intervals_impl(expr* c, bool sign);
// Core derivative computation
expr_ref derive_rec(expr* r);
expr_ref derive_core(expr* r);
// Helpers for specific regex constructs
expr_ref derive_to_re(expr* s, sort* seq_sort);
expr_ref derive_range(expr* lo, expr* hi, sort* seq_sort);
expr_ref derive_of_pred(expr* pred, sort* seq_sort);
// Nullable check: returns a Boolean expression
expr_ref is_nullable(expr* r);
expr_ref is_nullable_symbolic_regex(expr* r, sort* seq_sort);
// Smart constructors with path-aware simplification and ACI canonicalization
expr_ref mk_union(expr* a, expr* b);
bool are_complements(expr* a, expr* b);
unsigned union_id(expr* e); // complement-aware ID for sorting
bool is_subset(expr* a, expr* b);
expr_ref mk_union_core(expr* a, expr* b);
void add_union_elem(expr_ref_vector& set, expr* e);
expr_ref mk_inter(expr* a, expr* b);
expr_ref mk_inter_core(expr* a, expr* b);
expr_ref mk_concat(expr* a, expr* b);
expr_ref mk_complement(expr* a);
expr_ref mk_complement_core(expr* a);
expr_ref mk_xor(expr *a, expr *b);
expr_ref mk_xor_core(expr *a, expr *b);
expr_ref mk_core(decl_kind k, expr* a, expr* b);
expr_ref mk_ite(expr* c, expr* t, expr* e);
// Distribute concatenation through ITE/union in derivative
expr_ref mk_deriv_concat(expr* d, expr* tail);
expr_ref mk_deriv_concat_core(expr* d, expr* tail);
// Extract head character and tail from a sequence expression
bool get_head_tail(expr* s1, expr* s2, expr_ref& hd, expr_ref& tl);
// Predicate implication for character range conditions.
bool pred_implies(bool sign_a, expr* a, bool sign_b, expr* b);
bool pred_implies(expr* a, expr* b);
// Normalize reverse(r)
expr_ref mk_regex_reverse(expr* r);
// Condition evaluation helpers
lbool eval_cond(expr* cond);
lbool eval_range_cond(expr* c);
void intersect_intervals(unsigned lo, unsigned hi);
void exclude_interval(unsigned lo, unsigned hi);
// Cofactor enumeration over a transition regex (ITE-tree).
void get_cofactors_rec(expr* r, expr_ref_pair_vector& result);
// Re-apply union/intersection simplifications bottom-up to a cofactor
// leaf. decompose_ite substitutes ITE branch values structurally
// (no simplification), so leaves can contain un-normalized nodes such
// as union(R, none) or inter(R, none); this rebuilds them through
// mk_union/mk_inter so equal states share a canonical form.
expr_ref clean_leaf(expr* r);
sort* re_sort(expr* r) { return r->get_sort(); }
sort* seq_sort(expr* r) { sort* s = nullptr; m_util.is_re(r, s); return s; }
sort* ele_sort(expr* r) { sort* s = seq_sort(r); sort* e = nullptr; m_util.is_seq(s, e); return e; }
void reset();
void reset_op_caches();
public:
derive(ast_manager& m, seq_rewriter& re);
/**
* Compute the derivative of regex r with respect to element ele.
* When ele is a de Bruijn variable, produces a symbolic ITE-tree.
* When ele is a concrete character, produces the concrete derivative.
*/
expr_ref operator()(derivative_kind k, expr* ele, expr* r);
/**
* Convenience: symbolic derivative using de Bruijn var 0.
*/
expr_ref operator()(derivative_kind k, expr* r);
/**
* Nullable check: returns a Boolean expression that is true iff r accepts the empty string.
*/
expr_ref nullable(expr* r) { return is_nullable(r); }
/**
* Enumerate the cofactors (min-terms) of a transition regex r taken with
* respect to element ele. r is an ITE-tree over character predicates on
* ele; for every feasible path through the tree this produces a pair
* (path_condition, leaf_regex). Infeasible character-interval
* combinations are pruned using the same path/interval context that the
* derivative engine uses while hoisting ITEs.
*/
void get_cofactors(expr* ele, expr* r, expr_ref_pair_vector& result);
/**
* Compute the symbolic derivative of r and enumerate its reachable
* leaves in fully ITE-hoisted normal form.
*
* Concretely this returns, for every feasible minterm (character
* class) of δ(r), a pair (path_condition, target_regex). Every
* if-then-else over the input character (including ones that would
* otherwise be buried under a concat/union) is hoisted to the top
* via the same path/interval pruning used by the derivative engine,
* so each target_regex is free of (:var 0) and its nullability is
* always decidable. Unions are kept intact as single leaves (a
* union leaf denotes a single bisimulation state). Infeasible
* minterms are pruned, so all returned leaves are reachable.
*
* This is the entry point the regex_bisim equivalence procedure
* uses: it consumes the target_regex of each pair and ignores the
* (redundant) path condition.
*/
void derivative_cofactors(expr* r, expr_ref_pair_vector& result);
};
}

View file

@ -0,0 +1,160 @@
/*++
Copyright (c) 2026 Microsoft Corporation
Module Name:
seq_range_collapse.cpp
Abstract:
Implementation of regex <-> range_predicate translation for the
boolean-combination-of-ranges fragment. See header for the recognized
grammar and the canonical regex AST emitted by materialization.
Authors:
Margus Veanes (veanes) 2026
--*/
#include "ast/rewriter/seq_range_collapse.h"
namespace seq {
bool regex_to_range_predicate(seq_util& u, expr* r, range_predicate& out) {
// The range algebra only models sets of single characters over the
// unsigned character domain [0, max_char]. Guard against any regex
// whose element type is not a sequence of characters (e.g. a regex
// over (Seq Int) or (Seq (Seq Char))): for such regexes the
// re.range/re.union/... matchers below would silently fabricate a
// character-class predicate and change semantics. Reject them up
// front so callers fall back to the generic regex path.
sort* seq_sort = nullptr;
if (!u.is_re(r, seq_sort) || !u.is_string(seq_sort))
return false;
unsigned const max_char = u.max_char();
auto& re = u.re;
if (re.is_empty(r)) {
out = range_predicate::empty(max_char);
return true;
}
if (re.is_full_char(r)) {
out = range_predicate::top(max_char);
return true;
}
unsigned lo = 0, hi = 0;
expr* lo_e = nullptr;
expr* hi_e = nullptr;
if (re.is_range(r, lo_e, hi_e)) {
auto extract_char = [&](expr* e, unsigned& c) -> bool {
if (u.is_const_char(e, c)) return true;
expr* inner = nullptr;
if (u.str.is_unit(e, inner) && u.is_const_char(inner, c)) return true;
zstring s;
if (u.str.is_string(e, s) && s.length() == 1) {
c = s[0];
return true;
}
return false;
};
if (!extract_char(lo_e, lo) || !extract_char(hi_e, hi))
return false;
// Empty/inverted range [lo > hi] is the empty regex.
if (lo > hi) {
out = range_predicate::empty(max_char);
return true;
}
out = range_predicate::range(lo, hi, max_char);
return true;
}
expr *a = nullptr, *b = nullptr, *c = nullptr;
if (re.is_union(r, a, b)) {
range_predicate pa(max_char), pb(max_char);
if (!regex_to_range_predicate(u, a, pa)) return false;
if (!regex_to_range_predicate(u, b, pb)) return false;
out = pa | pb;
return true;
}
auto mk_diff = [&](expr *a, expr *b) -> bool {
range_predicate pa(max_char), pb(max_char);
if (!regex_to_range_predicate(u, a, pa))
return false;
if (!regex_to_range_predicate(u, b, pb))
return false;
out = pa - pb;
return true;
};
if (re.is_diff(r, a, b))
return mk_diff(a, b);
if (re.is_intersection(r, a, b) && re.is_complement(b, c))
return mk_diff(a, c);
if (re.is_intersection(r, a, b) && re.is_complement(a, c))
return mk_diff(b, c);
if (re.is_intersection(r, a, b)) {
range_predicate pa(max_char), pb(max_char);
if (!regex_to_range_predicate(u, a, pa)) return false;
if (!regex_to_range_predicate(u, b, pb)) return false;
out = pa & pb;
return true;
}
// NOTE: re.complement is intentionally NOT handled here.
// re.complement is the SEQUENCE-level complement: its language
// includes the empty string, strings of length >= 2, and any
// length-1 string outside the operand. A character-class
// range_predicate can only describe a set of length-1 strings,
// so collapsing re.complement(R) to ~R (character-level
// complement) would change semantics whenever R is wrapped in
// any sequence-level context (e.g. re.diff at the top level,
// or membership tests). De-Morgan equivalences and the
// special cases re.complement(re.empty) / re.complement(re.full)
// are already handled directly in seq_rewriter::mk_re_complement.
return false;
}
static expr_ref mk_unit_string_from_char(seq_util& u, unsigned c) {
return expr_ref(u.str.mk_string(zstring(c)), u.get_manager());
}
static expr_ref mk_single_range_regex(seq_util& u, unsigned lo, unsigned hi, sort* re_sort) {
ast_manager& m = u.get_manager();
return expr_ref(u.re.mk_range(re_sort, lo, hi), m);
}
expr_ref range_predicate_to_regex(seq_util& u, range_predicate const& p, sort* seq_sort) {
ast_manager& m = u.get_manager();
sort* re_sort = u.re.mk_re(seq_sort);
if (p.is_empty())
return expr_ref(u.re.mk_empty(re_sort), m);
unsigned const n = p.num_ranges();
SASSERT(n > 0);
if (n == 1) {
auto [lo, hi] = p[0];
return mk_single_range_regex(u, lo, hi, re_sort);
}
// Build single-range AST nodes first, then sort by expression id
// so the resulting right-associated union matches the canonical
// id-sorted shape that seq_rewriter::merge_regex_sets expects.
// Without this the merge algorithm produces incorrect unions
// when it has to combine our materialized output with another
// (id-sorted) regex set.
expr_ref_vector ranges(m);
for (unsigned i = 0; i < n; ++i) {
auto [lo, hi] = p[i];
ranges.push_back(mk_single_range_regex(u, lo, hi, re_sort));
}
std::sort(ranges.data(), ranges.data() + ranges.size(),
[](expr* a, expr* b) { return a->get_id() < b->get_id(); });
expr_ref acc(ranges.get(n - 1), m);
for (unsigned i = n - 1; i-- > 0; )
acc = expr_ref(u.re.mk_union(ranges.get(i), acc), m);
return acc;
}
}

View file

@ -0,0 +1,71 @@
/*++
Copyright (c) 2026 Microsoft Corporation
Module Name:
seq_range_collapse.h
Abstract:
Recognize regexes that are boolean combinations of character-class
primitives (re.empty, re.full_char, re.range with concrete chars,
and re.union/inter/comp/diff over translatable arguments), and
materialize a seq::range_predicate back into a canonical regex AST.
Together with seq_rewriter integration, this lets any boolean
combination of character-class regexes collapse to a canonical
multi-range form, so that equivalent character classes share AST
identity, and downstream consumers (derivative, OneStep, caching)
can short-circuit them as pure range predicates.
Authors:
Margus Veanes (veanes) 2026
--*/
#pragma once
#include "ast/rewriter/seq_range_predicate.h"
#include "ast/seq_decl_plugin.h"
namespace seq {
/**
* If r is a boolean combination of character-class regex primitives
* over the unsigned character domain [0, max_char], compute the
* equivalent range_predicate and return true. Otherwise return false
* with out untouched.
*
* Recognized fragment (all character-class-preserving operations):
* re.empty -> empty
* re.full_char_set -> top
* re.range "c_lo" "c_hi" (concrete) -> [c_lo, c_hi]
* re.union r1 r2 -> p1 | p2
* re.intersection r1 r2 -> p1 & p2
* re.diff r1 r2 -> p1 - p2
*
* Notably re.complement is NOT recognized: it is a SEQUENCE-level
* complement (over all of Σ*), not a character-class complement, so
* collapsing it would change semantics whenever the result is used
* in any non-character-class context. Sequence-level rewrites for
* re.complement (double-comp, deMorgan, etc.) are handled directly
* in seq_rewriter::mk_re_complement.
*/
bool regex_to_range_predicate(seq_util& u, expr* r, range_predicate& out);
/**
* Canonical materialization of p as a regex AST over the given
* sequence sort. Two range_predicates with equal canonical
* representations produce structurally identical regex ASTs:
*
* empty -> re.empty
* top -> re.full_char_set
* single range [lo, hi] -> re.range "lo" "hi"
* multiple ranges -> right-associated re.union of single
* ranges, in increasing order of lo
* (matching the canonical range order
* held by range_predicate).
*/
expr_ref range_predicate_to_regex(seq_util& u, range_predicate const& p, sort* seq_sort);
}

View file

@ -0,0 +1,292 @@
/*++
Copyright (c) 2026 Microsoft Corporation
Module Name:
seq_range_predicate.cpp
Abstract:
Implementation of the specialized range-algebra used by symbolic
derivative computation and regex rewriting. See seq_range_predicate.h
for the algebraic specification.
All Boolean operations are implemented as single linear sweeps over
the canonical sorted range vectors and produce canonical output
(sorted, disjoint, non-adjacent).
Authors:
Margus Veanes (veanes) 2026
--*/
#include "ast/rewriter/seq_range_predicate.h"
#include "util/debug.h"
#include <algorithm>
#include <ostream>
namespace seq {
// -----------------------------------------------------------------------
// Factories
// -----------------------------------------------------------------------
range_predicate range_predicate::empty(unsigned max_char) {
return range_predicate(max_char);
}
range_predicate range_predicate::top(unsigned max_char) {
range_predicate r(max_char);
r.m_ranges.push_back({0u, max_char});
SASSERT(r.well_formed());
return r;
}
range_predicate range_predicate::singleton(unsigned c, unsigned max_char) {
SASSERT(c <= max_char);
range_predicate r(max_char);
r.m_ranges.push_back({c, c});
SASSERT(r.well_formed());
return r;
}
range_predicate range_predicate::range(unsigned lo, unsigned hi, unsigned max_char) {
range_predicate r(max_char);
if (lo <= hi && lo <= max_char) {
unsigned clipped_hi = hi <= max_char ? hi : max_char;
r.m_ranges.push_back({lo, clipped_hi});
}
SASSERT(r.well_formed());
return r;
}
// -----------------------------------------------------------------------
// Invariants and observers
// -----------------------------------------------------------------------
bool range_predicate::well_formed() const {
for (unsigned i = 0; i < m_ranges.size(); ++i) {
auto [lo, hi] = m_ranges[i];
if (lo > hi) return false;
if (hi > m_max_char) return false;
if (i > 0) {
unsigned prev_hi = m_ranges[i - 1].second;
// Non-adjacent and sorted: prev_hi + 1 < lo, with care
// around prev_hi == UINT_MAX which we never expect because
// hi <= m_max_char.
if (prev_hi + 1 >= lo) return false;
}
}
return true;
}
bool range_predicate::contains(unsigned c) const {
// Binary search on first element of pairs.
unsigned lo = 0, hi = m_ranges.size();
while (lo < hi) {
unsigned mid = lo + (hi - lo) / 2;
auto [a, b] = m_ranges[mid];
if (c < a) hi = mid;
else if (c > b) lo = mid + 1;
else return true;
}
return false;
}
uint64_t range_predicate::cardinality() const {
uint64_t n = 0;
for (auto [lo, hi] : m_ranges)
n += static_cast<uint64_t>(hi) - static_cast<uint64_t>(lo) + 1u;
return n;
}
// -----------------------------------------------------------------------
// Equality, ordering, hashing
// -----------------------------------------------------------------------
bool range_predicate::equals(range_predicate const& o) const {
if (m_max_char != o.m_max_char) return false;
if (m_ranges.size() != o.m_ranges.size()) return false;
for (unsigned i = 0; i < m_ranges.size(); ++i)
if (m_ranges[i] != o.m_ranges[i]) return false;
return true;
}
bool range_predicate::operator<(range_predicate const& o) const {
if (m_max_char != o.m_max_char)
return m_max_char < o.m_max_char;
unsigned n = std::min(m_ranges.size(), o.m_ranges.size());
for (unsigned i = 0; i < n; ++i) {
auto a = m_ranges[i];
auto b = o.m_ranges[i];
if (a.first != b.first) return a.first < b.first;
if (a.second != b.second) return a.second < b.second;
}
return m_ranges.size() < o.m_ranges.size();
}
unsigned range_predicate::hash() const {
// FNV-1a 32-bit over (max_char, then each (lo, hi)).
uint32_t h = 2166136261u;
auto step = [&](uint32_t x) {
h ^= x;
h *= 16777619u;
};
step(m_max_char);
for (auto [lo, hi] : m_ranges) {
step(lo);
step(hi);
}
return h;
}
// -----------------------------------------------------------------------
// Boolean operations
// -----------------------------------------------------------------------
namespace {
// Append (lo, hi) to result, merging with the previous range if
// adjacent or overlapping. Maintains canonical form.
inline void append_merged(svector<std::pair<unsigned, unsigned>>& result,
unsigned lo, unsigned hi) {
SASSERT(lo <= hi);
if (!result.empty() && result.back().second + 1 >= lo) {
if (result.back().second < hi)
result.back().second = hi;
} else {
result.push_back({lo, hi});
}
}
}
range_predicate range_predicate::operator|(range_predicate const& o) const {
SASSERT(m_max_char == o.m_max_char);
range_predicate r(m_max_char);
unsigned i = 0, j = 0;
const unsigned n = m_ranges.size();
const unsigned m = o.m_ranges.size();
while (i < n && j < m) {
auto a = m_ranges[i];
auto b = o.m_ranges[j];
if (a.first <= b.first) {
append_merged(r.m_ranges, a.first, a.second);
++i;
} else {
append_merged(r.m_ranges, b.first, b.second);
++j;
}
}
while (i < n) {
auto a = m_ranges[i++];
append_merged(r.m_ranges, a.first, a.second);
}
while (j < m) {
auto b = o.m_ranges[j++];
append_merged(r.m_ranges, b.first, b.second);
}
SASSERT(r.well_formed());
return r;
}
range_predicate range_predicate::operator&(range_predicate const& o) const {
SASSERT(m_max_char == o.m_max_char);
range_predicate r(m_max_char);
unsigned i = 0, j = 0;
const unsigned n = m_ranges.size();
const unsigned m = o.m_ranges.size();
while (i < n && j < m) {
auto [a_lo, a_hi] = m_ranges[i];
auto [b_lo, b_hi] = o.m_ranges[j];
unsigned lo = std::max(a_lo, b_lo);
unsigned hi = std::min(a_hi, b_hi);
if (lo <= hi)
r.m_ranges.push_back({lo, hi});
// Advance the range that ends first.
if (a_hi < b_hi) ++i;
else if (b_hi < a_hi) ++j;
else { ++i; ++j; }
}
SASSERT(r.well_formed());
return r;
}
range_predicate range_predicate::operator~() const {
range_predicate r(m_max_char);
unsigned cursor = 0;
for (auto [lo, hi] : m_ranges) {
if (cursor < lo)
r.m_ranges.push_back({cursor, lo - 1});
// Step past hi without overflow: hi <= m_max_char and we
// only step when more characters remain.
if (hi >= m_max_char) {
cursor = m_max_char + 1; // sentinel: no more characters
break;
}
cursor = hi + 1;
}
if (cursor <= m_max_char)
r.m_ranges.push_back({cursor, m_max_char});
SASSERT(r.well_formed());
return r;
}
range_predicate range_predicate::operator-(range_predicate const& o) const {
SASSERT(m_max_char == o.m_max_char);
// A - B by linear sweep: for each range of A, subtract overlapping
// ranges of B. Both inputs are sorted so we advance j monotonically.
range_predicate r(m_max_char);
unsigned j = 0;
const unsigned m = o.m_ranges.size();
for (auto [a_lo, a_hi] : m_ranges) {
unsigned cursor = a_lo;
while (j < m && o.m_ranges[j].second < cursor)
++j;
unsigned k = j;
while (k < m && o.m_ranges[k].first <= a_hi) {
auto [b_lo, b_hi] = o.m_ranges[k];
if (cursor < b_lo)
r.m_ranges.push_back({cursor, std::min(a_hi, b_lo - 1)});
if (b_hi >= a_hi) {
cursor = a_hi + 1;
break;
}
cursor = b_hi + 1;
++k;
}
if (cursor <= a_hi)
r.m_ranges.push_back({cursor, a_hi});
}
SASSERT(r.well_formed());
return r;
}
range_predicate range_predicate::operator^(range_predicate const& o) const {
SASSERT(m_max_char == o.m_max_char);
// (A | B) - (A & B), but implemented directly with one linear sweep
// over the union of breakpoints.
return (*this | o) - (*this & o);
}
// -----------------------------------------------------------------------
// Display
// -----------------------------------------------------------------------
std::ostream& range_predicate::display(std::ostream& out) const {
if (m_ranges.empty()) {
return out << "[]";
}
out << "[";
bool first = true;
for (auto [lo, hi] : m_ranges) {
if (!first) out << ",";
first = false;
if (lo == hi)
out << lo;
else
out << lo << "-" << hi;
}
return out << "]";
}
}

View file

@ -0,0 +1,127 @@
/*++
Copyright (c) 2026 Microsoft Corporation
Module Name:
seq_range_predicate.h
Abstract:
Specialized range-algebra over an unsigned character domain [0, max_char].
A range_predicate represents a subset of the character domain as a
sorted sequence of non-overlapping, non-adjacent, non-empty ranges:
[(lo_0, hi_0), (lo_1, hi_1), ...] with hi_i + 1 < lo_{i+1}.
The representation is canonical, so two range_predicates over the same
domain are extensionally equivalent iff their internal vectors are
elementwise equal.
All Boolean operations (union, intersection, complement, difference)
are linear in the total number of ranges and produce the canonical
representation.
Intended use:
* path conditions for symbolic derivative computation,
* OneStep predicates capturing length-1 acceptance,
* smart-constructor side conditions for regex rewrites such as
R & psi --> toregex(OneStep(R) & psi).
The type is a pure value: no ast_manager allocation occurs in its
construction or its Boolean operations. Conversion to and from
expr* is the responsibility of a separate translator (see callers
in seq_derive / seq_rewriter).
Authors:
Margus Veanes (veanes) 2026
--*/
#pragma once
#include "util/vector.h"
#include <iosfwd>
#include <utility>
namespace seq {
class range_predicate {
using range_t = std::pair<unsigned, unsigned>;
using ranges_t = svector<range_t>;
// Sorted by first; ranges are disjoint and non-adjacent;
// every range satisfies lo <= hi <= m_max_char.
ranges_t m_ranges;
unsigned m_max_char { 0 };
// Invariant check used in debug builds.
bool well_formed() const;
public:
range_predicate() = default;
explicit range_predicate(unsigned max_char) : m_max_char(max_char) {}
// ---------------- Factory functions ----------------
static range_predicate empty(unsigned max_char);
static range_predicate top(unsigned max_char);
static range_predicate singleton(unsigned c, unsigned max_char);
static range_predicate range(unsigned lo, unsigned hi, unsigned max_char);
// ---------------- Observers ----------------
unsigned max_char() const { return m_max_char; }
unsigned num_ranges() const { return m_ranges.size(); }
range_t operator[](unsigned i) const { return m_ranges[i]; }
ranges_t const& ranges() const { return m_ranges; }
bool is_empty() const { return m_ranges.empty(); }
bool is_top() const {
return m_ranges.size() == 1
&& m_ranges[0].first == 0
&& m_ranges[0].second == m_max_char;
}
bool is_singleton(unsigned& c) const {
if (m_ranges.size() != 1) return false;
if (m_ranges[0].first != m_ranges[0].second) return false;
c = m_ranges[0].first;
return true;
}
bool contains(unsigned c) const;
// Number of characters in the predicate (well-defined for any domain).
uint64_t cardinality() const;
// ---------------- Equality, ordering, hashing ----------------
bool equals(range_predicate const& o) const;
bool operator==(range_predicate const& o) const { return equals(o); }
bool operator!=(range_predicate const& o) const { return !equals(o); }
// Total order: lexicographic on the canonical range sequence,
// with shorter sequences ordered before longer prefixes.
// Predicates over different domains compare by max_char first.
bool operator<(range_predicate const& o) const;
bool less_than(range_predicate const& o) const { return *this < o; }
unsigned hash() const;
// ---------------- Boolean operations ----------------
range_predicate operator|(range_predicate const& o) const; // union
range_predicate operator&(range_predicate const& o) const; // intersection
range_predicate operator-(range_predicate const& o) const; // difference
range_predicate operator^(range_predicate const& o) const; // symmetric diff
range_predicate operator~() const; // complement
// ---------------- Display ----------------
std::ostream& display(std::ostream& out) const;
};
inline std::ostream& operator<<(std::ostream& out, range_predicate const& p) {
return p.display(out);
}
}

View file

@ -85,41 +85,6 @@ namespace seq {
return is_ground(r);
}
/*
Collect the leaves of a t-regex der (an ITE / antimirov union /
union-tree with regex leaves) into the output vector. Empty
(re.empty) leaves are dropped.
Returns false if we encountered an unexpected node (e.g. a free
variable creeping in) in that case the caller should bail out.
*/
bool regex_bisim::collect_leaves(expr* der, expr_ref_vector& leaves) {
ptr_vector<expr> work;
obj_hashtable<expr> seen;
work.push_back(der);
seen.insert(der);
while (!work.empty()) {
expr* e = work.back();
work.pop_back();
expr* c = nullptr, * t = nullptr, * f = nullptr;
if (m.is_ite(e, c, t, f) ||
m_util.re.is_union(e, t, f) ||
m_util.re.is_antimirov_union(e, t, f)) {
if (seen.insert_if_not_there(t))
work.push_back(t);
if (seen.insert_if_not_there(f))
work.push_back(f);
continue;
}
if (m_util.re.is_empty(e))
continue;
if (!m_util.is_re(e))
return false;
leaves.push_back(e);
}
return true;
}
/*
Fast inequivalence check based on the get_info().classical flag.
@ -228,15 +193,19 @@ namespace seq {
m_worklist.pop_back();
// Compute the symbolic derivative wrt the canonical variable
// (:var 0). The result is a transition regex (ITE tree) whose
// leaves are regex expressions. We use the classical Brzozowski
// entry point so the derivative stays as a single TRegex and
// does not lift unions to the top via antimirov nodes — this
// preserves the XOR-pair invariant the bisimulation relies on.
expr_ref d(m_rw.mk_brz_derivative(r), m);
// (:var 0) and enumerate its reachable leaves in fully
// ITE-hoisted normal form. Every if-then-else over the input
// character — even one that would otherwise be buried under a
// concat or union — is hoisted to the top and infeasible
// minterms are pruned, so each leaf is a ground regex free of
// (:var 0) whose nullability is always decidable. Unions are
// kept intact as single leaves (a union leaf denotes a single
// bisimulation state, never a split into separate states).
expr_ref_pair_vector cofs(m);
m_rw.brz_derivative_cofactors(r, cofs);
expr_ref_vector leaves(m);
if (!collect_leaves(d, leaves))
return l_undef;
for (auto const& p : cofs)
leaves.push_back(p.second);
// First pass: check for any nullable leaf (definitive
// distinguishing empty-continuation word) or any classically

View file

@ -74,7 +74,6 @@ namespace seq {
unsigned node_of(expr* r);
bool merge_leaf(expr* xor_pair);
bool collect_leaves(expr* der, expr_ref_vector& leaves);
lbool nullability(expr* r);
bool is_supported(expr* r);
// Returns true if the leaf l proves that the original pair is

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,9 @@ Notes:
--*/
#pragma once
#include "seq_split.h"
#include "ast/seq_decl_plugin.h"
#include "ast/rewriter/seq_derive.h"
#include "ast/ast_pp.h"
#include "ast/arith_decl_plugin.h"
#include "ast/rewriter/rewriter_types.h"
@ -129,16 +131,21 @@ class seq_rewriter {
void insert(decl_kind op, expr* a, expr* b, expr* c, expr* r);
};
friend class seq::derive;
seq_util m_util;
seq_subset m_subset;
seq_split m_split;
arith_util m_autil;
bool_rewriter m_br;
seq::derive m_derive;
// re2automaton m_re2aut;
op_cache m_op_cache;
expr_ref_vector m_es, m_lhs, m_rhs;
bool m_coalesce_chars;
bool m_in_bisim { false };
bool m_coalesce_chars = true;
bool m_in_bisim { false };
unsigned m_re_deriv_depth { 0 };
static const unsigned m_max_re_deriv_depth = 512;
enum length_comparison {
shorter_c,
@ -175,50 +182,22 @@ class seq_rewriter {
//replace b in a by c into result
void replace_all_subvectors(expr_ref_vector const& as, expr_ref_vector const& bs, expr* c, expr_ref_vector& result);
// Calculate derivative, memoized and enforcing a normal form
expr_ref is_nullable_rec(expr* r);
expr_ref mk_derivative_rec(expr* ele, expr* r);
expr_ref mk_der_op(decl_kind k, expr* a, expr* b);
expr_ref mk_der_op_rec(decl_kind k, expr* a, expr* b);
expr_ref mk_der_concat(expr* a, expr* b);
expr_ref mk_der_union(expr* a, expr* b);
expr_ref mk_der_inter(expr* a, expr* b);
expr_ref mk_der_xor(expr* a, expr* b);
expr_ref mk_der_compl(expr* a);
expr_ref mk_der_cond(expr* cond, expr* ele, sort* seq_sort);
expr_ref mk_der_antimirov_union(expr* r1, expr* r2);
bool ite_bdds_compatible(expr* a, expr* b);
/* if r has the form deriv(en..deriv(e1,to_re(s))..) returns 's = [e1..en]' else returns '() in r'*/
expr_ref is_nullable_symbolic_regex(expr* r, sort* seq_sort);
#ifdef Z3DEBUG
bool check_deriv_normal_form(expr* r, int level = 3);
#endif
// For replace_all(x, a, b) in R: transform R so that
// - occurrences of b_ch are replaced by union(to_re(a_str), to_re(b_str))
// - occurrences of a_ch are replaced by empty (replace_all never outputs a)
expr_ref re_replace_char(expr *r, unsigned a_ch, unsigned b_ch, expr *a_str, expr *b_str);
void mk_antimirov_deriv_rec(expr* e, expr* r, expr* path, expr_ref& result);
expr_ref mk_antimirov_deriv(expr* e, expr* r, expr* path);
expr_ref mk_in_antimirov_rec(expr* s, expr* d);
expr_ref mk_in_antimirov(expr* s, expr* d);
expr_ref mk_antimirov_deriv_intersection(expr* elem, expr* d1, expr* d2, expr* path);
expr_ref mk_antimirov_deriv_concat(expr* d, expr* r);
expr_ref mk_antimirov_deriv_negate(expr* elem, expr* d);
expr_ref mk_antimirov_deriv_union(expr* d1, expr* d2);
expr_ref mk_antimirov_deriv_restrict(expr* elem, expr* d1, expr* cond);
expr_ref mk_regex_reverse(expr* r);
expr_ref mk_regex_concat(expr* r1, expr* r2);
expr_ref merge_regex_sets(expr* r1, expr* r2, expr* unit, std::function<bool(expr*, expr*&, expr*&)>& decompose, std::function<expr* (expr*, expr*)>& compose);
// elem is (:var 0) and path a condition that may have (:var 0) as a free variable
// simplify path, e.g., (:var 0) = 'a' & (:var 0) = 'b' is simplified to false
expr_ref simplify_path(expr* elem, expr* path);
// expr_ref simplify_path(expr* elem, expr* path);
bool lt_char(expr* ch1, expr* ch2);
bool eq_char(expr* ch1, expr* ch2);
bool neq_char(expr* ch1, expr* ch2);
bool le_char(expr* ch1, expr* ch2);
bool pred_implies(expr* a, expr* b);
bool are_complements(expr* r1, expr* r2) const;
bool is_subset(expr* r1, expr* r2) const;
@ -264,6 +243,14 @@ class seq_rewriter {
br_status mk_re_union0(expr* a, expr* b, expr_ref& result);
br_status mk_re_inter0(expr* a, expr* b, expr_ref& result);
br_status mk_re_complement(expr* a, expr_ref& result);
// Range-set collapse helpers: if the operands form a boolean
// combination of character-class regexes, materialize the result as a
// canonical regex over a single range_predicate. See
// ast/rewriter/seq_range_collapse.h for the recognized fragment.
// NOTE: re.complement is intentionally not in this set because it
// operates at the sequence level, not the character-class level.
bool try_collapse_re_union(expr* a, expr* b, expr_ref& result);
bool try_collapse_re_inter(expr* a, expr* b, expr_ref& result);
br_status mk_re_star(expr* a, expr_ref& result);
br_status mk_re_diff(expr* a, expr* b, expr_ref& result);
br_status mk_re_xor(expr* a, expr* b, expr_ref& result);
@ -347,10 +334,10 @@ class seq_rewriter {
lbool some_string_in_re(expr_mark& visited, expr* r, unsigned_vector& str);
public:
seq_rewriter(ast_manager & m, params_ref const & p = params_ref()):
m_util(m), m_subset(m_util.re), m_split(*this), m_autil(m), m_br(m, p), // m_re2aut(m),
seq_rewriter(ast_manager & m, params_ref const & p = params_ref()) :
m_util(m), m_subset(m_util.re), m_split(*this), m_autil(m), m_br(m, p), m_derive(m, *this), // m_re2aut(m),
m_op_cache(m), m_es(m),
m_lhs(m), m_rhs(m), m_coalesce_chars(true) {
m_lhs(m), m_rhs(m) {
}
ast_manager & m() const { return m_util.get_manager(); }
family_id get_fid() const { return m_util.get_family_id(); }
@ -361,7 +348,7 @@ public:
static void get_param_descrs(param_descrs & r);
bool coalesce_chars() const { return m_coalesce_chars; }
// bool coalesce_chars() const { return m_coalesce_chars; }
br_status mk_app_core(func_decl * f, unsigned num_args, expr * const * args, expr_ref & result);
br_status mk_eq_core(expr * lhs, expr * rhs, expr_ref & result);
@ -377,6 +364,34 @@ public:
return result;
}
expr_ref mk_xor0(expr *a, expr *b) {
expr_ref result(m());
if (mk_re_xor0(a, b, result) == BR_FAILED)
result = re().mk_xor(a, b);
return result;
}
expr_ref mk_union(expr *a, expr *b) {
expr_ref result(m());
if (mk_re_union(a, b, result) == BR_FAILED)
result = re().mk_union(a, b);
return result;
}
expr_ref mk_inter(expr *a, expr *b) {
expr_ref result(m());
if (mk_re_inter(a, b, result) == BR_FAILED)
result = re().mk_inter(a, b);
return result;
}
expr_ref mk_complement(expr *a) {
expr_ref result(m());
if (mk_re_complement(a, result) == BR_FAILED)
result = re().mk_complement(a);
return result;
}
/*
* makes concat and simplifies
*/
@ -460,11 +475,35 @@ public:
variable v0 = (:var 0). Unlike `mk_derivative` this entry point keeps
the symbolic derivative as a single transition regex (TRegex): boolean
operators are pushed into the ITE leaves rather than lifted to the top
via _OP_RE_ANTIMIROV_UNION. Used by the regex_bisim equivalence
as a union. Used by the regex_bisim equivalence
procedure which relies on each leaf of D(p XOR q) being a coherent
XOR pair (D_v p) XOR (D_v q).
*/
expr_ref mk_brz_derivative(expr* r);
expr_ref mk_brz_derivative(expr *r) {
return mk_derivative(r);
}
/*
Enumerate the cofactors (min-terms) of a transition regex r taken with
respect to ele. Produces (path_condition, leaf_regex) pairs for every
feasible path through the ITE-tree, pruning infeasible character ranges.
Delegates to the derivative engine so the same path/interval context used
while hoisting ITEs is reused for the leaf simplification.
*/
void get_cofactors(expr* ele, expr* r, expr_ref_pair_vector& result) {
m_derive.get_cofactors(ele, r, result);
}
/*
Compute the symbolic derivative of r and enumerate its reachable leaves
in fully ITE-hoisted normal form: a list of (path_condition, target)
pairs where every target is free of (:var 0) (so nullability is always
decidable) and unions are kept intact as single states. Used by
regex_bisim, which consumes the targets and ignores the path conditions.
*/
void brz_derivative_cofactors(expr* r, expr_ref_pair_vector& result) {
m_derive.derivative_cofactors(r, result);
}
// heuristic elimination of element from condition that comes form a derivative.
// special case optimization for conjunctions of equalities, disequalities and ranges.
@ -475,15 +514,7 @@ public:
/* Apply simplifications to the intersection to keep it normalized (r1 and r2 are not normalized)*/
expr_ref mk_regex_inter_normalize(expr* r1, expr* r2);
/*
* Extract some sequence that is a member of r.
* result is set to a concrete sequence expression if l_true is returned.
* For string-typed regexes, delegates to some_string_in_re.
* For other sequence types, checks nullability and returns the empty
* sequence if the regex accepts it; otherwise returns l_undef.
* Returns l_false if the regex is known to be empty.
*/
lbool some_seq_in_re(expr* r, expr_ref& result);
expr_ref mk_regex_concat(expr *r1, expr *r2);
/*
* Extract some string that is a member of r.

View file

@ -1,3 +1,4 @@
/*++
Copyright (c) 2026 Microsoft Corporation

View file

@ -19,7 +19,7 @@ Author:
bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
while (true) {
if (a == b)
return true;
if (m_re.is_empty(a))
@ -30,7 +30,7 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
return true;
if (depth >= m_max_depth)
return false;
return false;
expr* a1 = nullptr, * a2 = nullptr, * b1 = nullptr, * b2 = nullptr;
unsigned la, ua, lb, ub;
@ -39,16 +39,12 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
if (m_re.is_dot_plus(b) && m_re.get_info(a).nullable == l_false)
return true;
// a ⊆ a*
if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth))
return true;
// e ⊆ a*
if (m_re.is_epsilon(a) && m_re.is_star(b, b1))
return true;
// R ⊆ R*
if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth + 1))
// a ⊆ a*: if b = b1* and a ⊆ b1, then a ⊆ b1*
if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth))
return true;
// R1* ⊆ R2* if R1 ⊆ R2
@ -112,6 +108,12 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b1) && is_subset_rec(a, b2, depth))
return true;
// prefix absorption: P·R' ⊆ Σ*·R' for any prefix P (since P ⊆ Σ*).
// Detect that a has R' (= b2) as a concatenation suffix, where b = Σ*·R'.
// Covers contains-patterns, e.g. Σ*·a·Σ*·b·Σ* ⊆ Σ*·b·Σ*.
if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b1) && ends_with(a, b2))
return true;
// R ⊆ R'·Σ* if R ⊆ R'
if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b2) && is_subset_rec(a, b1, depth))
return true;
@ -144,3 +146,30 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
bool seq_subset::is_subset(expr* a, expr* b) const {
return is_subset_rec(a, b, 0);
}
bool seq_subset::ends_with(expr* a, expr* suf) const {
if (a == suf)
return true;
// Flatten both regexes into their sequence of concatenation factors
// (independent of left/right associativity) and test list-suffix equality.
ptr_vector<expr> af, sf;
flatten_concat(a, af);
flatten_concat(suf, sf);
if (sf.size() > af.size())
return false;
unsigned off = af.size() - sf.size();
for (unsigned i = 0; i < sf.size(); ++i)
if (af[off + i] != sf[i])
return false;
return true;
}
void seq_subset::flatten_concat(expr* a, ptr_vector<expr>& out) const {
expr* a1 = nullptr, * a2 = nullptr;
if (m_re.is_concat(a, a1, a2)) {
flatten_concat(a1, out);
flatten_concat(a2, out);
}
else
out.push_back(a);
}

View file

@ -24,6 +24,12 @@ class seq_subset {
bool is_subset_rec(expr* a, expr* b, unsigned depth) const;
// true if regex a, viewed as a flattened concatenation, has suf as a
// structural (concatenation) suffix.
bool ends_with(expr* a, expr* suf) const;
void flatten_concat(expr* a, ptr_vector<expr>& out) const;
public:
explicit seq_subset(seq_util::rex& re) : m_re(re) {}
bool is_subset(expr* a, expr* b) const;

View file

@ -0,0 +1,681 @@
/**
* term_enumeration.cpp - Bottom-up term enumeration module for Z3
*
* Inspired by the Probe synthesizer (Barke et al., "Just-in-Time Learning
* for Bottom-Up Enumerative Synthesis"). Adapted to use Z3's internal APIs.
*
* Key ideas:
* - Terms are enumerated bottom-up by "cost" (calculated by tree size).
* - A grammar describes which function symbols (operators) and leaves
* (constants, variables) are available for enumeration.
*/
#include <sstream>
#include <functional>
#include <string>
#include "util/vector.h"
#include "util/scoped_ptr_vector.h"
#include "util/obj_hashtable.h"
#include "util/uint_set.h"
#include "ast/ast.h"
#include "ast/ast_ll_pp.h"
#include "ast/ast_pp.h"
#include "ast/rewriter/th_rewriter.h"
#include "ast/rewriter/term_enumeration.h"
namespace term_enum {
// ============================================================================
// grammar production rule
// ============================================================================
/**
* A production describes how to construct a term from child terms.
* - domain: the sort required for each child
* - range: the sort of the produced term
* - builder: given a vector of child exprs, produce the result expr
*/
struct production {
std::string name;
sort_ref range;
sort_ref_vector domain;
std::function<expr_ref(expr_ref_vector const&)> builder;
bool is_leaf() const { return domain.empty(); }
};
// ============================================================================
// grammar
// ============================================================================
/**
* A grammar groups productions into leaves (arity 0) and operators (arity > 0).
*/
class grammar {
public:
grammar(ast_manager& m) : m(m), m_pinned(m) {}
void add_production(production* p) {
if (p->is_leaf())
m_leaves.push_back(p);
else
m_operators.push_back(p);
}
scoped_ptr_vector<production> const& leaves() const { return m_leaves; }
scoped_ptr_vector<production> const& operators() const { return m_operators; }
ast_manager& mgr() const { return m; }
void add_func_decl(func_decl *f) {
if (m_seen.contains(f))
return;
m_pinned.push_back(f);
m_seen.insert(f);
sort_ref range(f->get_range(), m);
sort_ref_vector dom(m);
for (unsigned i = 0; i < f->get_arity(); ++i)
dom.push_back(sort_ref(f->get_domain(i), m));
add_production(alloc(production, {f->get_name().str(), range, dom, [this, f](expr_ref_vector const &args) {
return expr_ref(m.mk_app(f, args), m);
}}));
}
void add_expr(expr *e) {
if (m_seen.contains(e))
return;
m_pinned.push_back(e);
m_seen.insert(e);
sort_ref range(e->get_sort(), m);
sort_ref_vector dom(m);
std::stringstream ss;
ss << mk_bounded_pp(e, m);
std::string name = ss.str();
add_production(alloc(production, {name, range, dom, [this, e](expr_ref_vector const&) { return expr_ref(e, m); }}));
}
std::ostream& display(std::ostream& out) const {
out << "Leaves:\n";
for (auto const *p : m_leaves) {
out << " " << p->name << " : " << mk_pp(p->range, m) << "\n";
}
out << "Operators:\n";
for (auto const *p : m_operators) {
out << " " << p->name << " : (";
for (unsigned i = 0; i < p->domain.size(); ++i) {
if (i > 0)
out << ", ";
out << mk_pp(p->domain[i], m);
}
out << ") -> " << mk_pp(p->range, m) << "\n";
}
return out;
}
private:
ast_manager& m;
ast_ref_vector m_pinned;
scoped_ptr_vector<production> m_leaves;
scoped_ptr_vector<production> m_operators;
obj_hashtable<ast> m_seen;
};
// ============================================================================
// Term Bank - stores enumerated terms by cost and sort
// ============================================================================
using cost_terms = vector<std::pair<expr*, unsigned>>;
class term_bank {
using sort_term_map = obj_map<sort, ptr_vector<expr>>;
public:
term_bank(ast_manager& m) : m(m), m_pinned(m) {}
~term_bank() {
for (auto s : m_terms)
dealloc(s);
}
void reset() {
m_pinned.reset();
m_terms.clear();
}
void add(expr* term, unsigned cost) {
sort* s = term->get_sort();
m_pinned.push_back(term);
if (cost >= m_terms.size())
m_terms.resize(cost + 1);
if (!m_terms[cost])
m_terms[cost] = alloc(sort_term_map);
m_terms[cost]->insert_if_not_there(s, ptr_vector<expr>()).push_back(term);
}
/** Get all terms of a given sort up to (and including) max_cost */
cost_terms get_by_sort(sort* s, unsigned max_cost) const {
cost_terms result;
for (unsigned c = 0; c <= max_cost; ++c) {
if (c >= m_terms.size())
break;
if (!m_terms[c]->contains(s))
continue;
for (auto t : m_terms[c]->find(s))
result.push_back({t, c});
}
return result;
}
// Return true if there is at least one term at/above `cost` whose sort is
// not in `sorts` (i.e., enumeration can still produce a new requested sort).
bool is_productive(unsigned cost, uint_set const& sorts) {
for (unsigned i = cost; i < m_terms.size(); ++i) {
if (!m_terms[i])
continue;
for (auto const& entry : *m_terms[i]) {
sort* term_sort = entry.m_key;
if (!sorts.contains(term_sort->get_small_id()))
return true;
}
}
return false;
}
ptr_vector<expr> null_ptr_vector;
ptr_vector<expr> const &get_by_cost_and_sort(unsigned cost, sort *s) const {
if (cost >= m_terms.size() || !m_terms[cost] || !m_terms[cost]->contains(s))
return null_ptr_vector;
return m_terms[cost]->find(s);
}
std::ostream& display(std::ostream& out) const {
for (unsigned cost = 0; cost < m_terms.size(); ++cost) {
if (!m_terms[cost])
continue;
out << "cost " << cost << ":\n";
for (auto& [s, terms] : *m_terms[cost]) {
out << " sort " << mk_pp(s, m) << ":\n";
for (expr* e : terms) {
out << " #" << e->get_id() << " ";
if (cost == 0) {
out << mk_bounded_pp(e, m);
}
else if (is_app(e)) {
app* a = to_app(e);
out << a->get_decl()->get_name() << "(";
bool first = true;
for (expr* arg : *a) {
if (!first) out << ", ";
first = false;
out << "#" << arg->get_id();
}
out << ")";
}
out << "\n";
}
}
}
return out;
}
private:
ast_manager& m;
expr_ref_vector m_pinned;
// cost -> sort -> terms
ptr_vector<sort_term_map> m_terms;
};
// ============================================================================
// Children Iterator - generates all combinations of child terms
// ============================================================================
/**
* Iterates over all tuples (c1, c2, ..., cn) where each ci has the required
* sort, drawn from the term bank, with at least one child at the current
* cost - 1 (to avoid regenerating previously seen terms).
*/
class children_iterator {
public:
children_iterator(ast_manager& m, production const& prod, term_bank const& bank, unsigned current_cost)
: m(m), m_done(false)
{
m_arity = prod.domain.size();
if (m_arity == 0) {
m_done = true;
return;
}
for (unsigned i = 0; i < m_arity; ++i) {
m_candidates.push_back(bank.get_by_sort(prod.domain[i], current_cost - 1));
if (m_candidates.back().empty()) {
m_done = true;
return;
}
}
m_indices.resize(m_arity, 0);
}
bool has_next(unsigned cost) {
while (!m_done) {
if (m.limit().is_canceled())
return false;
if (has_child_at_cost(cost))
return true;
advance();
}
return false;
}
expr_ref_vector next(unsigned& cost) {
expr_ref_vector result(m);
cost = 1;
for (unsigned i = 0; i < m_arity; ++i) {
auto [e, c] = m_candidates[i].get(m_indices[i]);
cost += c;
result.push_back(e);
}
advance();
return result;
}
private:
ast_manager& m;
unsigned m_arity;
bool m_done;
vector<cost_terms> m_candidates;
svector<unsigned> m_indices;
bool has_child_at_cost(unsigned cost) const {
for (unsigned i = 0; i < m_arity; ++i) {
auto [e, c] = m_candidates[i].get(m_indices[i]);
if (c + 1 == cost)
return true;
}
return false;
}
void advance() {
for (auto i = m_arity; i-- > 0;) {
m_indices[i]++;
if (m_indices[i] < m_candidates[i].size()) return;
m_indices[i] = 0;
}
m_done = true;
}
};
// ============================================================================
// bottom_up_enumerator - the main bottom-up term enumeration engine
// ============================================================================
class bottom_up_enumerator {
public:
bottom_up_enumerator(grammar& grammar)
: m_grammar(grammar), m(grammar.mgr()),
m_bank(grammar.mgr()), m_pending(grammar.mgr()), m_rewriter(grammar.mgr())
{}
void set_target_sort(sort *s) {
m_target_sort = s;
}
bool has_next() {
if (m_pending) return true;
m_pending = find_next();
return m_pending != nullptr;
}
expr_ref next() {
if (!m_pending)
m_pending = find_next();
expr_ref result(m_pending, m);
m_pending = nullptr;
return result;
}
term_bank const& bank() const { return m_bank; }
std::ostream& display(std::ostream& out) const {
m_grammar.display(out);
return m_bank.display(out);
}
void reset() {
m_cost = 0;
m_leaf_idx = 0;
m_op_idx = 0;
m_state = State::Leaves;
m_bank.reset();
m_pending = nullptr;
m_rewriter.reset();
m_seen_terms.reset();
m_children_iter.reset();
}
expr* add_term(expr_ref const& term, unsigned cost) {
expr_ref simplified(m);
m_rewriter(term, simplified);
if (m_seen_terms.contains(simplified))
return nullptr;
IF_VERBOSE(10, verbose_stream() << "add " << simplified << "\n");
m_seen_terms.insert(simplified);
m_bank.add(simplified, cost);
return simplified;
}
private:
enum class State { Leaves, Operators, Done };
grammar& m_grammar;
ast_manager& m;
term_bank m_bank;
unsigned m_cost = 0;
unsigned m_leaf_idx = 0;
unsigned m_op_idx = 0;
unsigned m_bank_idx = 0;
unsigned m_bank_size = 0;
bool m_made_progress = false;
uint_set m_sorts_produced;
State m_state = State::Leaves;
expr_ref m_pending;
th_rewriter m_rewriter;
obj_hashtable<expr> m_seen_terms;
std::unique_ptr<children_iterator> m_children_iter;
sort *m_target_sort = nullptr;
bool sort_matches(expr* e) const {
return !m_target_sort || e->get_sort() == m_target_sort;
}
expr* find_next() {
while (true) {
if (m.limit().is_canceled()) {
m_state = State::Done;
return nullptr;
}
switch (m_state) {
case State::Leaves:
while (m_leaf_idx < m_grammar.leaves().size()) {
production const &prod = *m_grammar.leaves()[m_leaf_idx];
m_leaf_idx++;
expr_ref_vector empty_args(m);
expr_ref term = prod.builder(empty_args);
expr* r = add_term(term, 0);
if (r && sort_matches(r))
return r;
}
m_state = State::Operators;
m_cost = 1;
m_op_idx = 0;
m_bank_idx = 0;
m_bank_size = get_bank_size();
m_made_progress = false;
m_sorts_produced.reset();
m_children_iter.reset();
break;
case State::Operators: {
expr* result = enumerate_operators();
if (result)
return result;
m_cost++;
m_op_idx = 0;
m_bank_idx = 0;
m_bank_size = get_bank_size();
m_children_iter.reset();
if (!m_made_progress && !m_bank.is_productive(m_cost, m_sorts_produced)) {
m_state = State::Done;
return nullptr;
}
if (m_sorts_produced.contains(m_target_sort->get_small_id()))
m_sorts_produced.reset();
m_made_progress = false;
break;
}
case State::Done:
return nullptr;
}
}
}
unsigned get_bank_size() const {
auto const &terms = m_bank.get_by_cost_and_sort(m_cost, m_target_sort);
return terms.size();
}
expr *enumerate_operators() {
auto const &ops = m_grammar.operators();
while (true) {
if (m.limit().is_canceled())
return nullptr;
// first find terms at m_cost that were already created
if (m_bank_idx < m_bank_size) {
auto const &terms = m_bank.get_by_cost_and_sort(m_cost, m_target_sort);
auto t = terms.get(m_bank_idx);
m_bank_idx++;
SASSERT(sort_matches(t));
return t;
}
// then create new terms using children at cost below current m_cost.
if (m_children_iter && m_children_iter->has_next(m_cost)) {
unsigned new_cost = 0;
expr_ref_vector children = m_children_iter->next(new_cost);
production const &prod = *ops[m_op_idx - 1];
expr_ref term = prod.builder(children);
// IF_VERBOSE(0, verbose_stream() << term << "\n");
SASSERT(new_cost >= m_cost);
expr* r = add_term(term, new_cost);
if (!r)
continue;
unsigned sort_id = r->get_sort()->get_small_id();
if (!m_sorts_produced.contains(sort_id))
m_made_progress = true;
m_sorts_produced.insert(sort_id);
if (sort_matches(r) && new_cost == m_cost) {
return r;
}
continue;
}
if (m_op_idx >= ops.size())
return nullptr;
production const &prod = *ops[m_op_idx];
m_op_idx++;
m_children_iter = std::make_unique<children_iterator>(m, prod, m_bank, m_cost);
}
}
};
} // namespace term_enum
// ============================================================================
// term_enumeration public interface implementation
// ============================================================================
struct term_enumeration::imp {
ast_manager& m;
term_enum::grammar m_grammar;
term_enum::bottom_up_enumerator m_bottom_up_enumerator;
std::function<unsigned(expr*)> m_cost;
imp(ast_manager& m) :
m(m), m_grammar(m), m_bottom_up_enumerator(m_grammar) {}
void add_production(func_decl* f) {
m_grammar.add_func_decl(f);
}
void add_production(expr* e) {
m_grammar.add_expr(e);
}
void set_cost(std::function<unsigned(expr*)> const& cost) {
// TODO
}
std::ostream& display(std::ostream& out) const {
return m_bottom_up_enumerator.display(out);
}
};
// -- iterator implementation --
struct term_enumeration::iterator::iter_imp {
imp& m_imp;
ast_manager & m;
sort* m_sort;
unsigned m_cost = 0;
unsigned m_idx = 0;
vector<expr_ref_vector> m_levels;
expr_ref m_current;
bool m_end;
vector<expr_ref_vector> m_vars;
vector<ptr_vector<sort>> m_decls;
vector<vector<symbol>> m_names;
iter_imp(imp& i, sort* s) : m_imp(i), m(i.m), m_sort(s), m_current(i.m), m_end(false) {
m_imp.m_bottom_up_enumerator.reset();
init_sort();
advance();
}
// Sentinel constructor
iter_imp(imp& i) :
m_imp(i), m(i.m), m_sort(nullptr), m_current(i.m), m_end(true) {
UNREACHABLE();
}
void init_sort() {
array_util autil(m);
sort *range = m_sort;
while (autil.is_array(range)) {
m_vars.push_back(expr_ref_vector(m));
m_decls.push_back(ptr_vector<sort>());
m_names.push_back(vector<symbol>());
for (unsigned i = 0; i < get_array_arity(range); ++i) {
m_decls.back().push_back(get_array_domain(range, i));
m_vars.back().push_back(nullptr);
m_names.back().push_back(symbol());
}
expr_ref_vector args(m);
args.push_back(m.mk_const("a", range));
for (unsigned i = 0; i < m_decls.back().size(); ++i) {
args.push_back(m.mk_var(i, m_decls.back().get(i)));
}
app_ref sel(autil.mk_select(args), m);
m_imp.m_grammar.add_func_decl(sel->get_decl());
range = get_array_range(range);
}
unsigned n = 0;
for (unsigned i = m_decls.size(); i-- > 0;) {
for (unsigned j = m_decls[i].size(); j-- > 0;) {
m_vars[i][j] = m.mk_var(n, m_decls[i][j]);
m_names[i][j] = symbol(n);
m_imp.add_production(m_vars[i].get(j));
n++;
}
}
m_sort = range;
m_imp.m_bottom_up_enumerator.set_target_sort(range);
}
void mk_lambda() {
if (!m_current)
return;
for (unsigned i = m_decls.size(); i-- > 0;)
m_current = m.mk_lambda(m_decls[i].size(), m_decls[i].data(), m_names[i].data(), m_current);
}
void advance() {
if (m_end)
return;
m_current = m_imp.m_bottom_up_enumerator.next();
SASSERT(!m_current || m_current->get_sort() == m_sort);
mk_lambda();
if (!m_current)
m_end = true;
}
};
term_enumeration::iterator::iterator(imp& i, sort* s) {
m_imp = alloc(iter_imp, i, s);
}
term_enumeration::iterator::iterator(std::nullptr_t) {
m_imp = nullptr;
}
term_enumeration::iterator::~iterator() {
dealloc(m_imp);
}
expr* term_enumeration::iterator::operator*() {
return m_imp ? m_imp->m_current.get() : nullptr;
}
term_enumeration::iterator& term_enumeration::iterator::operator++() {
if (m_imp) m_imp->advance();
return *this;
}
term_enumeration::iterator term_enumeration::iterator::operator++(int) {
iterator tmp(*this);
++(*this);
return tmp;
}
bool term_enumeration::iterator::operator==(iterator const& other) const {
if (!m_imp && !other.m_imp) return true;
if (!m_imp) return other.m_imp->m_end;
if (!other.m_imp) return m_imp->m_end;
return m_imp->m_end == other.m_imp->m_end &&
m_imp->m_current == other.m_imp->m_current;
}
// -- terms implementation --
term_enumeration::terms::terms(imp* i, sort* s) : m_imp(i), m_sort(s) {}
term_enumeration::iterator term_enumeration::terms::begin() {
return iterator(*m_imp, m_sort);
}
term_enumeration::iterator term_enumeration::terms::end() {
return iterator(nullptr);
}
// -- term_enumeration implementation --
term_enumeration::term_enumeration(ast_manager& m) {
m_imp = alloc(imp, m);
}
term_enumeration::~term_enumeration() {
dealloc(m_imp);
}
void term_enumeration::add_production(func_decl* f) {
m_imp->add_production(f);
}
void term_enumeration::add_production(expr* e) {
m_imp->add_production(e);
}
void term_enumeration::set_cost(std::function<unsigned(expr*)> const& cost) {
m_imp->set_cost(cost);
}
term_enumeration::terms term_enumeration::enum_terms(sort* s) {
return terms(m_imp, s);
}
std::ostream& term_enumeration::display(std::ostream& out) const {
return m_imp->display(out);
}

View file

@ -0,0 +1,50 @@
#pragma once
#include "ast/ast.h"
#include <functional>
class term_enumeration {
struct imp;
imp* m_imp;
public:
term_enumeration(ast_manager& m);
~term_enumeration();
void add_production(func_decl* f);
void add_production(expr* e);
// void add_production(sort *s, std::function<expr *()> g);
// cost function associated with expressions.
// terms are enumerated with increasing cost.
void set_cost(std::function<unsigned(expr*)> const& cost);
class iterator {
struct iter_imp;
iter_imp* m_imp;
public:
iterator(imp& i, sort* s);
iterator(std::nullptr_t);
~iterator();
expr* operator*();
iterator operator++(int);
iterator& operator++();
bool operator!=(iterator const& other) const {
return !(*this == other);
}
bool operator==(iterator const &other) const;
};
class terms {
imp* m_imp;
sort* m_sort;
public:
terms(imp* i, sort* s);
iterator begin();
iterator end();
};
terms enum_terms(sort* s);
std::ostream& display(std::ostream& out) const;
};

View file

@ -241,7 +241,6 @@ void seq_decl_plugin::init() {
m_sigs[OP_RE_OF_PRED] = alloc(psig, m, "re.of.pred", 1, 1, &predA, reA);
m_sigs[OP_RE_REVERSE] = alloc(psig, m, "re.reverse", 1, 1, &reA, reA);
m_sigs[OP_RE_DERIVATIVE] = alloc(psig, m, "re.derivative", 1, 2, AreA, reA);
m_sigs[_OP_RE_ANTIMIROV_UNION] = alloc(psig, m, "re.union", 1, 2, reAreA, reA);
m_sigs[OP_SEQ_TO_RE] = alloc(psig, m, "seq.to.re", 1, 1, &seqA, reA);
m_sigs[OP_SEQ_IN_RE] = alloc(psig, m, "seq.in.re", 1, 2, seqAreA, boolT);
m_sigs[OP_SEQ_REPLACE_RE_ALL] = alloc(psig, m, "str.replace_re_all", 1, 3, seqAreAseqA, seqA);
@ -413,7 +412,6 @@ func_decl* seq_decl_plugin::mk_func_decl(decl_kind k, unsigned num_parameters, p
case OP_RE_COMPLEMENT:
case OP_RE_REVERSE:
case OP_RE_DERIVATIVE:
case _OP_RE_ANTIMIROV_UNION:
m_has_re = true;
Z3_fallthrough;
case OP_SEQ_UNIT:
@ -423,7 +421,7 @@ func_decl* seq_decl_plugin::mk_func_decl(decl_kind k, unsigned num_parameters, p
case OP_STRING_LE:
case OP_STRING_IS_DIGIT:
case OP_STRING_TO_CODE:
case OP_STRING_FROM_CODE:
case OP_STRING_FROM_CODE:
match(*m_sigs[k], arity, domain, range, rng);
return m.mk_func_decl(m_sigs[k]->m_name, arity, domain, rng, func_decl_info(m_family_id, k));
@ -1213,6 +1211,17 @@ app* seq_util::rex::mk_of_pred(expr* p) {
return m.mk_app(m_fid, OP_RE_OF_PRED, 0, nullptr, 1, &p);
}
app* seq_util::rex::mk_range(sort* re_sort, unsigned lo, unsigned hi) {
if (lo > hi)
return mk_empty(re_sort);
if (lo == 0 && hi == u.max_char())
return mk_full_char(re_sort);
app* lo_str = u.str.mk_string(zstring(lo));
if (lo == hi)
return mk_to_re(lo_str);
return mk_range(lo_str, u.str.mk_string(zstring(hi)));
}
bool seq_util::rex::is_loop(expr const* n, expr*& body, unsigned& lo, unsigned& hi) const {
if (is_loop(n)) {
app const* a = to_app(n);
@ -1441,7 +1450,7 @@ std::ostream& seq_util::rex::pp::print(std::ostream& out, expr* e) const {
print(out, r1);
print(out, r2);
}
else if (re.is_antimirov_union(e, r1, r2) || re.is_union(e, r1, r2)) {
else if (re.is_union(e, r1, r2)) {
out << "(";
print(out, r1);
out << (html_encode ? "&#x22C3;" : "|");
@ -1674,11 +1683,19 @@ seq_util::rex::info seq_util::rex::mk_info_rec(app* e) const {
case OP_RE_OPTION:
i1 = get_info_rec(e->get_arg(0));
return i1.opt();
case OP_RE_RANGE:
case OP_RE_RANGE: {
// A concrete range [lo, hi] with lo <= hi is non-empty and classical.
zstring slo, shi;
if (u.str.is_string(e->get_arg(0), slo) && slo.length() == 1 &&
u.str.is_string(e->get_arg(1), shi) && shi.length() == 1 &&
slo[0] <= shi[0])
return info(true, l_false, 1, true);
// Symbolic or unknown: not classical
return info(true, l_false, 1, false);
}
case OP_RE_FULL_CHAR_SET:
case OP_RE_OF_PRED:
//TBD: check if the character predicate contains uninterpreted symbols or is nonground or is unsat
//TBD: check if the range is unsat
return info(true, l_false, 1, false);
case OP_RE_CONCAT:
i1 = get_info_rec(e->get_arg(0));

View file

@ -109,7 +109,6 @@ enum seq_op_kind {
_OP_REGEXP_EMPTY,
_OP_REGEXP_FULL_CHAR,
_OP_RE_IS_NULLABLE,
_OP_RE_ANTIMIROV_UNION, // Lifted union for antimirov-style derivatives
_OP_SEQ_SKOLEM,
LAST_SEQ_OP
};
@ -525,6 +524,8 @@ public:
app* mk_to_re(expr* s) { return m.mk_app(m_fid, OP_SEQ_TO_RE, 1, &s); }
app* mk_in_re(expr* s, expr* r) { return m.mk_app(m_fid, OP_SEQ_IN_RE, s, r); }
app* mk_range(expr* s1, expr* s2) { return m.mk_app(m_fid, OP_RE_RANGE, s1, s2); }
// Smart constructor: returns re.empty / str.to_re / re.range based on lo vs hi.
app* mk_range(sort* re_sort, unsigned lo, unsigned hi);
app* mk_concat(expr* r1, expr* r2) { return m.mk_app(m_fid, OP_RE_CONCAT, r1, r2); }
app* mk_union(expr* r1, expr* r2) { return m.mk_app(m_fid, OP_RE_UNION, r1, r2); }
app* mk_inter(expr* r1, expr* r2) { return m.mk_app(m_fid, OP_RE_INTERSECT, r1, r2); }
@ -546,7 +547,6 @@ public:
app* mk_of_pred(expr* p);
app* mk_reverse(expr* r) { return m.mk_app(m_fid, OP_RE_REVERSE, r); }
app* mk_derivative(expr* ele, expr* r) { return m.mk_app(m_fid, OP_RE_DERIVATIVE, ele, r); }
app* mk_antimirov_union(expr* r1, expr* r2) { return m.mk_app(m_fid, _OP_RE_ANTIMIROV_UNION, r1, r2); }
bool is_to_re(expr const* n) const { return is_app_of(n, m_fid, OP_SEQ_TO_RE); }
bool is_concat(expr const* n) const { return is_app_of(n, m_fid, OP_RE_CONCAT); }
@ -580,7 +580,6 @@ public:
bool is_of_pred(expr const* n) const { return is_app_of(n, m_fid, OP_RE_OF_PRED); }
bool is_reverse(expr const* n) const { return is_app_of(n, m_fid, OP_RE_REVERSE); }
bool is_derivative(expr const* n) const { return is_app_of(n, m_fid, OP_RE_DERIVATIVE); }
bool is_antimirov_union(expr const* n) const { return is_app_of(n, m_fid, _OP_RE_ANTIMIROV_UNION); }
MATCH_UNARY(is_to_re);
MATCH_BINARY(is_concat);
MATCH_BINARY(is_union);
@ -595,7 +594,6 @@ public:
MATCH_UNARY(is_of_pred);
MATCH_UNARY(is_reverse);
MATCH_BINARY(is_derivative);
MATCH_BINARY(is_antimirov_union);
bool is_loop(expr const* n, expr*& body, unsigned& lo, unsigned& hi) const;
bool is_loop(expr const* n, expr*& body, unsigned& lo) const;
bool is_loop(expr const* n, expr*& body, expr*& lo, expr*& hi) const;

View file

@ -45,7 +45,7 @@ Author:
class dependent_expr_state {
unsigned m_qhead = 0;
bool m_suffix_frozen = false;
unsigned m_num_recfun = 0, m_num_lambdas = 0;
unsigned m_num_recfun = 0;
lbool m_has_quantifiers = l_undef;
ast_mark m_frozen;
func_decl_ref_vector m_frozen_trail;

View file

@ -16,7 +16,7 @@ Abstract:
All variables x, y, z, .. can eventually be eliminated, but the tactic requires a global
analysis between each elimination. We address this by using reference counts and maintaining
a heap of reference counts.
- it does not accomodate side constraints. The more general invertibility reduction methods, such
- it does not accommodate side constraints. The more general invertibility reduction methods, such
as those introduced for bit-vectors use side constraints.
- it is not modular: we detach the expression invertion routines to self-contained code.

View file

@ -20,7 +20,7 @@ It traverses the same sub-terms many times.
Outline of a presumably better scheme:
1. maintain map FV: term -> bit-set where bitset reprsents set of free variables. Assume the number of variables is bounded.
1. maintain map FV: term -> bit-set where bitset represents set of free variables. Assume the number of variables is bounded.
FV is built from initial terms.
2. maintain parent: term -> term-list of parent occurrences.
3. repeat

View file

@ -17,45 +17,51 @@ Notes:
--*/
#pragma once
#define ATOMIC_CMD(CLS_NAME, NAME, DESCR, ACTION) \
#define ATOMIC_CMD(CLS_NAME, NAME, DESCR, ACTION) \
class CLS_NAME : public cmd { \
public: \
CLS_NAME():cmd(NAME) {} \
char const * get_usage() const override { return 0; } \
char const * get_descr(cmd_context & ctx) const override { \
return DESCR; \
} \
unsigned get_arity() const override { return 0; } \
void execute(cmd_context & ctx) override { ACTION } \
}
#define UNARY_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \
class CLS_NAME : public cmd { \
public: \
CLS_NAME():cmd(NAME) {} \
virtual char const * get_usage() const { return 0; } \
virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \
virtual unsigned get_arity() const { return 0; } \
virtual void execute(cmd_context & ctx) { ACTION } \
};
#define UNARY_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \
class CLS_NAME : public cmd { \
public: \
CLS_NAME():cmd(NAME) {} \
virtual char const * get_usage() const { return USAGE; } \
virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \
virtual unsigned get_arity() const { return 1; } \
virtual cmd_arg_kind next_arg_kind(cmd_context & ctx) const { return ARG_KIND; } \
virtual void set_next_arg(cmd_context & ctx, ARG_TYPE arg) { ACTION } \
char const * get_usage() const override { return USAGE; } \
char const * get_descr(cmd_context & ctx) const override { \
return DESCR; \
} \
unsigned get_arity() const override { return 1; } \
cmd_arg_kind next_arg_kind(cmd_context & ctx) const override { \
return ARG_KIND; \
} \
void set_next_arg(cmd_context & ctx, ARG_TYPE arg) override { ACTION } \
}
// Macro for creating commands where the first argument is a symbol
// The second argument cannot be a symbol
#define BINARY_SYM_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \
class CLS_NAME : public cmd { \
symbol m_sym; \
symbol m_sym; \
public: \
CLS_NAME():cmd(NAME) {} \
virtual char const * get_usage() const { return USAGE; } \
virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \
virtual unsigned get_arity() const { return 2; } \
virtual void prepare(cmd_context & ctx) { m_sym = symbol::null; } \
virtual cmd_arg_kind next_arg_kind(cmd_context & ctx) const { \
return m_sym == symbol::null ? CPK_SYMBOL : ARG_KIND; \
char const * get_usage() const override { return USAGE; } \
char const * get_descr(cmd_context & ctx) const override { return DESCR; } \
unsigned get_arity() const override { return 2; } \
void prepare(cmd_context & ctx) override { m_sym = symbol::null; } \
cmd_arg_kind next_arg_kind(cmd_context & ctx) const override { \
return m_sym == symbol::null ? CPK_SYMBOL : ARG_KIND; \
} \
virtual void set_next_arg(cmd_context & ctx, symbol const & s) { m_sym = s; } \
virtual void set_next_arg(cmd_context & ctx, ARG_TYPE arg) { ACTION } \
};
void set_next_arg(cmd_context & ctx, symbol const & s) override { m_sym = s; } \
void set_next_arg(cmd_context & ctx, ARG_TYPE arg) override { ACTION } \
}
class ast;
class expr;

View file

@ -7,7 +7,6 @@
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "ast/arith_decl_plugin.h"
#include "ast/array_decl_plugin.h"
@ -65,7 +64,8 @@ enum class token_kind {
slash_tok,
minus_tok,
at_tok,
lambda_tok
lambda_tok,
modal_tok
};
struct parse_error : public std::exception {
@ -87,6 +87,7 @@ struct token {
std::string text;
unsigned line = 1;
unsigned col = 1;
bool dquote = false; // true for double-quoted strings ("..."): TPTP distinct objects
};
class lexer {
@ -163,6 +164,7 @@ public:
if (peek() == '\'' || peek() == '"') {
char q = get();
t.kind = token_kind::str;
t.dquote = (q == '"');
while (!eof()) {
char c = get();
if (c == '\\' && !eof()) {
@ -252,7 +254,7 @@ public:
case '^': t.kind = token_kind::lambda_tok; return t;
case '{':
// Modal operators: {$box}, {$dia}, etc. — lex as identifier including braces
t.kind = token_kind::id;
t.kind = token_kind::modal_tok;
t.text.push_back(c);
while (!eof() && peek() != '}')
t.text.push_back(get());
@ -277,10 +279,10 @@ public:
};
struct parsed_type {
std::vector<sort*> domain;
ptr_vector<sort> domain;
sort* range = nullptr;
parsed_type(sort* s): range(s) {}
parsed_type(std::vector<sort*> const& d, sort* r): domain(d), range(r) {}
parsed_type(ptr_vector<sort> const& d, sort* r): domain(d), range(r) {}
};
class tptp_parser {
@ -290,13 +292,20 @@ class tptp_parser {
array_util m_array;
sort* m_univ;
bool m_has_conjecture = false;
unsigned m_dropped_formulas = 0; // axioms/definitions skipped due to encoding errors
bool m_last_name_quoted = false;
bool m_last_name_dquoted = false; // last parsed name was a double-quoted distinct object
// Distinct objects: TPTP double-quoted strings ("...") denote pairwise distinct
// domain elements. Collected here (deduplicated by name) so a single global
// (distinct ...) constraint can be asserted before solving.
std::unordered_map<std::string, expr*> m_distinct_objects;
std::string m_expected_status; // SZS status from the input annotation, if any
std::unordered_map<std::string, sort*> m_sorts;
sort_ref_vector m_pinned_sorts; // prevents cached sorts from being freed
std::unordered_map<std::string, func_decl*> m_decls;
func_decl_ref_vector m_pinned_decls; // prevents cached func_decls from being freed
expr_ref_vector m_pinned_exprs; // prevents bound variable apps from being freed
std::unordered_map<std::string, std::pair<std::vector<sort*>, sort*>> m_typed_decls;
std::unordered_map<std::string, std::pair<ptr_vector<sort>, sort*>> m_typed_decls;
std::vector<std::unordered_map<std::string, app*>> m_bound;
bool m_in_at_arg = false; // true when parsing inside @ argument (lambda body stops consuming @)
struct implicit_var_scope {
@ -424,6 +433,7 @@ class tptp_parser {
std::string parse_name() {
if (is(token_kind::id) || is(token_kind::str)) {
m_last_name_quoted = is(token_kind::str);
m_last_name_dquoted = is(token_kind::str) && m_curr.dquote;
std::string r = m_curr.text;
next();
return r;
@ -449,7 +459,7 @@ class tptp_parser {
// For higher-order types like ($i > $o), create an uninterpreted sort
// Function type A > B is represented as Array(A, B).
// Multi-argument A * B > C is represented as Array(A, Array(B, C)) (curried).
sort* get_ho_sort(std::vector<sort*> const& domain, sort* range) {
sort* get_ho_sort(ptr_vector<sort> const& domain, sort* range) {
sort* s = range;
for (int i = (int)domain.size() - 1; i >= 0; --i)
s = m_array.mk_array_sort(domain[i], s);
@ -513,7 +523,8 @@ class tptp_parser {
if (itt != m_typed_decls.end()) {
std::string typed_decl_key = mk_decl_key(name, arity, 'd');
auto itd = m_decls.find(typed_decl_key);
if (itd != m_decls.end()) return itd->second;
if (itd != m_decls.end())
return itd->second;
auto const& sig = itt->second;
func_decl* f = m.mk_func_decl(symbol(name), sig.first.size(), sig.first.data(), sig.second);
m_pinned_decls.push_back(f);
@ -525,7 +536,7 @@ class tptp_parser {
auto itd = m_decls.find(key);
if (itd != m_decls.end()) return itd->second;
std::vector<sort*> dom(arity, m_univ);
ptr_vector<sort> dom(arity, m_univ);
func_decl* f = m.mk_func_decl(symbol(name), arity, dom.data(), pred ? m.mk_bool_sort() : m_univ);
m_pinned_decls.push_back(f);
m_decls.emplace(key, f);
@ -571,6 +582,14 @@ class tptp_parser {
expr_ref coerce_arg(expr_ref const& e, sort* target) {
sort* actual = e->get_sort();
if (actual == target) return e;
// Int <-> Real conversions must use the arithmetic semantics (to_real /
// to_int), never an uninterpreted boxing function: an uninterpreted box
// severs the numeric link between the two sides and lets the solver build
// spurious models (e.g. it would make floor/ceiling identities sat).
if (m_arith.is_int(actual) && m_arith.is_real(target))
return expr_ref(m_arith.mk_to_real(e), m);
if (m_arith.is_real(actual) && m_arith.is_int(target))
return expr_ref(m_arith.mk_to_int(e), m);
// Create a boxing function from actual sort to target sort
std::string box_name = std::string("$box_") + actual->get_name().str() + "_to_" + target->get_name().str();
std::string key = mk_decl_key(box_name, 1, 'f');
@ -697,14 +716,14 @@ class tptp_parser {
// <defined_type> ::= $oType | $o | $iType | $i | $tType | $real | $rat | $int
parsed_type parse_type_atom() {
if (accept(token_kind::lparen)) {
std::vector<sort*> prod = parse_type_product_raw();
ptr_vector<sort> prod = parse_type_product_raw();
if (accept(token_kind::gt_tok)) {
// Full function type inside parens: (A * B > C) or (A > B > C)
parsed_type rhs = parse_type_expr();
std::vector<sort*> full_domain = prod;
ptr_vector<sort> full_domain = prod;
if (!rhs.domain.empty()) {
// Nested higher-order: (A > B > C) → flatten
full_domain.insert(full_domain.end(), rhs.domain.begin(), rhs.domain.end());
full_domain.append(rhs.domain);
}
expect(token_kind::rparen, "')'");
// Return with domain/range preserved for proper flattening
@ -743,15 +762,15 @@ class tptp_parser {
// Grammar: <thf_xprod_type> ::= <thf_unitary_type> * <thf_unitary_type>
// | <thf_xprod_type> * <thf_unitary_type>
// Product types form the domain in mapping types: (A * B) > C
std::vector<sort*> parse_type_product_raw() {
ptr_vector<sort> parse_type_product_raw() {
parsed_type first = parse_type_atom();
if (!first.domain.empty() && first.range == nullptr) {
// Already a parenthesized product from nested parens
std::vector<sort*> args = first.domain;
ptr_vector<sort> args = first.domain;
while (accept(token_kind::star_tok)) {
parsed_type t = parse_type_atom();
if (!t.domain.empty()) {
args.insert(args.end(), t.domain.begin(), t.domain.end());
args.append(t.domain);
} else {
args.push_back(t.range);
}
@ -761,28 +780,28 @@ class tptp_parser {
if (!first.domain.empty()) {
// Function type as first element of product — use ho_sort
sort* ho = get_ho_sort(first.domain, first.range);
std::vector<sort*> args;
ptr_vector<sort> args;
args.push_back(ho);
while (accept(token_kind::star_tok)) {
parsed_type t = parse_type_atom();
if (!t.domain.empty() && t.range != nullptr) {
args.push_back(get_ho_sort(t.domain, t.range));
} else if (!t.domain.empty()) {
args.insert(args.end(), t.domain.begin(), t.domain.end());
args.append(t.domain);
} else {
args.push_back(t.range);
}
}
return args;
}
std::vector<sort*> args;
ptr_vector<sort> args;
args.push_back(first.range);
while (accept(token_kind::star_tok)) {
parsed_type t = parse_type_atom();
if (!t.domain.empty() && t.range != nullptr) {
args.push_back(get_ho_sort(t.domain, t.range));
} else if (!t.domain.empty()) {
args.insert(args.end(), t.domain.begin(), t.domain.end());
args.append(t.domain);
} else {
args.push_back(t.range);
}
@ -801,7 +820,7 @@ class tptp_parser {
return first;
}
// Build product vector
std::vector<sort*> args;
ptr_vector<sort> args;
if (!first.domain.empty() && first.range != nullptr) {
// Function type used as element in a product
args.push_back(get_ho_sort(first.domain, first.range));
@ -816,7 +835,7 @@ class tptp_parser {
if (!t.domain.empty() && t.range != nullptr) {
args.push_back(get_ho_sort(t.domain, t.range));
} else if (!t.domain.empty()) {
args.insert(args.end(), t.domain.begin(), t.domain.end());
args.append(t.domain);
} else {
args.push_back(t.range);
}
@ -833,7 +852,7 @@ class tptp_parser {
if (is(token_kind::type_forall_tok) || is(token_kind::type_exists_tok)) {
next();
expect(token_kind::lbrack, "'['");
std::vector<sort*> type_params;
ptr_vector<sort> type_params;
if (!accept(token_kind::rbrack)) {
do {
std::string tv = parse_name();
@ -848,8 +867,8 @@ class tptp_parser {
parsed_type inner = parse_type_expr();
// Prepend type params to domain
if (!type_params.empty()) {
std::vector<sort*> full_domain = type_params;
full_domain.insert(full_domain.end(), inner.domain.begin(), inner.domain.end());
ptr_vector<sort> full_domain = type_params;
full_domain.append(inner.domain);
return parsed_type(full_domain, inner.range);
}
return inner;
@ -858,7 +877,7 @@ class tptp_parser {
if (accept(token_kind::gt_tok)) {
parsed_type rhs = parse_type_expr();
// prod is either a product (domain non-empty, range==nullptr) or a single sort (domain empty)
std::vector<sort*> domain;
ptr_vector<sort> domain;
if (!prod.domain.empty() && prod.range == nullptr) {
domain = prod.domain;
} else if (!prod.domain.empty() && prod.range != nullptr) {
@ -869,7 +888,7 @@ class tptp_parser {
}
if (!rhs.domain.empty()) {
// Higher-order result type: A > (B > C) flattened to (A, B) > C
domain.insert(domain.end(), rhs.domain.begin(), rhs.domain.end());
domain.append(rhs.domain);
return parsed_type(domain, rhs.range);
}
return parsed_type(domain, rhs.range);
@ -925,7 +944,7 @@ class tptp_parser {
// Grammar: (same as parse_term, primary productions)
expr_ref parse_term_primary() {
if (accept(token_kind::lparen)) {
expr_ref e = parse_formula();
expr_ref e = parse_formula(false);
expect(token_kind::rparen, "')'");
return e;
}
@ -946,6 +965,7 @@ class tptp_parser {
return parse_numeral_from_name(n);
}
bool dq_name = m_last_name_dquoted;
expr_ref b(m);
// Check bound variables: uppercase (quantifier vars) AND lowercase (let-bound names)
if (!m_last_name_quoted && find_bound(n, b)) {
@ -971,11 +991,11 @@ class tptp_parser {
// $ite needs special parsing: first arg is formula, rest are formulas (branches can be equalities)
if (n == "$ite") {
expect(token_kind::lparen, "'('");
args.push_back(parse_formula());
args.push_back(parse_formula(true));
expect(token_kind::comma, "','");
args.push_back(parse_formula());
args.push_back(parse_formula(false));
expect(token_kind::comma, "','");
args.push_back(parse_formula());
args.push_back(parse_formula(false));
expect(token_kind::rparen, "')'");
}
else if (n == "$let") {
@ -995,14 +1015,17 @@ class tptp_parser {
}
func_decl* f = mk_decl_or_ho_const(n, args.size(), false);
if (!args.empty()) coerce_args(f, args);
return expr_ref(args.empty() ? m.mk_const(f) : m.mk_app(f, args.size(), args.data()), m);
coerce_args(f, args);
expr_ref term(args.empty() ? m.mk_const(f) : m.mk_app(f, args.size(), args.data()), m);
if (dq_name && args.empty())
register_distinct_object(n, term);
return term;
}
// Grammar: <tff_logic_formula> ::= <tff_unitary_formula> | <tff_binary_formula>
// <thf_logic_formula> ::= <thf_unitary_formula> | <thf_binary_formula>
// Entry point for formula parsing (wraps parse_expr with default precedence).
expr_ref parse_formula();
expr_ref parse_formula(bool is_boolean);
// Grammar: <thf_apply_formula> ::= <thf_unitary_formula> @ <thf_unitary_formula>
// | <thf_apply_formula> @ <thf_unitary_formula>
@ -1042,7 +1065,7 @@ class tptp_parser {
return parse_lambda_expr();
}
if (accept(token_kind::lparen)) {
expr_ref e = parse_formula();
expr_ref e = parse_formula(false);
expect(token_kind::rparen, "')'");
// Do NOT call apply_at here — outer apply_at owns the remaining @ tokens
return e;
@ -1074,7 +1097,7 @@ class tptp_parser {
// Quantifier body in @-arg should NOT consume @ — those belong to enclosing application
bool save_in_at_arg = m_in_at_arg;
m_in_at_arg = true;
expr_ref body = parse_formula();
expr_ref body = parse_formula(false);
m_in_at_arg = save_in_at_arg;
m_bound.pop_back();
return mk_quantifier(is_forall, vars, body);
@ -1092,7 +1115,7 @@ class tptp_parser {
std::string key = mk_decl_key(name_str, 0, 'c') + "\x1f" + std::to_string(range->get_id());
auto it = m_decls.find(key);
if (it != m_decls.end()) return it->second;
func_decl* f = m.mk_func_decl(name, 0, static_cast<sort**>(nullptr), range);
func_decl* f = m.mk_const_decl(name, range);
m_pinned_decls.push_back(f);
m_decls.emplace(key, f);
return f;
@ -1121,31 +1144,29 @@ class tptp_parser {
// Coerce two expressions to have the same sort for equality.
// In TPTP, = is term equality and m_univ is the default sort.
// If one side has Bool sort (parsed as predicate), coerce it to m_univ.
// If sorts already match and are not Bool, returns lhs unchanged.
// If sorts already match, returns lhs unchanged; otherwise applies minimal
// arithmetic/box coercions so both sides of an equality share a sort.
expr_ref coerce_eq(expr_ref lhs, expr_ref& rhs) {
// Coerce Bool-sorted operands to m_univ since = is term equality in TPTP
if (m.is_bool(lhs->get_sort()) && is_app(lhs) && !m.is_true(lhs) && !m.is_false(lhs))
lhs = coerce_to_univ(lhs);
if (m.is_bool(rhs->get_sort()) && is_app(rhs) && !m.is_true(rhs) && !m.is_false(rhs))
rhs = coerce_to_univ(rhs);
if (lhs->get_sort() == rhs->get_sort()) return lhs;
// No coercion is needed when both sides already share a sort. In particular
// `=` between two Boolean ($o) operands is logical equivalence (iff): keep
// them Boolean instead of forcing one into the term universe U.
if (lhs->get_sort() == rhs->get_sort())
return lhs;
if (m_arith.is_int_real(lhs) && m_arith.is_int_real(rhs))
return lhs;
// Coerce 0-arity constants to match the other side's sort
if (is_app(lhs) && to_app(lhs)->get_num_args() == 0 && lhs->get_sort() != rhs->get_sort()) {
if (is_app(lhs) && to_app(lhs)->get_num_args() == 0) {
return coerce_zero_arity(to_app(lhs), rhs->get_sort());
}
if (is_app(rhs) && to_app(rhs)->get_num_args() == 0 && lhs->get_sort() != rhs->get_sort()) {
if (is_app(rhs) && to_app(rhs)->get_num_args() == 0) {
rhs = coerce_zero_arity(to_app(rhs), lhs->get_sort());
return lhs;
}
// Last resort: coerce both sides to have the same sort
if (lhs->get_sort() != rhs->get_sort()) {
// Prefer coercing to rhs sort, falling back to m_univ
sort* target = rhs->get_sort();
lhs = coerce_arg(lhs, target);
}
// Prefer coercing to rhs sort, falling back to m_univ
sort* target = rhs->get_sort();
lhs = coerce_arg(lhs, target);
return lhs;
}
@ -1176,7 +1197,7 @@ class tptp_parser {
// Bind parameter variables for parsing the RHS
if (!param_scope.empty())
m_bound.push_back(param_scope);
expr_ref value = parse_formula();
expr_ref value = parse_formula(false);
if (!param_scope.empty())
m_bound.pop_back();
// For function-style definitions, wrap value in lambdas
@ -1206,7 +1227,7 @@ class tptp_parser {
// --- Part 1: Parse type declarations ---
std::vector<std::string> let_names;
std::vector<sort*> let_sorts;
ptr_vector<sort> let_sorts;
auto parse_one_typing = [&]() {
std::string name = parse_name();
@ -1264,7 +1285,7 @@ class tptp_parser {
// --- Part 3: Parse body with let-bound names in scope ---
m_bound.push_back(scope);
expr_ref body = parse_formula();
expr_ref body = parse_formula(false);
m_bound.pop_back();
expect(token_kind::rparen, "')'");
@ -1288,7 +1309,7 @@ class tptp_parser {
// <defined_pred> ::= $less | $lesseq | $greater | $greatereq | $is_int | $is_rat | ...
// <defined_infix_pred> ::= = | !=
// Also handles: let-bound name resolution, implicit variable creation.
expr_ref parse_atomic_formula() {
expr_ref parse_atomic_formula(bool is_boolean) {
if (accept(token_kind::lparen)) {
// Check for parenthesized connective used as higher-order term: (~), (&), (|), etc.
if (is(token_kind::not_tok) || is(token_kind::and_tok) || is(token_kind::or_tok) ||
@ -1307,31 +1328,50 @@ class tptp_parser {
token saved = m_curr;
next();
if (accept(token_kind::rparen)) {
// Parenthesized connective: treat as HO constant with array sort
// A parenthesized connective used as a higher-order term, e.g.
// "(~) @ p" or "(|) @ p @ q". Encode it as a genuine lambda over Bool
// carrying the real logical semantics, so that application beta-reduces
// to the actual connective (e.g. "(~) @ p" ==> "not p"). Encoding it as
// an uninterpreted array constant instead would sever it from Boolean
// logic and make valid higher-order theorems spuriously
// CounterSatisfiable (the (~)/(|) applications would be unrelated to the
// truth values of their operands).
(void)op_text;
sort* bool_sort = m.mk_bool_sort();
sort* ho_sort;
if (arity == 1)
ho_sort = m_array.mk_array_sort(bool_sort, bool_sort);
else
ho_sort = m_array.mk_array_sort(bool_sort, m_array.mk_array_sort(bool_sort, bool_sort));
std::string key = mk_decl_key(op_text, 0, 'h');
auto it = m_decls.find(key);
func_decl* f;
if (it != m_decls.end()) {
f = it->second;
} else {
f = m.mk_func_decl(symbol(op_text), 0, static_cast<sort**>(nullptr), ho_sort);
m_pinned_decls.push_back(f);
m_decls.emplace(key, f);
symbol xn("X"), yn("Y");
if (arity == 1) {
// (~) ==> ^[X:$o] : ~X
expr_ref body(m.mk_not(m.mk_var(0, bool_sort)), m);
return expr_ref(m.mk_lambda(1, &bool_sort, &xn, body), m);
}
return expr_ref(m.mk_const(f), m);
// binary connective ==> ^[X:$o] : ^[Y:$o] : (X <op> Y)
// de Bruijn: X is var(1) (outer binder), Y is var(0) (inner binder).
expr* vx = m.mk_var(1, bool_sort);
expr* vy = m.mk_var(0, bool_sort);
expr_ref opbody(m);
switch (saved.kind) {
case token_kind::and_tok: opbody = m.mk_and(vx, vy); break;
case token_kind::or_tok: opbody = m.mk_or(vx, vy); break;
case token_kind::implies_tok: opbody = m.mk_implies(vx, vy); break;
case token_kind::iff_tok: opbody = m.mk_eq(vx, vy); break;
case token_kind::xor_tok: opbody = m.mk_xor(vx, vy); break;
default: opbody = m.mk_eq(vx, vy); break;
}
expr_ref inner(m.mk_lambda(1, &bool_sort, &yn, opbody), m);
return expr_ref(m.mk_lambda(1, &bool_sort, &xn, inner), m);
}
// Not a parenthesized connective — lparen was consumed and connective was consumed
// but ')' didn't follow. Parse as formula with the connective already consumed.
expr_ref inner(m);
if (saved.kind == token_kind::not_tok) {
expr_ref e = parse_formula();
inner = expr_ref(m.mk_not(e), m);
// "( ~ <formula> )": the '~' is a unary connective binding only the
// next unary unit; ordinary binary connectives then apply at their
// own precedence (e.g. "( ~ p | q )" is "(~p) | q", NOT "~(p | q)").
// We have already consumed '(' and '~', so negate the next unit and
// resume precedence-climbing parsing from that negated left operand.
expr_ref operand = parse_unary_formula(true);
expr_ref neg(m.mk_not(ensure_bool(operand)), m);
inner = parse_binary_rest(neg, PREC_IFF, true);
} else {
// Binary connective at start of parens — shouldn't happen in valid TPTP
throw parse_error("unexpected connective after '(' at " + loc());
@ -1342,12 +1382,15 @@ class tptp_parser {
// Parentheses create a new scope for @ consumption
bool save_in_at_arg = m_in_at_arg;
m_in_at_arg = false;
expr_ref e = parse_formula();
expr_ref e = parse_formula(is_boolean);
expect(token_kind::rparen, "')'");
m_in_at_arg = save_in_at_arg;
return e;
}
if (accept(token_kind::modal_tok))
throw parse_error("modal operators not supported in TPTP input at " + loc());
// Handle negative numerals in formula position: -2 = $uminus(2)
if (accept(token_kind::minus_tok)) {
expr_ref t = parse_term();
@ -1358,9 +1401,9 @@ class tptp_parser {
if (accept(token_kind::lbrack)) {
if (accept(token_kind::rbrack))
return expr_ref(m.mk_const(symbol("$nil"), m_univ), m);
expr_ref first = parse_formula();
expr_ref first = parse_formula(is_boolean);
while (accept(token_kind::comma))
parse_formula(); // consume remaining elements
parse_formula(is_boolean); // consume remaining elements
expect(token_kind::rbrack, "']'");
return first;
}
@ -1373,6 +1416,7 @@ class tptp_parser {
return parse_numeral_from_name(n);
}
bool dq_name = m_last_name_dquoted;
// Check if name is let-bound (works for both uppercase vars and lowercase let-bound names)
{
expr_ref b(m);
@ -1417,7 +1461,7 @@ class tptp_parser {
}
expect(token_kind::colon, "':'");
m_bound.push_back(scope);
expr_ref body = parse_formula();
expr_ref body = parse_formula(is_boolean);
m_bound.pop_back();
// Approximate choice as existential quantification
return mk_quantifier(false, vars, body);
@ -1427,11 +1471,11 @@ class tptp_parser {
// $ite needs special parsing: first arg is formula, rest are formulas (branches can be equalities)
if (n == "$ite") {
expect(token_kind::lparen, "'('");
args.push_back(parse_formula());
args.push_back(parse_formula(true));
expect(token_kind::comma, "','");
args.push_back(parse_formula());
args.push_back(parse_formula(is_boolean));
expect(token_kind::comma, "','");
args.push_back(parse_formula());
args.push_back(parse_formula(is_boolean));
expect(token_kind::rparen, "')'");
}
else if (n == "$let") {
@ -1465,18 +1509,19 @@ class tptp_parser {
auto typed = m_typed_decls.find(mk_typed_key(n, args.size()));
if (typed != m_typed_decls.end()) {
func_decl* f = args.empty() ? mk_decl_or_ho_const(n, 0, false) : mk_decl(n, args.size(), false);
if (!args.empty()) coerce_args(f, args);
return expr_ref(args.empty() ? m.mk_const(f) : m.mk_app(f, args.size(), args.data()), m);
coerce_args(f, args);
return expr_ref(m.mk_app(f, args.size(), args.data()), m);
}
if (args.empty() && (is(token_kind::equal_tok) || is(token_kind::neq_tok))) {
func_decl* f = mk_decl_or_ho_const(n, 0, false);
return expr_ref(m.mk_const(f), m);
}
is_boolean = is_boolean && !is(token_kind::equal_tok) && !is(token_kind::neq_tok);
func_decl* pred = mk_decl_or_ho_const(n, args.size(), true);
if (!args.empty()) coerce_args(pred, args);
return expr_ref(args.empty() ? m.mk_const(pred) : m.mk_app(pred, args.size(), args.data()), m);
func_decl* pred = mk_decl_or_ho_const(n, args.size(), is_boolean);
coerce_args(pred, args);
expr_ref atom(m.mk_app(pred, args.size(), args.data()), m);
if (dq_name && args.empty() && !m.is_bool(atom))
register_distinct_object(n, atom);
return atom;
}
// Grammar: <thf_abstraction> ::= ^ [<thf_variable_list>] : <thf_unitary_formula>
@ -1513,7 +1558,7 @@ class tptp_parser {
// Lambda body does NOT consume @ — @ belongs to the enclosing application
bool save_in_at_arg = m_in_at_arg;
m_in_at_arg = true;
expr_ref body = parse_formula();
expr_ref body = parse_formula(false);
m_in_at_arg = save_in_at_arg;
m_bound.pop_back();
if (vars.empty())
@ -1538,9 +1583,9 @@ class tptp_parser {
// <thf_quantified_formula> ::= <thf_quantification> <thf_unitary_formula>
// <fof_quantifier> ::= ! | ?
// Also handles: $ite, $let, lambda (^), parenthesized formulas, and atomic formulas.
expr_ref parse_unary_formula() {
expr_ref parse_unary_formula(bool is_boolean) {
if (accept(token_kind::not_tok)) {
expr_ref e = parse_unary_formula();
expr_ref e = parse_unary_formula(true);
return expr_ref(m.mk_not(ensure_bool(e)), m);
}
@ -1552,7 +1597,7 @@ class tptp_parser {
next(); // consume '['
if (accept(token_kind::dot)) {
expect(token_kind::rbrack, "']'");
expr_ref sub = parse_unary_formula();
expr_ref sub = parse_unary_formula(is_boolean);
func_decl* f = mk_modal_op("box");
return expr_ref(m.mk_app(f, sub.get()), m);
}
@ -1561,7 +1606,7 @@ class tptp_parser {
std::string first_name = m_curr.text;
next();
if (accept(token_kind::rbrack)) {
expr_ref sub = parse_unary_formula();
expr_ref sub = parse_unary_formula(is_boolean);
func_decl* f = mk_modal_op(mod_name);
return expr_ref(m.mk_app(f, sub.get()), m);
}
@ -1575,11 +1620,11 @@ class tptp_parser {
else if (should_create_implicit_var(first_name))
first = expr_ref(get_or_create_implicit_var(first_name), m);
else {
func_decl* f = mk_decl_or_ho_const(first_name, 0, false);
func_decl* f = mk_decl_or_ho_const(first_name, 0, is_boolean);
first = expr_ref(m.mk_const(f), m);
}
while (accept(token_kind::comma))
parse_formula(); // consume remaining elements
parse_formula(is_boolean); // consume remaining elements
expect(token_kind::rbrack, "']'");
return first;
}
@ -1587,9 +1632,9 @@ class tptp_parser {
// We already consumed '[', so parse as tuple inline
if (accept(token_kind::rbrack))
return expr_ref(m.mk_const(symbol("$nil"), m_univ), m);
expr_ref first = parse_formula();
expr_ref first = parse_formula(is_boolean);
while (accept(token_kind::comma))
parse_formula(); // consume remaining elements
parse_formula(is_boolean); // consume remaining elements
expect(token_kind::rbrack, "']'");
return first;
}
@ -1605,7 +1650,7 @@ class tptp_parser {
next();
}
expect(token_kind::gt_tok, "'>'");
expr_ref sub = parse_unary_formula();
expr_ref sub = parse_unary_formula(is_boolean);
func_decl* f = mk_modal_op(mod_name);
return expr_ref(m.mk_app(f, sub.get()), m);
}
@ -1652,7 +1697,13 @@ class tptp_parser {
}
expect(token_kind::colon, "':'");
m_bound.push_back(scope);
expr_ref body = parse_formula();
// A TPTP quantifier body is a <unit_formula>: the quantifier binds
// tighter than the binary connectives & | => <= <=> <~> ~| ~&. Parse
// at equality precedence so the body absorbs an infix =/!= but stops
// at any lower-precedence connective, which stays in the enclosing
// expression. E.g. "! [X] : p(X) & q(X)" is "(! [X] : p(X)) & q(X)",
// and "! [X] : (...) => g" keeps "=> g" outside the quantifier scope.
expr_ref body = parse_expr(PREC_EQ, true, is_boolean);
m_bound.pop_back();
return mk_quantifier(is_forall, vars, body);
}
@ -1672,10 +1723,10 @@ class tptp_parser {
expect(token_kind::rbrack, "']'");
}
expect(token_kind::colon, "':'");
return parse_formula();
return parse_formula(is_boolean);
}
return parse_atomic_formula();
return parse_atomic_formula(is_boolean);
}
// Grammar: <tff_binary_formula> ::= <tff_binary_nonassoc> | <tff_binary_assoc>
@ -1687,8 +1738,15 @@ class tptp_parser {
// <tff_and_formula> ::= <tff_unit_formula> & <tff_unit_formula>
// | <tff_and_formula> & <tff_unit_formula>
// Implements a Pratt-style (precedence climbing) parser for binary connectives.
expr_ref parse_expr(unsigned min_prec, bool consume_at = true) {
expr_ref e = parse_unary_formula();
expr_ref parse_expr(unsigned min_prec, bool consume_at, bool is_boolean) {
expr_ref e = parse_unary_formula(is_boolean);
return parse_binary_rest(e, min_prec, consume_at);
}
// Precedence-climbing loop continued from an already-parsed left operand `e`.
// Split out from parse_expr so callers that have consumed a leading unary unit
// (e.g. a '~' immediately after '(') can resume binary-connective parsing.
expr_ref parse_binary_rest(expr_ref e, unsigned min_prec, bool consume_at = true) {
for (;;) {
// Handle @ (function application) with highest precedence
// But NOT when we're inside a lambda body that's an @ argument
@ -1719,7 +1777,15 @@ class tptp_parser {
if (it->second.precedence < min_prec) break;
next(); // consume the operator token
unsigned next_prec = it->second.right_assoc ? it->second.precedence : it->second.precedence + 1;
expr_ref rhs = parse_expr(next_prec, consume_at);
// Operands of every connective except '='/'!=' are Boolean; only equality
// takes term operands. Derive the operand context from the operator rather
// than inheriting is_boolean from the left operand. Otherwise, once a
// term-valued equality literal (e.g. 'a = b') sets is_boolean to false, a
// predicate atom later in the same clause ('a = b | q') would be parsed as a
// term and boxed into '$box_U_to_Bool(q)', severing it from the Boolean
// predicate 'q' used elsewhere and making refutable problems satisfiable.
bool rhs_is_boolean = it->second.precedence != PREC_EQ;
expr_ref rhs = parse_expr(next_prec, consume_at, rhs_is_boolean);
expr_ref_vector args(m);
args.push_back(e);
args.push_back(rhs);
@ -1786,12 +1852,27 @@ class tptp_parser {
// Try relative to current file's directory
std::string local = normalize_path(dirname(curr_file) + "/" + name);
if (file_exists(local)) return local;
// Try TPTP environment variable (standard TPTP convention)
// Try TPTP environment variable (standard TPTP convention): includes such as
// "Axioms/MAT001^0.ax" are resolved relative to the TPTP root directory named
// by $TPTP. This is required when a problem is run from a directory that does
// not contain the Axioms/ tree (e.g. an isolated benchmark harness workspace).
char const* root = std::getenv("TPTP");
if (root) {
std::string env = normalize_path(std::string(root) + "/" + name);
if (file_exists(env)) return env;
}
// Walk up ancestor directories of the current file. TPTP include paths are
// relative to the TPTP root directory (e.g. "Axioms/BOO001-0.ax"), while the
// problem file typically lives in a subdirectory such as "Problems/BOO/".
std::string dir = dirname(curr_file);
for (;;) {
size_t idx = dir.find_last_of("/\\");
if (idx == std::string::npos) break;
dir = dir.substr(0, idx);
if (dir.empty()) break;
std::string candidate = normalize_path(dir + "/" + name);
if (file_exists(candidate)) return candidate;
}
// Try relative to current working directory (common when running from TPTP root)
std::string cwd_relative = normalize_path(name);
if (file_exists(cwd_relative)) return cwd_relative;
@ -1825,7 +1906,7 @@ class tptp_parser {
// <annotations> ::= ,<source><optional_info> | <null>
void parse_annotated() {
expect(token_kind::lparen, "'('");
parse_name();
std::string formula_name = parse_name();
expect(token_kind::comma, "','");
std::string role = to_lower(parse_name());
expect(token_kind::comma, "','");
@ -1836,12 +1917,14 @@ class tptp_parser {
else if (role == "logic") {
// Modal logic declarations ($modal == [...]) — skip the formula body
skip_annotations_until_rparen();
warning_msg("non-classical logics are not supported");
++m_dropped_formulas;
}
else {
try {
implicit_var_scope implicit_scope;
scoped_implicit_vars scoped(*this, implicit_scope);
expr_ref f = parse_formula();
expr_ref f = parse_formula(true);
if (!implicit_scope.order.empty()) {
f = mk_quantifier(true, implicit_scope.order, f);
}
@ -1854,13 +1937,30 @@ class tptp_parser {
}
m_cmd.assert_expr(f);
} catch (z3_exception const& ex) {
// Sort mismatch or other semantic error in this formula — skip it
IF_VERBOSE(2, verbose_stream() << "skipping formula due to: " << ex.what() << "\n");
// Sort mismatch or other semantic error in this formula — skip it.
// A dropped axiom/definition removes constraints from the problem, so a
// subsequent "sat" verdict is unsound: it may only hold because the
// dropped formula was missing. The count is used to downgrade a sat
// result to GaveUp rather than report a spurious CounterSatisfiable.
++m_dropped_formulas;
std::ostringstream oss;
oss << "skipping formula '" << formula_name << "' due to: " << ex.what();
warning_msg(oss.str().c_str());
// Skip to '.' to resync the parser for the next annotated formula
while (!is(token_kind::eof_tok) && !is(token_kind::dot))
next();
if (is(token_kind::dot)) next();
return;
} catch (std::exception const& ex) {
++m_dropped_formulas;
std::ostringstream oss;
oss << "skipping formula '" << formula_name << "' (role " << role << ") due to: " << ex.what() << "\n";
warning_msg(oss.str().c_str());
while (!is(token_kind::eof_tok) && !is(token_kind::dot))
next();
if (is(token_kind::dot))
next();
return;
}
}
@ -2114,6 +2214,12 @@ public:
}};
}
// Record a double-quoted string constant as a TPTP distinct object (deduplicated by name).
void register_distinct_object(std::string const& name, expr* c) {
if (m_distinct_objects.emplace(name, c).second)
m_pinned_exprs.push_back(c);
}
void parse_input(std::istream& in, std::string const& current_file) {
// Save parser state so that included files don't clobber the caller's lexer.
std::string saved_input = std::move(m_input);
@ -2123,6 +2229,7 @@ public:
std::ostringstream buf;
buf << in.rdbuf();
m_input = buf.str();
extract_expected_status(m_input);
m_lex = std::make_unique<lexer>(m_input);
next();
parse_toplevel(current_file);
@ -2149,6 +2256,78 @@ public:
}
bool has_conjecture() const { return m_has_conjecture; }
// TPTP double-quoted strings ("...") denote pairwise distinct domain elements.
// Assert a single global distinctness constraint over all collected distinct objects
// so that e.g. "Apple" != "Microsoft" is recognized as a theorem.
void assert_distinct_objects() {
if (m_distinct_objects.size() < 2) return;
expr_ref_vector objs(m);
for (auto const& kv : m_distinct_objects)
objs.push_back(kv.second);
m_cmd.assert_expr(expr_ref(m.mk_distinct(objs.size(), objs.data()), m));
}
// Number of axioms/definitions that were dropped during parsing because the
// higher-order encoding could not type-check them. When non-zero, a "sat"
// verdict cannot be trusted (the missing constraints may be exactly what
// makes the problem unsatisfiable).
unsigned dropped_formulas() const { return m_dropped_formulas; }
std::string const& expected_status() const { return m_expected_status; }
// Scan TPTP comments for an SZS/Status annotation, e.g.
// % Status : Unsatisfiable
// % SZS status Theorem
// Only the first annotation found (the top-level file's) is recorded.
void extract_expected_status(std::string const& text) {
if (!m_expected_status.empty())
return;
std::istringstream in(text);
std::string line;
while (std::getline(in, line)) {
// TPTP comment lines start with '%'.
size_t i = line.find_first_not_of(" \t");
if (i == std::string::npos || line[i] != '%')
continue;
++i; // skip '%'
// Skip leading '%' and spaces.
i = line.find_first_not_of("% \t", i);
if (i == std::string::npos)
continue;
std::string rest = line.substr(i);
std::string status;
// Form 1: "SZS status <Word>"
if (rest.compare(0, 4, "SZS ") == 0) {
size_t p = rest.find("status");
if (p == std::string::npos)
continue;
p += 6; // length of "status"
status = next_status_word(rest, p);
}
// Form 2: "Status : <Word>" / "Status : <Word>"
else if (rest.compare(0, 6, "Status") == 0) {
size_t p = rest.find(':', 6);
if (p == std::string::npos)
continue;
status = next_status_word(rest, p + 1);
}
if (!status.empty()) {
m_expected_status = status;
return;
}
}
}
static std::string next_status_word(std::string const& s, size_t p) {
size_t a = s.find_first_not_of(" \t", p);
if (a == std::string::npos)
return "";
size_t b = a;
while (b < s.size() && (isalnum((unsigned char)s[b]) || s[b] == '_'))
++b;
return s.substr(a, b - a);
}
};
expr_ref tptp_parser::parse_term() {
@ -2172,12 +2351,41 @@ expr_ref tptp_parser::parse_term() {
return e;
}
expr_ref tptp_parser::parse_formula() {
return parse_expr(PREC_IFF);
expr_ref tptp_parser::parse_formula(bool is_boolean) {
return parse_expr(PREC_IFF, true, is_boolean);
}
}
// Classify an SZS status into the coarse verdict used for cross-checking.
// unsat: a refutation/proof exists (Theorem, Unsatisfiable, ContradictoryAxioms, ...)
// sat: a model exists (Satisfiable, CounterSatisfiable, ...)
// other: no comparable verdict (Open, Unknown, Timeout, GaveUp, empty, ...)
enum class szs_verdict { unsat, sat, other };
static szs_verdict classify_szs(std::string const& s) {
if (s == "Theorem" || s == "Unsatisfiable" || s == "ContradictoryAxioms" || s == "Unsat")
return szs_verdict::unsat;
if (s == "Satisfiable" || s == "CounterSatisfiable" || s == "CounterTheorem" || s == "Sat")
return szs_verdict::sat;
return szs_verdict::other;
}
// Emit the SZS status produced by z3. If the input carries an annotated status
// that contradicts the produced verdict, prepend "BUG" to flag the mismatch.
static void report_szs_status(char const* produced, std::string const& expected) {
szs_verdict pv = classify_szs(produced);
szs_verdict ev = classify_szs(expected);
bool is_bug = !expected.empty() &&
(pv == szs_verdict::unsat || pv == szs_verdict::sat) &&
(ev == szs_verdict::unsat || ev == szs_verdict::sat) &&
pv != ev;
if (is_bug)
std::cout << "% SZS status BUG " << produced << " (expected " << expected << ")\n";
else
std::cout << "% SZS status " << produced << "\n";
}
static unsigned read_tptp_stream(std::istream& in, char const* current_file) {
register_on_timeout_proc(on_timeout);
try {
@ -2186,6 +2394,7 @@ static unsigned read_tptp_stream(std::istream& in, char const* current_file) {
tptp_parser p(ctx);
p.parse_input(in, current_file ? current_file : ".");
p.assert_distinct_objects();
// Suppress default check-sat output; TPTP frontend reports SZS status explicitly.
std::ostringstream sink;
@ -2194,12 +2403,24 @@ static unsigned read_tptp_stream(std::istream& in, char const* current_file) {
ctx.check_sat(0, nullptr);
switch (ctx.cs_state()) {
case cmd_context::css_unsat:
if (p.has_conjecture()) std::cout << "% SZS status Theorem\n";
else std::cout << "% SZS status Unsatisfiable\n";
if (p.has_conjecture()) report_szs_status("Theorem", p.expected_status());
else report_szs_status("Unsatisfiable", p.expected_status());
break;
case cmd_context::css_sat:
if (p.has_conjecture()) std::cout << "% SZS status CounterSatisfiable\n";
else std::cout << "% SZS status Satisfiable\n";
// A "sat" verdict is only sound if the whole problem was encoded. If any
// axiom/definition was dropped during parsing (e.g. an unsupported
// higher-order construct), the model may be spurious — the dropped
// constraints could rule it out. Report GaveUp instead of a misleading
// CounterSatisfiable/Satisfiable (which would otherwise be flagged BUG
// against an annotated Theorem/Unsatisfiable status).
if (p.dropped_formulas() > 0) {
std::cout << "% SZS status GaveUp\n";
std::cout << "% SZS reason " << p.dropped_formulas()
<< " formula(s) dropped during encoding; model is not certified\n";
break;
}
if (p.has_conjecture()) report_szs_status("CounterSatisfiable", p.expected_status());
else report_szs_status("Satisfiable", p.expected_status());
if (g_display_model) {
model_ref mdl;
if (ctx.is_model_available(mdl))

View file

@ -297,7 +297,7 @@ namespace lp {
void limit_j(unsigned bound_j, const mpq& u, bool coeff_before_j_is_pos, bool is_lower_bound, bool strict) {
auto* lar = &m_bp.lp();
const auto& row = this->m_row;
auto* row = &this->m_row;
auto explain = [row, bound_j, coeff_before_j_is_pos, is_lower_bound, strict, lar]() {
(void) strict;
TRACE(bound_analyzer, tout << "explain_bound_on_var_on_coeff, bound_j = " << bound_j << ", coeff_before_j_is_pos = " << coeff_before_j_is_pos << ", is_lower_bound = " << is_lower_bound << ", strict = " << strict << "\n";);
@ -305,7 +305,7 @@ namespace lp {
int j_sign = (coeff_before_j_is_pos ? 1 : -1) * bound_sign;
u_dependency* ret = nullptr;
for (auto const& r : row) {
for (auto const& r : *row) {
unsigned j = r.var();
if (j == bound_j)
continue;

View file

@ -580,7 +580,7 @@ namespace lp {
const lar_term* m_t;
undo_add_term(imp& s, const lar_term* t) : m_s(s), m_t(t) {}
void undo() {
void undo() override {
m_s.undo_add_term_method(m_t);
}
};

View file

@ -16,6 +16,9 @@ Author:
Revision History:
--*/
#include <algorithm>
#include <unordered_map>
#include "math/lp/int_solver.h"
#include "math/lp/lar_solver.h"
#include "math/lp/int_cube.h"
@ -81,32 +84,264 @@ namespace lp {
SASSERT(lp_status::OPTIMAL == lra.get_status() || lp_status::FEASIBLE == lra.get_status());
}
impq int_cube::get_cube_delta_for_term(const lar_term& t) const {
if (t.size() == 2) {
bool seen_minus = false;
bool seen_plus = false;
for(lar_term::ival p : t) {
if (!lia.column_is_int(p.j()))
goto usual_delta;
const mpq & c = p.coeff();
if (c == one_of_type<mpq>()) {
seen_plus = true;
} else if (c == -one_of_type<mpq>()) {
seen_minus = true;
} else {
goto usual_delta;
// The largest cube test of Bromberger and Weidenbach:
// maximize x_e subject to Ax + a'(x_e/2) <= b, x_e >= 0, where a'_i = ||a_i||_1,
// with the 1-norm taken over the integer variables of the row.
// The solution is the center z of a largest cube contained in the polyhedron.
// If the maximal edge length is at least 1, then the rounding of z is
// an integer solution; otherwise the rounding is checked, and possibly repaired,
// against the original constraints.
lia_move int_cube::find_largest_cube() {
lia.settings().stats().m_lcube_calls++;
TRACE(cube,
for (unsigned j = 0; j < lra.number_of_vars(); ++j)
lia.display_column(tout, j);
tout << lra.constraints();
);
lra.push();
// The edge rows are ephemeral: suppress the add-term callback,
// dioph_eq's reaction to it is not undone by pop().
auto add_term_cb = lra.m_add_term_callback;
lra.m_add_term_callback = nullptr;
unsigned x_e = lra.add_var(UINT_MAX, false); // the edge length of the cube
lra.add_var_bound(x_e, lconstraint_kind::GE, mpq(0));
bool ok = add_cube_edge_rows(x_e);
lra.m_add_term_callback = add_term_cb;
if (!ok) {
lra.pop();
lra.set_status(lp_status::OPTIMAL);
return lia_move::undef;
}
lp_status st = lra.find_feasible_solution();
if (st != lp_status::FEASIBLE && st != lp_status::OPTIMAL) {
TRACE(cube, tout << "cannot find a feasible solution";);
lra.pop();
lra.move_non_basic_columns_to_bounds();
// it can happen that we found an integer solution here
return !lra.r_basis_has_inf_int()? lia_move::sat: lia_move::undef;
}
impq e; // the maximal edge length
st = lra.maximize_term(x_e, e, /*fix_int_cols*/ false);
if (lia.settings().get_cancel_flag()) {
lra.pop();
return lia_move::undef;
}
if (st == lp_status::UNBOUNDED) {
// infinite lattice width: the polyhedron contains cubes of arbitrary edge length
lra.add_var_bound(x_e, lconstraint_kind::GE, mpq(1));
st = lra.find_feasible_solution();
if (st != lp_status::FEASIBLE && st != lp_status::OPTIMAL) {
lra.pop();
return lia_move::undef;
}
lra.pop();
return sat_after_rounding();
}
TRACE(cube, tout << "max edge length = " << e << "\n";);
if (e >= impq(mpq(1))) {
lra.pop();
return sat_after_rounding();
}
// the largest cube is smaller than the unit cube:
// the rounded center is only a candidate
lra.pop();
return round_and_repair();
}
bool int_cube::add_cube_edge_rows(unsigned x_e) {
// snapshot the term columns: add_edge_rows_for_term appends to lra.terms()
svector<unsigned> term_columns;
for (const lar_term* t : lra.terms())
term_columns.push_back(t->j());
for (unsigned j : term_columns)
if (!add_edge_rows_for_term(j, x_e)) {
TRACE(cube, tout << "cannot add the edge rows";);
return false;
}
return true;
}
// i is the column index having the term
bool int_cube::add_edge_rows_for_term(unsigned i, unsigned x_e) {
if (!lra.column_associated_with_row(i))
return true;
const lar_term& t = lra.get_term(i);
impq delta = get_cube_delta_for_term(t);
TRACE(cube, lra.print_term_as_indices(t, tout); tout << ", delta = " << delta << "\n";);
if (is_zero(delta))
return true;
if (!is_zero(delta.y))
// the infinitesimal delta does not scale with x_e: tighten statically,
// it is sound for any edge length
return lra.tighten_term_bounds_by_delta(i, delta);
if (lra.column_has_upper_bound(i)) {
impq u = lra.get_upper_bound(i); // copy: add_term invalidates bound references
vector<std::pair<mpq, unsigned>> coeffs = {{mpq(1), i}, {delta.x, x_e}};
unsigned s = lra.add_term(coeffs, UINT_MAX);
lra.add_var_bound(s, is_zero(u.y) ? lconstraint_kind::LE : lconstraint_kind::LT, u.x);
}
if (lra.column_has_lower_bound(i)) {
impq l = lra.get_lower_bound(i); // copy: add_term invalidates bound references
vector<std::pair<mpq, unsigned>> coeffs = {{mpq(1), i}, {-delta.x, x_e}};
unsigned s = lra.add_term(coeffs, UINT_MAX);
lra.add_var_bound(s, is_zero(l.y) ? lconstraint_kind::GE : lconstraint_kind::GT, l.x);
}
return true;
}
lia_move int_cube::sat_after_rounding() {
lra.round_to_integer_solution();
lra.set_status(lp_status::FEASIBLE);
SASSERT(lia.settings().get_cancel_flag() || lia.is_feasible());
TRACE(cube, tout << "largest cube success";);
lia.settings().stats().m_lcube_success++;
return lia_move::sat;
}
lia_move int_cube::round_and_repair() {
lra.backup_x(); // remember the cube center
vector<flip_candidate> flips;
for (unsigned j = 0; j < lra.column_count(); ++j) {
if (!lra.column_is_int(j) || lra.column_has_term(j))
continue;
const impq& v = lra.get_column_value(j);
if (v.is_int())
continue;
flips.push_back({j, floor(v), false});
}
lra.round_to_integer_solution();
for (auto& f : flips)
f.m_at_hi = lra.get_column_value(f.m_j).x > f.m_lo;
if (repair_rounded_candidate(flips)) {
lra.set_status(lp_status::FEASIBLE);
SASSERT(lia.settings().get_cancel_flag() || lia.is_feasible());
TRACE(cube, tout << "largest cube success";);
lia.settings().stats().m_lcube_success++;
return lia_move::sat;
}
// return to the cube center: an interior point of the polyhedron
lra.restore_x();
lra.set_status(lp_status::FEASIBLE);
return lia_move::undef;
}
// Checks the rounded center against the original constraints. On failure
// searches the vertices of the lattice cell around the center greedily:
// flip a coordinate between floor and ceiling to maximally decrease the
// total bound violation, within a budget.
bool int_cube::repair_rounded_candidate(vector<flip_candidate>& flips) {
vector<bounded_row> rows;
for (const lar_term* t : lra.terms()) {
unsigned j = t->j();
if (!lra.column_associated_with_row(j))
continue;
if (!lra.column_has_upper_bound(j) && !lra.column_has_lower_bound(j))
continue;
bounded_row r;
r.m_j = j;
r.m_val = t->apply(lra.r_x());
rows.push_back(r);
}
auto row_violation = [&](unsigned ri, const impq& v) {
impq w;
unsigned j = rows[ri].m_j;
if (lra.column_has_upper_bound(j) && v > lra.get_upper_bound(j))
w += v - lra.get_upper_bound(j);
if (lra.column_has_lower_bound(j) && v < lra.get_lower_bound(j))
w += lra.get_lower_bound(j) - v;
return w;
};
impq violation;
for (unsigned ri = 0; ri < rows.size(); ++ri)
violation += row_violation(ri, rows[ri].m_val);
if (is_zero(violation))
return true; // the rounded center fits as it is
if (flips.empty())
return false;
std::unordered_map<unsigned, unsigned> flip_of_var;
for (unsigned fi = 0; fi < flips.size(); ++fi)
flip_of_var[flips[fi].m_j] = fi;
// occurrences of the flip candidates in the bounded rows
vector<vector<std::pair<unsigned, mpq>>> occs(flips.size());
for (unsigned ri = 0; ri < rows.size(); ++ri) {
const lar_term& t = lra.get_term(rows[ri].m_j);
for (lar_term::ival p : t) {
auto it = flip_of_var.find(p.j());
if (it != flip_of_var.end())
occs[it->second].push_back({ri, p.coeff()});
}
}
unsigned budget = std::min(2 * flips.size(), lia.settings().lcube_flips());
bool flipped = false;
while (!is_zero(violation) && budget-- > 0) {
unsigned best_fi = UINT_MAX;
impq best_gain;
for (unsigned fi = 0; fi < flips.size(); ++fi) {
if (occs[fi].empty())
continue;
mpq step = flips[fi].m_at_hi ? mpq(-1) : mpq(1);
impq gain;
for (const auto& o : occs[fi]) {
const impq& v = rows[o.first].m_val;
gain += row_violation(o.first, v + impq(step * o.second)) - row_violation(o.first, v);
}
if (gain < best_gain) {
best_gain = gain;
best_fi = fi;
}
}
if (seen_minus && seen_plus)
return zero_of_type<impq>();
return impq(0, 1);
if (best_fi == UINT_MAX)
return false; // no flip decreases the violation
mpq step = flips[best_fi].m_at_hi ? mpq(-1) : mpq(1);
for (const auto& o : occs[best_fi])
rows[o.first].m_val += impq(step * o.second);
flips[best_fi].m_at_hi = !flips[best_fi].m_at_hi;
violation += best_gain;
flipped = true;
TRACE(cube, tout << "flipped column " << flips[best_fi].m_j << ", violation = " << violation << "\n";);
}
if (!is_zero(violation))
return false;
// apply the repaired candidate
for (const auto& f : flips)
lra.set_column_value(f.m_j, impq(f.m_at_hi ? f.m_lo + 1 : f.m_lo));
for (const lar_term* t : lra.terms()) {
unsigned j = t->j();
if (!lra.column_associated_with_row(j))
continue;
lra.set_column_value(j, t->apply(lra.r_x()));
}
if (flipped)
lia.settings().stats().m_lcube_flip_success++;
return true;
}
impq int_cube::get_cube_delta_for_term(const lar_term& t) const {
if (t.size() == 2) {
bool seen_minus = false, seen_plus = false, all_ok = true;
for (lar_term::ival p : t) {
if (!lia.column_is_int(p.j())) { all_ok = false; break; }
const mpq& c = p.coeff();
if (c == one_of_type<mpq>()) seen_plus = true;
else if (c == -one_of_type<mpq>()) seen_minus = true;
else { all_ok = false; break; }
}
if (all_ok) {
if (seen_minus && seen_plus)
return zero_of_type<impq>();
return impq(0, 1);
}
}
usual_delta:
mpq delta = zero_of_type<mpq>();
for (lar_term::ival p : t)
if (lia.column_is_int(p.j()))
delta += abs(p.coeff());
delta *= mpq(1, 2);
return impq(delta);
}

View file

@ -10,9 +10,15 @@ Abstract:
Cube finder
This routine attempts to find a feasible integer solution
by tightnening bounds and running an LRA solver on the
by tightnening bounds and running an LRA solver on the
tighter system.
find_largest_cube() implements the largest cube test of
Bromberger and Weidenbach (Fast Cube Tests for LIA Constraint
Solving, IJCAR 2016): a fresh variable x_e for the cube edge
length is introduced and maximized; the center of the largest
cube is rounded to a candidate integer solution.
Author:
Nikolaj Bjorner (nbjorner)
Lev Nachmanson (levnach)
@ -21,7 +27,10 @@ Revision History:
--*/
#pragma once
#include "util/vector.h"
#include "math/lp/lia_move.h"
#include "math/lp/numeric_pair.h"
#include "math/lp/lar_term.h"
namespace lp {
class int_solver;
@ -29,12 +38,30 @@ namespace lp {
class int_cube {
class int_solver& lia;
class lar_solver& lra;
// a fractional integer coordinate of the cube center:
// the candidate value is m_lo or m_lo + 1
struct flip_candidate {
unsigned m_j = 0;
mpq m_lo;
bool m_at_hi = false;
};
// a term column with at least one bound, tracked during the repair
struct bounded_row {
unsigned m_j = 0;
impq m_val;
};
bool tighten_term_for_cube(unsigned i);
bool tighten_terms_for_cube();
void find_feasible_solution();
impq get_cube_delta_for_term(const lar_term& t) const;
bool add_edge_rows_for_term(unsigned i, unsigned x_e);
bool add_cube_edge_rows(unsigned x_e);
lia_move sat_after_rounding();
lia_move round_and_repair();
bool repair_rounded_candidate(vector<flip_candidate>& flips);
public:
int_cube(int_solver& lia);
lia_move operator()();
lia_move find_largest_cube();
};
}

View file

@ -44,7 +44,11 @@ namespace lp {
dioph_eq m_dio;
int_gcd_test m_gcd;
unsigned m_initial_dio_calls_period;
unsigned m_lcube_period;
// The number of consecutive genuine dio calls that returned undef, reset on a dio
// conflict. Drives the decision to start running Gomory with dio.
unsigned m_dio_undef_in_a_row = 0;
bool column_is_int_inf(unsigned j) const {
return lra.column_is_int(j) && (!lia.value_is_int(j));
}
@ -52,7 +56,8 @@ namespace lp {
imp(int_solver& lia): lia(lia), lra(lia.lra), lrac(lia.lrac), m_hnf_cutter(lia), m_dio(lia), m_gcd(lia) {
m_hnf_cut_period = settings().hnf_cut_period();
m_initial_dio_calls_period = settings().dio_calls_period();
}
m_lcube_period = settings().m_int_find_cube_period;
}
bool has_lower(unsigned j) const {
switch (lrac.m_column_types()[j]) {
@ -176,14 +181,16 @@ namespace lp {
if (r == lia_move::conflict) {
m_dio.explain(*this->m_ex);
lia.settings().dio_calls_period() = m_initial_dio_calls_period;
lia.settings().dio_enable_gomory_cuts() = false;
m_dio_undef_in_a_row = 0;
lia.settings().stop_running_gomory_with_dio(); // dio was productive: stop running Gomory
lia.settings().set_run_gcd_test(false);
return lia_move::conflict;
}
if (r == lia_move::undef) {
lia.settings().dio_calls_period() *= 2;
if (lra.settings().dio_calls_period() >= 16) {
lia.settings().dio_enable_gomory_cuts() = true;
lia.settings().dio_calls_period() *= 2; // throttle dio scheduling on failure
++m_dio_undef_in_a_row;
if (m_dio_undef_in_a_row >= lra.settings().dio_gomory_enable_period()) {
lia.settings().start_running_gomory_with_dio(); // dio persistently unproductive: start running Gomory
lia.settings().set_run_gcd_test(true);
}
}
@ -191,26 +198,66 @@ namespace lp {
}
lp_settings& settings() { return lra.settings(); }
// Decide whether a periodic heuristic fires on this call. When
// random_hammers is enabled the gate is drawn at random with the same
// 1/period expected rate instead of a deterministic "every k-th call"
// modulus: a deterministic period can phase-lock with the search on
// some families and drown the solver in conflicts while another handler
// is starved; randomizing the gate breaks that resonance.
bool hit_period(unsigned period) {
if (period <= 1)
return true;
if (settings().random_hammers())
return settings().random_next(period) == 0;
return m_number_of_calls % period == 0;
}
bool should_find_cube() {
return m_number_of_calls % settings().m_int_find_cube_period == 0;
return hit_period(settings().m_int_find_cube_period);
}
// The largest cube test is throttled exponentially: when the polyhedron
// does not contain a large enough cube it is unlikely to contain one
// later, after more constraints are added, so each failure doubles the
// period and a success resets it.
bool should_find_lcube() {
return settings().lcube() && hit_period(m_lcube_period);
}
lia_move find_lcube() {
lia_move r = int_cube(lia).find_largest_cube();
if (r == lia_move::undef) {
if (m_lcube_period < (1u << 30))
m_lcube_period *= 2;
}
else
m_lcube_period = settings().m_int_find_cube_period;
return r;
}
bool should_gomory_cut() {
bool dio_allows_gomory = !settings().dio() || settings().dio_enable_gomory_cuts() ||
m_dio.some_terms_are_ignored();
return dio_allows_gomory && m_number_of_calls % settings().m_int_gomory_cut_period == 0;
return dio_allows_gomory && hit_period(settings().m_int_gomory_cut_period);
}
bool should_solve_dioph_eq() {
return lia.settings().dio() && (m_number_of_calls % settings().dio_calls_period() == 0);
bool ret = lia.settings().dio() && hit_period(settings().dio_calls_period());
if (!ret && lia.settings().dio_calls_period() > m_initial_dio_calls_period) {
unsigned dec = settings().dio_calls_period_decrease();
unsigned& period = lia.settings().dio_calls_period();
period = period > m_initial_dio_calls_period + dec ? period - dec : m_initial_dio_calls_period;
}
return ret;
}
// HNF
bool should_hnf_cut() {
return (!settings().dio() || settings().dio_enable_hnf_cuts())
&& settings().enable_hnf() && m_number_of_calls % settings().hnf_cut_period() == 0;
&& settings().enable_hnf() && hit_period(settings().hnf_cut_period());
}
lia_move hnf_cut() {
@ -245,11 +292,12 @@ namespace lp {
++m_number_of_calls;
if (r == lia_move::undef) r = patch_basic_columns();
if (r == lia_move::undef && should_find_cube()) r = int_cube(lia)();
if (r == lia_move::undef && should_find_cube()) r = int_cube(lia)();
if (r == lia_move::undef && should_find_lcube()) r = find_lcube();
if (r == lia_move::undef) lra.move_non_basic_columns_to_bounds();
if (r == lia_move::undef && should_hnf_cut()) r = hnf_cut();
if (r == lia_move::undef && should_gomory_cut()) r = gomory(lia).get_gomory_cuts(2);
if (r == lia_move::undef && should_solve_dioph_eq()) r = solve_dioph_eq();
if (r == lia_move::undef && should_gomory_cut()) r = gomory(lia).get_gomory_cuts(2);
if (r == lia_move::undef) r = int_branch(lia)();
if (settings().get_cancel_flag()) r = lia_move::undef;
return r;

View file

@ -864,7 +864,7 @@ namespace lp {
}
lp_status lar_solver::maximize_term(unsigned j,
impq& term_max) {
impq& term_max, bool fix_int_cols) {
TRACE(lar_solver, print_values(tout););
SASSERT(get_core_solver().m_r_solver.calc_current_x_is_feasible_include_non_basis());
lar_term term = get_term_to_maximize(j);
@ -879,6 +879,11 @@ namespace lp {
return lp_status::UNBOUNDED;
}
if (!fix_int_cols) {
set_status(lp_status::OPTIMAL);
return lp_status::OPTIMAL;
}
impq opt_val = term_max;
bool change = false;
@ -1120,8 +1125,10 @@ namespace lp {
void lar_solver::explain_fixed_column(unsigned j, explanation& ex) {
SASSERT(column_is_fixed(j));
auto* deps = get_bound_constraint_witnesses_for_column(j);
for (auto ci : flatten(deps))
const column& ul = m_imp->m_columns[j];
m_imp->m_tmp_dependencies.reset();
m_imp->m_dependencies.linearize(ul.lower_bound_witness(), ul.upper_bound_witness(), m_imp->m_tmp_dependencies);
for (auto ci : m_imp->m_tmp_dependencies)
ex.push_back(ci);
}

View file

@ -205,7 +205,9 @@ public:
set_column_value(j, v);
}
lp_status maximize_term(unsigned j_or_term, impq& term_max);
// fix_int_cols: after maximizing try to move the integer columns to integer values;
// pass false to keep the optimal (possibly fractional) vertex intact, e.g., for the largest cube test
lp_status maximize_term(unsigned j_or_term, impq& term_max, bool fix_int_cols);
core_solver_pretty_printer<lp::mpq, lp::impq> pp(std::ostream& out) const;

View file

@ -5,9 +5,15 @@ def_module_params(module_name='lp',
params=(('dio', BOOL, True, 'use Diophantine equalities'),
('dio_branching_period', UINT, 100, 'Period of calling branching on undef in Diophantine handler'),
('dio_cuts_enable_gomory', BOOL, False, 'enable Gomory cuts together with Diophantine cuts, only relevant when dioph_eq is true'),
('dio_gomory_enable_period', UINT, 16, 'number of consecutive unproductive (undef) Diophantine-handler calls after which the controller starts running Gomory cuts and the gcd test alongside dio; a dio conflict resets the count and stops them; set very large to never start them this way so Gomory follows dio_cuts_enable_gomory only'),
('dio_cuts_enable_hnf', BOOL, True, 'enable hnf cuts together with Diophantine cuts, only relevant when dioph_eq is true'),
('dio_ignore_big_nums', BOOL, True, 'Ignore the terms with big numbers in the Diophantine handler, only relevant when dioph_eq is true'),
('dio_calls_period', UINT, 1, 'Period of calling the Diophantine handler in the final_check()'),
('dio_run_gcd', BOOL, False, 'Run the GCD heuristic if dio is on, if dio is disabled the option is not used'),
('dio_calls_period_decrease', UINT, 2, 'Amount by which dio_calls_period is decreased on each final_check() call where the Diophantine handler is not triggered, until it returns to its initial value'),
('dio_run_gcd', BOOL, False, 'Run the GCD heuristic if dio is on, if dio is disabled the option is not used'),
('lcube', BOOL, True, 'use the largest cube test for integer feasibility'),
('lcube_flips', UINT, 16, 'maximal number of coordinate flips when repairing the rounded largest cube center, only relevant when lcube is true'),
('int_hammer_period', UINT, 4, 'period (in final_check calls) for the integer cut/cube heuristics (find_cube, hnf, gomory); a smaller value calls them more often'),
('random_hammers', BOOL, True, 'draw the periodic integer heuristic gates (find_cube, lcube, hnf, gomory, dio) at random with the same 1/period rate instead of a deterministic every-k-th-call modulus'),
))

View file

@ -37,11 +37,21 @@ void lp::lp_settings::updt_params(params_ref const& _p) {
auto eps = p.arith_epsilon();
m_epsilon = rational(std::max(1, (int)(100000*eps)), 100000);
m_dio = lp_p.dio();
m_dio_enable_gomory_cuts = lp_p.dio_cuts_enable_gomory();
m_dio_cuts_enable_gomory = lp_p.dio_cuts_enable_gomory();
m_dio_gomory_enable_period = lp_p.dio_gomory_enable_period();
m_dio_enable_hnf_cuts = lp_p.dio_cuts_enable_hnf();
m_dump_bound_lemmas = p.arith_dump_bound_lemmas();
m_dio_ignore_big_nums = lp_p.dio_ignore_big_nums();
m_dio_calls_period = lp_p.dio_calls_period();
m_dio_calls_period_decrease = lp_p.dio_calls_period_decrease();
m_dio_run_gcd = lp_p.dio_run_gcd();
m_random_hammers = lp_p.random_hammers();
m_lcube = lp_p.lcube();
m_lcube_flips = lp_p.lcube_flips();
unsigned hammer_period = lp_p.int_hammer_period();
SASSERT(hammer_period != 0);
m_int_find_cube_period = hammer_period;
m_int_gomory_cut_period = hammer_period;
m_hnf_cut_period = hammer_period;
m_max_conflicts = p.max_conflicts();
}

View file

@ -112,6 +112,9 @@ struct statistics {
unsigned m_gcd_conflicts = 0;
unsigned m_cube_calls = 0;
unsigned m_cube_success = 0;
unsigned m_lcube_calls = 0;
unsigned m_lcube_success = 0;
unsigned m_lcube_flip_success = 0;
unsigned m_patches = 0;
unsigned m_patches_success = 0;
unsigned m_hnf_cutter_calls = 0;
@ -152,6 +155,9 @@ struct statistics {
st.update("arith-gcd-conflict", m_gcd_conflicts);
st.update("arith-cube-calls", m_cube_calls);
st.update("arith-cube-success", m_cube_success);
st.update("arith-lcube-calls", m_lcube_calls);
st.update("arith-lcube-success", m_lcube_success);
st.update("arith-lcube-flip-success", m_lcube_flip_success);
st.update("arith-patches", m_patches);
st.update("arith-patches-success", m_patches_success);
st.update("arith-hnf-calls", m_hnf_cutter_calls);
@ -252,15 +258,27 @@ private:
bool m_print_external_var_name = false;
bool m_propagate_eqs = false;
bool m_dio = false;
bool m_dio_enable_gomory_cuts = false;
bool m_dio_cuts_enable_gomory = false;
bool m_run_gomory_with_dio = false;
unsigned m_dio_gomory_enable_period = 16;
bool m_dio_enable_hnf_cuts = true;
bool m_dump_bound_lemmas = false;
bool m_dio_ignore_big_nums = false;
unsigned m_dio_calls_period = 4;
unsigned m_dio_calls_period_decrease = 2;
bool m_dio_run_gcd = true;
bool m_random_hammers = true;
bool m_lcube = true;
unsigned m_lcube_flips = 16;
public:
bool lcube() const { return m_lcube; }
unsigned lcube_flips() const { return m_lcube_flips; }
unsigned dio_calls_period() const { return m_dio_calls_period; }
unsigned & dio_calls_period() { return m_dio_calls_period; }
unsigned dio_calls_period_decrease() const { return m_dio_calls_period_decrease; }
unsigned & dio_calls_period_decrease() { return m_dio_calls_period_decrease; }
bool random_hammers() const { return m_random_hammers; }
bool & random_hammers() { return m_random_hammers; }
bool print_external_var_name() const { return m_print_external_var_name; }
bool propagate_eqs() const { return m_propagate_eqs;}
unsigned hnf_cut_period() const { return m_hnf_cut_period; }
@ -268,8 +286,19 @@ public:
unsigned random_next() { return m_rand(); }
unsigned random_next(unsigned u ) { return m_rand(u); }
bool dio() { return m_dio; }
bool & dio_enable_gomory_cuts() { return m_dio_enable_gomory_cuts; }
bool dio_enable_gomory_cuts() const { return m_dio && m_dio_enable_gomory_cuts; }
// Static config: did the user request Gomory cuts up front? (lp.dio_cuts_enable_gomory)
bool dio_cuts_enable_gomory() const { return m_dio_cuts_enable_gomory; }
// dio_calls_period at which the Diophantine back-off starts running Gomory (lp.dio_gomory_enable_period)
unsigned dio_gomory_enable_period() const { return m_dio_gomory_enable_period; }
// Runtime flag owned by the Diophantine controller, kept separate from the static
// config above so toggling it never clobbers the user's parameter: once dio has
// backed off enough it starts running Gomory cuts alongside dio, and a productive
// dio conflict stops them again.
void start_running_gomory_with_dio() { m_run_gomory_with_dio = true; }
void stop_running_gomory_with_dio() { m_run_gomory_with_dio = false; }
// Effective state read by should_gomory_cut(): allowed if either the user enabled it
// statically or the dio controller started running it, guarded by dio being active.
bool dio_enable_gomory_cuts() const { return m_dio && (m_dio_cuts_enable_gomory || m_run_gomory_with_dio); }
bool dio_run_gcd() const { return m_dio && m_dio_run_gcd; }
bool dio_enable_hnf_cuts() const { return m_dio && m_dio_enable_hnf_cuts; }
bool dio_ignore_big_nums() const { return m_dio_ignore_big_nums; }

View file

@ -969,7 +969,6 @@ lbool solver::check_assignment() {
catch (z3_exception &) {
statistics &st = m_imp->m_nla_core.lp_settings().stats().m_st;
m_imp->m_nlsat->collect_statistics(st);
IF_VERBOSE(0, verbose_stream() << "check-assignment\n");
if (m_imp->m_limit.is_canceled()) {
return l_undef;
}

View file

@ -60,6 +60,14 @@ std::ostream& operator<<(std::ostream& out, const row_strip<T>& r) {
return out << "\n";
}
// Below, static_matrix has a superclass when Z3DEBUG is set, and some
// methods are overrides in that case.
#ifdef Z3DEBUG
#define DEBUG_OVERRIDE override
#else
#define DEBUG_OVERRIDE
#endif
// each assignment for this matrix should be issued only once!!!
template <typename T, typename X>
class static_matrix
@ -119,9 +127,13 @@ public:
void init_empty_matrix(unsigned m, unsigned n);
unsigned row_count() const { return static_cast<unsigned>(m_rows.size()); }
unsigned row_count() const DEBUG_OVERRIDE {
return static_cast<unsigned>(m_rows.size());
}
unsigned column_count() const { return static_cast<unsigned>(m_columns.size()); }
unsigned column_count() const DEBUG_OVERRIDE {
return static_cast<unsigned>(m_columns.size());
}
unsigned lowest_row_in_column(unsigned col);
@ -197,7 +209,7 @@ public:
void cross_out_row_from_column(unsigned col, unsigned k);
T get_elem(unsigned i, unsigned j) const;
T get_elem(unsigned i, unsigned j) const DEBUG_OVERRIDE;
unsigned number_of_non_zeroes_in_column(unsigned j) const { return static_cast<unsigned>(m_columns[j].size()); }
@ -218,8 +230,8 @@ public:
#ifdef Z3DEBUG
unsigned get_number_of_rows() const { return row_count(); }
unsigned get_number_of_columns() const { return column_count(); }
virtual void set_number_of_rows(unsigned /*m*/) { }
virtual void set_number_of_columns(unsigned /*n*/) { }
void set_number_of_rows(unsigned /*m*/) override { }
void set_number_of_columns(unsigned /*n*/) override { }
#endif
T get_balance() const;

View file

@ -22,6 +22,7 @@ Notes:
#include "util/mpbqi.h"
#include "util/timeit.h"
#include "util/common_msgs.h"
#include "util/index_sort_with_mutations.h"
#include "math/polynomial/algebraic_numbers.h"
#include "math/polynomial/upolynomial.h"
#include "math/polynomial/sexpr2upolynomial.h"
@ -593,10 +594,57 @@ namespace algebraic_numbers {
}
}
// Sort an index permutation with a bounds-safe, mutation-aware merge
// sort. The comparator (compare/lt) is NOT pure: it MUTATES the
// algebraic numbers it compares (refining their isolating intervals) and
// may throw on the resource limit, so std::sort would be undefined
// behavior here. See util/index_sort_with_mutations.h for the rationale.
void merge_sort_roots_perm(numeral_vector & r, unsigned_vector & perm) {
unsigned n = perm.size();
if (n < 2)
return;
unsigned_vector scratch;
scratch.resize(n, 0);
// Strict, total, stable index comparator: decided sign first, then index
// tiebreak (covers the equal/limit case so the order stays deterministic).
auto idx_lt = [&](unsigned x, unsigned y) {
::sign s = compare(r[x], r[y]);
return s != sign_zero ? s == sign_neg : x < y;
};
stable_index_merge_sort(perm.data(), scratch.data(), n, idx_lt);
}
void sort_roots(numeral_vector & r) {
if (m_limit.inc()) {
// DEBUG_CODE(check_transitivity(r););
std::sort(r.begin(), r.end(), lt_proc(m_wrapper));
if (!m_limit.inc())
return;
// DEBUG_CODE(check_transitivity(r););
unsigned n = r.size();
if (n < 2)
return;
unsigned_vector perm;
perm.resize(n, 0);
for (unsigned i = 0; i < n; ++i)
perm[i] = i;
merge_sort_roots_perm(r, perm);
// Apply the permutation in place via swap cycles. anum swap is a cheap
// pointer swap (move nulls the source), so this is O(n) cheap moves.
unsigned_vector pos; // pos[v] = current position of element v
pos.resize(n, 0);
unsigned_vector at; // at[p] = element currently at position p
at.resize(n, 0);
for (unsigned i = 0; i < n; ++i) {
pos[i] = i;
at[i] = i;
}
for (unsigned target = 0; target < n; ++target) {
unsigned want = perm[target]; // element that should end up at target
unsigned cur = pos[want]; // where it currently is
if (cur == target)
continue;
unsigned other = at[target]; // element currently at target
std::swap(r[target], r[cur]);
at[target] = want; at[cur] = other;
pos[want] = target; pos[other] = cur;
}
}

View file

@ -97,7 +97,7 @@ expr * datatype_factory::get_almost_fresh_value(sort * s) {
unsigned num = constructor->get_arity();
for (unsigned i = 0; i < num; ++i) {
sort * s_arg = constructor->get_domain(i);
if (!found_fresh_arg && (!m_util.is_datatype(s_arg) || !m_util.are_siblings(s, s_arg))) {
if (!found_fresh_arg && (!m_util.is_datatype(s_arg) || !m_util.are_siblings(s, s_arg) || !m_util.is_recursive(s_arg))) {
expr * new_arg = m_model.get_fresh_value(s_arg);
if (new_arg != nullptr) {
found_fresh_arg = true;
@ -105,7 +105,7 @@ expr * datatype_factory::get_almost_fresh_value(sort * s) {
continue;
}
}
if (!found_fresh_arg && m_util.is_datatype(s_arg) && m_util.are_siblings(s, s_arg)) {
if (!found_fresh_arg && m_util.is_datatype(s_arg) && m_util.are_siblings(s, s_arg) && m_util.is_recursive(s_arg)) {
recursive = true;
expr * last_fresh = get_last_fresh_value(s_arg);
args.push_back(last_fresh);

View file

@ -231,10 +231,8 @@ void func_interp::insert_new_entry(expr * const * args, expr * r) {
m_args_are_values = false;
m_entries.push_back(new_entry);
if (!m_entry_table && m_entries.size() > 500) {
m_entry_table = alloc(entry_table, 1024,
func_entry_hash(m_arity), func_entry_eq(m_arity));
for (func_entry* curr : m_entries)
m_entry_table->insert(curr);
init_table();
ptr_vector<expr> null_args;
null_args.resize(m_arity, nullptr);
m_key = func_entry::mk(m(), m_arity, null_args.data(), nullptr);
@ -243,6 +241,12 @@ void func_interp::insert_new_entry(expr * const * args, expr * r) {
m_entry_table->insert(new_entry);
}
void func_interp::init_table() {
m_entry_table = alloc(entry_table, 1024, func_entry_hash(m_arity), func_entry_eq(m_arity));
for (func_entry *curr : m_entries)
m_entry_table->insert(curr);
}
void func_interp::del_entry(unsigned idx) {
auto* e = m_entries[idx];
if (m_entry_table)
@ -307,6 +311,12 @@ void func_interp::compress() {
if (j < m_entries.size()) {
reset_interp_cache();
m_entries.shrink(j);
if (m_entry_table) {
dealloc(m_entry_table);
m_entry_table = nullptr;
if (m_entries.size() > 500)
init_table();
}
}
// other compression, if else is a default branch.
// or function encode identity.

View file

@ -40,7 +40,7 @@ class func_entry {
// m_result and m_args[i] must be ground terms.
expr * m_result;
expr * m_args[];
expr * m_args[0];
static unsigned get_obj_size(unsigned arity) { return sizeof(func_entry) + arity * sizeof(expr*); }
func_entry(ast_manager & m, unsigned arity, expr * const * args, expr * result);
@ -104,6 +104,8 @@ class func_interp {
void reset_interp_cache();
void init_table();
expr * get_interp_core() const;
expr_ref get_array_interp_core(func_decl * f) const;

View file

@ -513,7 +513,7 @@ void non_auf_macro_solver::collect_candidates(ptr_vector<quantifier> const& qs,
TRACE(model_finder, tout << "considering macro for: " << f->get_name() << "\n";
m->display(tout); tout << "\n";);
if (m->is_unconditional() && (!qi->is_auf() || m->get_weight() >= m_mbqi_force_template)) {
full_macros.insert(f, std::make_pair(m, q));
full_macros.insert(f, {m, q});
cond_macros.erase(f);
}
else if (!full_macros.contains(f) && !qi->is_auf())
@ -524,10 +524,8 @@ void non_auf_macro_solver::collect_candidates(ptr_vector<quantifier> const& qs,
}
void non_auf_macro_solver::process_full_macros(obj_map<func_decl, mq_pair> const& full_macros, obj_hashtable<quantifier>& removed) {
for (auto const& kv : full_macros) {
func_decl* f = kv.m_key;
cond_macro* m = kv.m_value.first;
quantifier* q = kv.m_value.second;
for (auto const &[f, v] : full_macros) {
auto [m, q] = v;
SASSERT(m->is_unconditional());
if (add_macro(f, m->get_def())) {
get_qinfo(q)->set_the_one(f);

View file

@ -5,6 +5,7 @@
#include "math/polynomial/polynomial.h"
#include "nlsat_common.h"
#include "util/vector.h"
#include "util/index_sort_with_mutations.h"
#include "util/trace.h"
#include <algorithm>
@ -956,6 +957,22 @@ namespace nlsat {
return m_pm.id(a.ire.p) < m_pm.id(b.ire.p);
}
// Sort an index permutation with a bounds-safe, mutation-aware merge
// sort. The comparator (root_function_lt / anum_manager::lt -> compare)
// is NOT pure: it MUTATES the algebraic numbers it compares by refining
// their isolating intervals, and may throw on the resource limit, so a
// single std::sort would be undefined behavior and can crash via an
// out-of-bounds read on timeout. See util/index_sort_with_mutations.h
// for the full rationale.
template<typename Less>
void merge_sort_perm(std_vector<unsigned>& perm, Less less) {
unsigned n = static_cast<unsigned>(perm.size());
if (n < 2)
return;
std_vector<unsigned> scratch(n);
stable_index_merge_sort(perm.data(), scratch.data(), n, less);
}
// Apply a permutation to a range of root_functions using swap cycles,
// avoiding the bulk anum allocations that std::sort's move operations cause.
void apply_permutation(std_vector<root_function>& rfs, unsigned offset, std_vector<unsigned> const& perm) {
@ -982,7 +999,7 @@ namespace nlsat {
if (mid_pos > 1) {
std_vector<unsigned> perm(mid_pos);
std::iota(perm.begin(), perm.end(), 0u);
std::sort(perm.begin(), perm.end(), [&](unsigned a, unsigned b) {
merge_sort_perm(perm, [&](unsigned a, unsigned b) {
return root_function_lt(rfs[a], rfs[b], true);
});
apply_permutation(rfs, 0, perm);
@ -993,7 +1010,7 @@ namespace nlsat {
if (upper_sz > 1) {
std_vector<unsigned> perm(upper_sz);
std::iota(perm.begin(), perm.end(), 0u);
std::sort(perm.begin(), perm.end(), [&](unsigned a, unsigned b) {
merge_sort_perm(perm, [&](unsigned a, unsigned b) {
return root_function_lt(rfs[mid_pos + a], rfs[mid_pos + b], false);
});
apply_permutation(rfs, mid_pos, perm);
@ -1192,20 +1209,29 @@ namespace nlsat {
if (root_vals.size() < 2)
return;
std::sort(root_vals.begin(), root_vals.end(), [&](auto const& a, auto const& b) {
return m_am.lt(a.first, b.first);
// Sort root values by an index permutation with the bounds-safe,
// mutation-aware merge sort (see merge_sort_perm). As in
// sort_root_function_partitions, the comparator (anum_manager::lt ->
// compare) MUTATES the algebraic numbers it compares (it refines their
// isolating intervals and may hit the resource limit and throw), so it is
// not a fixed strict weak ordering over a single sort; std::sort here would
// be undefined behavior and crash via an out-of-bounds read on timeout.
std_vector<unsigned> perm(root_vals.size());
std::iota(perm.begin(), perm.end(), 0u);
merge_sort_perm(perm, [&](unsigned a, unsigned b) {
return m_am.lt(root_vals[a].first, root_vals[b].first);
});
TRACE(lws,
tout << " Sorted roots:\n";
for (unsigned j = 0; j < root_vals.size(); ++j)
m_pm.display(m_am.display_decimal(tout << " [" << j << "] val=", root_vals[j].first, 5) << " poly=", root_vals[j].second) << "\n";
for (unsigned j = 0; j < perm.size(); ++j)
m_pm.display(m_am.display_decimal(tout << " [" << j << "] val=", root_vals[perm[j]].first, 5) << " poly=", root_vals[perm[j]].second) << "\n";
);
std::set<std::pair<poly*, poly*>> added_pairs;
for (unsigned j = 0; j + 1 < root_vals.size(); ++j) {
poly* p1 = root_vals[j].second;
poly* p2 = root_vals[j + 1].second;
for (unsigned j = 0; j + 1 < perm.size(); ++j) {
poly* p1 = root_vals[perm[j]].second;
poly* p2 = root_vals[perm[j + 1]].second;
if (!p1 || !p2 || p1 == p2)
continue;
if (p1 > p2) std::swap(p1, p2);

View file

@ -261,7 +261,7 @@ namespace nlsat {
new_set->m_full = full;
new_set->m_ref_count = 0;
new_set->m_num_intervals = sz;
memcpy(new_set->m_intervals, buf.data(), sizeof(interval)*sz);
memcpy(static_cast<void*>(new_set->m_intervals), static_cast<const void*>(buf.data()), sizeof(interval)*sz);
return new_set;
}

View file

@ -28,7 +28,6 @@ z3_add_component(params
seq_rewriter_params.pyg
sls_params.pyg
smt_params_helper.pyg
smt_parallel_params.pyg
solver_params.pyg
tactic_params.pyg
EXTRA_REGISTER_MODULE_HEADERS

View file

@ -1,12 +0,0 @@
def_module_params('smt_parallel',
export=True,
description='Experimental parameters for parallel solving',
params=(
('inprocessing', BOOL, False, 'integrate in-processing as a heuristic simplification'),
('sls', BOOL, False, 'add sls-tactic as a separate worker thread outside the search tree parallelism'),
('num_global_bb_fl_threads', UINT, 0, 'run failed-literal backbone worker threads; default is 0 (off), supported values are 1 (negative mode only) or 2 (negative and positive mode)'),
('num_global_bb_batch_threads', UINT, 0, 'run Janota-style chunking backbone worker threads; default is 0 (off), supported values are 1 (negative mode only) or 2 (negative and positive mode)'),
('local_backbones', BOOL, False, 'enable local backbones experiment within the search tree parallelism'),
('core_minimize', BOOL, True, 'minimize unsat cores used for parallel cube backtracking'),
('ablate_backtracking', BOOL, False, 'ablation: pass entire cube as core instead of unsat core during backtracking'),
))

View file

@ -2467,6 +2467,7 @@ private:
cur_vals[i] = var_lbs[i];
var_subst vs(m, false);
inv_var_shifter shift(m);
expr_ref_vector disjuncts(m);
while (true) {
@ -2479,6 +2480,7 @@ private:
for (expr* p : payload) {
expr_ref inst(m);
inst = vs(p, subst_map.size(), subst_map.data());
shift(inst, num_decls, inst);
inst_conjs.push_back(inst);
}
expr_ref inst_body(m);

View file

@ -1290,6 +1290,15 @@ namespace qe {
TRACE(qe, tout << fml << "\n";);
// qe/qe2 over a quantifier-free formula has nothing to eliminate.
// Under check-sat semantics the free variables are implicitly
// existentially quantified, so decide satisfiability directly
// instead of leaving an undecided residual goal (which would be
// reported as 'unknown').
flet<qsat_mode> _mode(m_mode,
(m_mode == qsat_qe || m_mode == qsat_qe_rec) && !has_quantifiers(fml)
? qsat_sat : m_mode);
if (m_mode == qsat_qe_rec) {
fml = elim_rec(fml);
in->reset();

View file

@ -132,6 +132,8 @@ namespace sat {
m_best_phase.reset();
m_phase.reset();
m_prev_phase.reset();
m_phase_birthdate.reset();
m_best_phase_birthdate.reset();
m_assigned_since_gc.reset();
m_last_conflict.reset();
m_last_propagation.reset();
@ -161,6 +163,8 @@ namespace sat {
m_phase[v] = src.m_phase[v];
m_best_phase[v] = src.m_best_phase[v];
m_prev_phase[v] = src.m_prev_phase[v];
m_phase_birthdate[v] = src.m_phase_birthdate[v];
m_best_phase_birthdate[v] = src.m_best_phase_birthdate[v];
// inherit activity:
m_activity[v] = src.m_activity[v];
@ -267,6 +271,8 @@ namespace sat {
m_phase[v] = false;
m_best_phase[v] = false;
m_prev_phase[v] = false;
m_phase_birthdate[v] = 0;
m_best_phase_birthdate[v] = 0;
m_assigned_since_gc[v] = false;
m_last_conflict[v] = 0;
m_last_propagation[v] = 0;
@ -308,6 +314,8 @@ namespace sat {
m_phase.push_back(false);
m_best_phase.push_back(false);
m_prev_phase.push_back(false);
m_phase_birthdate.push_back(0);
m_best_phase_birthdate.push_back(0);
m_assigned_since_gc.push_back(false);
m_last_conflict.push_back(0);
m_last_propagation.push_back(0);
@ -645,6 +653,26 @@ namespace sat {
return 3*cls_allocator().get_allocation_size()/2 + memory::get_allocation_size() > memory::get_max_memory_size();
}
void solver::set_phase(literal l) {
if (l.var() >= num_vars())
return;
bool value = !l.sign();
set_phase(l.var(), value);
set_best_phase(l.var(), value);
}
void solver::set_phase(bool_var v, bool value) {
if (m_phase[v] != value)
m_phase_birthdate[v] = m_stats.m_conflicts;
m_phase[v] = value;
}
void solver::set_best_phase(bool_var v, bool value) {
if (m_best_phase[v] != value)
m_best_phase_birthdate[v] = m_stats.m_conflicts;
m_best_phase[v] = value;
}
struct solver::cmp_activity {
solver& s;
cmp_activity(solver& s):s(s) {}
@ -896,7 +924,7 @@ namespace sat {
m_assignment[(~l).index()] = l_false;
bool_var v = l.var();
m_justification[v] = j;
m_phase[v] = !l.sign();
set_phase(v, !l.sign());
m_assigned_since_gc[v] = true;
m_trail.push_back(l);
@ -904,17 +932,17 @@ namespace sat {
case BH_VSIDS:
break;
case BH_CHB:
m_last_propagation[v] = m_stats.m_conflict;
m_last_propagation[v] = m_stats.m_conflicts;
break;
}
if (m_config.m_anti_exploration) {
uint64_t age = m_stats.m_conflict - m_canceled[v];
uint64_t age = m_stats.m_conflicts - m_canceled[v];
if (age > 0) {
double decay = pow(0.95, static_cast<double>(age));
set_activity(v, static_cast<unsigned>(m_activity[v] * decay));
// NB. MapleSAT does not update canceled.
m_canceled[v] = m_stats.m_conflict;
m_canceled[v] = m_stats.m_conflicts;
}
}
@ -1378,8 +1406,10 @@ namespace sat {
lbool r = m_local_search->check(_lits.size(), _lits.data(), nullptr);
auto const& mdl = m_local_search->get_model();
if (mdl.size() == m_best_phase.size()) {
for (unsigned i = 0; i < m_best_phase.size(); ++i)
m_best_phase[i] = l_true == mdl[i];
for (unsigned i = 0; i < m_best_phase.size(); ++i) {
bool is_true = l_true == mdl[i];
set_best_phase(i, is_true);
}
if (r == l_true) {
m_conflicts_since_restart = 0;
@ -1671,12 +1701,12 @@ namespace sat {
while (!m_case_split_queue.empty()) {
if (m_config.m_anti_exploration) {
next = m_case_split_queue.min_var();
auto age = m_stats.m_conflict - m_canceled[next];
auto age = m_stats.m_conflicts - m_canceled[next];
while (age > 0) {
set_activity(next, static_cast<unsigned>(m_activity[next] * pow(0.95, static_cast<double>(age))));
m_canceled[next] = m_stats.m_conflict;
m_canceled[next] = m_stats.m_conflicts;
next = m_case_split_queue.min_var();
age = m_stats.m_conflict - m_canceled[next];
age = m_stats.m_conflicts - m_canceled[next];
}
}
next = m_case_split_queue.next_var();
@ -1714,6 +1744,25 @@ namespace sat {
}
}
void solver::get_backbone_candidates(literal_vector& lits, unsigned max_num) {
struct candidate {
literal lit;
uint64_t age;
};
svector<candidate> cands;
uint64_t now = m_stats.m_conflicts;
for (bool_var v = 0; v < num_vars(); ++v) {
if (value(v) != l_undef || was_eliminated(v))
continue;
bool is_pos = guess(v);
cands.push_back({ literal(v, !is_pos), now - get_phase_birthdate(v) });
}
std::stable_sort(cands.begin(), cands.end(),
[](candidate const& a, candidate const& b) { return a.age > b.age; });
for (unsigned i = 0; i < cands.size() && i < max_num; ++i)
lits.push_back(cands[i].lit);
}
bool solver::decide() {
bool_var next;
lbool phase = l_undef;
@ -2145,8 +2194,9 @@ namespace sat {
for (bool_var v = 0; v < num; ++v) {
if (!was_eliminated(v)) {
m_model[v] = value(v);
m_phase[v] = value(v) == l_true;
m_best_phase[v] = value(v) == l_true;
bool is_true = value(v) == l_true;
set_phase(v, is_true);
set_best_phase(v, is_true);
}
}
TRACE(sat_mc_bug, m_mc.display(tout););
@ -2274,7 +2324,7 @@ namespace sat {
m_restart_logs++;
std::stringstream strm;
strm << "(sat.stats " << std::setw(6) << m_stats.m_conflict << " "
strm << "(sat.stats " << std::setw(6) << m_stats.m_conflicts << " "
<< std::setw(6) << m_stats.m_decision << " "
<< std::setw(4) << m_stats.m_restart
<< mk_stat(*this)
@ -2432,7 +2482,7 @@ namespace sat {
m_conflicts_since_init++;
m_conflicts_since_restart++;
m_conflicts_since_gc++;
m_stats.m_conflict++;
m_stats.m_conflicts++;
if (m_step_size > m_config.m_step_size_min)
m_step_size -= m_config.m_step_size_dec;
@ -2564,7 +2614,7 @@ namespace sat {
tout << "missed " << lit << "@" << lvl(lit) << "\n";);
CTRACE(sat, idx == 0, display(tout););
if (idx == 0)
IF_VERBOSE(0, verbose_stream() << "num-conflicts: " << m_stats.m_conflict << "\n");
IF_VERBOSE(0, verbose_stream() << "num-conflicts: " << m_stats.m_conflicts << "\n");
VERIFY(idx > 0);
idx--;
}
@ -2874,7 +2924,7 @@ namespace sat {
inc_activity(var);
break;
case BH_CHB:
m_last_conflict[var] = m_stats.m_conflict;
m_last_conflict[var] = m_stats.m_conflicts;
break;
default:
break;
@ -2915,14 +2965,15 @@ namespace sat {
for (unsigned i = head; i < sz; ++i) {
bool_var v = m_trail[i].var();
TRACE(forget_phase, tout << "forgetting phase of v" << v << "\n";);
m_phase[v] = m_rand() % 2 == 0;
bool value = m_rand() % 2 == 0;
set_phase(v, value);
}
if (is_sat_phase() && head >= m_best_phase_size) {
m_best_phase_size = head;
IF_VERBOSE(12, verbose_stream() << "sticky trail: " << head << "\n");
for (unsigned i = 0; i < head; ++i) {
bool_var v = m_trail[i].var();
m_best_phase[v] = m_phase[v];
set_best_phase(v, m_phase[v]);
}
set_has_new_best_phase(true);
}
@ -2971,23 +3022,30 @@ namespace sat {
void solver::do_rephase() {
switch (m_config.m_phase) {
case PS_ALWAYS_TRUE:
for (auto& p : m_phase) p = true;
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, true);
break;
case PS_ALWAYS_FALSE:
for (auto& p : m_phase) p = false;
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, false);
break;
case PS_FROZEN:
break;
case PS_BASIC_CACHING:
switch (m_rephase.count % 4) {
case 0:
for (auto& p : m_phase) p = (m_rand() % 2) == 0;
for (unsigned i = 0; i < m_phase.size(); ++i) {
bool value = (m_rand() % 2) == 0;
set_phase(i, value);
}
break;
case 1:
for (auto& p : m_phase) p = false;
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, false);
break;
case 2:
for (auto& p : m_phase) p = !p;
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, !m_phase[i]);
break;
default:
break;
@ -2995,18 +3053,21 @@ namespace sat {
break;
case PS_SAT_CACHING:
if (m_search_state == s_sat)
for (unsigned i = 0; i < m_phase.size(); ++i)
m_phase[i] = m_best_phase[i];
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, m_best_phase[i]);
break;
case PS_RANDOM:
for (auto& p : m_phase) p = (m_rand() % 2) == 0;
for (unsigned i = 0; i < m_phase.size(); ++i) {
bool value = (m_rand() % 2) == 0;
set_phase(i, value);
}
break;
case PS_LOCAL_SEARCH:
if (m_search_state == s_sat) {
if (m_rand() % 2 == 0)
bounded_local_search();
for (unsigned i = 0; i < m_phase.size(); ++i)
m_phase[i] = m_best_phase[i];
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, m_best_phase[i]);
}
break;
@ -3601,6 +3662,8 @@ namespace sat {
m_phase.shrink(v);
m_best_phase.shrink(v);
m_prev_phase.shrink(v);
m_phase_birthdate.shrink(v);
m_best_phase_birthdate.shrink(v);
m_assigned_since_gc.shrink(v);
m_simplifier.reset_todos();
}
@ -3644,7 +3707,7 @@ namespace sat {
SASSERT(value(v) == l_undef);
m_case_split_queue.unassign_var_eh(v);
if (m_config.m_anti_exploration) {
m_canceled[v] = m_stats.m_conflict;
m_canceled[v] = m_stats.m_conflicts;
}
}
m_trail.shrink(old_sz);
@ -3812,7 +3875,7 @@ namespace sat {
double multiplier = m_config.m_reward_offset * (is_sat ? m_config.m_reward_multiplier : 1.0);
for (unsigned i = qhead; i < m_trail.size(); ++i) {
auto v = m_trail[i].var();
auto d = m_stats.m_conflict - m_last_conflict[v] + 1;
auto d = m_stats.m_conflicts - m_last_conflict[v] + 1;
if (d == 0) d = 1;
auto reward = multiplier / d;
auto activity = m_activity[v];
@ -4745,7 +4808,7 @@ namespace sat {
st.update("sat mk var", m_mk_var);
st.update("sat gc clause", m_gc_clause);
st.update("sat del clause", m_del_clause);
st.update("sat conflicts", m_conflict);
st.update("sat conflicts", m_conflicts);
st.update("sat decisions", m_decision);
st.update("sat propagations 2ary", m_bin_propagate);
st.update("sat propagations 3ary", m_ter_propagate);

View file

@ -60,7 +60,7 @@ namespace sat {
unsigned m_mk_bin_clause;
unsigned m_mk_ter_clause;
unsigned m_mk_clause;
unsigned m_conflict;
unsigned m_conflicts;
unsigned m_propagate;
unsigned m_bin_propagate;
unsigned m_ter_propagate;
@ -148,6 +148,8 @@ namespace sat {
bool_vector m_phase;
bool_vector m_best_phase;
bool_vector m_prev_phase;
svector<uint64_t> m_phase_birthdate;
svector<uint64_t> m_best_phase_birthdate;
bool m_new_best_phase = false;
svector<char> m_assigned_since_gc;
search_state m_search_state;
@ -373,12 +375,18 @@ namespace sat {
bool was_eliminated(bool_var v) const { return m_eliminated[v]; }
void set_eliminated(bool_var v, bool f) override;
bool was_eliminated(literal l) const { return was_eliminated(l.var()); }
void set_phase(literal l) override { if (l.var() < num_vars()) m_best_phase[l.var()] = m_phase[l.var()] = !l.sign(); }
void set_phase(literal l) override;
void set_phase(bool_var v, bool value);
void set_best_phase(bool_var v, bool value);
bool get_phase(bool_var b) { return m_phase.get(b, false); }
bool get_best_phase(bool_var b) { return m_best_phase.get(b, false); }
uint64_t get_phase_birthdate(bool_var b) const { return m_phase_birthdate.get(b, 0); }
uint64_t get_best_phase_birthdate(bool_var b) const { return m_best_phase_birthdate.get(b, 0); }
void set_has_new_best_phase(bool b) { m_new_best_phase = b; }
bool has_new_best_phase() const { return m_new_best_phase; }
void move_to_front(bool_var b);
unsigned get_activity(bool_var v) const { return m_activity[v]; }
void get_backbone_candidates(literal_vector& lits, unsigned max_num);
unsigned scope_lvl() const { return m_scope_lvl; }
unsigned search_lvl() const { return m_search_lvl; }
bool at_search_lvl() const { return m_scope_lvl == m_search_lvl; }
@ -440,6 +448,8 @@ namespace sat {
void set_par(parallel* p, unsigned id);
bool canceled() { return !m_rlimit.inc(); }
config const& get_config() const { return m_config; }
void set_max_conflicts(unsigned n) { m_config.m_max_conflicts = n; }
unsigned get_max_conflicts() const { return m_config.m_max_conflicts; }
void set_drat(bool d) { m_config.m_drat = d; }
drat& get_drat() { return m_drat; }
drat* get_drat_ptr() { return &m_drat; }

View file

@ -27,7 +27,6 @@ Notes:
#include "solver/tactic2solver.h"
#include "solver/parallel_params.hpp"
#include "solver/parallel_tactical.h"
#include "solver/parallel_tactical2.h"
#include "tactic/tactical.h"
#include "tactic/aig/aig_tactic.h"
#include "tactic/core/propagate_values_tactic.h"
@ -391,6 +390,15 @@ public:
if (m_preprocess) m_preprocess->collect_statistics(st);
m_solver.collect_statistics(st);
}
void set_max_conflicts(unsigned max_conflicts) override {
m_solver.set_max_conflicts(max_conflicts);
}
unsigned get_max_conflicts() const override {
return m_solver.get_max_conflicts();
}
void get_unsat_core(expr_ref_vector & r) override {
r.reset();
r.append(m_core.size(), m_core.data());
@ -405,6 +413,46 @@ public:
}
}
unsigned get_assign_level(expr* e) const override {
m.is_not(e, e);
sat::bool_var bv = m_map.to_bool_var(e);
return bv == sat::null_bool_var ? UINT_MAX : m_solver.lvl(bv);
}
bool is_relevant(expr* e) const override {
m.is_not(e, e);
sat::bool_var bv = m_map.to_bool_var(e);
if (bv == sat::null_bool_var)
return true;
auto* ext = dynamic_cast<euf::solver*>(m_solver.get_extension());
return !ext || ext->is_relevant(bv);
}
unsigned get_num_bool_vars() const override {
return m_solver.num_vars();
}
sat::bool_var get_bool_var(expr* e) const override {
m.is_not(e, e);
return m_map.to_bool_var(e);
}
expr* bool_var2expr(sat::bool_var v) const override {
return v < m_solver.num_vars() ? m_map.bool_var2expr(v) : nullptr;
}
lbool get_assignment(sat::bool_var v) const override {
return v < m_solver.num_vars() ? m_solver.value(v) : l_undef;
}
double get_activity(sat::bool_var v) const override {
return v < m_solver.num_vars() ? static_cast<double>(m_solver.get_activity(v)) : 0.0;
}
bool was_eliminated(sat::bool_var v) const override {
return v < m_solver.num_vars() && m_solver.was_eliminated(v);
}
expr_ref_vector get_trail(unsigned max_level) override {
expr_ref_vector result(m);
unsigned sz = m_solver.trail_size();
@ -482,6 +530,70 @@ public:
return fmls;
}
expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override {
if (!is_internalized()) {
lbool r = internalize_formulas();
if (r != l_true)
return expr_ref(m);
}
convert_internalized();
if (m_solver.inconsistent())
return expr_ref(m);
obj_hashtable<expr> invalid_split_atoms_set;
for (expr* e : invalid_split_atoms) {
expr* atom = e;
m.is_not(e, atom);
invalid_split_atoms_set.insert(atom);
}
expr_ref result(m);
double score = 0.0;
unsigned n = 0;
unsigned search_lvl = m_solver.search_lvl();
for (auto& kv : m_map) {
sat::bool_var v = kv.m_value;
if (was_eliminated(v))
continue;
if (get_assignment(v) != l_undef && m_solver.lvl(v) <= search_lvl)
continue;
expr* e = kv.m_key;
if (!e)
continue;
expr* atom = e;
m.is_not(e, atom);
if (invalid_split_atoms_set.contains(atom))
continue;
double new_score = get_activity(v);
if (new_score > score || !result || (new_score == score && m_solver.rand()(++n) == 0)) {
score = new_score;
result = e;
}
}
return result;
}
void get_backbone_candidates(vector<solver::scored_literal>& candidates, unsigned max_num) override {
if (!is_internalized()) {
lbool r = internalize_formulas();
if (r != l_true)
return;
}
convert_internalized();
sat::literal_vector lits;
m_solver.get_backbone_candidates(lits, max_num);
expr_ref_vector lit2expr(m);
lit2expr.resize(m_solver.num_vars() * 2);
m_map.mk_inv(lit2expr);
uint64_t now = m_solver.get_stats().m_conflicts;
for (sat::literal lit : lits) {
expr* e = lit2expr.get(lit.index());
if (!e)
continue;
candidates.push_back(scored_literal(m, e, static_cast<double>(now - m_solver.get_phase_birthdate(lit.var()))));
}
}
expr* congruence_next(expr* e) override { return e; }
expr* congruence_root(expr* e) override { return e; }
expr_ref congruence_explain(expr* a, expr* b) override { return expr_ref(m.mk_eq(a, b), m); }
@ -1186,7 +1298,5 @@ tactic * mk_psat_tactic(ast_manager& m, params_ref const& p) {
parallel_params pp(p);
if (pp.enable())
return mk_parallel_tactic(mk_inc_sat_solver(m, p, false), p);
if (pp.enable2())
return mk_parallel_tactic2(mk_inc_sat_solver(m, p, false), p);
return mk_sat_tactic(m);
}

View file

@ -203,6 +203,8 @@ namespace array {
ctx.push_vec(d.m_parent_lambdas, lambda);
if (should_prop_upward(d))
propagate_select_axioms(d, lambda);
if (d.m_has_default)
push_axiom(default_axiom(lambda));
}
void solver::add_parent_default(theory_var v) {

View file

@ -49,6 +49,13 @@ sat::bool_var atom2bool_var::to_bool_var(expr * n) const {
return m_mapping[idx].m_value;
}
expr* atom2bool_var::bool_var2expr(sat::bool_var v) const {
for (auto const& kv : m_mapping)
if (kv.m_value == v)
return kv.m_key;
return nullptr;
}
struct collect_boolean_interface_proc {
struct visitor {
obj_hashtable<expr> & m_r;

View file

@ -29,6 +29,7 @@ public:
atom2bool_var(ast_manager & m):expr2var(m) {}
void insert(expr * n, sat::bool_var v) { expr2var::insert(n, v); }
sat::bool_var to_bool_var(expr * n) const;
expr* bool_var2expr(sat::bool_var v) const;
void mk_inv(expr_ref_vector & lit2expr) const;
void mk_var_inv(expr_ref_vector & var2expr) const;
// return true if the mapping contains uninterpreted atoms.

View file

@ -155,7 +155,7 @@ namespace bv {
void ackerman::propagate() {
auto* n = m_queue;
vv* k = nullptr;
unsigned num_prop = static_cast<unsigned>(s.s().get_stats().m_conflict * s.get_config().m_dack_factor);
unsigned num_prop = static_cast<unsigned>(s.s().get_stats().m_conflicts * s.get_config().m_dack_factor);
num_prop = std::min(num_prop, m_table.size());
for (unsigned i = 0; i < num_prop; ++i, n = k) {
k = n->next();

View file

@ -171,7 +171,7 @@ namespace euf {
SASSERT(ctx.s().at_base_lvl());
auto* n = m_queue;
inference* k = nullptr;
unsigned num_prop = static_cast<unsigned>(ctx.s().get_stats().m_conflict * ctx.m_config.m_dack_factor);
unsigned num_prop = static_cast<unsigned>(ctx.s().get_stats().m_conflicts * ctx.m_config.m_dack_factor);
num_prop = std::min(num_prop, m_table.size());
for (unsigned i = 0; i < num_prop; ++i, n = k) {
k = n->next();

View file

@ -5097,7 +5097,7 @@ namespace seq {
svector<sat::literal>& mem_literals) const {
SASSERT(m_root);
const auto deps = collect_conflict_deps();
vector<dep_source> vs;
vector<dep_source, false> vs;
m_dep_mgr.linearize(deps, vs);
for (dep_source const& d : vs) {
if (std::holds_alternative<enode_pair>(d))

View file

@ -189,7 +189,7 @@ bool theory_seq::len_based_split(depeq const& e) {
expr_ref_vector const& rs = e.rs;
int offset = 0;
if (!has_len_offset(ls, rs, offset))
if (!has_len_offset(ls, rs, offset) || offset == 0)
return false;
TRACE(seq, tout << "split based on length\n";);

View file

@ -470,9 +470,10 @@ namespace smt {
re_expr = m_seq.re.mk_inter(re_expr, loop);
}
zstring str;
expr_ref witness(m);
// We checked non-emptiness during Nielsen already
lbool wr = m_rewriter.some_seq_in_re(re_expr, witness);
lbool wr = m_rewriter.some_string_in_re(re_expr, str);
if (wr != l_true) {
// some_seq_in_re can fail (l_undef / l_false) on regexes it does
// not fully support — notably projection operators (re.proj),
@ -482,7 +483,7 @@ namespace smt {
wr = derivative_witness(m_sg.mk(re_expr), witness);
}
if (wr == l_true) {
SASSERT(witness);
witness = m_seq.str.mk_string(str);
m_trail.push_back(witness);
m_factory->register_value(witness);
return witness;

View file

@ -248,6 +248,17 @@ namespace smt {
th.add_axiom(~lit);
return true;
}
// Fall back to antimirov NFA reachability. The lazy state graph
// keys states by AST identity and cannot close on intersections /
// complements whose derivative product states do not canonicalize,
// so it never detects their emptiness. re_is_empty decides
// emptiness directly (the same procedure propagate_eq already uses
// for re.none equalities).
if (re_is_empty(r) == l_true) {
STRACE(seq_regex_brief, tout << "(empty:re) ";);
th.add_axiom(~lit);
return true;
}
}
return false;
}
@ -413,8 +424,15 @@ namespace smt {
}
expr_ref seq_regex::symmetric_diff(expr* r1, expr* r2) {
seq_rewriter rw(m);
auto r = rw.mk_symmetric_diff(r1, r2);
expr_ref r(m);
if (r1 == r2)
r = re().mk_empty(r1->get_sort());
else if (re().is_empty(r1))
r = r2;
else if (re().is_empty(r2))
r = r1;
else
r = re().mk_union(re().mk_diff(r1, r2), re().mk_diff(r2, r1));
rewrite(r);
return r;
}
@ -478,6 +496,24 @@ namespace smt {
if (re().is_empty(r))
//trivially true
return;
// When one side is re.none the equation is a pure emptiness check on
// the other regex (symmetric_diff already returned it as r). Decide
// it directly by antimirov NFA reachability instead of running the
// bisimulation/XOR closure, which would build large un-canonicalized
// product states for intersections of contains-patterns.
if ((re().is_empty(r1) || re().is_empty(r2)) && is_ground(r)) {
switch (re_is_empty(r)) {
case l_true:
STRACE(seq_regex_brief, tout << "empty:eq ";);
return; // languages equal (both empty): trivially true
case l_false:
STRACE(seq_regex_brief, tout << "empty:neq ";);
th.add_axiom(~th.mk_eq(r1, r2, false), false_literal);
return;
case l_undef:
break;
}
}
// Try the bisimulation procedure on ground regexes first. If it
// returns a definite answer, dispatch the corresponding axiom and
// bypass the symbolic emptiness/derivative closure.
@ -579,16 +615,16 @@ namespace smt {
lits.push_back(null_lit);
expr_ref_pair_vector cofactors(m);
get_cofactors(d, cofactors);
for (auto const& p : cofactors) {
if (is_member(p.second, u))
seq_rw().get_cofactors(hd, d, cofactors);
for (auto const& [c, r] : cofactors) {
if (is_member(r, u))
continue;
expr_ref cond(p.first, m);
expr_ref cond(c, m);
seq_rw().elim_condition(hd, cond);
rewrite(cond);
if (m.is_false(cond))
continue;
expr_ref next_non_empty = sk().mk_is_non_empty(p.second, re().mk_union(u, p.second), n);
expr_ref next_non_empty = sk().mk_is_non_empty(r, re().mk_union(u, r), n);
if (!m.is_true(cond))
next_non_empty = m.mk_and(cond, next_non_empty);
lits.push_back(th.mk_literal(next_non_empty));
@ -689,40 +725,113 @@ namespace smt {
}
/*
Return a list of all target regexes in the derivative of a regex r,
ignoring the conditions along each path.
Decide emptiness of a ground regex r via antimirov-mode NFA
reachability.
The derivative construction uses (:var 0) and tries
to eliminate unsat condition paths but it does not perform
full satisfiability checks and it is not guaranteed
that all targets are actually reachable
The symbolic derivative engine runs in antimirov mode, so the
derivative of an intersection distributes into a *set* of individual
product states inter(A_i, B_j) (each a small, ground regex) rather
than one giant union-of-intersections term. get_derivative_targets
enumerates these NFA successor states.
We short-circuit to l_false (non-empty) as soon as a reachable state
is nullable (accepts the empty word) or classical (a regex built only
from to_re/all/union/concat/star/plus/opt/loop, hence non-empty). An
intersection itself is never classical, but once one operand reduces
to Σ* the intersection collapses (via the derivative's subset
simplification) to the other, classical, operand.
If the worklist is exhausted with no such state, r is empty (l_true).
Returns l_undef if a step bound is hit, so callers can fall back to
the general procedure.
*/
void seq_regex::get_derivative_targets(expr* r, expr_ref_vector& targets) {
// constructs the derivative wrt (:var 0)
expr_ref d(seq_rw().mk_derivative(r), m);
// use DFS to collect all the targets (leaf regexes) in d.
expr* _1 = nullptr, * e1 = nullptr, * e2 = nullptr;
obj_hashtable<expr>::entry* _2 = nullptr;
vector<expr*> workset;
workset.push_back(d);
obj_hashtable<expr> done;
done.insert(d);
while (workset.size() > 0) {
expr* e = workset.back();
workset.pop_back();
if (m.is_ite(e, _1, e1, e2) || re().is_union(e, e1, e2)) {
if (done.insert_if_not_there_core(e1, _2))
workset.push_back(e1);
if (done.insert_if_not_there_core(e2, _2))
workset.push_back(e2);
lbool seq_regex::re_is_empty(expr* r) {
if (re().is_empty(r))
return l_true;
expr_ref_vector pinned(m);
obj_hashtable<expr> visited;
ptr_vector<expr> work;
work.push_back(r);
visited.insert(r);
pinned.push_back(r);
unsigned const bound = 100000;
unsigned steps = 0;
while (!work.empty()) {
if (++steps > bound)
return l_undef;
expr* s = work.back();
work.pop_back();
auto info = re().get_info(s);
if (!info.is_known())
return l_undef;
// ε ∈ L(s) or s is a non-empty classical regex ⇒ L(r) non-empty.
if (info.nullable == l_true || info.classical)
return l_false;
// Dead state: prune (min_length == UINT_MAX means no word is
// accepted from here).
if (info.min_length == UINT_MAX)
continue;
expr_ref_vector targets(m);
get_derivative_targets(s, targets);
for (expr* t : targets) {
if (visited.contains(t))
continue;
visited.insert(t);
pinned.push_back(t);
work.push_back(t);
}
else if (!re().is_empty(e))
targets.push_back(e);
}
return l_true;
}
/*
Return a list of all reachable target regexes in the derivative of a
regex r.
The derivative is taken wrt (:var 0) and its reachable leaves are
enumerated with the path-aware cofactor engine, which conjoins the
ITE-path conditions and prunes infeasible character-range combinations
(e.g. a nested branch requiring elem = 'a' and elem = 'B'). Each leaf
is re-normalized with the path-aware smart constructors so that
semantically equal states stay syntactically identical (essential for
state dedup in the emptiness closure).
Without this pruning the naive ITE-tree DFS would reach infeasible
leaves; an infeasible classical (intersection/complement-free) leaf
would then be misjudged as a non-empty residual.
*/
void seq_regex::get_derivative_targets(expr* r, expr_ref_vector& targets) {
expr_ref_pair_vector cofactors(m);
seq_rw().brz_derivative_cofactors(r, cofactors);
for (auto const& [c, t] : cofactors) {
if (!re().is_empty(t))
targets.push_back(t);
}
}
/*
Return a list of all (cond, leaf) pairs in a given derivative
expression r, where elem is the character symbol the derivative was
taken with respect to.
The transition regexes produced by the symbolic derivative engine are
ITE-trees over character predicates ci on elem (equalities such as
elem = 'A', and ranges such as 'a' <= elem <= 'z'). These predicates
are typically mutually exclusive, so the number of feasible truth
assignments to {c1,..,ck} ("minterms") is small.
The enumeration is delegated to seq::derive (via seq_rw().get_cofactors)
so it reuses the very same path/interval context that the derivative
engine uses while hoisting ITEs: each feasible path through the ITE-tree
yields one (path_condition, leaf) cofactor, infeasible character-range
combinations are pruned, and the leaf is simplified with the path-aware
smart constructors.
This is used by:
propagate_is_empty
propagate_is_non_empty
*/
/*
is_empty(r, u) => ~is_nullable(r)
@ -750,11 +859,11 @@ namespace smt {
d = mk_derivative_wrapper(hd, r);
literal_vector lits;
expr_ref_pair_vector cofactors(m);
get_cofactors(d, cofactors);
for (auto const& p : cofactors) {
if (is_member(p.second, u))
seq_rw().get_cofactors(hd, d, cofactors);
for (auto const& [c, r] : cofactors) {
if (is_member(r, u))
continue;
expr_ref cond(p.first, m);
expr_ref cond(c, m);
seq_rw().elim_condition(hd, cond);
rewrite(cond);
if (m.is_false(cond))
@ -765,7 +874,7 @@ namespace smt {
expr_ref ncond(mk_not(m, cond), m);
lits.push_back(th.mk_literal(mk_forall(m, hd, ncond)));
}
expr_ref is_empty1 = sk().mk_is_empty(p.second, re().mk_union(u, p.second), n);
expr_ref is_empty1 = sk().mk_is_empty(r, re().mk_union(u, r), n);
lits.push_back(th.mk_literal(is_empty1));
th.add_axiom(lits);
}
@ -878,50 +987,4 @@ namespace smt {
return std::string("id") + std::to_string(e->get_id());
}
/**
Return a list of all (cond, leaf) pairs in a given
expression r.
Note: this implementation is inefficient: it simply collects all expressions under an if and
iterates over all combinations.
*/
void seq_regex::get_cofactors(expr *r, expr_ref_pair_vector &result) {
obj_hashtable<expr> ifs;
expr *cond = nullptr, *r1 = nullptr, *r2 = nullptr;
for (expr *e : subterms::ground(expr_ref(r, m)))
if (m.is_ite(e, cond, r1, r2))
ifs.insert(cond);
expr_ref_vector rs(m);
vector<expr_ref_vector> conds;
conds.push_back(expr_ref_vector(m));
rs.push_back(r);
for (expr *c : ifs) {
unsigned sz = conds.size();
expr_safe_replace rep1(m);
expr_safe_replace rep2(m);
rep1.insert(c, m.mk_true());
rep2.insert(c, m.mk_false());
expr_ref r2(m);
for (unsigned i = 0; i < sz; ++i) {
expr_ref_vector cs = conds[i];
cs.push_back(m.mk_not(c));
conds.push_back(cs);
conds[i].push_back(c);
expr_ref r1(rs.get(i), m);
rep1(r1, r2);
rs[i] = r2;
rep2(r1, r2);
rs.push_back(r2);
}
}
for (unsigned i = 0; i < conds.size(); ++i) {
expr_ref conj = mk_and(conds[i]);
expr_ref r(rs.get(i), m);
ctx.get_rewriter()(r);
if (!m.is_false(conj) && !re().is_empty(r))
result.push_back(conj, r);
}
}
}

View file

@ -165,6 +165,12 @@ namespace smt {
expr_ref mk_deriv_accept(expr* s, unsigned i, expr* r);
void get_derivative_targets(expr* r, expr_ref_vector& targets);
// Decide emptiness of a ground regex by antimirov-mode NFA
// reachability: explore derivative target states, short-circuiting to
// "non-empty" on the first reachable nullable or classical state.
// Returns l_true (empty), l_false (non-empty), l_undef (gave up).
lbool re_is_empty(expr* r);
/*
Pretty print the regex of the state id to the out stream,
seq_regex_ptr must be a pointer to seq_regex and the
@ -183,9 +189,6 @@ namespace smt {
bool block_if_empty(expr* r, literal lit);
void get_cofactors(expr *r, expr_ref_pair_vector &result);
public:
seq_regex(theory_seq& th);

View file

@ -119,6 +119,10 @@ namespace smt {
if (!m_setup.already_configured()) {
m_fparams.updt_params(p);
}
else {
// selected parameters are safe to update after initialization
m_fparams.m_max_conflicts = p.get_uint("max_conflicts", m_fparams.m_max_conflicts);
}
for (auto th : m_theory_set)
if (th)
th->updt_params();
@ -3673,6 +3677,13 @@ namespace smt {
}
}
void context::setup_for_parallel() {
// Native SMT parallel configures the parent context before cloning workers.
// context::copy then configures/internalizes each worker copy while
// preprocessing is still enabled.
setup_context(m_fparams.m_auto_config);
}
config_mode context::get_config_mode(bool use_static_features) const {
if (!m_fparams.m_auto_config)
return CFG_BASIC;
@ -4693,7 +4704,6 @@ namespace smt {
theory_id th_id = l->get_id();
for (enode * parent : enode::parents(n)) {
auto p = parent->get_expr();
family_id fid = parent->get_family_id();
if (fid != th_id && fid != m.get_basic_family_id()) {
if (is_beta_redex(parent, n))

View file

@ -64,6 +64,7 @@ namespace smt {
class model_generator;
class context;
class kernel;
struct oom_exception : public z3_error {
oom_exception() : z3_error(ERR_MEMOUT) {}
@ -85,6 +86,7 @@ namespace smt {
friend class model_generator;
friend class lookahead;
friend class parallel;
friend class kernel;
public:
statistics m_stats;
@ -294,6 +296,10 @@ namespace smt {
return m_fparams;
}
smt_params const& get_fparams() const {
return m_fparams;
}
params_ref const & get_params() {
return m_params;
}
@ -454,6 +460,8 @@ namespace smt {
svector<double> const & get_activity_vector() const { return m_activity; }
double get_activity(bool_var v) const { return m_activity[v]; }
unsigned get_num_assignments() const { return m_stats.m_num_assignments; }
unsigned get_birthdate(bool_var v) const { return m_birthdate[v]; }
void set_activity(bool_var v, double act) { m_activity[v] = act; }
@ -540,6 +548,8 @@ namespace smt {
return m_scope_lvl == m_search_lvl;
}
void pop_to_search_level() { pop_to_search_lvl(); }
bool tracking_assumptions() const {
return !m_assumptions.empty() && m_search_lvl > m_base_lvl;
}
@ -872,7 +882,7 @@ namespace smt {
void undo_mk_enode();
void apply_sort_cnstr(app * term, enode * e);
void apply_sort_cnstr(expr * term, enode * e);
bool simplify_aux_clause_literals(unsigned & num_lits, literal * lits, literal_buffer & simp_lits);
@ -1699,6 +1709,8 @@ namespace smt {
lbool setup_and_check(bool reset_cancel = true);
void setup_for_parallel();
void reduce_assertions();
bool resource_limits_exceeded();
@ -1917,5 +1929,3 @@ namespace smt {
std::ostream& operator<<(std::ostream& out, enode_pp const& p);
};

View file

@ -597,9 +597,10 @@ namespace smt {
SASSERT(is_lambda(q));
if (e_internalized(q))
return;
mk_enode(q, true, /* do suppress args */
auto e = mk_enode(q, true, /* do suppress args */
false, /* it is a term, so it should not be merged with true/false */
true);
apply_sort_cnstr(q, e);
}
bool context::has_lambda() {
@ -1084,8 +1085,8 @@ namespace smt {
/**
\brief Apply sort constraints on e.
*/
void context::apply_sort_cnstr(app * term, enode * e) {
sort * s = term->get_decl()->get_range();
void context::apply_sort_cnstr(expr * term, enode * e) {
sort * s = term->get_sort();
theory * th = m_theories.get_plugin(s->get_family_id());
if (th) {
th->apply_sort_cnstr(e, s);

View file

@ -284,10 +284,22 @@ namespace smt {
smt_params_helper::collect_param_descrs(d);
}
void kernel::pop_to_base_level() {
m_imp->m_kernel.pop_to_base_lvl();
}
void kernel::set_preprocess(bool f) {
m_imp->m_kernel.get_fparams().m_preprocess = f;
}
context & kernel::get_context() {
return m_imp->m_kernel;
}
context const& kernel::get_context() const {
return m_imp->m_kernel;
}
void kernel::get_levels(ptr_vector<expr> const& vars, unsigned_vector& depth) {
m_imp->m_kernel.get_levels(vars, depth);
}

View file

@ -300,6 +300,10 @@ namespace smt {
*/
static void collect_param_descrs(param_descrs & d);
void pop_to_base_level();
void set_preprocess(bool f);
void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause);
/**
@ -340,6 +344,6 @@ namespace smt {
\warning This method should not be used in new code.
*/
context & get_context();
context const& get_context() const;
};
};

View file

@ -219,7 +219,7 @@ namespace smt {
if (use_inv) {
unsigned sk_term_gen = 0;
expr * sk_term = m_model_finder.get_inv(q, i, sk_value, sk_term_gen);
expr * sk_term = m_model_finder.get_inv(q, i, sk_value, *cex, sk_term_gen);
if (sk_term != nullptr) {
TRACE(model_checker, tout << "Found inverse " << mk_pp(sk_term, m) << "\n";);
SASSERT(!m.is_model_value(sk_term));
@ -233,15 +233,10 @@ namespace smt {
}
else {
expr * sk_term = get_term_from_ctx(sk_value);
func_decl * f = nullptr;
if (sk_term != nullptr) {
TRACE(model_checker, tout << "sk term " << mk_pp(sk_term, m) << "\n");
sk_value = sk_term;
}
// last ditch: am I an array?
else if (false && autil.is_as_array(sk_value, f) && cex->get_func_interp(f) && cex->get_func_interp(f)->get_array_interp(f)) {
sk_value = cex->get_func_interp(f)->get_array_interp(f);
}
}
if (contains_model_value(sk_value)) {

View file

@ -18,6 +18,7 @@ Revision History:
--*/
#include "util/backtrackable_set.h"
#include "ast/ast_util.h"
#include "ast/has_free_vars.h"
#include "ast/macros/macro_util.h"
#include "ast/arith_decl_plugin.h"
#include "ast/bv_decl_plugin.h"
@ -31,6 +32,7 @@ Revision History:
#include "ast/ast_ll_pp.h"
#include "ast/well_sorted.h"
#include "ast/ast_smt2_pp.h"
#include "ast/rewriter/term_enumeration.h"
#include "model/model_pp.h"
#include "model/model_macro_solver.h"
#include "smt/smt_model_finder.h"
@ -107,9 +109,15 @@ namespace smt {
}
}
expr* get_inv(expr* v) const {
expr* get_inv(expr* v, model& mdl) const {
expr* t = nullptr;
m_inv.find(v, t);
if (!t) {
for (auto [k, term] : m_inv) {
if (mdl.are_equal(k, v))
return term;
}
}
return t;
}
@ -120,14 +128,11 @@ namespace smt {
}
void mk_inverse(evaluator& ev) {
for (auto const& kv : m_elems) {
expr* t = kv.m_key;
for (auto const &[t, gen] : m_elems) {
SASSERT(!contains_model_value(t));
unsigned gen = kv.m_value;
expr* t_val = ev.eval(t, true);
if (!t_val) break;
TRACE(model_finder, tout << mk_pp(t, m) << " " << mk_pp(t_val, m) << "\n";);
expr* old_t = nullptr;
if (m_inv.find(t_val, old_t)) {
unsigned old_t_gen = 0;
@ -187,14 +192,14 @@ namespace smt {
\brief Base class used to solve model construction constraints.
*/
class node {
unsigned m_id;
node* m_find{ nullptr };
unsigned m_eqc_size{ 1 };
unsigned m_id = 0;
node* m_find = nullptr;
unsigned m_eqc_size = 1;
sort* m_sort; // sort of the elements in the instantiation set.
sort* m_sort = nullptr; // sort of the elements in the instantiation set.
bool m_mono_proj{ false }; // relevant for integers & reals & bit-vectors
bool m_signed_proj{ false }; // relevant for bit-vectors.
bool m_mono_proj = false; // relevant for integers & reals & bit-vectors
bool m_signed_proj = false; // relevant for bit-vectors.
ptr_vector<node> m_avoid_set;
ptr_vector<expr> m_exceptions;
@ -291,7 +296,7 @@ namespace smt {
}
void insert(expr* n, unsigned generation) {
if (is_ground(n))
if (is_ground(n) || (has_quantifiers(n) && !has_free_vars(n))) // this is a closed term
get_root()->m_set->insert(n, generation);
}
@ -599,7 +604,10 @@ namespace smt {
}
else {
r = tmp;
TRACE(model_finder, tout << "eval\n" << mk_pp(n, m) << "\n----->\n" << mk_pp(r, m) << "\n";);
TRACE(model_finder, tout << "eval-failed\n" << mk_pp(n, m) << "\n----->\n" << mk_pp(r, m) << "\n";);
if (is_lambda(tmp)) {
r = m.mk_fresh_const("lambda", tmp->get_sort());
}
}
m_eval_cache[model_completion].insert(n, r);
m_eval_cache_range.push_back(r);
@ -1235,8 +1243,8 @@ namespace smt {
void populate_inst_sets(quantifier* q, func_decl* mhead, ptr_vector<instantiation_set>& uvar_inst_sets, context* ctx) override {
if (m_f != mhead)
return;
uvar_inst_sets.reserve(m_var_j + 1, 0);
if (uvar_inst_sets[m_var_j] == 0)
uvar_inst_sets.reserve(m_var_j + 1, nullptr);
if (uvar_inst_sets[m_var_j] == nullptr)
uvar_inst_sets[m_var_j] = alloc(instantiation_set, ctx->get_manager());
instantiation_set* s = uvar_inst_sets[m_var_j];
SASSERT(s != nullptr);
@ -1369,6 +1377,81 @@ namespace smt {
};
class ho_var : public qinfo {
unsigned m_var_i;
public:
ho_var(ast_manager& m, unsigned i) : qinfo(m), m_var_i(i) {
}
char const *get_kind() const override {
return "ho_var";
}
bool is_equal(qinfo const *qi) const override {
if (qi->get_kind() != get_kind())
return false;
ho_var const *other = static_cast<ho_var const *>(qi);
return m_var_i == other->m_var_i;
}
void display(std::ostream &out) const override {
out << "(" << "ho-var: " << m_var_i << ")";
}
void process_auf(quantifier *q, auf_solver &s, context *ctx) override {
/* node * S_i = */ s.get_uvar(q, m_var_i);
}
void populate_inst_sets(quantifier *q, auf_solver &s, context *ctx) override {
node *S = s.get_uvar(q, m_var_i);
sort *srt = S->get_sort();
IF_VERBOSE(3, verbose_stream() << "ho_var::populate_inst_sets: " << q->get_id() << " " << mk_pp(srt, m) << "\n";);
term_enumeration tn(m);
// Add ground terms of type S.
// Add productions for functions in E-graph
// add other possible relevant functions such as equality over srt, Boolean operators
ast_mark visited;
tn.add_production(m.mk_true());
tn.add_production(m.mk_false());
for (enode *n : ctx->enodes()) {
if (!ctx->is_relevant(n))
continue;
auto e = n->get_expr();
if (srt == n->get_sort()) {
TRACE(model_finder, tout << "inserting " << mk_pp(e, m) << " into inst set\n");
S->insert(e, n->get_generation());
}
else if (is_app(e) && to_app(e)->get_decl()->is_skolem())
;
else if (is_uninterp_const(e)) {
TRACE(model_finder, tout << "add production " << mk_pp(e, m) << "\n");
tn.add_production(e);
}
else if (is_uninterp(e)) {
auto f = to_app(e)->get_decl();
if (visited.is_marked(f))
continue;
visited.mark(f, true);
TRACE(model_finder, tout << "add function " << mk_pp(f, m) << "\n");
tn.add_production(f);
}
}
unsigned max_count = 20;
for (auto t : tn.enum_terms(srt)) {
if (max_count == 0)
break;
--max_count;
unsigned generation = 0; // todo - inherited from sub-term of t?
TRACE(model_finder, tout << "ho_var: adding term " << mk_ismt2_pp(t, m)
<< " to instantiation set of S" << std::endl;);
S->insert(t, generation);
}
}
};
/**
\brief auf_arr is a term (pattern) of the form:
@ -2105,7 +2188,10 @@ namespace smt {
process_app(to_app(curr));
}
else if (is_var(curr)) {
m_info->m_is_auf = false; // unexpected occurrence of variable.
if (m_array_util.is_array(curr)) {
insert_qinfo(alloc(ho_var, m, to_var(curr)->get_idx()));
}
m_info->m_is_auf = false;
}
else {
SASSERT(is_lambda(curr));
@ -2163,7 +2249,6 @@ namespace smt {
}
SASSERT(is_quantifier(atom));
UNREACHABLE();
}
void process_literal(expr* atom, polarity pol) {
@ -2203,9 +2288,15 @@ namespace smt {
if (is_app(curr)) {
if (to_app(curr)->get_family_id() == m.get_basic_family_id() && m.is_bool(curr)) {
switch (static_cast<basic_op_kind>(to_app(curr)->get_decl_kind())) {
case OP_IMPLIES:
case OP_IMPLIES:
process_literal(to_app(curr)->get_arg(0), neg(pol));
process_literal(to_app(curr)->get_arg(1), pol);
break;
case OP_XOR:
UNREACHABLE(); // simplifier eliminated ANDs, IMPLIEs, and XORs
for (expr *arg : *to_app(curr)) {
visit_formula(arg, pol);
visit_formula(arg, neg(pol));
}
break;
case OP_OR:
case OP_AND:
@ -2515,11 +2606,12 @@ namespace smt {
Store in generation the generation of the result
*/
expr* model_finder::get_inv(quantifier* q, unsigned i, expr* val, unsigned& generation) {
expr* model_finder::get_inv(quantifier* q, unsigned i, expr* val, model& mdl,unsigned& generation) {
instantiation_set const* s = get_uvar_inst_set(q, i);
if (s == nullptr)
return nullptr;
expr* t = s->get_inv(val);
expr* t = s->get_inv(val, mdl);
if (m_auf_solver->is_default_representative(t))
return val;
if (t != nullptr) {
@ -2555,16 +2647,27 @@ namespace smt {
obj_map<expr, expr*> const& inv = s->get_inv_map();
if (inv.empty())
continue; // nothing to do
ptr_buffer<expr> eqs;
for (auto const& [val, _] : inv) {
if (val->get_sort() == sk->get_sort())
eqs.push_back(m.mk_eq(sk, val));
expr_ref_vector eqs(m), defs(m);
for (auto const& [val, term] : inv) {
if (val->get_sort() == sk->get_sort()) {
if (is_lambda(term)) {
eqs.push_back(m.mk_eq(sk, val));
defs.push_back(m.mk_eq(val, term));
}
else
eqs.push_back(m.mk_eq(sk, val));
}
}
if (!eqs.empty()) {
expr_ref new_cnstr(m);
new_cnstr = m.mk_or(eqs);
TRACE(model_finder, tout << "assert_restriction:\n" << mk_pp(new_cnstr, m) << "\n";);
aux_ctx->assert_expr(new_cnstr);
for (auto def : defs) {
TRACE(model_finder, tout << "assert_def:\n" << mk_pp(def, m) << "\n";);
aux_ctx->assert_expr(def);
}
asserted_something = true;
}
}

View file

@ -113,7 +113,7 @@ namespace smt {
void fix_model(proto_model * m);
quantifier * get_flat_quantifier(quantifier * q);
expr * get_inv(quantifier * q, unsigned i, expr * val, unsigned & generation);
expr * get_inv(quantifier * q, unsigned i, expr * val, model& m, unsigned & generation);
bool restrict_sks_to_inst_set(context * aux_ctx, quantifier * q, expr_ref_vector const & sks);
void restart_eh();

View file

@ -25,7 +25,7 @@ Author:
#include "smt/smt_parallel.h"
#include "smt/smt_lookahead.h"
#include "solver/solver_preprocess.h"
#include "params/smt_parallel_params.hpp"
#include "solver/parallel_params.hpp"
#include <cmath>
#include <mutex>
@ -550,7 +550,7 @@ namespace smt {
if (m_ablate_backtracking) {
// Ablation: for each target, pass the entire path from root to that node
for (auto const& target : targets) {
if (m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch))
if (m_search_tree.is_lease_canceled(target.leased_node))
continue;
// Reconstruct the full path from root to this target node
@ -626,7 +626,7 @@ namespace smt {
ctx->set_logic(p.ctx.m_setup.get_logic());
context::copy(p.ctx, *ctx, true);
ctx->pop_to_base_lvl();
ctx->get_fparams().m_preprocess = false;
ctx->get_fparams().m_preprocess = false; // avoid preprocessing lemmas that are exchanged
}
void parallel::core_minimizer_worker::cancel() {
@ -763,26 +763,42 @@ namespace smt {
if (m_config.m_global_backbones) {
bb_candidates local_candidates = find_backbone_candidates();
b.collect_backbone_candidates(m_l2g, local_candidates);
if (!m.inc())
bool lease_canceled = false;
if (!b.checkpoint_worker(id, lease, lease_canceled))
return;
if (lease_canceled) {
LOG_WORKER(1, " abandoning canceled lease\n");
continue;
}
}
lbool r = check_cube(cube);
if (b.lease_canceled(lease)) {
bool lease_canceled = false;
if (!b.checkpoint_worker(id, lease, lease_canceled))
return;
if (lease_canceled) {
LOG_WORKER(1, " abandoning canceled lease\n");
lease = {};
m.limit().dec_cancel();
continue;
}
if (!m.inc())
return;
switch (r) {
case l_undef: {
update_max_thread_conflicts();
LOG_WORKER(1, " found undef cube\n");
// Escalating the per-thread conflict budget and re-splitting the
// cube only helps when the cube was abandoned because the per-cube
// conflict limit was reached. For any other source of incompleteness
// (an incomplete theory, quantifiers, lambdas, resource limits, ...)
// the verdict cannot change, so re-checking the same cube would spin
// forever and the run hangs to a wall-clock timeout. Record a sound
// 'unknown' verdict and stop working this branch instead.
std::string reason = ctx->last_failure_as_string();
if (reason != "max-conflicts-reached") {
LOG_WORKER(1, " undef cube not conflict-limited (" << reason << "); reporting unknown\n");
b.set_unknown(reason);
return;
}
update_max_thread_conflicts();
if (m_config.m_max_cube_depth <= cube.size())
goto check_cube_start;
@ -790,7 +806,6 @@ namespace smt {
if (!atom)
goto check_cube_start;
b.try_split(m_l2g, id, lease, atom, m_config.m_threads_max_conflicts);
lease = {};
simplify();
break;
}
@ -825,7 +840,6 @@ namespace smt {
b.backtrack(m_l2g, id, core_to_use, lease);
if (m_config.m_core_minimize)
b.enqueue_core_minimization(m_l2g, source, unsat_core);
lease = {};
if (m_config.m_share_conflicts)
b.collect_clause(m_l2g, id, mk_not(mk_and(unsat_core)));
@ -854,10 +868,10 @@ namespace smt {
m_num_initial_atoms = ctx->get_num_bool_vars();
ctx->get_fparams().m_preprocess = false; // avoid preprocessing lemmas that are exchanged
smt_parallel_params pp(p.ctx.m_params);
m_config.m_inprocessing = pp.inprocessing();
m_config.m_global_backbones = pp.num_global_bb_batch_threads() > 0 || pp.num_global_bb_fl_threads() > 0;
m_config.m_local_backbones = pp.local_backbones();
parallel_params pp(p.ctx.m_params);
m_config.m_inprocessing = false;
m_config.m_global_backbones = pp.num_bb_threads() > 0;
m_config.m_local_backbones = false;
m_config.m_core_minimize = pp.core_minimize();
m_config.m_ablate_backtracking = pp.ablate_backtracking();
@ -887,9 +901,9 @@ namespace smt {
ctx->pop_to_base_lvl();
m_shared_units_prefix = ctx->assigned_literals().size();
m_num_initial_atoms = ctx->get_num_bool_vars();
ctx->get_fparams().m_preprocess = false; // avoid preprocessing lemmas that are exchanged
smt_parallel_params pp(p.ctx.m_params);
m_use_failed_literal_test = pp.num_global_bb_fl_threads() > 0;
m_use_failed_literal_test = false;
}
parallel::bb_candidates parallel::worker::find_backbone_candidates(unsigned k) {
@ -1105,14 +1119,48 @@ namespace smt {
return r;
}
void parallel::batch_manager::release_lease_unlocked(unsigned worker_id, node* n) {
if (worker_id >= m_worker_leases.size())
void parallel::batch_manager::set_canceled_unlocked() {
if (m_state != state::is_running)
return;
auto &lease = m_worker_leases[worker_id];
if (!lease.leased_node || lease.leased_node != n)
cancel_background_threads();
}
void parallel::batch_manager::set_canceled() {
std::scoped_lock lock(mux);
set_canceled_unlocked();
}
void parallel::batch_manager::release_worker_lease_unlocked(unsigned worker_id, node_lease& lease) {
if (worker_id >= m_worker_leases.size()) {
lease = {};
return;
m_search_tree.dec_active_workers(lease.leased_node);
}
auto& stored_lease = m_worker_leases[worker_id];
if (!stored_lease.leased_node || stored_lease.leased_node != lease.leased_node) {
lease = {};
return;
}
bool cancel_signaled = stored_lease.cancel_signaled;
m_search_tree.dec_active_workers(stored_lease.leased_node);
stored_lease = {};
lease = {};
if (cancel_signaled)
p.m_workers[worker_id]->limit().dec_cancel();
}
bool parallel::batch_manager::attempt_release_canceled_lease_unlocked(unsigned worker_id, node_lease& lease) {
if (m_state != state::is_running || !lease.leased_node || worker_id >= m_worker_leases.size())
return false;
auto& stored_lease = m_worker_leases[worker_id];
if (stored_lease.leased_node != lease.leased_node)
return false;
if (!m_search_tree.is_lease_canceled(stored_lease.leased_node))
return false;
release_worker_lease_unlocked(worker_id, lease);
return true;
}
void parallel::batch_manager::cancel_closed_leases_unlocked(unsigned source_worker_id) {
@ -1124,7 +1172,7 @@ namespace smt {
// only cancel workers that currently hold a lease, whose lease is canceled,
// and haven't already been signaled (prevents multiple inc_cancel() for same lease)
if (lease.leased_node && !lease.cancel_signaled && m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch)) {
if (lease.leased_node && !lease.cancel_signaled && m_search_tree.is_lease_canceled(lease.leased_node)) {
p.m_workers[worker_id]->cancel_lease();
m_worker_leases[worker_id].cancel_signaled = true;
}
@ -1132,7 +1180,7 @@ namespace smt {
}
void parallel::batch_manager::backtrack(ast_translation &l2g, unsigned worker_id, expr_ref_vector const &core,
node_lease const &lease) {
node_lease& lease) {
std::scoped_lock lock(mux);
vector<cube_config::literal> g_core;
for (auto c : core)
@ -1277,7 +1325,7 @@ namespace smt {
if (!g_core.empty()) {
collect_matching_targets_unlocked(source, g_core[0].get(), g_core, targets);
for (auto const& target : targets) {
if (!m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch))
if (!m_search_tree.is_lease_canceled(target.leased_node))
m_search_tree.backtrack(target.leased_node, g_core);
}
}
@ -1331,7 +1379,7 @@ namespace smt {
for (node* t : matches) {
if (!t || t == source)
continue;
if (m_search_tree.is_lease_canceled(t, t->get_cancel_epoch()))
if (m_search_tree.is_lease_canceled(t))
continue;
// When source is provided, keep only external matches. Nodes in the
@ -1358,12 +1406,12 @@ namespace smt {
if (!is_highest_ancestor)
continue;
targets.push_back({ t, t->get_cancel_epoch() });
targets.push_back({t});
}
}
void parallel::batch_manager::backtrack_unlocked(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core,
node_lease const* lease, vector<node_lease> const* targets) {
node_lease* lease, vector<node_lease> const* targets) {
if (m_state != state::is_running)
return;
@ -1374,17 +1422,25 @@ namespace smt {
SASSERT(lease != nullptr || targets != nullptr);
bool did_backtrack = false;
if (lease && !m_search_tree.is_lease_canceled(lease->leased_node, lease->cancel_epoch)) {
// we close/backtrack regardless of whether this lease is stale or not, as long as the lease isn't canceled
// i.e. worker 1 splits this node, but then worker 2 determines UNSAT --> worker 2 is stale but we still close this node and backtrack
did_backtrack = true;
IF_VERBOSE(1, verbose_stream() << "Batch manager backtracking.\n");
release_lease_unlocked(worker_id, lease->leased_node);
m_search_tree.backtrack(lease->leased_node, g_core);
if (lease) {
if (!m_search_tree.is_lease_canceled(lease->leased_node)) {
// we close/backtrack regardless of whether this lease is stale or not, as long as the lease isn't canceled
// i.e. worker 1 splits this node, but then worker 2 determines UNSAT --> worker 2 is stale but we still close this node and backtrack
did_backtrack = true;
IF_VERBOSE(1, verbose_stream() << "Batch manager backtracking.\n");
node* leased_node = lease->leased_node;
release_worker_lease_unlocked(worker_id, *lease);
m_search_tree.backtrack(leased_node, g_core);
}
else {
// the lease was canceled by another worker. don't backtrack on this node with whatever new core we just found with this thread
// however, we do proceed to external targets, since the new code may have exposed new external targets we can close/backtrack
attempt_release_canceled_lease_unlocked(worker_id, *lease);
}
}
if (targets) {
for (auto const& target : *targets) {
if (m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch))
if (m_search_tree.is_lease_canceled(target.leased_node))
continue;
did_backtrack = true;
@ -1410,37 +1466,59 @@ namespace smt {
}
void parallel::batch_manager::try_split(ast_translation &l2g, unsigned worker_id,
node_lease const &lease, expr *atom, unsigned effort) {
node_lease& lease, expr *atom, unsigned effort) {
std::scoped_lock lock(mux);
if (m_state != state::is_running)
return;
if (m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch))
if (m_search_tree.is_lease_canceled(lease.leased_node)) {
attempt_release_canceled_lease_unlocked(worker_id, lease);
return;
}
expr_ref lit(m), nlit(m);
lit = l2g(atom);
nlit = mk_not(m, lit);
bool did_split = m_search_tree.try_split(lease.leased_node, lease.cancel_epoch, lit, nlit, effort);
node* leased_node = lease.leased_node;
VERIFY(!leased_node->path_contains_atom(lit));
VERIFY(!leased_node->path_contains_atom(nlit));
bool did_split = m_search_tree.try_split(leased_node, lit, nlit, effort);
release_lease_unlocked(worker_id, lease.leased_node);
release_worker_lease_unlocked(worker_id, lease);
if (did_split) {
++m_stats.m_num_cubes;
m_stats.m_max_cube_depth = std::max(m_stats.m_max_cube_depth, lease.leased_node->depth() + 1);
m_stats.m_max_cube_depth = std::max(m_stats.m_max_cube_depth, leased_node->depth() + 1);
IF_VERBOSE(1, verbose_stream() << "Batch manager splitting on literal: " << mk_bounded_pp(lit, m, 3) << "\n");
}
}
void parallel::batch_manager::release_lease(unsigned worker_id, node_lease const &lease) {
bool parallel::batch_manager::checkpoint_worker(unsigned worker_id, node_lease& lease, bool& lease_canceled) {
std::scoped_lock lock(mux);
release_lease_unlocked(worker_id, lease.leased_node);
lease_canceled = false;
SASSERT(worker_id < p.m_workers.size());
if (attempt_release_canceled_lease_unlocked(worker_id, lease)) {
lease_canceled = true;
return true;
}
if (p.m_workers[worker_id]->limit().inc())
return true;
if (attempt_release_canceled_lease_unlocked(worker_id, lease)) {
lease_canceled = true;
return true;
}
set_canceled_unlocked();
return false;
}
bool parallel::batch_manager::lease_canceled(node_lease const &lease) {
std::scoped_lock lock(mux);
return m_state == state::is_running && m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch);
return m_state == state::is_running && m_search_tree.is_lease_canceled(lease.leased_node);
}
void parallel::batch_manager::collect_clause(ast_translation &l2g, unsigned source_worker_id, expr *clause) {
@ -1662,7 +1740,7 @@ namespace smt {
void parallel::batch_manager::set_sat(ast_translation &l2g, model &m) {
std::scoped_lock lock(mux);
IF_VERBOSE(1, verbose_stream() << "Batch manager setting SAT.\n");
if (m_state != state::is_running)
if (m_state != state::is_running && m_state != state::is_unknown)
return;
m_state = state::is_sat;
p.ctx.set_model(m.translate(l2g));
@ -1672,7 +1750,7 @@ namespace smt {
void parallel::batch_manager::set_unsat(ast_translation &l2g, expr_ref_vector const &unsat_core) {
std::scoped_lock lock(mux);
IF_VERBOSE(1, verbose_stream() << "Batch manager setting UNSAT.\n");
if (m_state != state::is_running)
if (m_state != state::is_running && m_state != state::is_unknown)
return;
m_state = state::is_unsat;
@ -1683,6 +1761,16 @@ namespace smt {
cancel_background_threads();
}
void parallel::batch_manager::set_unknown(std::string const &reason) {
std::scoped_lock lock(mux);
IF_VERBOSE(1, verbose_stream() << "Batch manager setting UNKNOWN: " << reason << ".\n");
if (m_state != state::is_running)
return; // a definitive sat/unsat verdict or exception already won.
m_state = state::is_unknown;
m_reason_unknown = reason;
cancel_background_threads();
}
void parallel::batch_manager::set_exception(unsigned error_code) {
std::scoped_lock lock(mux);
IF_VERBOSE(1, verbose_stream() << "Batch manager setting exception code: " << error_code << ".\n");
@ -1714,6 +1802,8 @@ namespace smt {
return l_false;
case state::is_sat:
return l_true;
case state::is_unknown:
return l_undef;
case state::is_exception_msg:
throw default_exception(m_exception_msg.c_str());
case state::is_exception_code:
@ -1745,7 +1835,6 @@ namespace smt {
IF_VERBOSE(2, m_search_tree.display(verbose_stream()); verbose_stream() << "\n";);
lease.leased_node = t;
lease.cancel_epoch = t->get_cancel_epoch();
if (id >= m_worker_leases.size())
m_worker_leases.resize(id + 1);
m_worker_leases[id] = lease;
@ -1779,8 +1868,9 @@ namespace smt {
m_worker_leases.reset();
m_worker_leases.resize(p.m_workers.size());
smt_parallel_params pp(p.ctx.m_params);
parallel_params pp(p.ctx.m_params);
m_ablate_backtracking = pp.ablate_backtracking();
m_canceled = false;
}
void parallel::batch_manager::collect_statistics(::statistics &st) const {
@ -1794,20 +1884,26 @@ namespace smt {
}
lbool parallel::operator()(expr_ref_vector const &asms) {
smt_parallel_params pp(ctx.m_params);
unsigned num_global_bb_batch_threads = pp.num_global_bb_batch_threads();
if (num_global_bb_batch_threads > 2)
throw default_exception("smt_parallel.num_global_bb_batch_threads must be 0, 1, or 2");
unsigned num_workers = std::min((unsigned)std::thread::hardware_concurrency(), ctx.get_fparams().m_threads);
unsigned num_sls_threads = (pp.sls() ? 1 : 0);
parallel_params pp(ctx.m_params);
unsigned num_global_bb_threads = pp.num_bb_threads();
if (num_global_bb_threads > 2)
throw default_exception("parallel.num_bb_threads must be 0, 1, or 2");
unsigned total_threads = std::min((unsigned)std::thread::hardware_concurrency(), ctx.get_fparams().m_threads);
unsigned num_workers = total_threads;
unsigned num_sls_threads = 0;
unsigned num_core_min_threads = (pp.core_minimize() ? 1 : 0);
unsigned num_global_bb_fl_threads = pp.num_global_bb_fl_threads();
if (num_global_bb_fl_threads > 2)
throw default_exception("smt_parallel.num_global_bb_fl_threads must be 0, 1, or 2");
if (num_global_bb_fl_threads > 0 && num_global_bb_batch_threads > 0)
throw default_exception("smt_parallel.num_global_bb_fl_threads and smt_parallel.num_global_bb_batch_threads cannot both be enabled");
unsigned num_global_bb_threads = num_global_bb_fl_threads > 0 ? num_global_bb_fl_threads : num_global_bb_batch_threads;
unsigned total_threads = num_workers + num_sls_threads + num_core_min_threads + num_global_bb_threads;
if (num_workers > 2 + num_core_min_threads)
num_workers -= num_core_min_threads;
else
num_core_min_threads = 0;
if (num_workers > 2 + num_global_bb_threads)
num_workers -= num_global_bb_threads;
else
num_global_bb_threads = 0;
if (num_workers > 2 + num_sls_threads)
num_workers -= num_sls_threads;
else
num_sls_threads = 0;
IF_VERBOSE(1, verbose_stream() << "Parallel SMT with " << total_threads << " threads\n";);
ast_manager &m = ctx.m;
@ -1841,7 +1937,7 @@ namespace smt {
m_sls_worker = alloc(sls_worker, *this);
sl.push_child(&(m_sls_worker->limit()));
}
if (pp.core_minimize()) {
if (num_core_min_threads == 1) {
m_core_minimizer_worker = alloc(core_minimizer_worker, *this, asms);
sl.push_child(&(m_core_minimizer_worker->limit()));
}
@ -1856,18 +1952,52 @@ namespace smt {
<< m_global_backbones_workers.size() << " global backbone threads.\n";);
m_batch_manager.initialize(num_global_bb_threads);
auto safe_run = [&](auto&& run_fn, reslimit& lim) {
try {
run_fn();
if (lim.is_canceled())
m_batch_manager.set_canceled();
} catch (z3_error &err) {
IF_VERBOSE(0, verbose_stream() << "Exception in parallel solver: " << err.what() << "\n");
if (!lim.is_canceled())
m_batch_manager.set_exception(err.error_code());
else
m_batch_manager.set_canceled();
} catch (z3_exception &ex) {
IF_VERBOSE(0, verbose_stream() << "Exception in parallel solver: " << ex.what() << "\n");
if (!lim.is_canceled() && !is_cancellation_exception(ex.what()))
m_batch_manager.set_exception(ex.what());
else
m_batch_manager.set_canceled();
} catch (...) {
IF_VERBOSE(0, verbose_stream() << "Unknown exception in parallel solver\n");
if (!lim.is_canceled())
m_batch_manager.set_exception("unknown exception");
else
m_batch_manager.set_canceled();
}
};
// Launch threads
vector<std::thread> threads(total_threads);
unsigned thread_idx = 0;
for (auto* w : m_workers)
threads[thread_idx++] = std::thread([&, w]() { w->run(); });
threads[thread_idx++] = std::thread([w, &safe_run]() {
safe_run([w]() { w->run(); }, w->limit());
});
if (m_sls_worker)
threads[thread_idx++] = std::thread([&]() { m_sls_worker->run(); });
threads[thread_idx++] = std::thread([this, &safe_run]() {
safe_run([this]() { m_sls_worker->run(); }, m_sls_worker->limit());
});
if (m_core_minimizer_worker)
threads[thread_idx++] = std::thread([&]() { m_core_minimizer_worker->run(); });
threads[thread_idx++] = std::thread([this, &safe_run]() {
safe_run([this]() { m_core_minimizer_worker->run(); }, m_core_minimizer_worker->limit());
});
for (auto* w : m_global_backbones_workers)
threads[thread_idx++] = std::thread([&, w]() { w->run(); });
threads[thread_idx++] = std::thread([w, &safe_run]() {
safe_run([w]() { w->run(); }, w->limit());
});
// Wait for all threads to finish
@ -1884,7 +2014,10 @@ namespace smt {
for (auto* bb_w : m_global_backbones_workers)
bb_w->collect_statistics(ctx.m_aux_stats);
return m_batch_manager.get_result();
lbool result = m_batch_manager.get_result();
if (result == l_undef && !m_batch_manager.get_reason_unknown().empty())
ctx.set_reason_unknown(m_batch_manager.get_reason_unknown().c_str());
return result;
}
} // namespace smt

View file

@ -32,6 +32,13 @@ namespace smt {
struct cube_config {
using literal = expr_ref;
static bool literal_is_null(expr_ref const& l) { return l == nullptr; }
static bool same_atom(expr_ref const& a, expr_ref const& b) {
expr* atom_a = a.get();
expr* atom_b = b.get();
a.get_manager().is_not(atom_a, atom_a);
b.get_manager().is_not(atom_b, atom_b);
return atom_a == atom_b;
}
static std::ostream& display_literal(std::ostream& out, expr_ref const& l) { return out << mk_bounded_pp(l, l.get_manager()); }
};
@ -74,6 +81,7 @@ namespace smt {
is_running,
is_sat,
is_unsat,
is_unknown,
is_exception_msg,
is_exception_code
};
@ -102,6 +110,7 @@ namespace smt {
unsigned m_exception_code = 0;
std::string m_exception_msg;
std::string m_reason_unknown;
vector<shared_clause> shared_clause_trail; // store all shared clauses with worker IDs
obj_hashtable<expr> shared_clause_set; // for duplicate filtering on per-thread clause expressions
@ -145,7 +154,11 @@ namespace smt {
w->cancel();
}
std::atomic<bool> m_canceled = false;
void cancel_background_threads() {
if (m_canceled.exchange(true))
return; // already canceled
cancel_workers();
cancel_sls_worker();
if (!p.m_global_backbones_workers.empty()) {
@ -171,9 +184,11 @@ namespace smt {
}
void backtrack_unlocked(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core,
node_lease const* lease = nullptr, vector<node_lease> const* targets = nullptr);
node_lease* lease = nullptr, vector<node_lease> const* targets = nullptr);
void collect_clause_unlocked(ast_translation &l2g, unsigned source_worker_id, expr *clause);
void release_lease_unlocked(unsigned worker_id, node* n);
void set_canceled_unlocked();
void release_worker_lease_unlocked(unsigned worker_id, node_lease& lease);
bool attempt_release_canceled_lease_unlocked(unsigned worker_id, node_lease& lease);
void cancel_closed_leases_unlocked(unsigned source_worker_id);
void collect_matching_targets_unlocked(node* source, expr* lit, vector<cube_config::literal> const& core,
vector<node_lease>& targets);
@ -187,6 +202,8 @@ namespace smt {
void set_unsat(ast_translation& l2g, expr_ref_vector const& unsat_core);
void set_sat(ast_translation& l2g, model& m);
void set_unknown(std::string const& reason);
void set_canceled();
void set_exception(std::string const& msg);
void set_exception(unsigned error_code);
void collect_statistics(::statistics& st) const;
@ -210,20 +227,21 @@ namespace smt {
}
bool get_cube(ast_translation& g2l, unsigned id, expr_ref_vector& cube, bool is_first_run, node_lease& lease);
void backtrack(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, node_lease const& lease);
void backtrack(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, node_lease& lease);
void enqueue_core_minimization(ast_translation& l2g, node* source, expr_ref_vector const& core);
bool wait_for_core_min_job(ast_translation& g2l, node*& source,
expr_ref_vector& core, reslimit& lim);
void publish_minimized_core(ast_translation& l2g, expr_ref_vector const& asms, node* source,
unsigned original_core_size, expr_ref_vector const& minimized_core);
void try_split(ast_translation& l2g, unsigned worker_id, node_lease const& lease, expr* atom, unsigned effort);
void release_lease(unsigned worker_id, node_lease const& lease);
void try_split(ast_translation& l2g, unsigned worker_id, node_lease& lease, expr* atom, unsigned effort);
bool checkpoint_worker(unsigned worker_id, node_lease& lease, bool& lease_canceled);
bool lease_canceled(node_lease const& lease);
void collect_clause(ast_translation& l2g, unsigned source_worker_id, expr* clause);
expr_ref_vector return_shared_clauses(ast_translation& g2l, unsigned& worker_limit, unsigned worker_id);
lbool get_result() const;
std::string const& get_reason_unknown() const { return m_reason_unknown; }
bool is_global_backbone_or_negation(ast_translation& l2g, expr* bb_cand) {
std::scoped_lock lock(mux);

View file

@ -22,12 +22,15 @@ Notes:
#include "ast/for_each_expr.h"
#include "ast/ast_pp.h"
#include "ast/func_decl_dependencies.h"
#include "smt/smt_context.h"
#include "smt/smt_kernel.h"
#include "params/smt_params.h"
#include "params/smt_params_helper.hpp"
#include "solver/solver_na2as.h"
#include "solver/mus.h"
#include <algorithm>
namespace {
class smt_solver : public solver_na2as {
@ -61,6 +64,7 @@ namespace {
smt_params m_smt_params;
smt::kernel m_context;
cuber* m_cuber;
random_gen m_rand;
symbol m_logic;
bool m_minimizing_core;
bool m_core_extend_patterns;
@ -84,16 +88,19 @@ namespace {
updt_params(p);
}
solver * translate(ast_manager & m, params_ref const & p) override {
ast_translation translator(get_manager(), m);
solver * translate(ast_manager & target, params_ref const & p) override {
ast_translation translator(get_manager(), target);
params_ref init;
init.copy(get_params());
init.copy(p);
smt_solver * result = alloc(smt_solver, m, p, m_logic);
smt_solver* result = alloc(smt_solver, target, init, m_logic);
smt::kernel::copy(m_context, result->m_context, true);
if (mc0())
if (mc0())
result->set_model_converter(mc0()->translate(translator));
for (auto & [k, v] : m_name2assertion) {
for (auto& [k, v] : m_name2assertion) {
expr* val = translator(k);
expr* key = translator(v);
result->assert_expr(val, key);
@ -212,6 +219,97 @@ namespace {
return m_context.get_trail(max_level);
}
expr_ref_vector get_assigned_literals() override {
expr_ref_vector result(m);
auto const& ctx = m_context.get_context();
for (auto lit : ctx.assigned_literals()) {
expr* atom = ctx.bool_var2expr(lit.var());
if (!atom)
continue;
result.push_back(lit.sign() ? m.mk_not(atom) : atom);
}
return result;
}
unsigned get_assign_level(expr* e) const override {
auto const& ctx = m_context.get_context();
get_manager().is_not(e, e);
if (!ctx.b_internalized(e))
return UINT_MAX;
return ctx.get_assign_level(ctx.get_bool_var(e));
}
bool is_relevant(expr* e) const override {
auto const& ctx = m_context.get_context();
get_manager().is_not(e, e);
return ctx.b_internalized(e) && ctx.is_relevant(e);
}
unsigned get_num_bool_vars() const override {
return m_context.get_context().get_num_bool_vars();
}
sat::bool_var get_bool_var(expr* e) const override {
auto const& ctx = m_context.get_context();
get_manager().is_not(e, e);
return ctx.b_internalized(e) ? ctx.get_bool_var(e) : sat::null_bool_var;
}
void pop_to_base_level() override {
m_context.pop_to_base_level();
}
void setup_for_parallel() override {
m_context.get_context().setup_for_parallel();
}
void set_preprocess(bool f) override {
m_context.set_preprocess(f);
}
void set_max_conflicts(unsigned max_conflicts) override {
auto& ctx = m_context.get_context();
ctx.get_fparams().m_max_conflicts = max_conflicts;
}
unsigned get_max_conflicts() const override {
return m_context.get_context().get_fparams().m_max_conflicts;
}
void get_backbone_candidates(vector<solver::scored_literal>& candidates, unsigned max_num) override {
ast_manager& m = get_manager();
auto& ctx = m_context.get_context();
unsigned curr_time = ctx.get_num_assignments();
vector<solver::scored_literal> all;
for (unsigned v = 0; v < ctx.get_num_bool_vars(); ++v) {
if (ctx.get_assignment(v) != l_undef && ctx.get_assign_level(v) == ctx.get_base_level())
continue;
expr* candidate = ctx.bool_var2expr(v);
if (!candidate)
continue;
auto const& d = ctx.get_bdata(v);
if (d.m_phase_available && !d.m_phase)
candidate = m.mk_not(candidate);
double age = static_cast<double>(curr_time - ctx.get_birthdate(v));
all.push_back(solver::scored_literal(m, candidate, age));
}
std::stable_sort(
all.begin(),
all.end(),
[](solver::scored_literal const& a, solver::scored_literal const& b) {
return a.score > b.score;
});
unsigned n = std::min<unsigned>(max_num, all.size());
for (unsigned i = 0; i < n; ++i)
candidates.push_back(all[i]);
}
void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause) override {
m_context.register_on_clause(ctx, on_clause);
}
@ -368,6 +466,39 @@ namespace {
return lits;
}
expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override {
ast_manager& m = get_manager();
auto& ctx = m_context.get_context();
obj_hashtable<expr> invalid_split_atoms_set;
for (expr* e : invalid_split_atoms) {
expr* atom = e;
m.is_not(e, atom);
invalid_split_atoms_set.insert(atom);
}
expr_ref result(m);
double score = 0.0;
unsigned n = 0;
ctx.pop_to_search_level();
for (unsigned v = 0; v < ctx.get_num_bool_vars(); ++v) {
if (ctx.get_assignment(v) != l_undef)
continue;
expr* e = ctx.bool_var2expr(v);
if (!e)
continue;
expr* atom = e;
m.is_not(e, atom);
if (invalid_split_atoms_set.contains(atom))
continue;
double new_score = ctx.get_activity(v);
if (new_score > score || !result || (new_score == score && m_rand(++n) == 0)) {
score = new_score;
result = e;
}
}
return result;
}
struct collect_fds_proc {
ast_manager & m;
func_decl_set & m_fds;
@ -537,4 +668,3 @@ public:
solver_factory * mk_smt_solver_factory() {
return alloc(smt_solver_factory);
}

View file

@ -31,7 +31,6 @@ Notes:
#include "solver/solver.h"
#include "solver/mus.h"
#include "solver/parallel_tactical.h"
#include "solver/parallel_tactical2.h"
#include "solver/parallel_params.hpp"
#include <mutex>
@ -431,8 +430,6 @@ static tactic * mk_seq_smt_tactic(ast_manager& m, params_ref const & p) {
tactic * mk_parallel_smt_tactic(ast_manager& m, params_ref const& p) {
parallel_params pp(p);
if (pp.enable2())
return mk_parallel_tactic2(mk_smt_solver(m, p, symbol::null), p);
return mk_parallel_tactic(mk_smt_solver(m, p, symbol::null), p);
}
@ -440,8 +437,6 @@ tactic * mk_smt_tactic_core(ast_manager& m, params_ref const& p, symbol const& l
parallel_params pp(p);
if (pp.enable())
return mk_parallel_tactic(mk_smt_solver(m, p, logic), p);
if (pp.enable2())
return mk_parallel_tactic2(mk_smt_solver(m, p, logic), p);
return mk_seq_smt_tactic(m, p);
}
@ -450,7 +445,7 @@ tactic * mk_smt_tactic_core_using(ast_manager& m, bool auto_config, params_ref c
params_ref p = _p;
p.set_bool("auto_config", auto_config);
tactic *t = nullptr;
if (pp.enable() || pp.enable2())
if (pp.enable())
t = mk_parallel_smt_tactic(m, p);
else
t = mk_seq_smt_tactic(m, p);

View file

@ -396,15 +396,15 @@ namespace smt {
}
final_check_status theory_array::assert_delayed_axioms() {
if (!m_params.m_array_delay_exp_axiom)
return FC_DONE;
final_check_status r = FC_DONE;
unsigned num_vars = get_num_vars();
for (unsigned v = 0; v < num_vars; ++v) {
var_data * d = m_var_data[v];
if (d->m_prop_upward && instantiate_axiom2b_for(v))
r = FC_CONTINUE;
}
if (m_params.m_array_delay_exp_axiom) {
unsigned num_vars = get_num_vars();
for (unsigned v = 0; v < num_vars; ++v) {
var_data *d = m_var_data[v];
if (d->m_prop_upward && instantiate_axiom2b_for(v))
r = FC_CONTINUE;
}
}
return r;
}

View file

@ -29,6 +29,7 @@ namespace smt {
unsigned m_num_map_axiom, m_num_default_map_axiom;
unsigned m_num_select_const_axiom, m_num_default_store_axiom, m_num_default_const_axiom, m_num_default_as_array_axiom;
unsigned m_num_select_as_array_axiom, m_num_default_lambda_axiom, m_num_choice_axiom;
unsigned m_num_select_lambda_axiom;
void reset() { memset(this, 0, sizeof(theory_array_stats)); }
theory_array_stats() { reset(); }
};

View file

@ -67,7 +67,6 @@ namespace smt {
return mk_select(num_args, args);
}
app * theory_array_base::mk_store(unsigned num_args, expr * const * args) {
return m.mk_app(get_family_id(), OP_STORE, 0, nullptr, num_args, args);
}
@ -279,7 +278,7 @@ namespace smt {
SASSERT(n1->get_num_args() == n2->get_num_args());
unsigned n = n1->get_num_args();
// skipping first argument of the select.
for(unsigned i = 1; i < n; ++i) {
for (unsigned i = 1; i < n; ++i) {
if (n1->get_arg(i)->get_root() != n2->get_arg(i)->get_root()) {
return false;
}
@ -295,9 +294,8 @@ namespace smt {
enode * r1 = v1->get_root();
enode * r2 = v2->get_root();
if (r1->get_class_size() > r2->get_class_size()) {
std::swap(r1, r2);
}
if (r1->get_class_size() > r2->get_class_size())
std::swap(r1, r2);
m_array_value.reset();
// populate m_array_value if the select(a, i) parent terms of r1
@ -335,7 +333,7 @@ namespace smt {
return false; // axiom was already instantiated
if (already_diseq(n1, n2))
return false;
m_extensionality_todo.push_back(std::make_pair(n1, n2));
m_extensionality_todo.push_back({n1, n2});
return true;
}
@ -348,7 +346,7 @@ namespace smt {
enode * nodes[2] = { a1, a2 };
if (!ctx.add_fingerprint(this, 1, 2, nodes))
return; // axiom was already instantiated
m_congruent_todo.push_back(std::make_pair(a1, a2));
m_congruent_todo.push_back({a1, a2});
}
@ -536,6 +534,7 @@ namespace smt {
unsigned num_vars = get_num_vars();
for (unsigned i = 0; i < num_vars; ++i) {
enode * n = get_enode(i);
TRACE(array, tout << enode_pp(n, ctx) << " is_relevant: " << ctx.is_relevant(n) << " is_array: " << is_array_sort(n) << "\n";);
if (!ctx.is_relevant(n) || !is_array_sort(n)) {
continue;
}
@ -581,11 +580,12 @@ namespace smt {
enode * n2 = get_enode(v2);
sort * s2 = n2->get_sort();
if (s1 == s2 && !ctx.is_diseq(n1, n2)) {
app * eq = mk_eq_atom(n1->get_expr(), n2->get_expr());
if (!ctx.b_internalized(eq) || !ctx.is_relevant(eq)) {
app_ref eq = app_ref(mk_eq_atom(n1->get_expr(), n2->get_expr()), m);
TRACE(array_bug, tout << "mk_interface_eqs: adding: " << eq << "\n";);
if (!ctx.b_internalized(eq.get()) || !ctx.is_relevant(eq.get())) {
result++;
ctx.internalize(eq, true);
ctx.mark_as_relevant(eq);
ctx.mark_as_relevant(eq.get());
}
}
}
@ -850,7 +850,7 @@ namespace smt {
if (i < num_args) {
SASSERT(!parent_sel_set->contains(sel) || (*(parent_sel_set->find(sel)))->get_root() == sel->get_root());
parent_sel_set->insert(sel);
todo.push_back(std::make_pair(parent_root, sel));
todo.push_back({parent_root, sel});
}
}
}

View file

@ -393,6 +393,10 @@ namespace smt {
SASSERT(is_map(map));
instantiate_select_map_axiom(s, map);
}
for (enode *lam : d_full->m_lambdas) {
SASSERT(is_lambda(lam->get_expr()));
instantiate_select_lambda_axiom(s, lam);
}
if (!m_params.m_array_delay_exp_axiom && d->m_prop_upward) {
for (enode * map : d_full->m_parent_maps) {
SASSERT(is_map(map));
@ -468,7 +472,6 @@ namespace smt {
SASSERT(map->get_num_args() > 0);
func_decl* f = to_func_decl(map->get_decl()->get_parameter(0).get_ast());
TRACE(array_map_bug, tout << "invoked instantiate_select_map_axiom\n";
tout << sl->get_owner_id() << " " << mp->get_owner_id() << "\n";
tout << mk_ismt2_pp(sl->get_expr(), m) << "\n" << mk_ismt2_pp(mp->get_expr(), m) << "\n";);
@ -518,6 +521,34 @@ namespace smt {
return try_assign_eq(sel1, sel2);
}
bool theory_array_full::instantiate_select_lambda_axiom(enode* sl, enode* lambda) {
app* select = sl->get_app();
SASSERT(is_select(select));
SASSERT(is_lambda(lambda->get_expr()));
SASSERT(lambda->get_sort() == sl->get_arg(0)->get_sort());
if (!ctx.add_fingerprint(lambda, lambda->get_owner_id(), sl->get_num_args() - 1, sl->get_args() + 1)) {
return false;
}
m_stats.m_num_select_lambda_axiom++;
unsigned num_args = select->get_num_args();
ptr_buffer<expr> args;
args.push_back(lambda->get_expr());
for (unsigned i = 1; i < num_args; ++i)
args.push_back(select->get_arg(i));
expr_ref sel1(m), sel2(m);
sel1 = mk_select(args.size(), args.data());
sel2 = sel1;
ctx.get_rewriter()(sel2);
ctx.internalize(sel1, false);
ctx.internalize(sel2, false);
TRACE(array, tout << mk_bounded_pp(sel1, m) << "\n==\n" << mk_bounded_pp(sel2, m) << "\n";);
return try_assign_eq(sel1, sel2);
}
//
//
@ -881,6 +912,7 @@ namespace smt {
st.update("array def as-array", m_stats.m_num_default_as_array_axiom);
st.update("array sel as-array", m_stats.m_num_select_as_array_axiom);
st.update("array def lambda", m_stats.m_num_default_lambda_axiom);
st.update("array sel lambda", m_stats.m_num_select_lambda_axiom);
st.update("array choice ax", m_stats.m_num_choice_axiom);
}
}

View file

@ -83,7 +83,7 @@ namespace smt {
bool instantiate_default_map_axiom(enode* map);
bool instantiate_default_as_array_axiom(enode* arr);
bool instantiate_default_lambda_def_axiom(enode* arr);
bool instantiate_select_lambda_axiom(enode *lambda);
bool instantiate_choice_axiom(enode* ch);
bool instantiate_parent_stores_default(theory_var v);
@ -96,6 +96,7 @@ namespace smt {
bool instantiate_select_const_axiom(enode* select, enode* cnst);
bool instantiate_select_as_array_axiom(enode* select, enode* arr);
bool instantiate_select_map_axiom(enode* select, enode* map);
bool instantiate_select_lambda_axiom(enode *select, enode *lambda);
bool instantiate_axiom_map_for(theory_var v);

View file

@ -437,11 +437,12 @@ namespace smt {
return lit == arg;
};
auto lit1 = clause.get(0);
[[maybe_unused]] auto lit2 = clause.get(1);
auto position = 0;
if (is_complement_to(is_true, lit1, e))
position = 0;
else {
SASSERT(is_complement_to(is_true, clause.get(1), e));
SASSERT(is_complement_to(is_true, lit2, e));
position = 1;
}

View file

@ -4044,7 +4044,7 @@ public:
if (!lp().is_feasible() || lp().has_changed_columns())
make_feasible();
vi = get_lpvar(v);
auto st = lp().maximize_term(vi, term_max);
auto st = lp().maximize_term(vi, term_max, /*fix_int_cols*/ true);
if (has_int() && lp().has_inf_int()) {
st = lp::lp_status::FEASIBLE;
lp().restore_x();

View file

@ -193,16 +193,16 @@ namespace smt {
void theory_nseq::new_eq_eh(theory_var v1, theory_var v2) {
try {
auto n1 = get_enode(v1);
auto n2 = get_enode(v2);
auto e1 = n1->get_expr();
auto e2 = n2->get_expr();
const auto n1 = get_enode(v1);
const auto n2 = get_enode(v2);
const auto e1 = n1->get_expr();
const auto e2 = n2->get_expr();
TRACE(seq, tout << mk_pp(e1, m) << " == " << mk_pp(e2, m) << "\n");
//std::cout << mk_pp(e1, m) << " == " << mk_pp(e2, m) << std::endl;
if (m_seq.is_re(e1)) {
expr_ref s(m);
auto r = m_rewriter.mk_symmetric_diff(e1, e2);
switch (m_rewriter.some_seq_in_re(r, s)) {
zstring s;
const auto r = m_rewriter.mk_symmetric_diff(e1, e2);
switch (m_rewriter.some_string_in_re(r, s)) {
case l_false:
// regexes are equivalent: nothing to do
return;
@ -237,15 +237,15 @@ namespace smt {
}
void theory_nseq::new_diseq_eh(theory_var v1, theory_var v2) {
auto n1 = get_enode(v1);
auto n2 = get_enode(v2);
auto e1 = n1->get_expr();
auto e2 = n2->get_expr();
const auto n1 = get_enode(v1);
const auto n2 = get_enode(v2);
const auto e1 = n1->get_expr();
const auto e2 = n2->get_expr();
TRACE(seq, tout << mk_pp(e1, m) << " != " << mk_pp(e2, m) << "\n");
if (m_seq.is_re(e1)) {
expr_ref s(m);
auto r = m_rewriter.mk_symmetric_diff(e1, e2);
switch (m_rewriter.some_seq_in_re(r, s)) {
zstring s;
auto r = m_rewriter.mk_symmetric_diff(e1, e2);
switch (m_rewriter.some_string_in_re(r, s)) {
case l_false: {
enode_pair_vector eqs;
const auto lit = mk_eq(e1, e2, false);

View file

@ -66,7 +66,6 @@ namespace smt {
unsigned m_final_check_ls_steps = 30000;
unsigned m_final_check_ls_steps_delta = 10000;
unsigned m_final_check_ls_steps_min = 10000;
unsigned m_final_check_ls_steps_max = 30000;
bool m_has_unassigned_clause_after_resolve = false;
unsigned m_after_resolve_decide_gap = 4;
unsigned m_after_resolve_decide_count = 0;

View file

@ -5,7 +5,6 @@ z3_add_component(solver
combined_solver.cpp
mus.cpp
parallel_tactical.cpp
parallel_tactical2.cpp
simplifier_solver.cpp
slice_solver.cpp
smt_logics.cpp

View file

@ -185,12 +185,12 @@ class asserted_formulas {
public: \
FUNCTOR m_functor; \
NAME(asserted_formulas& af):simplify_fmls(af, MSG), m_functor ARG {} \
virtual void simplify(justified_expr const& j, expr_ref& n, proof_ref& p) { \
m_functor(j.fml(), n, p); \
void simplify(justified_expr const& j, expr_ref& n, proof_ref& p) override { \
m_functor(j.fml(), n, p); \
} \
virtual void post_op() { if (REDUCE) af.reduce_and_solve(); } \
virtual bool should_apply() const { return APP; } \
}; \
void post_op() override { if (REDUCE) af.reduce_and_solve(); } \
bool should_apply() const override { return APP; } \
};
#define MK_SIMPLIFIERF(NAME, FUNCTOR, MSG, APP, REDUCE) MK_SIMPLIFIERA(NAME, FUNCTOR, MSG, APP, (af.m), REDUCE)

View file

@ -4,8 +4,11 @@ def_module_params('parallel',
export=True,
params=(
('enable', BOOL, False, 'enable parallel solver by default on selected tactics (for QF_BV)'),
('enable2', BOOL, False, 'enable (experimental) parallel solver by default on selected tactics (for QF_BV)'),
('threads.max', UINT, 10000, 'caps maximal number of threads below the number of processors'),
('num_bb_threads', UINT, 2, 'run Janota-style chunking backbone worker threads; default is 2 (negative and positive mode), supported values are 0 (off), 1 (negative mode only) or 2 (negative and positive mode)'),
('core_minimize', BOOL, True, 'minimize unsat cores used for parallel cube backtracking'),
('ablate_backtracking', BOOL, False, 'ablation: pass entire cube as core instead of unsat core during backtracking'),
('cube.lookahead', BOOL, False, 'use lookahead cubing in the parallel solver; when false, use VSIDS activity to select one split literal'),
('conquer.batch_size', UINT, 100, 'number of cubes to batch together for fast conquer'),
('conquer.restart.max', UINT, 5, 'maximal number of restarts during conquer phase'),
('conquer.delay', UINT, 10, 'delay of cubes until applying conquer'),

Some files were not shown because too many files have changed in this diff Show more