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

Bug/Performance fixes

Added argument to harvest non-primitive membership constraint benchmarks
This commit is contained in:
CEisenhofer 2026-07-01 14:47:36 +02:00
parent f5baba1068
commit 1f448a10a3
9 changed files with 314 additions and 13 deletions

View file

@ -64,6 +64,8 @@ void smt_params::updt_local_params(params_ref const & _p) {
m_nseq_signature = p.nseq_signature();
m_nseq_axiomatize_diseq = p.nseq_axiomatize_diseq();
m_nseq_eager = p.nseq_eager();
m_nseq_harvest = p.nseq_harvest();
m_nseq_harvest_dir = p.nseq_harvest_dir();
m_up_persist_clauses = p.up_persist_clauses();
validate_string_solver(m_string_solver);
if (_p.get_bool("arith.greatest_error_pivot", false))
@ -181,6 +183,7 @@ void smt_params::display(std::ostream & out) const {
DISPLAY_PARAM(m_nseq_regex_factorization_eager);
DISPLAY_PARAM(m_nseq_regex_dynamic_decomposition);
DISPLAY_PARAM(m_nseq_axiomatize_diseq);
DISPLAY_PARAM(m_nseq_harvest);
DISPLAY_PARAM(m_profile_res_sub);
DISPLAY_PARAM(m_display_bool_var2expr);

View file

@ -259,6 +259,8 @@ struct smt_params : public preprocessor_params,
bool m_nseq_signature = false;
bool m_nseq_axiomatize_diseq = false;
bool m_nseq_eager = true;
unsigned m_nseq_harvest = 0;
symbol m_nseq_harvest_dir;
smt_params(params_ref const & p = params_ref()):
m_string_solver(symbol("auto")){

View file

@ -143,6 +143,8 @@ def_module_params(module_name='smt',
('nseq.signature', BOOL, False, 'enable heuristic signature-based string equation splitting in Nielsen solver'),
('nseq.axiomatize_diseq', BOOL, False, 'eagerly axiomatize sequence disequalities'),
('nseq.eager', BOOL, True, 'enable the incremental eager structural Nielsen closure during propagation, detecting conflicts before final_check'),
('nseq.harvest', UINT, 0, 'benchmark-harvest mode: bound on non-progress Nielsen extension steps before dumping the current node as an .smt2 benchmark; 0 = disabled (normal sound reasoning). WARNING: intentionally unsound, for benchmark generation only'),
('nseq.harvest_dir', SYMBOL, '.', 'output directory for harvested .smt2 benchmarks (used only when nseq.harvest > 0)'),
('core.validate', BOOL, False, '[internal] validate unsat core produced by SMT context. This option is intended for debugging'),
('seq.split_w_len', BOOL, True, 'enable splitting guided by length constraints'),
('seq.validate', BOOL, False, 'enable self-validation of theory axioms created by seq theory'),

View file

@ -134,7 +134,8 @@ namespace seq {
// (sigma from the paper): head ∈ Δ ∧ tail ∈ ∇ for each ⟨Δ,∇⟩, with the
// lookahead oracle pruning non-viable ∇ during generation.
seq_rewriter rw(m);
if (!rw.split(regex->get_expr(), result, threshold, split_mode::strong, oracle)) {
// "strong" might cause explosive behavior; better do this only in the saturation
if (!rw.split(regex->get_expr(), result, threshold, split_mode::weak, oracle)) {
result.clear();
return { nullptr, nullptr };
}
@ -2030,6 +2031,25 @@ namespace seq {
// solver at the base level, so they are visible during all feasibility checks.
assert_root_constraints_to_solver();
if (harvest_mode()) {
// Benchmark-harvest mode: a single fixed-depth DFS pass at the harvest
// bound (no iterative deepening, to avoid re-harvesting / divergence).
// Every branch reaching the bound dumps a snapshot of the current node
// and returns unknown; SAT leaves also return unknown (see search_dfs),
// so the whole pass returns unknown. theory_nseq turns this into a
// blocking clause to force the SAT solver onto a different assignment.
m_depth_bound = m_harvest;
ptr_vector<nielsen_edge> cur_path;
m_siblings.clear();
const unsigned before = m_stats.m_num_dfs_nodes;
search_dfs(m_root, cur_path);
IF_VERBOSE(2, verbose_stream() << "nseq harvest: DFS explored "
<< (m_stats.m_num_dfs_nodes - before) << " nodes (bound="
<< m_depth_bound << ")\n");
++m_stats.m_num_unknown;
return search_result::unknown;
}
// Iterative deepening: increment by 1 on each failure.
// m_max_search_depth == 0 means unlimited; otherwise stop when bound exceeds it.
m_depth_bound = 3;
@ -2360,6 +2380,12 @@ namespace seq {
SASSERT(!node->is_currently_conflict());
if (node->is_satisfied()) {
// Benchmark-harvest mode: a satisfied node has only primitive memberships
// (uninteresting for the regex factorization benchmark) — do NOT declare SAT
// (that would terminate the harvest loop) and do NOT harvest; just dead-end
// this branch so the search keeps exploring others.
if (harvest_mode())
return search_result::unknown;
// Before declaring SAT, check leaf-node regex feasibility:
// for each variable with multiple regex constraints, verify
// that the intersection of all its regexes is non-empty.
@ -2424,14 +2450,31 @@ namespace seq {
}
// depth bound check
if (depth >= m_depth_bound)
if (depth >= m_depth_bound) {
// Benchmark-harvest mode: this node has reached the configured number of
// non-progress extension steps and is a genuine intermediate state
// (post-simplify, post-Parikh, not a conflict, not satisfied) whose
// memberships are the rewritten (often non-primitive) ones we want.
// Dump the snapshot, then backtrack.
if (harvest_mode())
harvest_node(node);
return search_result::unknown;
}
SASSERT(!node->is_currently_conflict());
// generate extensions only once per node; children persist across runs
if (!node->is_extended()) {
const bool ext = generate_extensions(node);
// Benchmark-harvest mode: with all regex modifiers disabled, a node whose
// only remaining work is on (non-primitive) memberships has no applicable
// word-equation modifier, so generate_extensions yields nothing. This is a
// harvest leaf: dump the snapshot and backtrack instead of failing the
// VERIFY(ext) below.
if (harvest_mode() && !ext) {
harvest_node(node);
return search_result::unknown;
}
IF_VERBOSE(1, display(verbose_stream(), node));
CTRACE(seq, !ext, display(tout, node) << to_dot() << "\n");
if (!ext) {
@ -3329,11 +3372,13 @@ namespace seq {
return ++m_stats.m_mod_eq_split, true;
// Priority 5b: CycleSubsumption - eliminate leading variable subsumed by stabilizer
if (apply_cycle_subsumption(node))
// (regex-related: skipped in benchmark-harvest mode)
if (!harvest_mode() && apply_cycle_subsumption(node))
return ++m_stats.m_mod_cycle_subsumption, true;
// Priority 6: CycleDecomp - stabilizer introduction for regex cycles using partial DFA projection
if (apply_cycle_decomposition(node))
// (regex-related: skipped in benchmark-harvest mode)
if (!harvest_mode() && apply_cycle_decomposition(node))
return ++m_stats.m_mod_star_intr, true;
// Priority 7: GPowerIntr - ground power introduction
@ -3341,7 +3386,8 @@ namespace seq {
return ++m_stats.m_mod_gpower_intr, true;
// Priority 8: Regex Factorization
if (apply_regex_factorization(node))
// (regex-related: skipped in benchmark-harvest mode)
if (!harvest_mode() && apply_regex_factorization(node))
return ++m_stats.m_mod_regex_factorization, true;
// Priority 8b: ConstNielsen - char vs var (2 children)
@ -3349,7 +3395,8 @@ namespace seq {
return ++m_stats.m_mod_const_nielsen, true;
// Priority 9: RegexIfSplit - split str_mem s ∈ ite(c,th,el) by branching on c
if (apply_regex_if_split(node))
// (regex-related: skipped in benchmark-harvest mode)
if (!harvest_mode() && apply_regex_if_split(node))
return ++m_stats.m_mod_regex_if_split, true;
// Priority 9b: SignatureSplit - heuristic string equation splitting
@ -3357,7 +3404,8 @@ namespace seq {
return ++m_stats.m_mod_signature_split, true;
// Priority 10: RegexVarSplit - split str_mem by minterms
if (apply_regex_var_split(node))
// (regex-related: skipped in benchmark-harvest mode)
if (!harvest_mode() && apply_regex_var_split(node))
return ++m_stats.m_mod_regex_var_split, true;
// Priority 11: PowerSplit - power unwinding with bounded prefix
@ -3373,11 +3421,16 @@ namespace seq {
return ++m_stats.m_mod_var_num_unwinding_eq, true;
// Priority 14: variable power unwinding for membership constraints
if (apply_var_num_unwinding_mem(node))
// (regex/membership-related: skipped in benchmark-harvest mode)
if (!harvest_mode() && apply_var_num_unwinding_mem(node))
return ++m_stats.m_mod_var_num_unwinding_mem, true;
// let's unwindind a disequality
if (axiomatize_diseq(node))
// (axiomatize_diseq requires the node to be satisfied-except-diseq; in
// benchmark-harvest mode a node may still carry non-primitive memberships
// because the regex modifiers are disabled, so skip it and let the caller
// treat the empty result as a harvest leaf)
if (!harvest_mode() && axiomatize_diseq(node))
return ++m_stats.m_ax_diseq, true;
return false;

View file

@ -886,6 +886,10 @@ namespace seq {
unsigned m_regex_factorization_threshold = 1;
bool m_regex_factorization_eager = false;
bool m_regex_dynamic_decomposition = true;
unsigned m_harvest = 0; // 0 = disabled (benchmark-harvest bound)
std::string m_harvest_dir = ".";
unsigned m_harvest_counter = 0; // file index; spans reset()/blocking iterations
std::unordered_set<unsigned> m_harvested_hashes; // dedup by structural hash; NOT cleared in reset()
unsigned m_fresh_cnt = 0;
nielsen_stats m_stats;
@ -1072,6 +1076,23 @@ namespace seq {
void set_regex_factorization_eager(bool e) { m_regex_factorization_eager = e; }
void set_regex_dynamic_decomposition(bool e) { m_regex_dynamic_decomposition = e; }
// benchmark-harvest mode (intentionally unsound; for benchmark generation only).
// m_harvest > 0 = bound on non-progress Nielsen extension steps before dumping the
// current node as an .smt2 benchmark; 0 = disabled (normal sound reasoning).
void set_harvest(unsigned b) { m_harvest = b; }
void set_harvest_dir(std::string d) { m_harvest_dir = std::move(d); }
bool harvest_mode() const { return m_harvest > 0; }
// write a snapshot of `n` (regex memberships + residual word equations) to an
// .smt2 file, deduplicated by structural hash. Terms are cleaned for portability
// (see clean_harvest_expr); the internal integer/length constraints are dropped.
void harvest_node(nielsen_node const* n);
// Rewrite `e` into portable SMT-LIB: peel seq.slice(...) applications to their
// root variable (first argument, chased until non-slice — matching the sgraph
// hashing convention). Sets ok=false if the term still contains an nseq-internal
// skolem that cannot be represented (e.g. a power token); the node is then skipped.
expr_ref clean_harvest_expr(expr* e, bool& ok);
// display for debugging
std::ostream& display(std::ostream& out) const;

View file

@ -19,8 +19,13 @@ Author:
#include "smt/seq/seq_nielsen.h"
#include "ast/arith_decl_plugin.h"
#include "ast/ast_pp.h"
#include "ast/ast_smt2_pp.h"
#include "ast/decl_collector.h"
#include "util/obj_hashtable.h"
#include "util/trace.h"
#include <sstream>
#include <fstream>
#include <iomanip>
namespace seq {
@ -88,6 +93,157 @@ namespace seq {
return out;
}
// Rewrite `e` into portable SMT-LIB: peel seq.slice(...) applications to their root
// variable (first argument, chased until non-slice — matching the sgraph hashing
// convention in euf_sgraph.cpp), rebuilding all other applications with cleaned args.
// Sets ok=false if any remaining subterm is an nseq-internal skolem we cannot
// represent (e.g. a power token, a Parikh counter); the node is then skipped.
expr_ref nielsen_graph::clean_harvest_expr(expr* e, bool& ok) {
if (!ok)
return expr_ref(m);
// peel slice wrappers down to the underlying root variable
while (m_sk.is_slice(e))
e = to_app(e)->get_arg(0);
if (!is_app(e)) { // quantifiers / vars should not appear here
ok = false;
return expr_ref(e, m);
}
app* a = to_app(e);
// a leaf that is still an nseq-internal skolem (e.g. a power) cannot be emitted
if (a->get_num_args() == 0) {
if (m_sk.is_skolem(e)) {
ok = false;
return expr_ref(e, m);
}
return expr_ref(e, m);
}
expr_ref_vector args(m);
for (expr* arg : *a) {
expr_ref c = clean_harvest_expr(arg, ok);
if (!ok)
return expr_ref(m);
args.push_back(c);
}
return expr_ref(m.mk_app(a->get_decl(), args.size(), args.data()), m);
}
// Benchmark-harvest: dump a snapshot of `n` (regex memberships + residual word
// equations) as a self-contained, portable SMT-LIB2 benchmark. Terms are cleaned
// via clean_harvest_expr (slice tails -> root variables); the internal
// integer/length side constraints are dropped (they carry non-portable Parikh /
// slice-length skolems and are implied by the memberships). Deduplicated by
// structural node hash. Intended purely for benchmark generation; the surrounding
// harvest mode is unsound.
void nielsen_graph::harvest_node(nielsen_node const* n) {
// dedup by structural hash (computed earlier in search_dfs)
const unsigned h = n->hash();
IF_VERBOSE(2, verbose_stream() << "nseq harvest: node " << n->id()
<< " hash=" << h << " #eq=" << n->str_eqs().size()
<< " #mem=" << n->str_mems().size() << "\n");
if (!m_harvested_hashes.insert(h).second) {
IF_VERBOSE(2, verbose_stream() << "nseq harvest: deduped\n");
return;
}
// only interesting if there is at least one membership
if (n->str_mems().empty()) {
IF_VERBOSE(2, verbose_stream() << "nseq harvest: no memberships, skip\n");
return;
}
// clean every term first; skip the whole node if anything is unrepresentable
bool ok = true;
vector<std::pair<expr_ref, expr_ref>> eqs; // cleaned word equations
vector<std::pair<expr_ref, expr_ref>> mems; // cleaned memberships (str, regex)
for (auto const& eq : n->str_eqs()) {
if (eq.is_trivial())
continue;
expr_ref l = clean_harvest_expr(eq.m_lhs->get_expr(), ok);
expr_ref r = clean_harvest_expr(eq.m_rhs->get_expr(), ok);
if (!ok) break;
eqs.push_back({l, r});
}
for (auto const& mem : n->str_mems()) {
if (!ok) break;
expr_ref s = clean_harvest_expr(mem.m_str->get_expr(), ok);
expr_ref re = clean_harvest_expr(mem.m_regex->get_expr(), ok);
if (!ok) break;
mems.push_back({s, re});
}
if (!ok) {
IF_VERBOSE(2, verbose_stream() << "nseq harvest: unrepresentable term, skip\n");
return;
}
std::ostringstream fname;
fname << m_harvest_dir << "/harvest_"
<< std::setw(6) << std::setfill('0') << m_harvest_counter << ".smt2";
std::ofstream out(fname.str());
if (!out) {
// fall back to the current working directory on open failure
std::ostringstream alt;
alt << "harvest_" << std::setw(6) << std::setfill('0') << m_harvest_counter << ".smt2";
out.open(alt.str());
if (!out) {
IF_VERBOSE(0, verbose_stream()
<< "nseq harvest: could not open output file " << fname.str() << "\n");
return;
}
}
++m_harvest_counter;
// collect all uninterpreted declarations used by the cleaned snapshot
decl_collector coll(m);
for (auto const& [l, r] : eqs) {
coll.visit(l);
coll.visit(r);
}
for (auto const& [s, re] : mems) {
coll.visit(s);
coll.visit(re);
}
coll.order_deps(0);
smt2_pp_environment_dbg env(m);
params_ref pp;
out << "; harvested from nseq node " << n->id() << " (hash " << h << ")\n";
out << "(set-logic QF_S)\n";
// user-defined (uninterpreted) sorts, if any
for (sort* s : coll.get_sorts()) {
if (s->get_family_id() == null_family_id)
out << "(declare-sort " << s->get_name() << " 0)\n";
}
// function/constant declarations
for (func_decl* f : coll.get_func_decls()) {
if (coll.should_declare(f)) {
ast_smt2_pp(out, f, env, pp);
out << "\n";
}
}
// residual word equations
for (auto const& [l, r] : eqs) {
out << "(assert (= ";
ast_smt2_pp(out, l, env, pp);
out << " ";
ast_smt2_pp(out, r, env, pp);
out << "))\n";
}
// regex memberships (the rewritten, often non-primitive, ones we are after)
for (auto const& [s, re] : mems) {
out << "(assert (str.in_re ";
ast_smt2_pp(out, s, env, pp);
out << " ";
ast_smt2_pp(out, re, env, pp);
out << "))\n";
}
out << "(check-sat)\n";
IF_VERBOSE(1, verbose_stream() << "nseq harvest: wrote " << fname.str()
<< " (" << mems.size() << " memberships)\n");
}
std::ostream& nielsen_graph::display(std::ostream& out) const {
out << "nielsen_graph with " << m_nodes.size() << " nodes, "
<< m_edges.size() << " edges\n";

View file

@ -518,6 +518,10 @@ namespace smt {
void theory_nseq::eager_structural_check() {
if (!get_fparams().m_nseq_eager)
return;
// Benchmark-harvest mode must see the full constraint set in final_check; the
// eager closure could resolve conflicts during propagation and bypass harvesting.
if (get_fparams().m_nseq_harvest)
return;
// Only re-run when the Nielsen-relevant constraint set actually grew.
if (m_eager_dirty == m_eager_seen)
return;
@ -1001,6 +1005,8 @@ namespace smt {
m_nielsen.set_signature_split(get_fparams().m_nseq_signature);
m_nielsen.set_regex_factorization_threshold(get_fparams().m_nseq_regex_factorization_threshold);
m_nielsen.set_regex_factorization_eager(get_fparams().m_nseq_regex_factorization_eager);
m_nielsen.set_harvest(get_fparams().m_nseq_harvest);
m_nielsen.set_harvest_dir(get_fparams().m_nseq_harvest_dir.str());
// assert length constraints derived from string equalities
if (assert_length_constraints()) {
@ -1034,7 +1040,10 @@ namespace smt {
// Regex membership pre-check: before running DFS, check intersection
// emptiness for each variable's regex constraints. This handles
// regex-only problems that the DFS cannot efficiently solve.
if (!m_nielsen.root()->is_currently_conflict() && get_fparams().m_nseq_regex_precheck) {
// In benchmark-harvest mode the pre-check would short-circuit SAT/UNSAT on
// the regex memberships before any word-equation rewriting happens, so skip it.
if (!m_nielsen.root()->is_currently_conflict() && get_fparams().m_nseq_regex_precheck
&& !get_fparams().m_nseq_harvest) {
switch (check_regex_memberships_precheck()) {
case l_true:
// conflict was asserted inside check_regex_memberships_precheck
@ -1099,6 +1108,19 @@ namespace smt {
return FC_DONE;
}
// Benchmark-harvest mode: solve() returns unknown after dumping the snapshots
// reachable under the current assignment. Block the current assignment so the
// SAT solver moves on to a different set of primitive constraints to harvest
// from. This is intentionally unsound (the run only ends once the SAT solver
// exhausts all assignments, yielding overall unsat) — for benchmark generation.
if (get_fparams().m_nseq_harvest) {
IF_VERBOSE(1, verbose_stream() << "nseq final_check: harvest, blocking assignment\n";);
if (block_current_assignment())
return FC_CONTINUE;
// nothing left to block → let the search conclude
return FC_GIVEUP;
}
TRACE(seq, display(tout << "unknown\n"));
IF_VERBOSE(1, verbose_stream() << "nseq final_check: solve UNKNOWN, FC_GIVEUP\n";);
return FC_GIVEUP;
@ -1160,6 +1182,44 @@ namespace smt {
return all_sat;
}
// -----------------------------------------------------------------------
// Benchmark-harvest: block the current assignment
// -----------------------------------------------------------------------
bool theory_nseq::block_current_assignment() {
// Build a clause that excludes the current assignment of the constraint
// literals (memberships, disequalities) and equalities feeding the Nielsen
// graph, so the SAT solver moves on to a different combination.
literal_vector clause;
for (auto const& item : m_prop_queue) {
if (std::holds_alternative<eq_item>(item)) {
auto const& eq = std::get<eq_item>(item);
// the two terms are currently equal in the egraph; forbid that
clause.push_back(~mk_eq(eq.m_l->get_expr(), eq.m_r->get_expr(), false));
}
else if (std::holds_alternative<deq_item>(item)) {
auto const& deq = std::get<deq_item>(item);
switch (ctx.get_assignment(deq.lit)) {
case l_true: clause.push_back(~deq.lit); break;
case l_false: clause.push_back(deq.lit); break;
default: break;
}
}
else if (std::holds_alternative<mem_item>(item)) {
auto const& mem = std::get<mem_item>(item);
switch (ctx.get_assignment(mem.lit)) {
case l_true: clause.push_back(~mem.lit); break;
case l_false: clause.push_back(mem.lit); break;
default: break;
}
}
}
if (clause.empty())
return false;
ctx.mk_th_axiom(get_id(), clause.size(), clause.data());
return true;
}
// -----------------------------------------------------------------------
// Conflict explanation
// -----------------------------------------------------------------------

View file

@ -148,6 +148,10 @@ namespace smt {
void populate_nielsen_graph();
void eager_structural_check();
void explain_nielsen_conflict();
// benchmark-harvest: block the current assignment of membership/equation
// literals so the SAT solver tries a different one. Returns false if there
// is nothing to block (empty clause). Intentionally unsound.
bool block_current_assignment();
void set_conflict(enode_pair_vector const& eqs, literal_vector const& lits) const;
void set_conflict(literal_vector const& lits) {
const enode_pair_vector eqs;