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

Replaced stabilizers by landing decomposition (faster!)

Rank membership constraints by estimated size of their automaton
Some refactoring
Some bug fixes
This commit is contained in:
Clemens Eisenhofer 2026-07-09 23:24:11 +02:00
parent 5fc81bd1ae
commit 596cc14e83
18 changed files with 996 additions and 1316 deletions

File diff suppressed because it is too large Load diff

View file

@ -9,10 +9,6 @@ Abstract:
Nielsen graph for string constraint solving.
Ports the constraint types and Nielsen graph structures from
ZIPT (https://github.com/CEisenhofer/ZIPT/tree/parikh/ZIPT/Constraints)
into Z3's smt/seq framework.
The Nielsen graph is used for solving word equations and regex
membership constraints via Nielsen transformations. Each node
contains a set of constraints (string equalities, regex memberships,
@ -71,7 +67,6 @@ namespace seq {
std::string snode_label_html(euf::snode const* n, ast_manager& m, bool html_escape);
// simplification result for constraint processing
// mirrors ZIPT's SimplifyResult enum
enum class simplify_result {
proceed, // no change, continue
conflict, // constraint is unsatisfiable
@ -81,7 +76,6 @@ namespace seq {
};
// reason for backtracking in the Nielsen graph
// mirrors ZIPT's BacktrackReasons enum
enum class backtrack_reason {
unevaluated,
extended,
@ -181,7 +175,6 @@ namespace seq {
bool split_lookahead_viable(expr* n_regex, euf::sgraph& sg, zstring const& c);
// string equality constraint: lhs = rhs
// mirrors ZIPT's StrEq (both sides are regex-free snode trees)
struct str_eq {
ast_manager& m;
euf::snode const* m_lhs; // assumed to be non-null
@ -315,7 +308,6 @@ namespace seq {
enum class mem_kind : unsigned char { plain, stab_view };
// regex membership constraint: str in regex
// mirrors ZIPT's StrMem
struct str_mem {
ast_manager& m;
euf::snode const* m_str; // assumed to be non-null
@ -359,6 +351,15 @@ namespace seq {
}
bool operator<(const str_mem& other) const {
// Regex-factorization heuristic: order primarily by the estimated
// automaton size of the regex so that the cheapest membership is
// factorized first (see snode::regex_weight / sgraph::compute_regex_weight).
// The remaining pointer comparisons are just deterministic tie-breakers
// to keep this a total order for canonicalization/sorting.
const unsigned w1 = m_regex->regex_weight();
const unsigned w2 = other.m_regex->regex_weight();
if (w1 != w2)
return w1 < w2;
if (m_str != other.m_str)
return m_str < other.m_str;
return m_regex < other.m_regex;
@ -398,7 +399,6 @@ namespace seq {
// string variable substitution: var -> replacement
// (can be used as well to substitute arbitrary nodes - like powers)
// mirrors ZIPT's Subst
struct nielsen_subst {
euf::snode const* m_var;
euf::snode const* m_replacement;
@ -427,12 +427,10 @@ namespace seq {
// -----------------------------------------------
// integer constraint: equality or inequality over length expressions
// mirrors ZIPT's IntEq and IntLe
// -----------------------------------------------
// integer constraint stored per nielsen_node, tracking arithmetic
// relationships between length variables and power exponents.
// mirrors ZIPT's IntEq / IntLe over Presburger arithmetic polynomials.
struct constraint {
expr_ref fml; // the formula (eq, le, or ge, unit-diseq expression)
dep_tracker dep; // tracks which input constraints contributed
@ -477,7 +475,6 @@ namespace seq {
};
// edge in the Nielsen graph connecting two nodes
// mirrors ZIPT's NielsenEdge
class nielsen_edge {
nielsen_node* m_src;
nielsen_node* m_tgt;
@ -525,7 +522,6 @@ namespace seq {
};
// node in the Nielsen graph
// mirrors ZIPT's NielsenNode
class nielsen_node {
friend class nielsen_graph;
@ -536,11 +532,11 @@ namespace seq {
vector<str_eq> m_str_eq; // string equalities
vector<str_deq> m_str_deq; // string disequalities
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
sat::literal m_conflict_external_literal = sat::null_literal;
dep_tracker m_conflict_internal = nullptr;
// character constraints (mirrors ZIPT's DisEqualities and CharRanges)
// character constraints
// key: snode id of the s_unit symbolic character
u_map<std::pair<char_set, dep_tracker>> m_char_ranges; // ?c in [lo, hi)
@ -639,7 +635,7 @@ namespace seq {
bool lower_bound(expr* e, rational& lo, dep_tracker& dep);
bool upper_bound(expr* e, rational& up, dep_tracker& dep);
// character constraint access (mirrors ZIPT's CharRanges)
// character constraint access
u_map<std::pair<char_set, dep_tracker>>& char_ranges() { return m_char_ranges; }
u_map<std::pair<char_set, dep_tracker>> const &char_ranges() const {
return m_char_ranges;
@ -758,7 +754,6 @@ namespace seq {
bool references_rigid() const;
// render constraint set as an HTML fragment for DOT node labels.
// mirrors ZIPT's NielsenNode.ToHtmlString()
std::ostream& to_html(std::ostream& out, obj_map<expr, std::string>& names, uint64_t& next_id, ast_manager& m) const;
std::ostream& to_html(std::ostream& out, ast_manager& m) const;
@ -813,6 +808,7 @@ namespace seq {
unsigned m_mod_eq_split = 0;
unsigned m_mod_star_intr = 0;
unsigned m_mod_cycle_subsumption = 0;
unsigned m_mod_view_land = 0;
unsigned m_mod_gpower_intr = 0;
unsigned m_mod_regex_factorization = 0;
unsigned m_mod_const_nielsen = 0;
@ -834,7 +830,6 @@ namespace seq {
};
// the overall Nielsen transformation graph
// mirrors ZIPT's NielsenGraph
class nielsen_graph {
friend class nielsen_node;
friend class nielsen_edge;
@ -954,10 +949,28 @@ namespace seq {
// egraph cannot release them on pop. We never shrink this — the
// cache is meant to be monotone.
expr_ref_vector m_partial_dfa_pin;
// Monotone snapshot index ν. Bumped by mark_scc_projection_edges only
// when the explored SCC's edge set actually grows; identifies which
// partial-DFA edges (m_projection_idx ≤ ν) belong to a projection's Q.
// Monotone snapshot index ν, bumped whenever a new view state set is
// recorded (mark_reachable_projection_edges). A view's Q is the EXACT
// snapshot stored under its ν in m_projection_snapshots (the paper's
// "recorded state set Q" of a view, Implementation Aspects); the
// per-edge watermark m_projection_idx ≤ ν is kept only as a fallback —
// on its own it would denote the union of ALL extractions up to ν,
// blurring a view's Q with unrelated heads' regions once exploration
// is partial (lazy mode).
unsigned m_projection_extract_idx = 0;
// ν → the snapshot of Q taken when the view index was minted. The
// state exprs are pinned via m_partial_dfa_pin, so the stored expr*
// (and their ids) stay valid across sgraph pops.
struct projection_snapshot {
uint_set m_ids; // expr ids of the states of Q
ptr_vector<expr> m_states; // the states themselves
};
std::unordered_map<unsigned, projection_snapshot> m_projection_snapshots;
// head expr id → (partial-DFA edge count at snapshot time, ν). The
// partial DFA only grows, so an unchanged edge count means the head's
// reachable set — hence its snapshot — is unchanged and ν is reused
// instead of minting a fresh snapshot per decomposition.
std::unordered_map<unsigned, std::pair<unsigned, unsigned>> m_projection_head_cache;
// Expr ids of regex states whose full reachable automaton has already
// been recorded into the partial DFA (lazy, once-per-component Q growth;
// see ensure_automaton_explored).
@ -1115,7 +1128,6 @@ namespace seq {
// output the graph in graphviz DOT format.
// nodes on the sat_path are highlighted green; conflict nodes red/darkred.
// mirrors ZIPT's NielsenGraph.ToDot()
std::ostream& to_dot(std::ostream& out) const;
std::string to_dot() const;
@ -1234,15 +1246,18 @@ namespace seq {
// unconstrained), replacing symbolic chars with their char ranges,
// then checking if the approximation intersected with `regex` is empty.
// Returns true if widening detects infeasibility (UNSAT).
// Mirrors ZIPT NielsenNode.CheckRegexWidening (NielsenNode.cs:1350-1380)
// Decided at the constraint level by check_concat_product_emptiness
// (one factor per token of Ω(str), one component for the right-hand
// side); land-state views participate exactly, both as the widened
// membership and as pinned constraints inside Ω.
bool check_regex_widening(nielsen_node const& node, str_mem const& mem, dep_tracker& dep);
// Check regex feasibility at a leaf node: for each variable with
// multiple primitive regex constraints, check that the intersection
// of all its regexes is non-empty.
// Returns true if all constraints are feasible.
// Mirrors ZIPT NielsenNode.CheckRegex)
bool check_leaf_regex(nielsen_node const& node, dep_tracker& dep);
// l_true: feasible; l_false: some intersection empty (dep set);
// l_undef: product-search budget exhausted — must not be treated as SAT.
lbool check_leaf_regex(nielsen_node const& node, dep_tracker& dep);
// -------------------------------------------------------------------
// Synchronous product over plain / view / guard / co-view components
@ -1277,6 +1292,18 @@ namespace seq {
// was reached), l_undef = budget exhausted / inconclusive.
lbool check_product_emptiness(vector<prod_comp> const& comps, unsigned max_states);
// Concatenation-aware variant (paper, "Pruning incrementally during
// construction"): emptiness of (L(F_0)·…·L(F_{k-1})) ⊓ L(rhs), where
// each factor F_i is the intersection of its components (an empty
// component list denotes Σ*) and rhs is one further component consumed
// synchronously with the whole concatenation. Used by regex widening;
// no closed-form regex for the concatenation is ever built, and view
// components participate exactly via their gated one-character law.
// l_true = empty, l_false = a common word exists, l_undef = budget
// exhausted / undecided acceptance (callers must NOT prune on it).
lbool check_concat_product_emptiness(vector<vector<prod_comp>> const& factors,
prod_comp const& rhs, unsigned max_states);
// acceptance / single-character step of one product component.
lbool comp_accepting(prod_comp const& c) const;
prod_comp comp_step(prod_comp const& c, euf::snode const* mt);
@ -1316,6 +1343,45 @@ namespace seq {
// (0 if there is nothing marked, i.e. no reachable edge).
unsigned mark_reachable_projection_edges(euf::snode const* head_re);
// Collect the snode handles of every state of Q_ν — the states incident
// to a partial-DFA edge with extraction index in [1, ν]. This is the
// enumeration counterpart of the membership test projection_state_in_Q
// (keep the two in sync); used to enumerate the landing states of the
// land-only view decomposition.
void collect_projection_states(unsigned nu, svector<euf::snode const*>& out);
// Length abstraction of the land-state views over one gated region:
// BFS distances d(·) from a head state over the recorded in-Q_ν edges
// (each edge consumes exactly one character/minterm), plus the stride
// g = gcd over reachable in-Q edges (u,v) of (d(u) + 1 d(v)).
// By the telescoping/potential argument, EVERY gated walk head→v has
// length ≡ d(v) (mod g); g = 0 means every such walk has length exactly
// d(v). Sound because at ν-minting time compute_frontier has saturated
// the in-Q edges (every one-character transition among Q states is
// recorded), and edges are only ever added.
struct view_len_info {
u_map<unsigned> m_dist; // expr id → shortest gated distance from the head
unsigned m_stride = 0; // 0 ⇒ walk lengths are exact
bool m_ok = false; // snapshot found and head ∈ Q_ν
};
void compute_view_length_info(unsigned nu, expr* from, view_len_info& info);
// Emit the length side constraints for a variable pinned to the view
// L_{Q_ν,{to}}(head) onto the branch edge e: len ≥ min and
// stride | (len min) (collapsed to len = min when exact). For a
// landing target outside Q_ν (view landing at root ∉ Q_ν) the final
// hop may be an UNRECORDED transition from any reachable in-Q state,
// so only the conservative variant (min = 1, stride weakened by the
// gcd of all reachable distances) is sound. These ride the normal
// side-constraint channel: branch-locally enforced by the subsolver
// (check_int_feasibility) and surfaced to the outer solver at a SAT
// leaf via add_nielsen_assumptions — keeping the outer arith model's
// len(x) realizable in the view, which seq_model::mk_fresh_value
// relies on (a non-realizable length forces a witness of a different
// length: an inconsistent model).
void add_view_length_constraints(nielsen_edge* e, view_len_info const& info, unsigned nu,
euf::snode const* pinned, expr* to, dep_tracker const& dep);
// A frontier edge (p, mt, q): p ∈ Q, mt a minterm of p, q = δ_mt(p) ∉ Q
// and q ≠ ⊥. The escape candidates of the landing decomposition.
struct frontier_edge {
@ -1365,7 +1431,6 @@ namespace seq {
// eq split modifier: splits a regex-free equation at a chosen index into
// two shorter equalities with optional padding (single progress child).
// Mirrors ZIPT's EqSplitModifier + StrEq.SplitEq.
bool apply_eq_split(nielsen_node* node);
// helper: classify whether a token has variable (symbolic) length
@ -1376,7 +1441,6 @@ namespace seq {
unsigned token_const_length(euf::snode const* tok) const;
// helper: find a split point in a regex-free equation.
// ports ZIPT's StrEq.SplitEq algorithm.
// returns true if a valid split point is found, with results in out params.
bool find_eq_split_point(
euf::snode_vector const& lhs_toks,
@ -1387,12 +1451,10 @@ namespace seq {
// power epsilon modifier: for a power token u^n in an equation,
// branch: (1) base u = ε, (2) power is empty (n = 0 semantics).
// mirrors ZIPT's PowerEpsilonModifier
bool apply_power_epsilon(nielsen_node* node);
// numeric comparison modifier: for equations involving power tokens
// u^m and u^n with the same base, branch on m < n vs n <= m.
// mirrors ZIPT's NumCmpModifier
bool apply_num_cmp(nielsen_node* node);
// CommPower-based power elimination split: when one side starts with
@ -1401,12 +1463,10 @@ namespace seq {
// (1) p < c (2) c ≤ p
// After branching, simplify_and_init's CommPower pass resolves the
// cancellation deterministically.
// mirrors ZIPT's SplitPowerElim + NumCmpModifier
bool apply_split_power_elim(nielsen_node* node);
// constant numeric unwinding: for a power token u^n vs a constant
// (non-variable), branch: (1) n = 0 (u^n = ε), (2) n >= 1 (peel one u).
// mirrors ZIPT's ConstNumUnwindingModifier
bool apply_const_num_unwinding(nielsen_node* node);
// regex if split: for str_mem s ∈ R where R decomposes as ite(c, th, el),
@ -1426,6 +1486,25 @@ namespace seq {
// the old split-and-guard apply_cycle_decomposition.
bool apply_landing_decomposition(nielsen_node* node);
// Land-only decomposition of a NON-PRIMITIVE view constraint (paper
// §5.3, "Landing decomposition on view constraints"): a substitution
// applied throughout the node (an escape, a Nielsen split, a power
// introduction) can hit a pinned variable and turn its primitive view
// into y·u ∈_{Q_ν,F} p with a leading variable y. Character-
// unwinding y (regex_var_split) would re-introduce exactly the
// cycle-unrolling divergence views exist to prevent, so instead:
// - p ∉ Q_ν (degenerate): L_{Q_ν,F}(p) ⊆ {ε} — conflict if p ∉ F or
// a character remains, else force the LHS variables to ε;
// - p ∈ Q_ν: branch on the landing state s of y over Q_ν F only
// (no escapes: every admissible LHS value keeps its proper-prefix
// states inside Q_ν and lands in F, so y's own landing state lies
// in Q_ν F), pinning y ∈_{Q_ν,{s}} p and advancing the residual
// to u ∈_{Q_ν,F} s.
// Removes the leading variable, introduces no fresh variables and
// branches over finitely many states, so the paper's termination
// argument extends unchanged.
bool apply_view_landing_decomposition(nielsen_node* node);
// cycle subsumption: for a str_mem x·rest ∈ R where x is constrained
// to L(Reg_x) ⊆ L(stabilizer of R), simplify to rest ∈ R.
// Fires without the novelty guard, using the current partial DFA state.
@ -1442,7 +1521,6 @@ namespace seq {
// generalized power introduction: for an equation where one head is
// a variable v and the other side has ground prefix + a variable x
// forming a cycle back to v, introduce v = base^n · suffix.
// mirrors ZIPT's GPowerIntrModifier
bool apply_gpower_intr(nielsen_node* node);
// generalized regex factorization (Boolean closure derivation rule).
@ -1467,8 +1545,7 @@ namespace seq {
rf_step_result rf_step(nielsen_node* node, rf_state* st, dep_tracker& conflict_dep);
// helper for apply_gpower_intr: fires the substitution.
// `fwd=true` uses left-to-right decomposition; `fwd=false` mirrors ZIPT's
// backward (right-to-left) direction.
// `fwd=true` uses left-to-right decomposition; `fwd=false`
bool fire_gpower_intro(nielsen_node* node, str_eq const& eq,
euf::snode const* var, euf::snode_vector const& ground_prefix_orig, bool fwd);
@ -1478,17 +1555,14 @@ namespace seq {
// regex variable split: for str_mem x·s ∈ R where x is a variable,
// split using minterms: x → ε, or x → c·x' for each minterm c.
// More general than regex_char_split, uses minterm partitioning.
// mirrors ZIPT's RegexVarSplitModifier
bool apply_regex_var_split(nielsen_node* node);
// power split: for a variable x facing a power token u^n,
// branch: x = u^m · prefix(u) with m < n, or x = u^n · x.
// mirrors ZIPT's PowerSplitModifier
bool apply_power_split(nielsen_node* node);
// variable numeric unwinding: for a power token u^n vs a variable,
// branch: (1) n = 0 (u^n = ε), (2) n >= 1 (peel one u).
// mirrors ZIPT's VarNumUnwindingModifier
bool apply_var_num_unwinding_eq(nielsen_node* node);
bool apply_var_num_unwinding_mem(nielsen_node* node);
@ -1516,7 +1590,6 @@ namespace seq {
// m_solver at the base level (outside push/pop). Called once at the start
// of solve(). Makes derived constraints immediately visible to m_solver
// for arithmetic pruning at every DFS node, not just the root.
// Mirrors ZIPT's Constraint.Shared forwarding mechanism.
void assert_root_constraints_to_solver();
// Generate |LHS| = |RHS| length constraints for a non-root node's own
@ -1524,7 +1597,6 @@ namespace seq {
// Called once per node (guarded by m_node_len_constraints_generated).
// Uses compute_length_expr (mod-count-aware) so that variables with
// non-zero modification counts get fresh length variables.
// Mirrors ZIPT's Constraint.Shared forwarding for per-node equations.
void generate_node_length_constraints(nielsen_node* node);
// check integer feasibility of the constraints along the current path.
@ -1538,7 +1610,7 @@ namespace seq {
dep_tracker get_subsolver_dependency(nielsen_node* n) const;
// check whether lhs <= rhs is implied by the path constraints.
// mirrors ZIPT's NielsenNode.IsLe(): temporarily asserts NOT(lhs <= rhs)
// 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* n, dep_tracker& dep);
@ -1551,8 +1623,6 @@ namespace seq {
// -----------------------------------------------
// Modification counter methods for substitution length tracking.
// mirrors ZIPT's NielsenEdge.IncModCount / DecModCount and
// NielsenNode constructor length assertion logic.
// -----------------------------------------------
// Get or create a fresh symbolic character variable for the given variable

View file

@ -257,7 +257,6 @@ namespace seq {
// -----------------------------------------------------------------------
// nielsen_node: display_html
// Render constraint set as an HTML fragment for DOT labels.
// Mirrors ZIPT's NielsenNode.ToHtmlString().
// -----------------------------------------------------------------------
// Helper: HTML-escape a string and replace literal \n with <br/>.
@ -743,7 +742,7 @@ namespace seq {
// -----------------------------------------------------------------------
// nielsen_graph: to_dot
// Output the graph in graphviz DOT format, optionally colour-highlighting
// the satisfying path. Mirrors ZIPT's NielsenGraph.ToDot().
// the satisfying path.
// -----------------------------------------------------------------------
// Convert a backtrack_reason to a short display string.

View file

@ -7,7 +7,7 @@ Module Name:
Abstract:
Parikh image filter implementation for the ZIPT-based Nielsen string
Parikh image filter implementation for the Nielsen string
solver. See seq_parikh.h for the full design description.
The key operation is compute_length_stride(re), which performs a

View file

@ -7,7 +7,7 @@ Module Name:
Abstract:
Parikh image filter for the ZIPT-based Nielsen string solver.
Parikh image filter for the Nielsen string solver.
Implements Parikh-based arithmetic constraint generation for
nielsen_node instances. For a regex membership constraint str r,
@ -29,10 +29,6 @@ Abstract:
set and discharged by the integer subsolver (see seq_nielsen.h,
simple_solver).
Implements the Parikh filter described in ZIPT
(https://github.com/CEisenhofer/ZIPT/tree/parikh/ZIPT/Constraints)
replacing ZIPT's PDD-based Parikh subsolver with Z3's linear arithmetic.
Author:
Clemens Eisenhofer 2026-03-10

View file

@ -19,15 +19,6 @@ Author:
namespace seq {
// NSB code review: change the stabilizers set to
// add the regexes in the domain of m_stabilizers to a trail (expr_ref_vector
// change the range to be a vector of expressions, not snodes
// add regexes in the range of m_stabilizers to the trail
// this is to ensure that the expressions are valid also after scope changes.
// maybe all regexes entered are created at base level for quantifier free formulas
// but we should not assume this. The sgraph also can change based on scope.
// the Stabilizer data-structure persists across search.
// Collect possible first characters of a syntactically known *string*
// expression (the body of to_re). Regex operators (union, complement,
// intersection, ...) are not expected here.
@ -78,203 +69,6 @@ namespace seq {
}
// -----------------------------------------------------------------------
// Stabilizer store
// -----------------------------------------------------------------------
void seq_regex::reset_stabilizers() {
m_stabilizers.reset();
m_self_stabilizing.reset();
}
void seq_regex::add_stabilizer(euf::snode const* regex, euf::snode const* stabilizer) {
if (!regex || !stabilizer)
return;
const unsigned id = regex->id();
auto& stabs = m_stabilizers.insert_if_not_there(id, euf::snode_vector());
// De-duplicate by pointer equality (mirrors ZIPT Environment.AddStabilizer
// which checks reference equality before adding).
for (euf::snode const* s : stabs)
if (s == stabilizer)
return;
stabs.push_back(stabilizer);
}
euf::snode const* seq_regex::get_stabilizer_union(euf::snode const* regex) {
if (!regex)
return nullptr;
if (!m_stabilizers.contains(regex->id()))
return nullptr;
auto& stabs = m_stabilizers[regex->id()];
if (stabs.empty())
return nullptr;
// Single stabilizer: return it directly.
if (stabs.size() == 1)
return stabs[0];
// Multiple stabilizers: build re.union chain.
// union(s1, union(s2, ... union(sN-1, sN)...))
euf::snode const* result = stabs[stabs.size() - 1];
for (unsigned i = stabs.size() - 1; i-- > 0; ) {
expr* lhs = stabs[i]->get_expr();
expr* rhs = result->get_expr();
result = m_sg.mk(seq.re.mk_union(lhs, rhs));
}
return result;
}
bool seq_regex::has_stabilizers(euf::snode const* regex) const {
if (!regex)
return false;
if (!m_stabilizers.contains(regex->id()))
return false;
return !m_stabilizers[regex->id()].empty();
}
euf::snode_vector const* seq_regex::get_stabilizers(euf::snode const* regex) const {
if (!regex)
return nullptr;
if (!m_stabilizers.contains(regex->id()))
return nullptr;
return &m_stabilizers[regex->id()];
}
void seq_regex::set_self_stabilizing(euf::snode const* regex) {
if (regex)
m_self_stabilizing.insert(regex->id());
}
bool seq_regex::is_self_stabilizing(euf::snode const* regex) const {
return regex && m_self_stabilizing.contains(regex->id());
}
// -----------------------------------------------------------------------
// Self-stabilizing auto-detection
// -----------------------------------------------------------------------
bool seq_regex::compute_self_stabilizing(euf::snode const* regex) const {
return false;
}
// -----------------------------------------------------------------------
// Self-stabilizing propagation through derivatives
// -----------------------------------------------------------------------
void seq_regex::propagate_self_stabilizing(euf::snode const* parent, euf::snode const* deriv) {
if (!parent || !deriv)
return;
// If the derivative is already known to be self-stabilizing (either
// inherently or from a prior propagation), nothing to do.
if (is_self_stabilizing(deriv))
return;
// If the derivative is itself inherently self-stabilizing
// (e.g., it is a star or full_seq), mark it now.
if (compute_self_stabilizing(deriv)) {
set_self_stabilizing(deriv);
return;
}
// Rule 1: Star parent.
// D(c, R*) = D(c, R) · R*. The derivative always contains the
// R* tail, so it is self-stabilizing regardless of D(c,R).
if (parent->is_star()) {
set_self_stabilizing(deriv);
return;
}
// Rule 2: Full_seq parent.
// D(c, Σ*) = Σ*, and Σ* is self-stabilizing.
// (The derivative should be Σ* itself; mark it for safety.)
if (parent->is_full_seq()) {
set_self_stabilizing(deriv);
return;
}
// Check if parent is self-stabilizing (either inherently or marked).
bool parent_ss = is_self_stabilizing(parent) || compute_self_stabilizing(parent);
// Rule 3: Concat parent R · S.
// D(c, R·S) = D(c,R)·S | (nullable(R) ? D(c,S) : ∅).
// If S is self-stabilizing, the D(c,R)·S branch inherits it.
// If the whole parent R·S is self-stabilizing, the derivative is too.
if (parent->is_concat() && parent->num_args() == 2) {
euf::snode const* tail = parent->arg(1);
bool tail_ss = is_self_stabilizing(tail) || compute_self_stabilizing(tail);
if (tail_ss || parent_ss) {
set_self_stabilizing(deriv);
return;
}
}
// Rule 4: Union parent R | S.
// D(c, R|S) = D(c,R) | D(c,S).
// Self-stabilizing if both children are self-stabilizing.
if (parent->is_union() && parent->num_args() == 2) {
euf::snode const* lhs = parent->arg(0);
euf::snode const* rhs = parent->arg(1);
bool lhs_ss = is_self_stabilizing(lhs) || compute_self_stabilizing(lhs);
bool rhs_ss = is_self_stabilizing(rhs) || compute_self_stabilizing(rhs);
if (lhs_ss && rhs_ss) {
set_self_stabilizing(deriv);
return;
}
}
// Rule 5: Intersection parent R ∩ S.
// D(c, R∩S) = D(c,R) ∩ D(c,S).
// Self-stabilizing if both children are self-stabilizing.
if (parent->is_intersect() && parent->num_args() == 2) {
euf::snode const* lhs = parent->arg(0);
euf::snode const* rhs = parent->arg(1);
bool lhs_ss = is_self_stabilizing(lhs) || compute_self_stabilizing(lhs);
bool rhs_ss = is_self_stabilizing(rhs) || compute_self_stabilizing(rhs);
if (lhs_ss && rhs_ss) {
set_self_stabilizing(deriv);
return;
}
}
// Rule 6: Complement parent ~R.
// D(c, ~R) = ~D(c, R).
// Preserves self-stabilizing from R.
if (parent->is_complement() && parent->num_args() == 1) {
euf::snode const* inner = parent->arg(0);
bool inner_ss = is_self_stabilizing(inner) || compute_self_stabilizing(inner);
if (inner_ss) {
set_self_stabilizing(deriv);
return;
}
}
// Rule 7: Generic self-stabilizing parent.
// If the parent was explicitly marked self-stabilizing (e.g., via
// a previous propagation), propagate to the derivative.
if (parent_ss) {
set_self_stabilizing(deriv);
return;
}
}
// -----------------------------------------------------------------------
// Derivative with propagation
// -----------------------------------------------------------------------
euf::snode const* seq_regex::derivative_with_propagation(euf::snode const* re, euf::snode const* elem) {
if (!re || !elem)
return nullptr;
euf::snode const* deriv = derivative(re, elem);
if (deriv)
propagate_self_stabilizing(re, deriv);
return deriv;
}
// -----------------------------------------------------------------------
// Uniform derivative (symbolic character consumption)
// -----------------------------------------------------------------------
@ -320,7 +114,7 @@ namespace seq {
}
// -----------------------------------------------------------------------
// Ground prefix consumption
// Basic regex predicates
// -----------------------------------------------------------------------
bool seq_regex::is_empty_regex(euf::snode const* re) const {
@ -549,7 +343,6 @@ namespace seq {
// -----------------------------------------------------------------------
// Multi-regex intersection emptiness check
// BFS over the product of Brzozowski derivative automata.
// Mirrors ZIPT NielsenNode.CheckEmptiness (NielsenNode.cs:1429-1469)
// -----------------------------------------------------------------------
lbool seq_regex::check_intersection_emptiness(euf::snode_vector const& regexes, unsigned max_states) {
@ -573,77 +366,6 @@ namespace seq {
return is_empty_bfs(result, max_states);
}
// -----------------------------------------------------------------------
// Language subset check: L(A) ⊆ L(B)
// via intersection(A, complement(B)) = ∅
// Mirrors ZIPT NielsenNode.IsLanguageSubset (NielsenNode.cs:1382-1385)
// -----------------------------------------------------------------------
lbool seq_regex::is_language_subset(euf::snode const* subset_re, euf::snode const* superset_re) {
if (!subset_re || !superset_re)
return l_undef;
// Quick checks
if (subset_re->is_fail() || is_empty_regex(subset_re))
return l_true; // ∅ ⊆ anything
if (superset_re->is_full_seq())
return l_true; // anything ⊆ Σ*
if (subset_re == superset_re)
return l_true; // L ⊆ L
// Build complement(superset)
expr* sup_expr = superset_re->get_expr();
euf::snode const* comp_sn = m_sg.mk(seq.re.mk_complement(sup_expr));
// Build intersection and check emptiness
// subset ∩ complement(superset) should be empty for subset relation
expr* sub_expr = subset_re->get_expr();
auto inter = seq.re.mk_inter(sub_expr, comp_sn->get_expr());
euf::snode const* inter_sn = m_sg.mk(inter);
return is_empty_bfs(inter_sn);
}
// -----------------------------------------------------------------------
// Collect primitive regex intersection for a variable
// -----------------------------------------------------------------------
euf::snode const* seq_regex::collect_primitive_regex_intersection(
euf::snode const* var, nielsen_node const& node, dep_manager& dep_mgr, dep_tracker& dep) const {
SASSERT(var);
euf::snode const* result = nullptr;
for (auto const& mem : node.str_mems()) {
// Primitive constraint: str is a single variable. View/guard
// memberships do not denote a plain regex on `var` (their m_regex
// is a derivative *state*), so skip them — yielding a coarser but
// sound over-approximation for the caller (regex widening).
if (!mem.is_primitive() || !mem.is_plain())
continue;
euf::snode const* first = mem.m_str->first();
// NSB review: why is this "first" and not mem.m_str?
SASSERT(first);
if (first != var)
continue;
TRACE(seq, tout << spp(first, m) << " " << mem_pp(mem) << "\n");
if (!result) {
result = mem.m_regex;
dep = dep_mgr.mk_join(dep, mem.m_dep);
}
else {
expr* r1 = result->get_expr();
expr* r2 = mem.m_regex->get_expr();
if (r1 && r2) {
auto inter = seq.re.mk_inter(r1, r2);
result = m_sg.mk(inter);
dep = dep_mgr.mk_join(dep, mem.m_dep);
}
}
}
return result;
}
// -----------------------------------------------------------------------
// Ground prefix consumption
// -----------------------------------------------------------------------
@ -662,8 +384,6 @@ namespace seq {
break;
if (deriv->is_fail())
return simplify_status::conflict;
// propagate self-stabilizing flag from parent to derivative
propagate_self_stabilizing(parent_re, deriv);
mem.m_str = m_sg.drop_first(mem.m_str);
mem.m_regex = deriv;
}
@ -776,244 +496,6 @@ namespace seq {
return true;
}
// -----------------------------------------------------------------------
// Stabilizer from cycle
// -----------------------------------------------------------------------
euf::snode const* seq_regex::stabilizer_from_cycle(euf::snode const* cycle_regex,
euf::snode const* current_regex) {
if (!cycle_regex || !current_regex)
return nullptr;
expr* re_expr = cycle_regex->get_expr();
auto star_expr = seq.re.mk_star(re_expr);
return m_sg.mk(star_expr);
}
// -----------------------------------------------------------------------
// Extract cycle history tokens
// -----------------------------------------------------------------------
euf::snode const* seq_regex::extract_cycle_history(seq::str_mem const& current,
seq::str_mem const& ancestor) {
// The history is built by simplify_and_init as a left-associative
// string concat chain: concat(concat(concat(nil, c1), c2), c3).
// Extract the tokens consumed since the ancestor.
return nullptr;
}
// -----------------------------------------------------------------------
// Get filtered stabilizer star
// Mirrors ZIPT StrMem.GetFilteredStabilizerStar (StrMem.cs:228-243)
// -----------------------------------------------------------------------
euf::snode const* seq_regex::get_filtered_stabilizer_star(euf::snode const* re,
euf::snode const* excluded_char) const {
if (!re)
return nullptr;
euf::snode_vector const* stabs = get_stabilizers(re);
if (!stabs || stabs->empty())
return nullptr;
euf::snode const* filtered_union = nullptr;
for (euf::snode const* s : *stabs) {
if (!s)
continue;
// Keep only stabilizers whose language cannot start with excluded_char
euf::snode const* d = m_sg.brzozowski_deriv(s, excluded_char);
if (d && d->is_fail()) {
if (!filtered_union) {
filtered_union = s;
}
else {
expr* e1 = filtered_union->get_expr();
expr* e2 = s->get_expr();
if (e1 && e2) {
auto u = seq.re.mk_union(e1, e2);
filtered_union = m_sg.mk(u);
}
}
}
}
if (!filtered_union)
return nullptr;
expr* fe = filtered_union->get_expr();
if (!fe)
return nullptr;
return m_sg.mk(seq.re.mk_star(fe));
}
// -----------------------------------------------------------------------
// Strengthened stabilizer construction with sub-cycle detection
// Mirrors ZIPT StrMem.StabilizerFromCycle (StrMem.cs:163-225)
// -----------------------------------------------------------------------
euf::snode const* seq_regex::strengthened_stabilizer(euf::snode const* cycle_regex,
euf::snode const* cycle_history) {
if (!cycle_regex || !cycle_history)
return nullptr;
// Flatten the history concat chain into a vector of character tokens.
euf::snode_vector tokens;
cycle_history->collect_tokens(tokens);
if (tokens.empty())
return nullptr;
// Replay tokens on the cycle regex, detecting sub-cycles.
// A sub-cycle is detected when the derivative returns to cycle_regex.
svector<std::pair<unsigned, unsigned>> sub_cycles;
unsigned cycle_start = 0;
euf::snode const* current_re = cycle_regex;
for (unsigned i = 0; i < tokens.size(); ++i) {
euf::snode const* tok = tokens[i];
if (!tok)
return nullptr;
euf::snode const* deriv = m_sg.brzozowski_deriv(current_re, tok);
if (!deriv)
return nullptr;
// Sub-cycle: derivative returned to the cycle entry regex
if (deriv == cycle_regex ||
(deriv->get_expr() && cycle_regex->get_expr() &&
deriv->get_expr() == cycle_regex->get_expr())) {
sub_cycles.push_back(std::make_pair(cycle_start, i + 1));
cycle_start = i + 1;
current_re = cycle_regex;
} else {
current_re = deriv;
}
}
// Remaining tokens that don't complete a sub-cycle
if (cycle_start < tokens.size())
sub_cycles.push_back(std::make_pair(cycle_start, tokens.size()));
if (sub_cycles.empty())
return nullptr;
// Build a stabilizer body for each sub-cycle.
// body = to_re(t0) · [filteredStar(R1, t1)] · to_re(t1) · ... · to_re(t_{n-1})
euf::snode const* overall_union = nullptr;
for (auto const& sc : sub_cycles) {
unsigned start = sc.first;
unsigned end = sc.second;
if (start >= end)
continue;
euf::snode const* re_state = cycle_regex;
euf::snode const* body = nullptr;
for (unsigned i = start; i < end; ++i) {
euf::snode const* tok = tokens[i];
if (!tok)
break;
// Insert filtered stabilizer star before each token after the first
if (i > start) {
euf::snode const* filtered = get_filtered_stabilizer_star(re_state, tok);
if (filtered) {
expr* fe = filtered->get_expr();
if (fe) {
if (!body)
body = filtered;
else {
expr* be = body->get_expr();
body = m_sg.mk(seq.re.mk_concat(be, fe));
}
}
}
}
// Convert char token to regex: to_re(unit(tok))
expr* tok_expr = tok->get_expr();
expr_ref unit_str(seq.str.mk_unit(tok_expr), m);
expr_ref tok_re(seq.re.mk_to_re(unit_str), m);
euf::snode const* tok_re_sn = m_sg.mk(tok_re);
if (!body) {
body = tok_re_sn;
} else {
expr* be = body->get_expr();
expr* te = tok_re_sn->get_expr();
if (be && te) {
expr_ref cat(seq.re.mk_concat(be, te), m);
body = m_sg.mk(cat);
}
}
// Advance the regex state
euf::snode const* deriv = m_sg.brzozowski_deriv(re_state, tok);
if (!deriv)
break;
re_state = deriv;
}
if (!body)
continue;
if (!overall_union) {
overall_union = body;
} else {
expr* oe = overall_union->get_expr();
expr* be = body->get_expr();
if (oe && be) {
expr_ref u(seq.re.mk_union(oe, be), m);
overall_union = m_sg.mk(u);
}
}
}
return overall_union;
}
// -----------------------------------------------------------------------
// Stabilizer-based subsumption (enhanced)
// Mirrors ZIPT StrMem.TrySubsume (StrMem.cs:354-386)
// -----------------------------------------------------------------------
bool seq_regex::try_subsume(str_mem const& mem, nielsen_node const& node) {
#if 0
SASSERT(mem.m_str && mem.m_regex);
// 1. Leading token must be a variable
euf::snode const* first = mem.m_str->first();
if (!first || !first->is_var())
return false;
// 2. Must have stabilizers for the regex
if (!has_stabilizers(mem.m_regex))
return false;
// 3. Build stabStar = star(union(all stabilizers for this regex))
euf::snode const* stab_union = get_stabilizer_union(mem.m_regex);
if (!stab_union)
return false;
expr* su_expr = stab_union->get_expr();
expr_ref stab_star(seq.re.mk_star(su_expr), m);
euf::snode const* stab_star_sn = m_sg.mk(stab_star);
// 4. Collect all primitive regex constraints on variable `first`
euf::snode const* x_range = collect_primitive_regex_intersection(first, node, dep);
if (!x_range)
return false;
// 5. Check L(x_range) ⊆ L(stab_star)
lbool result = is_language_subset(x_range, stab_star_sn);
return result == l_true;
#else
return false;
#endif
}
char_set seq_regex::minterm_to_char_set(expr* re_expr) {
unsigned max_c = seq.max_char();

View file

@ -10,13 +10,12 @@ Abstract:
Lazy regex membership processing for the Nielsen-based string solver.
Provides Brzozowski derivative computation, ground prefix/suffix
consumption, cycle detection in derivation histories, and
stabilizer-based subsumption for regex membership constraints.
consumption, BFS emptiness/intersection checks, and minterm/char-set
utilities for regex membership constraints.
Ports the following ZIPT StrMem operations:
- SimplifyCharRegex / SimplifyDir (ground prefix/suffix consumption)
- ExtractCycle / StabilizerFromCycle (cycle detection and widening)
- TrySubsume (stabilizer-based subsumption)
Cycle detection and stabilizer-based subsumption live in the landing
decomposition and land-state views of nielsen_graph (seq_nielsen.cpp);
the legacy stabilizer store that used to reside here has been removed.
The class wraps sgraph operations (brzozowski_deriv, compute_minterms,
drop_first, etc.) and provides a higher-level interface for
@ -45,22 +44,6 @@ namespace seq {
// cache for emptiness check (snode id -> lbool)
u_map<lbool> m_empty_cache;
// -----------------------------------------------------------------
// Stabilizer store (non-backtrackable, persists across solve() calls)
// Mirrors ZIPT Environment.stabilizers / selfStabilizing
// (Environment.cs:36-37)
// -----------------------------------------------------------------
// Maps regex snode id → list of stabilizer snodes.
// Each regex may accumulate multiple stabilizers from different
// cycle detections. The list is deduplicated by pointer equality.
u_map<euf::snode_vector> m_stabilizers;
// Set of regex snode ids that are self-stabilizing, i.e., the
// stabilizer for the regex is the regex itself (e.g., r*).
// Mirrors ZIPT Environment.selfStabilizing (Environment.cs:37)
uint_set m_self_stabilizing;
// -----------------------------------------------------------------
// BFS emptiness check helpers (private)
// -----------------------------------------------------------------
@ -100,75 +83,6 @@ namespace seq {
euf::sgraph& sg() { return m_sg; }
// -----------------------------------------------------------------
// Stabilizer store API
// Mirrors ZIPT Environment stabilizer management
// (Environment.cs:114-146)
// -----------------------------------------------------------------
// Reset all stabilizer data. Called at the start of each solve()
// invocation if fresh stabilizer state is desired.
// Note: stabilizers persist across depth-bound iterations by default;
// only call this to clear accumulated state.
void reset_stabilizers();
// Add a stabilizer for a regex. De-duplicates by pointer equality.
// Mirrors ZIPT Environment.AddStabilizer (Environment.cs:114-123).
void add_stabilizer(euf::snode const* regex, euf::snode const* stabilizer);
// Get the union of all stabilizers registered for a regex.
// Returns a single re.union snode combining all stabilizers,
// or nullptr if no stabilizers exist for the regex.
// Mirrors ZIPT Environment.GetStabilizerUnion (Environment.cs:125-128).
euf::snode const* get_stabilizer_union(euf::snode const* regex);
// Check if any stabilizers have been registered for a regex.
bool has_stabilizers(euf::snode const* regex) const;
// Get raw stabilizer list for a regex (read-only).
// Returns nullptr if no stabilizers exist.
euf::snode_vector const* get_stabilizers(euf::snode const* regex) const;
// Mark a regex as self-stabilizing (stabilizer == regex itself).
// Mirrors ZIPT Environment.SetSelfStabilizing (Environment.cs:143-146).
void set_self_stabilizing(euf::snode const* regex);
// Check if a regex is marked as self-stabilizing.
// Mirrors ZIPT Environment.IsSelfStabilizing (Environment.cs:134-141).
bool is_self_stabilizing(euf::snode const* regex) const;
// -----------------------------------------------------------------
// Self-stabilizing auto-detection and propagation through derivatives
// -----------------------------------------------------------------
// Determine if a regex is inherently self-stabilizing based on its
// structure. Returns true for:
// - R* (Kleene star): D(c, R*) = D(c,R)·R*, so R* is its own
// stabilizer regardless of the character.
// - Σ* (full_seq): D(c, Σ*) = Σ*, trivially self-stabilizing.
// - ∅ (fail/empty language): no live derivatives, trivially stable.
// - Complement of full_seq (~Σ* = ∅): also trivially stable.
// Does NOT mark the snode; call set_self_stabilizing to persist.
bool compute_self_stabilizing(euf::snode const* regex) const;
// After computing a derivative of parent, propagate the self-
// stabilizing flag to the derivative result if warranted.
// Applies structural rules:
// - If parent is R* → derivative is always self-stabilizing
// (derivative has the form D(c,R)·R* which contains the R* tail).
// - If parent is R·S and S is self-stabilizing → derivative may
// inherit from the self-stabilizing tail.
// - If parent is R|S and both are self-stabilizing → derivative is.
// - If parent is R∩S and both are self-stabilizing → derivative is.
// - If parent is ~R and R is self-stabilizing → derivative is.
// Updates the internal self-stabilizing set for the derivative.
void propagate_self_stabilizing(euf::snode const* parent, euf::snode const* deriv);
// Convenience: compute derivative and propagate self-stabilizing flags.
// Equivalent to calling derivative() followed by
// propagate_self_stabilizing().
euf::snode const* derivative_with_propagation(euf::snode const* re, euf::snode const* elem);
// -----------------------------------------------------------------
// Basic regex predicates
// -----------------------------------------------------------------
@ -192,21 +106,8 @@ namespace seq {
// l_true — intersection is definitely empty
// l_false — intersection is definitely non-empty
// l_undef — inconclusive (hit exploration bound)
// Mirrors ZIPT NielsenNode.CheckEmptiness (NielsenNode.cs:1429-1469)
lbool check_intersection_emptiness(euf::snode_vector const& regexes, unsigned max_states = UINT_MAX);
// Check if L(subset_re) ⊆ L(superset_re).
// Computed as: subset_re ∩ complement(superset_re) = ∅.
// Mirrors ZIPT NielsenNode.IsLanguageSubset (NielsenNode.cs:1382-1385)
lbool is_language_subset(euf::snode const* subset_re, euf::snode const* superset_re);
// Collect all primitive regex constraints on variable `var` from
// the node's str_mem list and return their intersection as a
// single regex snode (using re.inter).
// Returns nullptr if no primitive constraints found.
euf::snode const* collect_primitive_regex_intersection(
euf::snode const* var, nielsen_node const& node, dep_manager& dep_mgr, dep_tracker& dep) const;
// check if regex is the full language (Σ* / re.all)
static bool is_full_regex(euf::snode const* re) {
return re && re->is_full_seq();
@ -239,17 +140,12 @@ namespace seq {
// deriv(regex, c) = result. This enables deterministic consumption
// of symbolic (variable) characters without branching.
// Returns the uniform derivative if found, nullptr otherwise.
// Mirrors ZIPT's SimplifyCharRegex uniform-derivative fast path.
euf::snode const* try_uniform_derivative(euf::snode const* regex) const;
// compute derivative of a str_mem constraint: advance past one character.
// the string side is shortened by drop_first and the regex is derived.
// Propagates self-stabilizing flags from the parent regex to the derivative.
str_mem derive(str_mem const& mem, euf::snode const* elem) {
euf::snode const* parent_re = mem.m_regex;
euf::snode const* deriv = m_sg.brzozowski_deriv(parent_re, elem);
if (deriv)
propagate_self_stabilizing(parent_re, deriv);
euf::snode const* deriv = m_sg.brzozowski_deriv(mem.m_regex, elem);
euf::snode const* new_str = m_sg.drop_first(mem.m_str);
return str_mem(mem.m, new_str, deriv, mem.m_dep);
}
@ -316,54 +212,6 @@ namespace seq {
// (empty string in non-nullable regex, or derivative yields ∅).
bool process_str_mem(str_mem const& mem,
vector<str_mem>& out_mems);
// -----------------------------------------------------------------
// Cycle detection and stabilizers
// -----------------------------------------------------------------
// compute a Kleene star stabilizer from a cycle.
// given the regex at the cycle point and the current regex,
// builds r* that over-approximates any number of cycle iterations.
// returns nullptr if no stabilizer can be computed.
euf::snode const* stabilizer_from_cycle(euf::snode const* cycle_regex,
euf::snode const* current_regex);
// Strengthened stabilizer construction with sub-cycle detection.
// Replays the consumed character tokens from cycle_history on the
// cycle regex, detecting sub-cycles (where the derivative loops
// back to the original regex). For each sub-cycle, builds a
// stabilizer from the interleaved character tokens and filtered
// sub-stabilizers.
// Returns a union of all sub-cycle stabilizer bodies, or nullptr
// if no non-trivial stabilizer can be built.
// Mirrors ZIPT StrMem.StabilizerFromCycle (StrMem.cs:163-225).
euf::snode const* strengthened_stabilizer(euf::snode const* cycle_regex,
euf::snode const* cycle_history);
// Get filtered stabilizer star: for regex state re, retrieve
// existing stabilizers, filter out those whose language can
// start with any character in excluded_char, and wrap the
// remaining in star(union(...)).
// Returns nullptr (or empty-equivalent) if no valid stabilizers.
// Mirrors ZIPT StrMem.GetFilteredStabilizerStar (StrMem.cs:228-243).
euf::snode const* get_filtered_stabilizer_star(euf::snode const* re,
euf::snode const* excluded_char) const;
// Extract the cycle portion of a str_mem's history by comparing
// the current history with an ancestor's history length.
// Returns the sub-sequence of tokens consumed since the ancestor,
// or nullptr if the history did not advance.
euf::snode const* extract_cycle_history(str_mem const& current,
str_mem const& ancestor);
// try to subsume a str_mem constraint using stabilizer-based
// reasoning. Enhanced version: checks if the leading variable's
// language (intersection of all its primitive regex constraints)
// is a subset of star(union(stabilizers)) for the current regex.
// Falls back to cycle-based pointer equality check.
// returns true if the constraint can be dropped.
// Mirrors ZIPT StrMem.TrySubsume (StrMem.cs:354-386).
bool try_subsume(str_mem const& mem, nielsen_node const& node);
};
}

View file

@ -474,8 +474,19 @@ namespace smt {
// length and the regex are only loosely coupled), retry unconstrained so
// the emitted word still satisfies every regex/view constraint.
bool ok = m_nielsen->product_witness(var, *m_sat_node, len, w);
if (!ok && len != UINT_MAX)
if (!ok && len != UINT_MAX) {
ok = m_nielsen->product_witness(var, *m_sat_node, UINT_MAX, w);
// The view-length side constraints (add_view_length_constraints)
// keep the arith-assigned length realizable up to a semilinear
// over-approximation of the view language, so this retry should
// be rare. When it fires, the emitted word's length differs
// from the arith model's len(var): surface it instead of
// degrading the model silently.
IF_VERBOSE(1, if (ok && w.length() != len)
verbose_stream() << "nseq: view witness for " << mk_pp(var->get_expr(), m)
<< " has length " << w.length() << " but arith assigned " << len
<< " (model may be length-inconsistent)\n";);
}
if (ok) {
expr* witness = m_seq.str.mk_string(w);
m_trail.push_back(witness);

View file

@ -7,7 +7,6 @@ Module Name:
Abstract:
ZIPT string solver theory for Z3.
Implementation of theory_nseq.
Author:
@ -1745,7 +1744,7 @@ namespace smt {
// -----------------------------------------------------------------------
// Regex membership pre-check
// For each variable with regex membership constraints, check intersection
// emptiness before DFS. Mirrors ZIPT's per-variable regex evaluation.
// emptiness before DFS.
//
// Returns:
// l_true — conflict asserted (empty intersection for some variable)

View file

@ -7,7 +7,6 @@ Module Name:
Abstract:
ZIPT string solver theory for Z3.
Implements theory_nseq, a theory plugin for string/sequence constraints
using the Nielsen transformation graph (Nielsen graph).