3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-04 22:36:10 +00:00

Use subsolvers bounds rather than computing them inside nseq

This commit is contained in:
CEisenhofer 2026-03-25 18:53:13 +01:00
parent af4677950b
commit 813a06fa38
5 changed files with 169 additions and 169 deletions

View file

@ -20,6 +20,7 @@ Author:
#include "smt/seq/seq_nielsen.h" #include "smt/seq/seq_nielsen.h"
#include "smt/smt_kernel.h" #include "smt/smt_kernel.h"
#include "smt/smt_arith_value.h"
#include "params/smt_params.h" #include "params/smt_params.h"
namespace smt { namespace smt {
@ -32,6 +33,7 @@ namespace smt {
class context_solver : public seq::simple_solver { class context_solver : public seq::simple_solver {
smt_params m_params; // must be declared before m_kernel smt_params m_params; // must be declared before m_kernel
smt::kernel m_kernel; smt::kernel m_kernel;
arith_value m_arith_value;
static smt_params make_seq_len_params() { static smt_params make_seq_len_params() {
smt_params p; smt_params p;
@ -42,7 +44,10 @@ namespace smt {
public: public:
context_solver(ast_manager& m) : context_solver(ast_manager& m) :
m_params(make_seq_len_params()), m_params(make_seq_len_params()),
m_kernel(m, m_params) {} m_kernel(m, m_params),
m_arith_value(m) {
m_arith_value.init(&m_kernel.get_context());
}
lbool check() override { lbool check() override {
// std::cout << "Checking:\n"; // std::cout << "Checking:\n";
@ -69,8 +74,19 @@ namespace smt {
m_kernel.get_model(mdl); m_kernel.get_model(mdl);
} }
bool lower_bound(expr* e, rational& lo) const override {
bool is_strict = true;
return m_arith_value.get_lo(e, lo, is_strict) && !is_strict && lo.is_int();
}
bool upper_bound(expr* e, rational& hi) const override {
bool is_strict = true;
return m_arith_value.get_up(e, hi, is_strict) && !is_strict && hi.is_int();
}
void reset() override { void reset() override {
m_kernel.reset(); m_kernel.reset();
m_arith_value.init(&m_kernel.get_context());
} }
}; };

View file

@ -163,10 +163,9 @@ Abstract:
- IntEq / IntLe: integer equality and inequality constraints over Presburger - IntEq / IntLe: integer equality and inequality constraints over Presburger
arithmetic polynomials (PDD<BigInteger>) are entirely absent. The Z3 port arithmetic polynomials (PDD<BigInteger>) are entirely absent. The Z3 port
has no ConstraintsIntEq or ConstraintsIntLe in nielsen_node. has no ConstraintsIntEq or ConstraintsIntLe in nielsen_node.
- IntBounds / VarBoundWatcher: per-variable integer interval bounds and the - IntBounds / VarBoundWatcher: ZIPT-style cached interval maps and eager
watcher mechanism that reruns bound propagation when a string variable is watcher propagation are not stored in nielsen_node; bounds are queried
substituted PORTED as nielsen_node::{add_lower_int_bound, from the arithmetic subsolver on demand.
add_upper_int_bound, watch_var_bounds, init_var_bounds_from_mems}.
- AddLowerIntBound() / AddHigherIntBound(): incremental interval tightening - AddLowerIntBound() / AddHigherIntBound(): incremental interval tightening
PORTED as the above add_lower/upper_int_bound methods. PORTED as the above add_lower/upper_int_bound methods.
@ -238,11 +237,10 @@ Author:
#include "util/dependency.h" #include "util/dependency.h"
#include "util/map.h" #include "util/map.h"
#include "util/lbool.h" #include "util/lbool.h"
#include "util/rational.h"
#include "ast/ast.h" #include "ast/ast.h"
#include "ast/arith_decl_plugin.h"
#include "ast/seq_decl_plugin.h" #include "ast/seq_decl_plugin.h"
#include "ast/euf/euf_sgraph.h" #include "ast/euf/euf_sgraph.h"
#include <functional>
#include <map> #include <map>
#include "model/model.h" #include "model/model.h"
@ -280,6 +278,10 @@ namespace seq {
virtual void push() = 0; virtual void push() = 0;
virtual void pop(unsigned num_scopes) = 0; virtual void pop(unsigned num_scopes) = 0;
virtual void get_model(model_ref& mdl) { mdl = nullptr; } virtual void get_model(model_ref& mdl) { mdl = nullptr; }
// Optional bound queries on arithmetic expressions (non-strict integer bounds).
// Default implementation reports "unsupported".
virtual bool lower_bound(expr* e, rational& lo) const { return false; }
virtual bool upper_bound(expr* e, rational& hi) const { return false; }
virtual void reset() = 0; virtual void reset() = 0;
}; };
@ -524,11 +526,6 @@ namespace seq {
vector<str_mem> m_str_mem; // regex memberships vector<str_mem> m_str_mem; // regex memberships
vector<constraint> m_constraints; // integer equalities/inequalities (mirrors ZIPT's IntEq/IntLe) vector<constraint> m_constraints; // integer equalities/inequalities (mirrors ZIPT's IntEq/IntLe)
// per-variable integer bounds for len(var). Mirrors ZIPT's IntBounds.
// key: snode id of the string variable.
// default lb = 0 (unrestricted); default ub = UINT_MAX (unrestricted).
u_map<unsigned> m_var_lb; // lower bound: lb <= len(var)
u_map<unsigned> m_var_ub; // upper bound: len(var) <= ub
// character constraints (mirrors ZIPT's DisEqualities and CharRanges) // character constraints (mirrors ZIPT's DisEqualities and CharRanges)
// key: snode id of the s_unit symbolic character // key: snode id of the s_unit symbolic character
@ -585,27 +582,11 @@ namespace seq {
vector<constraint> const& constraints() const { return m_constraints; } vector<constraint> const& constraints() const { return m_constraints; }
vector<constraint>& constraints() { return m_constraints; } vector<constraint>& constraints() { return m_constraints; }
// IntBounds: tighten the lower bound for len(var). // Query current bounds for a variable from the arithmetic subsolver.
// Returns true if the bound was tightened (lb > current lower bound). // Falls der Subsolver keinen Bound liefert, werden konservative Defaults
// When tightened without conflict, adds a constraint len(var) >= lb. // 0 / UINT_MAX verwendet.
// When lb > current upper bound, sets arithmetic conflict (no constraint added) bool lower_bound(expr* e, rational& lo) const;
// and still returns true (the bound value changed). Check is_general_conflict() bool upper_bound(expr* e, rational& up) const;
// separately to distinguish tightening-with-conflict from normal tightening.
// Mirrors ZIPT's AddLowerIntBound().
bool set_lower_int_bound(euf::snode* var, unsigned lb, dep_tracker const& dep);
// IntBounds: tighten the upper bound for len(var).
// Returns true if the bound was tightened (ub < current upper bound).
// When tightened without conflict, adds a constraint len(var) <= ub.
// When current lower bound > ub, sets arithmetic conflict (no constraint added)
// and still returns true (the bound value changed). Check is_general_conflict()
// separately to distinguish tightening-with-conflict from normal tightening.
// Mirrors ZIPT's AddHigherIntBound().
bool set_upper_int_bound(euf::snode* var, unsigned ub, dep_tracker const& dep);
// Query current bounds for a variable (default: 0 / UINT_MAX if not set).
unsigned var_lb(euf::snode* var) const;
unsigned var_ub(euf::snode* var) const;
// character constraint access (mirrors ZIPT's DisEqualities / CharRanges) // character constraint access (mirrors ZIPT's DisEqualities / CharRanges)
u_map<ptr_vector<euf::snode>> const& char_diseqs() const { return m_char_diseqs; } u_map<ptr_vector<euf::snode>> const& char_diseqs() const { return m_char_diseqs; }
@ -695,18 +676,7 @@ namespace seq {
bool handle_empty_side(euf::sgraph& sg, euf::snode* non_empty_side, bool handle_empty_side(euf::sgraph& sg, euf::snode* non_empty_side,
dep_tracker const& dep, bool& changed); dep_tracker const& dep, bool& changed);
// VarBoundWatcher: after applying substitution s, propagate the bounds // Length bounds are queried from the arithmetic subsolver when needed.
// of s.m_var to variables appearing in s.m_replacement.
// When var has bounds [lo, hi], derives bounds for variables in replacement
// using the known constant-length contribution of non-variable tokens.
// Mirrors ZIPT's VarBoundWatcher re-check mechanism.
void update_var_bounds(nielsen_subst const& s);
// Initialize per-variable Parikh bounds from this node's regex memberships.
// For each str_mem constraint (str ∈ regex) where regex has length bounds
// [min_len, max_len], adds lower/upper bound constraints for len(str).
// Called from simplify_and_init to populate IntBounds at node creation.
void init_var_bounds_from_mems();
}; };
// search statistics collected during Nielsen graph solving // search statistics collected during Nielsen graph solving
@ -949,13 +919,13 @@ namespace seq {
// accessor for the seq_regex module // accessor for the seq_regex module
seq_regex* seq_regex_module() const { return m_seq_regex; } seq_regex* seq_regex_module() const { return m_seq_regex; }
dep_manager& dep_mgr() { return m_dep_mgr; }
dep_manager const& dep_mgr() const { return m_dep_mgr; }
private: private:
vector<dep_source, false> m_conflict_sources; vector<dep_source, false> m_conflict_sources;
dep_manager& dep_mgr() { return m_dep_mgr; }
dep_manager const& dep_mgr() const { return m_dep_mgr; }
// collect dependency information from conflicting constraints // collect dependency information from conflicting constraints
dep_tracker collect_conflict_deps() const; dep_tracker collect_conflict_deps() const;
@ -1131,13 +1101,13 @@ namespace seq {
// m_solver by search_dfs via push/pop, so a plain check() suffices. // m_solver by search_dfs via push/pop, so a plain check() suffices.
// l_undef (resource limit / timeout) is treated as feasible so that the // l_undef (resource limit / timeout) is treated as feasible so that the
// search continues rather than reporting a false unsatisfiability. // search continues rather than reporting a false unsatisfiability.
bool check_int_feasibility(nielsen_node* node); bool check_int_feasibility();
// check whether lhs <= rhs is implied by the path constraints. // check whether lhs <= rhs is implied by the path constraints.
// mirrors ZIPT's NielsenNode.IsLe(): temporarily asserts NOT(lhs <= rhs) // mirrors ZIPT's NielsenNode.IsLe(): temporarily asserts NOT(lhs <= rhs)
// and returns true iff the result is unsatisfiable (i.e., lhs <= rhs is // and returns true iff the result is unsatisfiable (i.e., lhs <= rhs is
// entailed). Path constraints are already in the solver incrementally. // entailed). Path constraints are already in the solver incrementally.
bool check_lp_le(expr* lhs, expr* rhs, nielsen_node* node); bool check_lp_le(expr* lhs, expr* rhs);
// create an integer constraint: lhs <kind> rhs // create an integer constraint: lhs <kind> rhs
constraint mk_constraint(expr* fml, dep_tracker const& dep); constraint mk_constraint(expr* fml, dep_tracker const& dep);

View file

@ -267,8 +267,16 @@ namespace seq {
// stride > 1 guaranteed from here onward. // stride > 1 guaranteed from here onward.
SASSERT(stride > 1); SASSERT(stride > 1);
unsigned lb = node.var_lb(mem.m_str); rational lb_r, ub_r;
unsigned ub = node.var_ub(mem.m_str); if (!node.lower_bound(mem.m_str->get_expr(), lb_r) || !node.upper_bound(mem.m_str->get_expr(), ub_r))
continue;
SASSERT(lb_r <= ub_r);
if (ub_r > INT_MAX)
continue;
const unsigned lb = (unsigned)lb_r.get_int32();
const unsigned ub = (unsigned)ub_r.get_int32();
// Check: ∃k ≥ 0 such that lb ≤ min_len + stride * k ≤ ub ? // Check: ∃k ≥ 0 such that lb ≤ min_len + stride * k ≤ ub ?
// //

View file

@ -17,8 +17,8 @@ Abstract:
len(str) = min_len + stride · k (k 0, k fresh integer) len(str) = min_len + stride · k (k 0, k fresh integer)
which tighten the arithmetic subproblem beyond the simple min/max which tighten the arithmetic subproblem beyond plain min/max bounds,
length bounds already produced by nielsen_node::init_var_bounds_from_mems(). where concrete variable bounds are queried from the arithmetic subsolver.
For example: For example:
str (ab)* min_len = 0, stride = 2 len(str) = 2·k str (ab)* min_len = 0, stride = 2 len(str) = 2·k
@ -110,8 +110,8 @@ namespace seq {
// Quick Parikh feasibility check (no solver call). // Quick Parikh feasibility check (no solver call).
// //
// For each single-variable membership str ∈ re, checks whether the // For each single-variable membership str ∈ re, checks whether the
// modular constraint len(str) = min_len + stride · k (k 0) // modular constraint len(str) = min_len + stride · k (k >= 0)
// has any solution given the current per-variable bounds stored in // has any solution given the current per-variable bounds obtained via
// node.var_lb(str) and node.var_ub(str). // node.var_lb(str) and node.var_ub(str).
// //
// Returns true when a conflict is detected (no valid k exists for // Returns true when a conflict is detected (no valid k exists for

View file

@ -860,7 +860,7 @@ static void test_regex_char_split_basic() {
ng.add_str_mem(x, regex); ng.add_str_mem(x, regex);
SASSERT(ng.root() != nullptr); SASSERT(ng.root() != nullptr);
auto sr = ng.root()->simplify_and_init(); auto sr = ng.root()->simplify_and_init({});
SASSERT(sr != seq::simplify_result::conflict); SASSERT(sr != seq::simplify_result::conflict);
bool extended = ng.generate_extensions(ng.root()); bool extended = ng.generate_extensions(ng.root());
@ -1300,7 +1300,7 @@ static void test_generate_extensions_regex_only() {
ng.add_str_mem(x, re_node); ng.add_str_mem(x, re_node);
seq::nielsen_node* root = ng.root(); seq::nielsen_node* root = ng.root();
root->simplify_and_init(); root->simplify_and_init({});
bool extended = ng.generate_extensions(root); bool extended = ng.generate_extensions(root);
SASSERT(extended); SASSERT(extended);
@ -1661,7 +1661,7 @@ static void test_simplify_prefix_cancel() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(abx, aby, dep)); node->add_str_eq(seq::str_eq(abx, aby, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::proceed); SASSERT(sr == seq::simplify_result::proceed);
SASSERT(node->str_eqs().size() == 1); SASSERT(node->str_eqs().size() == 1);
// after prefix cancel: remaining eq has variable-only sides // after prefix cancel: remaining eq has variable-only sides
@ -1691,7 +1691,7 @@ static void test_simplify_suffix_cancel_rtl() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(xa, ya, dep)); node->add_str_eq(seq::str_eq(xa, ya, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::proceed); SASSERT(sr == seq::simplify_result::proceed);
SASSERT(node->str_eqs().size() == 1); SASSERT(node->str_eqs().size() == 1);
SASSERT(node->str_eqs()[0].m_lhs->is_var()); SASSERT(node->str_eqs()[0].m_lhs->is_var());
@ -1720,7 +1720,7 @@ static void test_simplify_symbol_clash() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(ax, by, dep)); node->add_str_eq(seq::str_eq(ax, by, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::conflict); SASSERT(sr == seq::simplify_result::conflict);
SASSERT(node->is_general_conflict()); SASSERT(node->is_general_conflict());
SASSERT(node->reason() == seq::backtrack_reason::symbol_clash); SASSERT(node->reason() == seq::backtrack_reason::symbol_clash);
@ -1746,7 +1746,7 @@ static void test_simplify_empty_propagation() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(e, xy, dep)); node->add_str_eq(seq::str_eq(e, xy, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::satisfied); SASSERT(sr == seq::simplify_result::satisfied);
} }
@ -1768,7 +1768,7 @@ static void test_simplify_empty_vs_char() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(e, a, dep)); node->add_str_eq(seq::str_eq(e, a, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::conflict); SASSERT(sr == seq::simplify_result::conflict);
SASSERT(node->reason() == seq::backtrack_reason::symbol_clash); SASSERT(node->reason() == seq::backtrack_reason::symbol_clash);
} }
@ -1794,7 +1794,7 @@ static void test_simplify_multi_pass_clash() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(ab, ac, dep)); node->add_str_eq(seq::str_eq(ab, ac, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::conflict); SASSERT(sr == seq::simplify_result::conflict);
SASSERT(node->reason() == seq::backtrack_reason::symbol_clash); SASSERT(node->reason() == seq::backtrack_reason::symbol_clash);
} }
@ -1818,7 +1818,7 @@ static void test_simplify_trivial_removal() {
node->add_str_eq(seq::str_eq(e, e, dep)); // trivial node->add_str_eq(seq::str_eq(e, e, dep)); // trivial
node->add_str_eq(seq::str_eq(x, y, dep)); // non-trivial node->add_str_eq(seq::str_eq(x, y, dep)); // non-trivial
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::proceed); SASSERT(sr == seq::simplify_result::proceed);
SASSERT(node->str_eqs().size() == 1); SASSERT(node->str_eqs().size() == 1);
} }
@ -1841,7 +1841,7 @@ static void test_simplify_all_trivial_satisfied() {
node->add_str_eq(seq::str_eq(x, x, dep)); // trivial: same pointer node->add_str_eq(seq::str_eq(x, x, dep)); // trivial: same pointer
node->add_str_eq(seq::str_eq(e, e, dep)); // trivial: both empty node->add_str_eq(seq::str_eq(e, e, dep)); // trivial: both empty
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::satisfied); SASSERT(sr == seq::simplify_result::satisfied);
} }
@ -1867,7 +1867,7 @@ static void test_simplify_regex_infeasible() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_mem(seq::str_mem(e, regex, e, 0, dep)); node->add_str_mem(seq::str_mem(e, regex, e, 0, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::conflict); SASSERT(sr == seq::simplify_result::conflict);
SASSERT(node->reason() == seq::backtrack_reason::regex); SASSERT(node->reason() == seq::backtrack_reason::regex);
} }
@ -1895,7 +1895,7 @@ static void test_simplify_nullable_removal() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_mem(seq::str_mem(e, regex, e, 0, dep)); node->add_str_mem(seq::str_mem(e, regex, e, 0, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::satisfied); SASSERT(sr == seq::simplify_result::satisfied);
SASSERT(node->str_mems().empty()); SASSERT(node->str_mems().empty());
} }
@ -1923,7 +1923,7 @@ static void test_simplify_brzozowski_sat() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_mem(seq::str_mem(a, regex, e, 0, dep)); node->add_str_mem(seq::str_mem(a, regex, e, 0, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::satisfied); SASSERT(sr == seq::simplify_result::satisfied);
} }
@ -1955,7 +1955,7 @@ static void test_simplify_brzozowski_rtl_suffix() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_mem(seq::str_mem(xa, regex, e, 0, dep)); node->add_str_mem(seq::str_mem(xa, regex, e, 0, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::proceed); SASSERT(sr == seq::simplify_result::proceed);
SASSERT(node->str_mems().size() == 1); SASSERT(node->str_mems().size() == 1);
SASSERT(node->str_mems()[0].m_str->is_var()); SASSERT(node->str_mems()[0].m_str->is_var());
@ -1994,7 +1994,7 @@ static void test_simplify_multiple_eqs() {
node->add_str_eq(seq::str_eq(x, z, dep)); node->add_str_eq(seq::str_eq(x, z, dep));
SASSERT(node->str_eqs().size() == 3); SASSERT(node->str_eqs().size() == 3);
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::proceed); SASSERT(sr == seq::simplify_result::proceed);
// eq1 removed, eq2 simplified to x=y, eq3 kept → 2 eqs remain // eq1 removed, eq2 simplified to x=y, eq3 kept → 2 eqs remain
SASSERT(node->str_eqs().size() == 2); SASSERT(node->str_eqs().size() == 2);
@ -2580,7 +2580,7 @@ static void test_star_intr_no_backedge() {
seq::nielsen_node* root = ng.root(); seq::nielsen_node* root = ng.root();
SASSERT(root->backedge() == nullptr); SASSERT(root->backedge() == nullptr);
auto sr = root->simplify_and_init(); auto sr = root->simplify_and_init({});
SASSERT(sr != seq::simplify_result::conflict); SASSERT(sr != seq::simplify_result::conflict);
bool extended = ng.generate_extensions(root); bool extended = ng.generate_extensions(root);
@ -2614,7 +2614,7 @@ static void test_star_intr_with_backedge() {
seq::nielsen_node* root = ng.root(); seq::nielsen_node* root = ng.root();
root->set_backedge(root); // simulate backedge root->set_backedge(root); // simulate backedge
auto sr = root->simplify_and_init(); auto sr = root->simplify_and_init({});
// star(to_re("A")) is nullable, so empty string satisfies it // star(to_re("A")) is nullable, so empty string satisfies it
// simplify may remove the membership or proceed // simplify may remove the membership or proceed
if (sr == seq::simplify_result::satisfied) { if (sr == seq::simplify_result::satisfied) {
@ -2713,7 +2713,7 @@ static void test_regex_var_split_basic() {
ng.add_str_mem(x, regex); ng.add_str_mem(x, regex);
seq::nielsen_node* root = ng.root(); seq::nielsen_node* root = ng.root();
auto sr = root->simplify_and_init(); auto sr = root->simplify_and_init({});
SASSERT(sr != seq::simplify_result::conflict); SASSERT(sr != seq::simplify_result::conflict);
bool extended = ng.generate_extensions(root); bool extended = ng.generate_extensions(root);
@ -2790,7 +2790,7 @@ static void test_const_num_unwinding_no_power() {
seq::nielsen_node* root = ng.root(); seq::nielsen_node* root = ng.root();
// Should detect clash during simplify // Should detect clash during simplify
auto sr = root->simplify_and_init(); auto sr = root->simplify_and_init({});
SASSERT(sr == seq::simplify_result::conflict); SASSERT(sr == seq::simplify_result::conflict);
} }
@ -3252,7 +3252,7 @@ static void test_parikh_dep_tracking() {
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// IntBounds / VarBoundWatcher tests (Task: IntBounds/Constraint.Shared) // IntBounds / subsolver-bound tests (Task: IntBounds/Constraint.Shared)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// tracking solver: records assert_expr calls for inspection // tracking solver: records assert_expr calls for inspection
@ -3277,7 +3277,37 @@ public:
} }
}; };
// test add_lower_int_bound: basic tightening adds int_constraint static void add_len_ge(seq::nielsen_graph& ng, seq::nielsen_node* node, euf::snode* var, unsigned lb, seq::dep_tracker dep) {
ast_manager& m = ng.get_manager();
arith_util arith(m);
expr_ref len(ng.seq().str.mk_length(var->get_expr()), m);
node->add_constraint(seq::constraint(arith.mk_ge(len, arith.mk_int(lb)), dep, m));
}
static void add_len_le(seq::nielsen_graph& ng, seq::nielsen_node* node, euf::snode* var, unsigned ub, seq::dep_tracker dep) {
ast_manager& m = ng.get_manager();
arith_util arith(m);
expr_ref len(ng.seq().str.mk_length(var->get_expr()), m);
node->add_constraint(seq::constraint(arith.mk_le(len, arith.mk_int(ub)), dep, m));
}
static unsigned queried_lb(seq::nielsen_node* node, euf::snode* var) {
rational lb;
if (!node->lower_bound(var->get_expr(), lb))
return 0;
SASSERT(lb.is_unsigned());
return lb.is_unsigned() ? lb.get_unsigned() : 0;
}
static unsigned queried_ub(seq::nielsen_node* node, euf::snode* var) {
rational ub;
if (!node->upper_bound(var->get_expr(), ub))
return UINT_MAX;
SASSERT(ub.is_unsigned());
return ub.is_unsigned() ? ub.get_unsigned() : UINT_MAX;
}
// test lower-bound constraints affect queried bounds
static void test_add_lower_int_bound_basic() { static void test_add_lower_int_bound_basic() {
std::cout << "test_add_lower_int_bound_basic\n"; std::cout << "test_add_lower_int_bound_basic\n";
ast_manager m; ast_manager m;
@ -3296,33 +3326,28 @@ static void test_add_lower_int_bound_basic() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
// initially no bounds // initially no bounds
SASSERT(node->var_lb(x) == 0); SASSERT(queried_lb(node, x) == 0);
SASSERT(node->var_ub(x) == UINT_MAX); SASSERT(queried_ub(node, x) == UINT_MAX);
SASSERT(node->constraints().empty()); SASSERT(node->constraints().empty());
// add lower bound lb=3: should tighten and add constraint add_len_ge(ng, node, x, 3, dep);
bool tightened = node->set_lower_int_bound(x, 3, dep); SASSERT(queried_lb(node, x) == 3);
SASSERT(tightened);
SASSERT(node->var_lb(x) == 3);
SASSERT(node->constraints().size() == 1); SASSERT(node->constraints().size() == 1);
SASSERT(node->constraints()[0].fml); SASSERT(node->constraints()[0].fml);
// add weaker lb=2: no tightening // weaker bound does not change the effective lower bound
tightened = node->set_lower_int_bound(x, 2, dep); add_len_ge(ng, node, x, 2, dep);
SASSERT(!tightened); SASSERT(queried_lb(node, x) == 3);
SASSERT(node->var_lb(x) == 3);
SASSERT(node->constraints().size() == 1);
// add tighter lb=5: should tighten and add another constraint
tightened = node->set_lower_int_bound(x, 5, dep);
SASSERT(tightened);
SASSERT(node->var_lb(x) == 5);
SASSERT(node->constraints().size() == 2); SASSERT(node->constraints().size() == 2);
add_len_ge(ng, node, x, 5, dep);
SASSERT(queried_lb(node, x) == 5);
SASSERT(node->constraints().size() == 3);
std::cout << " ok\n"; std::cout << " ok\n";
} }
// test add_upper_int_bound: basic tightening adds int_constraint // test upper-bound constraints affect queried bounds
static void test_add_upper_int_bound_basic() { static void test_add_upper_int_bound_basic() {
std::cout << "test_add_upper_int_bound_basic\n"; std::cout << "test_add_upper_int_bound_basic\n";
ast_manager m; ast_manager m;
@ -3339,31 +3364,26 @@ static void test_add_upper_int_bound_basic() {
seq::nielsen_node* node = ng.root(); seq::nielsen_node* node = ng.root();
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
SASSERT(node->var_ub(x) == UINT_MAX); SASSERT(queried_ub(node, x) == UINT_MAX);
// add upper bound ub=10: tightens add_len_le(ng, node, x, 10, dep);
bool tightened = node->set_upper_int_bound(x, 10, dep); SASSERT(queried_ub(node, x) == 10);
SASSERT(tightened);
SASSERT(node->var_ub(x) == 10);
SASSERT(node->constraints().size() == 1); SASSERT(node->constraints().size() == 1);
SASSERT(node->constraints()[0].fml); SASSERT(node->constraints()[0].fml);
// add weaker ub=20: no tightening // weaker bound does not change the effective upper bound
tightened = node->set_upper_int_bound(x, 20, dep); add_len_le(ng, node, x, 20, dep);
SASSERT(!tightened); SASSERT(queried_ub(node, x) == 10);
SASSERT(node->var_ub(x) == 10);
SASSERT(node->constraints().size() == 1);
// add tighter ub=5: tightens
tightened = node->set_upper_int_bound(x, 5, dep);
SASSERT(tightened);
SASSERT(node->var_ub(x) == 5);
SASSERT(node->constraints().size() == 2); SASSERT(node->constraints().size() == 2);
add_len_le(ng, node, x, 5, dep);
SASSERT(queried_ub(node, x) == 5);
SASSERT(node->constraints().size() == 3);
std::cout << " ok\n"; std::cout << " ok\n";
} }
// test add_lower_int_bound: conflict when lb > ub // inconsistent local bounds are visible through lower_bound/upper_bound queries
static void test_add_bound_lb_gt_ub_conflict() { static void test_add_bound_lb_gt_ub_conflict() {
std::cout << "test_add_bound_lb_gt_ub_conflict\n"; std::cout << "test_add_bound_lb_gt_ub_conflict\n";
ast_manager m; ast_manager m;
@ -3380,14 +3400,9 @@ static void test_add_bound_lb_gt_ub_conflict() {
seq::nielsen_node* node = ng.root(); seq::nielsen_node* node = ng.root();
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
// set ub=3 first add_len_le(ng, node, x, 3, dep);
node->set_upper_int_bound(x, 3, dep); add_len_ge(ng, node, x, 5, dep);
SASSERT(!node->is_general_conflict()); SASSERT(queried_lb(node, x) > queried_ub(node, x));
// now set lb=5 > ub=3: should trigger conflict
node->set_lower_int_bound(x, 5, dep);
SASSERT(node->is_general_conflict());
SASSERT(node->reason() == seq::backtrack_reason::arithmetic);
std::cout << " ok\n"; std::cout << " ok\n";
} }
@ -3411,18 +3426,18 @@ static void test_bounds_cloned() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
// set bounds on parent // set bounds on parent
parent->set_lower_int_bound(x, 2, dep); add_len_ge(ng, parent, x, 2, dep);
parent->set_upper_int_bound(x, 7, dep); add_len_le(ng, parent, x, 7, dep);
parent->set_lower_int_bound(y, 1, dep); add_len_ge(ng, parent, y, 1, dep);
// clone to child // clone to child
seq::nielsen_node* child = ng.mk_child(parent); seq::nielsen_node* child = ng.mk_child(parent);
// child should have same bounds // child should have same bounds
SASSERT(child->var_lb(x) == 2); SASSERT(queried_lb(child, x) == 2);
SASSERT(child->var_ub(x) == 7); SASSERT(queried_ub(child, x) == 7);
SASSERT(child->var_lb(y) == 1); SASSERT(queried_lb(child, y) == 1);
SASSERT(child->var_ub(y) == UINT_MAX); SASSERT(queried_ub(child, y) == UINT_MAX);
// child's int_constraints should also be cloned (3 constraints: lb_x, ub_x, lb_y) // child's int_constraints should also be cloned (3 constraints: lb_x, ub_x, lb_y)
SASSERT(child->constraints().size() == parent->constraints().size()); SASSERT(child->constraints().size() == parent->constraints().size());
@ -3430,9 +3445,9 @@ static void test_bounds_cloned() {
std::cout << " ok\n"; std::cout << " ok\n";
} }
// test VarBoundWatcher: substitution x→a·y propagates bounds from x to y // Substitution propagates no direct bounds; bounds are owned by the subsolver.
static void test_var_bound_watcher_single_var() { static void test_subst_does_not_propagate_bounds_single_var() {
std::cout << "test_var_bound_watcher_single_var\n"; std::cout << "test_subst_does_not_propagate_bounds_single_var\n";
ast_manager m; ast_manager m;
reg_decl_plugins(m); reg_decl_plugins(m);
euf::egraph eg(m); euf::egraph eg(m);
@ -3450,26 +3465,24 @@ static void test_var_bound_watcher_single_var() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
// set bounds: 3 <= len(x) <= 7 // set bounds: 3 <= len(x) <= 7
node->set_lower_int_bound(x, 3, dep); add_len_ge(ng, node, x, 3, dep);
node->set_upper_int_bound(x, 7, dep); add_len_le(ng, node, x, 7, dep);
node->constraints().reset(); // clear for clean count
// apply substitution x → a·y // apply substitution x → a·y
euf::snode* ay = sg.mk_concat(a, y); euf::snode* ay = sg.mk_concat(a, y);
seq::nielsen_subst s(x, ay, dep); seq::nielsen_subst s(x, ay, dep);
node->apply_subst(sg, s); node->apply_subst(sg, s);
// VarBoundWatcher should propagate: 3 <= 1+len(y) <= 7 // No local propagation anymore: y keeps conservative defaults.
// => len(y) >= 2, len(y) <= 6 SASSERT(queried_lb(node, y) == 0);
SASSERT(node->var_lb(y) == 2); SASSERT(queried_ub(node, y) == UINT_MAX);
SASSERT(node->var_ub(y) == 6);
std::cout << " ok\n"; std::cout << " ok\n";
} }
// test VarBoundWatcher: substitution with all-concrete replacement detects conflict // Substitution with concrete replacement does not trigger immediate bound conflict.
static void test_var_bound_watcher_conflict() { static void test_subst_no_immediate_bound_conflict() {
std::cout << "test_var_bound_watcher_conflict\n"; std::cout << "test_subst_no_immediate_bound_conflict\n";
ast_manager m; ast_manager m;
reg_decl_plugins(m); reg_decl_plugins(m);
euf::egraph eg(m); euf::egraph eg(m);
@ -3487,24 +3500,21 @@ static void test_var_bound_watcher_conflict() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
// set bounds: 3 <= len(x) (so x must have at least 3 chars) // set bounds: 3 <= len(x) (so x must have at least 3 chars)
node->set_lower_int_bound(x, 3, dep); add_len_ge(ng, node, x, 3, dep);
node->constraints().reset();
// apply substitution x → a·b (const_len=2 < lb=3) // apply substitution x → a·b (no eager bound check at this stage)
euf::snode* ab = sg.mk_concat(a, b); euf::snode* ab = sg.mk_concat(a, b);
seq::nielsen_subst s(x, ab, dep); seq::nielsen_subst s(x, ab, dep);
node->apply_subst(sg, s); node->apply_subst(sg, s);
// should detect conflict: len(x) >= 3 but replacement has len=2 SASSERT(!node->is_general_conflict());
SASSERT(node->is_general_conflict());
SASSERT(node->reason() == seq::backtrack_reason::arithmetic);
std::cout << " ok\n"; std::cout << " ok\n";
} }
// test init_var_bounds_from_mems: simplify_and_init adds Parikh bounds // simplify_and_init materializes no local IntBounds; bounds come from subsolver state
static void test_simplify_adds_parikh_bounds() { static void test_simplify_does_not_add_local_parikh_bounds() {
std::cout << "test_simplify_adds_parikh_bounds\n"; std::cout << "test_simplify_does_not_add_local_parikh_bounds\n";
ast_manager m; ast_manager m;
reg_decl_plugins(m); reg_decl_plugins(m);
euf::egraph eg(m); euf::egraph eg(m);
@ -3527,14 +3537,12 @@ static void test_simplify_adds_parikh_bounds() {
seq::nielsen_node* node = ng.root(); seq::nielsen_node* node = ng.root();
// simplify_and_init should call init_var_bounds_from_mems // simplify_and_init no longer materializes local IntBounds.
seq::simplify_result sr = node->simplify_and_init(); seq::simplify_result sr = node->simplify_and_init({});
(void)sr; (void)sr;
// x ∈ to_re("AB") has min_len=2, max_len=2 SASSERT(queried_lb(node, x) == 0);
// so lb=2, ub=2 should be set on x SASSERT(queried_ub(node, x) == UINT_MAX);
SASSERT(node->var_lb(x) == 2);
SASSERT(node->var_ub(x) == 2);
std::cout << " ok\n"; std::cout << " ok\n";
} }
@ -3601,9 +3609,9 @@ static void test_assert_root_constraints_once() {
std::cout << " ok\n"; std::cout << " ok\n";
} }
// test VarBoundWatcher with multiple variables in replacement // Multiple-variable substitutions do not impose eager per-variable bounds.
static void test_var_bound_watcher_multi_var() { static void test_subst_does_not_propagate_bounds_multi_var() {
std::cout << "test_var_bound_watcher_multi_var\n"; std::cout << "test_subst_does_not_propagate_bounds_multi_var\n";
ast_manager m; ast_manager m;
reg_decl_plugins(m); reg_decl_plugins(m);
euf::egraph eg(m); euf::egraph eg(m);
@ -3621,17 +3629,15 @@ static void test_var_bound_watcher_multi_var() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
// set upper bound: len(x) <= 5 // set upper bound: len(x) <= 5
node->set_upper_int_bound(x, 5, dep); add_len_le(ng, node, x, 5, dep);
node->constraints().reset();
// apply substitution x → y·z (two vars, no constants) // apply substitution x → y·z (two vars, no constants)
euf::snode* yz = sg.mk_concat(y, z); euf::snode* yz = sg.mk_concat(y, z);
seq::nielsen_subst s(x, yz, dep); seq::nielsen_subst s(x, yz, dep);
node->apply_subst(sg, s); node->apply_subst(sg, s);
// len(y·z) <= 5 → each of y, z gets ub=5 SASSERT(queried_ub(node, y) == UINT_MAX);
SASSERT(node->var_ub(y) == 5); SASSERT(queried_ub(node, z) == UINT_MAX);
SASSERT(node->var_ub(z) == 5);
std::cout << " ok\n"; std::cout << " ok\n";
} }
@ -3671,7 +3677,7 @@ static void test_simplify_unit_prefix_split() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(lhs, rhs, dep)); node->add_str_eq(seq::str_eq(lhs, rhs, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::proceed); SASSERT(sr == seq::simplify_result::proceed);
// original eq stripped to x==y, plus a new unit(a)==unit(b) eq // original eq stripped to x==y, plus a new unit(a)==unit(b) eq
SASSERT(node->str_eqs().size() == 2); SASSERT(node->str_eqs().size() == 2);
@ -3716,7 +3722,7 @@ static void test_simplify_unit_prefix_split_empty_rest() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(lhs, ub, dep)); node->add_str_eq(seq::str_eq(lhs, ub, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
// unit(a)==unit(b) and x==empty are produced; x==empty forces x->epsilon and satisfied // unit(a)==unit(b) and x==empty are produced; x==empty forces x->epsilon and satisfied
SASSERT(sr == seq::simplify_result::satisfied || sr == seq::simplify_result::proceed); SASSERT(sr == seq::simplify_result::satisfied || sr == seq::simplify_result::proceed);
std::cout << " ok\n"; std::cout << " ok\n";
@ -3756,7 +3762,7 @@ static void test_simplify_unit_suffix_split() {
seq::dep_tracker dep = nullptr; seq::dep_tracker dep = nullptr;
node->add_str_eq(seq::str_eq(lhs, rhs, dep)); node->add_str_eq(seq::str_eq(lhs, rhs, dep));
auto sr = node->simplify_and_init(); auto sr = node->simplify_and_init({});
SASSERT(sr == seq::simplify_result::proceed); SASSERT(sr == seq::simplify_result::proceed);
// original eq stripped to x==y, plus a new unit(a)==unit(b) eq // original eq stripped to x==y, plus a new unit(a)==unit(b) eq
SASSERT(node->str_eqs().size() == 2); SASSERT(node->str_eqs().size() == 2);
@ -3880,17 +3886,17 @@ void tst_seq_nielsen() {
test_parikh_mixed_eq_mem(); test_parikh_mixed_eq_mem();
test_parikh_full_seq_no_bounds(); test_parikh_full_seq_no_bounds();
test_parikh_dep_tracking(); test_parikh_dep_tracking();
// IntBounds / VarBoundWatcher / Constraint.Shared tests // IntBounds / subsolver bounds / Constraint.Shared tests
test_add_lower_int_bound_basic(); test_add_lower_int_bound_basic();
test_add_upper_int_bound_basic(); test_add_upper_int_bound_basic();
test_add_bound_lb_gt_ub_conflict(); test_add_bound_lb_gt_ub_conflict();
test_bounds_cloned(); test_bounds_cloned();
test_var_bound_watcher_single_var(); test_subst_does_not_propagate_bounds_single_var();
test_var_bound_watcher_conflict(); test_subst_no_immediate_bound_conflict();
test_simplify_adds_parikh_bounds(); test_simplify_does_not_add_local_parikh_bounds();
test_assert_root_constraints_to_solver(); test_assert_root_constraints_to_solver();
test_assert_root_constraints_once(); test_assert_root_constraints_once();
test_var_bound_watcher_multi_var(); test_subst_does_not_propagate_bounds_multi_var();
test_simplify_unit_prefix_split(); test_simplify_unit_prefix_split();
test_simplify_unit_prefix_split_empty_rest(); test_simplify_unit_prefix_split_empty_rest();
test_simplify_unit_suffix_split(); test_simplify_unit_suffix_split();