diff --git a/src/smt/nseq_context_solver.h b/src/smt/nseq_context_solver.h index 803cee39e..c1fa008bc 100644 --- a/src/smt/nseq_context_solver.h +++ b/src/smt/nseq_context_solver.h @@ -20,6 +20,7 @@ Author: #include "smt/seq/seq_nielsen.h" #include "smt/smt_kernel.h" +#include "smt/smt_arith_value.h" #include "params/smt_params.h" namespace smt { @@ -32,6 +33,7 @@ namespace smt { class context_solver : public seq::simple_solver { smt_params m_params; // must be declared before m_kernel smt::kernel m_kernel; + arith_value m_arith_value; static smt_params make_seq_len_params() { smt_params p; @@ -42,7 +44,10 @@ namespace smt { public: context_solver(ast_manager& m) : 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 { // std::cout << "Checking:\n"; @@ -69,8 +74,19 @@ namespace smt { 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 { m_kernel.reset(); + m_arith_value.init(&m_kernel.get_context()); } }; diff --git a/src/smt/seq/seq_nielsen.h b/src/smt/seq/seq_nielsen.h index abb88de36..bc7932348 100644 --- a/src/smt/seq/seq_nielsen.h +++ b/src/smt/seq/seq_nielsen.h @@ -163,10 +163,9 @@ Abstract: - IntEq / IntLe: integer equality and inequality constraints over Presburger arithmetic polynomials (PDD) are entirely absent. The Z3 port has no ConstraintsIntEq or ConstraintsIntLe in nielsen_node. - - IntBounds / VarBoundWatcher: per-variable integer interval bounds and the - watcher mechanism that reruns bound propagation when a string variable is - substituted — PORTED as nielsen_node::{add_lower_int_bound, - add_upper_int_bound, watch_var_bounds, init_var_bounds_from_mems}. + - IntBounds / VarBoundWatcher: ZIPT-style cached interval maps and eager + watcher propagation are not stored in nielsen_node; bounds are queried + from the arithmetic subsolver on demand. - AddLowerIntBound() / AddHigherIntBound(): incremental interval tightening — PORTED as the above add_lower/upper_int_bound methods. @@ -238,11 +237,10 @@ Author: #include "util/dependency.h" #include "util/map.h" #include "util/lbool.h" +#include "util/rational.h" #include "ast/ast.h" -#include "ast/arith_decl_plugin.h" #include "ast/seq_decl_plugin.h" #include "ast/euf/euf_sgraph.h" -#include #include #include "model/model.h" @@ -280,6 +278,10 @@ namespace seq { virtual void push() = 0; virtual void pop(unsigned num_scopes) = 0; 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; }; @@ -524,11 +526,6 @@ namespace seq { vector m_str_mem; // regex memberships vector 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 m_var_lb; // lower bound: lb <= len(var) - u_map m_var_ub; // upper bound: len(var) <= ub // character constraints (mirrors ZIPT's DisEqualities and CharRanges) // key: snode id of the s_unit symbolic character @@ -585,27 +582,11 @@ namespace seq { vector const& constraints() const { return m_constraints; } vector& constraints() { return m_constraints; } - // IntBounds: tighten the lower bound for len(var). - // Returns true if the bound was tightened (lb > current lower bound). - // When tightened without conflict, adds a constraint len(var) >= lb. - // When lb > current upper bound, 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 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; + // Query current bounds for a variable from the arithmetic subsolver. + // Falls der Subsolver keinen Bound liefert, werden konservative Defaults + // 0 / UINT_MAX verwendet. + bool lower_bound(expr* e, rational& lo) const; + bool upper_bound(expr* e, rational& up) const; // character constraint access (mirrors ZIPT's DisEqualities / CharRanges) u_map> 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, dep_tracker const& dep, bool& changed); - // VarBoundWatcher: after applying substitution s, propagate the bounds - // 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(); + // Length bounds are queried from the arithmetic subsolver when needed. }; // search statistics collected during Nielsen graph solving @@ -949,13 +919,13 @@ namespace seq { // accessor for the seq_regex module 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: vector 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 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. // l_undef (resource limit / timeout) is treated as feasible so that the // 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. // mirrors ZIPT's NielsenNode.IsLe(): temporarily asserts NOT(lhs <= rhs) // and returns true iff the result is unsatisfiable (i.e., lhs <= rhs is // 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 rhs constraint mk_constraint(expr* fml, dep_tracker const& dep); diff --git a/src/smt/seq/seq_parikh.cpp b/src/smt/seq/seq_parikh.cpp index 0dfc60f0e..d88efd327 100644 --- a/src/smt/seq/seq_parikh.cpp +++ b/src/smt/seq/seq_parikh.cpp @@ -267,8 +267,16 @@ namespace seq { // stride > 1 guaranteed from here onward. SASSERT(stride > 1); - unsigned lb = node.var_lb(mem.m_str); - unsigned ub = node.var_ub(mem.m_str); + rational lb_r, ub_r; + 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 ? // diff --git a/src/smt/seq/seq_parikh.h b/src/smt/seq/seq_parikh.h index 6afff8167..2fb179a2d 100644 --- a/src/smt/seq/seq_parikh.h +++ b/src/smt/seq/seq_parikh.h @@ -17,8 +17,8 @@ Abstract: len(str) = min_len + stride · k (k ≥ 0, k fresh integer) - which tighten the arithmetic subproblem beyond the simple min/max - length bounds already produced by nielsen_node::init_var_bounds_from_mems(). + which tighten the arithmetic subproblem beyond plain min/max bounds, + where concrete variable bounds are queried from the arithmetic subsolver. For example: • str ∈ (ab)* → min_len = 0, stride = 2 → len(str) = 2·k @@ -110,8 +110,8 @@ namespace seq { // Quick Parikh feasibility check (no solver call). // // For each single-variable membership str ∈ re, checks whether the - // modular constraint len(str) = min_len + stride · k (k ≥ 0) - // has any solution given the current per-variable bounds stored in + // modular constraint len(str) = min_len + stride · k (k >= 0) + // has any solution given the current per-variable bounds obtained via // node.var_lb(str) and node.var_ub(str). // // Returns true when a conflict is detected (no valid k exists for diff --git a/src/test/seq_nielsen.cpp b/src/test/seq_nielsen.cpp index 8e72742c3..a4c1869e8 100644 --- a/src/test/seq_nielsen.cpp +++ b/src/test/seq_nielsen.cpp @@ -860,7 +860,7 @@ static void test_regex_char_split_basic() { ng.add_str_mem(x, regex); SASSERT(ng.root() != nullptr); - auto sr = ng.root()->simplify_and_init(); + auto sr = ng.root()->simplify_and_init({}); SASSERT(sr != seq::simplify_result::conflict); 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); seq::nielsen_node* root = ng.root(); - root->simplify_and_init(); + root->simplify_and_init({}); bool extended = ng.generate_extensions(root); SASSERT(extended); @@ -1661,7 +1661,7 @@ static void test_simplify_prefix_cancel() { seq::dep_tracker dep = nullptr; 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(node->str_eqs().size() == 1); // 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; 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(node->str_eqs().size() == 1); SASSERT(node->str_eqs()[0].m_lhs->is_var()); @@ -1720,7 +1720,7 @@ static void test_simplify_symbol_clash() { seq::dep_tracker dep = nullptr; 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(node->is_general_conflict()); SASSERT(node->reason() == seq::backtrack_reason::symbol_clash); @@ -1746,7 +1746,7 @@ static void test_simplify_empty_propagation() { seq::dep_tracker dep = nullptr; 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); } @@ -1768,7 +1768,7 @@ static void test_simplify_empty_vs_char() { seq::dep_tracker dep = nullptr; 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(node->reason() == seq::backtrack_reason::symbol_clash); } @@ -1794,7 +1794,7 @@ static void test_simplify_multi_pass_clash() { seq::dep_tracker dep = nullptr; 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(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(x, y, dep)); // non-trivial - auto sr = node->simplify_and_init(); + auto sr = node->simplify_and_init({}); SASSERT(sr == seq::simplify_result::proceed); 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(e, e, dep)); // trivial: both empty - auto sr = node->simplify_and_init(); + auto sr = node->simplify_and_init({}); SASSERT(sr == seq::simplify_result::satisfied); } @@ -1867,7 +1867,7 @@ static void test_simplify_regex_infeasible() { seq::dep_tracker dep = nullptr; 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(node->reason() == seq::backtrack_reason::regex); } @@ -1895,7 +1895,7 @@ static void test_simplify_nullable_removal() { seq::dep_tracker dep = nullptr; 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(node->str_mems().empty()); } @@ -1923,7 +1923,7 @@ static void test_simplify_brzozowski_sat() { seq::dep_tracker dep = nullptr; 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); } @@ -1955,7 +1955,7 @@ static void test_simplify_brzozowski_rtl_suffix() { seq::dep_tracker dep = nullptr; 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(node->str_mems().size() == 1); 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)); SASSERT(node->str_eqs().size() == 3); - auto sr = node->simplify_and_init(); + auto sr = node->simplify_and_init({}); SASSERT(sr == seq::simplify_result::proceed); // eq1 removed, eq2 simplified to x=y, eq3 kept → 2 eqs remain SASSERT(node->str_eqs().size() == 2); @@ -2580,7 +2580,7 @@ static void test_star_intr_no_backedge() { seq::nielsen_node* root = ng.root(); SASSERT(root->backedge() == nullptr); - auto sr = root->simplify_and_init(); + auto sr = root->simplify_and_init({}); SASSERT(sr != seq::simplify_result::conflict); bool extended = ng.generate_extensions(root); @@ -2614,7 +2614,7 @@ static void test_star_intr_with_backedge() { seq::nielsen_node* root = ng.root(); 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 // simplify may remove the membership or proceed if (sr == seq::simplify_result::satisfied) { @@ -2713,7 +2713,7 @@ static void test_regex_var_split_basic() { ng.add_str_mem(x, regex); seq::nielsen_node* root = ng.root(); - auto sr = root->simplify_and_init(); + auto sr = root->simplify_and_init({}); SASSERT(sr != seq::simplify_result::conflict); bool extended = ng.generate_extensions(root); @@ -2790,7 +2790,7 @@ static void test_const_num_unwinding_no_power() { seq::nielsen_node* root = ng.root(); // Should detect clash during simplify - auto sr = root->simplify_and_init(); + auto sr = root->simplify_and_init({}); 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 @@ -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() { std::cout << "test_add_lower_int_bound_basic\n"; ast_manager m; @@ -3296,33 +3326,28 @@ static void test_add_lower_int_bound_basic() { seq::dep_tracker dep = nullptr; // initially no bounds - SASSERT(node->var_lb(x) == 0); - SASSERT(node->var_ub(x) == UINT_MAX); + SASSERT(queried_lb(node, x) == 0); + SASSERT(queried_ub(node, x) == UINT_MAX); SASSERT(node->constraints().empty()); - // add lower bound lb=3: should tighten and add constraint - bool tightened = node->set_lower_int_bound(x, 3, dep); - SASSERT(tightened); - SASSERT(node->var_lb(x) == 3); + add_len_ge(ng, node, x, 3, dep); + SASSERT(queried_lb(node, x) == 3); SASSERT(node->constraints().size() == 1); SASSERT(node->constraints()[0].fml); - // add weaker lb=2: no tightening - tightened = node->set_lower_int_bound(x, 2, dep); - SASSERT(!tightened); - 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); + // weaker bound does not change the effective lower bound + add_len_ge(ng, node, x, 2, dep); + SASSERT(queried_lb(node, x) == 3); 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"; } -// 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() { std::cout << "test_add_upper_int_bound_basic\n"; ast_manager m; @@ -3339,31 +3364,26 @@ static void test_add_upper_int_bound_basic() { seq::nielsen_node* node = ng.root(); 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 - bool tightened = node->set_upper_int_bound(x, 10, dep); - SASSERT(tightened); - SASSERT(node->var_ub(x) == 10); + add_len_le(ng, node, x, 10, dep); + SASSERT(queried_ub(node, x) == 10); SASSERT(node->constraints().size() == 1); SASSERT(node->constraints()[0].fml); - // add weaker ub=20: no tightening - tightened = node->set_upper_int_bound(x, 20, dep); - SASSERT(!tightened); - 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); + // weaker bound does not change the effective upper bound + add_len_le(ng, node, x, 20, dep); + SASSERT(queried_ub(node, x) == 10); 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"; } -// 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() { std::cout << "test_add_bound_lb_gt_ub_conflict\n"; ast_manager m; @@ -3380,14 +3400,9 @@ static void test_add_bound_lb_gt_ub_conflict() { seq::nielsen_node* node = ng.root(); seq::dep_tracker dep = nullptr; - // set ub=3 first - node->set_upper_int_bound(x, 3, dep); - SASSERT(!node->is_general_conflict()); - - // 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); + add_len_le(ng, node, x, 3, dep); + add_len_ge(ng, node, x, 5, dep); + SASSERT(queried_lb(node, x) > queried_ub(node, x)); std::cout << " ok\n"; } @@ -3411,18 +3426,18 @@ static void test_bounds_cloned() { seq::dep_tracker dep = nullptr; // set bounds on parent - parent->set_lower_int_bound(x, 2, dep); - parent->set_upper_int_bound(x, 7, dep); - parent->set_lower_int_bound(y, 1, dep); + add_len_ge(ng, parent, x, 2, dep); + add_len_le(ng, parent, x, 7, dep); + add_len_ge(ng, parent, y, 1, dep); // clone to child seq::nielsen_node* child = ng.mk_child(parent); // child should have same bounds - SASSERT(child->var_lb(x) == 2); - SASSERT(child->var_ub(x) == 7); - SASSERT(child->var_lb(y) == 1); - SASSERT(child->var_ub(y) == UINT_MAX); + SASSERT(queried_lb(child, x) == 2); + SASSERT(queried_ub(child, x) == 7); + SASSERT(queried_lb(child, y) == 1); + SASSERT(queried_ub(child, y) == UINT_MAX); // child's int_constraints should also be cloned (3 constraints: lb_x, ub_x, lb_y) SASSERT(child->constraints().size() == parent->constraints().size()); @@ -3430,9 +3445,9 @@ static void test_bounds_cloned() { std::cout << " ok\n"; } -// test VarBoundWatcher: substitution x→a·y propagates bounds from x to y -static void test_var_bound_watcher_single_var() { - std::cout << "test_var_bound_watcher_single_var\n"; +// Substitution propagates no direct bounds; bounds are owned by the subsolver. +static void test_subst_does_not_propagate_bounds_single_var() { + std::cout << "test_subst_does_not_propagate_bounds_single_var\n"; ast_manager m; reg_decl_plugins(m); euf::egraph eg(m); @@ -3450,26 +3465,24 @@ static void test_var_bound_watcher_single_var() { seq::dep_tracker dep = nullptr; // set bounds: 3 <= len(x) <= 7 - node->set_lower_int_bound(x, 3, dep); - node->set_upper_int_bound(x, 7, dep); - node->constraints().reset(); // clear for clean count + add_len_ge(ng, node, x, 3, dep); + add_len_le(ng, node, x, 7, dep); // apply substitution x → a·y euf::snode* ay = sg.mk_concat(a, y); seq::nielsen_subst s(x, ay, dep); node->apply_subst(sg, s); - // VarBoundWatcher should propagate: 3 <= 1+len(y) <= 7 - // => len(y) >= 2, len(y) <= 6 - SASSERT(node->var_lb(y) == 2); - SASSERT(node->var_ub(y) == 6); + // No local propagation anymore: y keeps conservative defaults. + SASSERT(queried_lb(node, y) == 0); + SASSERT(queried_ub(node, y) == UINT_MAX); std::cout << " ok\n"; } -// test VarBoundWatcher: substitution with all-concrete replacement detects conflict -static void test_var_bound_watcher_conflict() { - std::cout << "test_var_bound_watcher_conflict\n"; +// Substitution with concrete replacement does not trigger immediate bound conflict. +static void test_subst_no_immediate_bound_conflict() { + std::cout << "test_subst_no_immediate_bound_conflict\n"; ast_manager m; reg_decl_plugins(m); euf::egraph eg(m); @@ -3487,24 +3500,21 @@ static void test_var_bound_watcher_conflict() { seq::dep_tracker dep = nullptr; // set bounds: 3 <= len(x) (so x must have at least 3 chars) - node->set_lower_int_bound(x, 3, dep); - node->constraints().reset(); + add_len_ge(ng, node, x, 3, dep); - // 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); seq::nielsen_subst s(x, ab, dep); node->apply_subst(sg, s); - // should detect conflict: len(x) >= 3 but replacement has len=2 - SASSERT(node->is_general_conflict()); - SASSERT(node->reason() == seq::backtrack_reason::arithmetic); + SASSERT(!node->is_general_conflict()); std::cout << " ok\n"; } -// test init_var_bounds_from_mems: simplify_and_init adds Parikh bounds -static void test_simplify_adds_parikh_bounds() { - std::cout << "test_simplify_adds_parikh_bounds\n"; +// simplify_and_init materializes no local IntBounds; bounds come from subsolver state +static void test_simplify_does_not_add_local_parikh_bounds() { + std::cout << "test_simplify_does_not_add_local_parikh_bounds\n"; ast_manager m; reg_decl_plugins(m); euf::egraph eg(m); @@ -3527,14 +3537,12 @@ static void test_simplify_adds_parikh_bounds() { seq::nielsen_node* node = ng.root(); - // simplify_and_init should call init_var_bounds_from_mems - seq::simplify_result sr = node->simplify_and_init(); + // simplify_and_init no longer materializes local IntBounds. + seq::simplify_result sr = node->simplify_and_init({}); (void)sr; - // x ∈ to_re("AB") has min_len=2, max_len=2 - // so lb=2, ub=2 should be set on x - SASSERT(node->var_lb(x) == 2); - SASSERT(node->var_ub(x) == 2); + SASSERT(queried_lb(node, x) == 0); + SASSERT(queried_ub(node, x) == UINT_MAX); std::cout << " ok\n"; } @@ -3601,9 +3609,9 @@ static void test_assert_root_constraints_once() { std::cout << " ok\n"; } -// test VarBoundWatcher with multiple variables in replacement -static void test_var_bound_watcher_multi_var() { - std::cout << "test_var_bound_watcher_multi_var\n"; +// Multiple-variable substitutions do not impose eager per-variable bounds. +static void test_subst_does_not_propagate_bounds_multi_var() { + std::cout << "test_subst_does_not_propagate_bounds_multi_var\n"; ast_manager m; reg_decl_plugins(m); euf::egraph eg(m); @@ -3621,17 +3629,15 @@ static void test_var_bound_watcher_multi_var() { seq::dep_tracker dep = nullptr; // set upper bound: len(x) <= 5 - node->set_upper_int_bound(x, 5, dep); - node->constraints().reset(); + add_len_le(ng, node, x, 5, dep); // apply substitution x → y·z (two vars, no constants) euf::snode* yz = sg.mk_concat(y, z); seq::nielsen_subst s(x, yz, dep); node->apply_subst(sg, s); - // len(y·z) <= 5 → each of y, z gets ub=5 - SASSERT(node->var_ub(y) == 5); - SASSERT(node->var_ub(z) == 5); + SASSERT(queried_ub(node, y) == UINT_MAX); + SASSERT(queried_ub(node, z) == UINT_MAX); std::cout << " ok\n"; } @@ -3671,7 +3677,7 @@ static void test_simplify_unit_prefix_split() { seq::dep_tracker dep = nullptr; 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); // original eq stripped to x==y, plus a new unit(a)==unit(b) eq SASSERT(node->str_eqs().size() == 2); @@ -3716,7 +3722,7 @@ static void test_simplify_unit_prefix_split_empty_rest() { seq::dep_tracker dep = nullptr; 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 SASSERT(sr == seq::simplify_result::satisfied || sr == seq::simplify_result::proceed); std::cout << " ok\n"; @@ -3756,7 +3762,7 @@ static void test_simplify_unit_suffix_split() { seq::dep_tracker dep = nullptr; 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); // original eq stripped to x==y, plus a new unit(a)==unit(b) eq SASSERT(node->str_eqs().size() == 2); @@ -3880,17 +3886,17 @@ void tst_seq_nielsen() { test_parikh_mixed_eq_mem(); test_parikh_full_seq_no_bounds(); test_parikh_dep_tracking(); - // IntBounds / VarBoundWatcher / Constraint.Shared tests + // IntBounds / subsolver bounds / Constraint.Shared tests test_add_lower_int_bound_basic(); test_add_upper_int_bound_basic(); test_add_bound_lb_gt_ub_conflict(); test_bounds_cloned(); - test_var_bound_watcher_single_var(); - test_var_bound_watcher_conflict(); - test_simplify_adds_parikh_bounds(); + test_subst_does_not_propagate_bounds_single_var(); + test_subst_no_immediate_bound_conflict(); + test_simplify_does_not_add_local_parikh_bounds(); test_assert_root_constraints_to_solver(); 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_empty_rest(); test_simplify_unit_suffix_split();