mirror of
https://github.com/Z3Prover/z3
synced 2026-07-15 11:35:42 +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:
parent
5fc81bd1ae
commit
596cc14e83
18 changed files with 996 additions and 1316 deletions
|
|
@ -242,7 +242,7 @@ namespace euf {
|
|||
}
|
||||
|
||||
//
|
||||
// Concat simplification rules from ZIPT:
|
||||
// Concat simplification rules:
|
||||
//
|
||||
// 1. Kleene star merging: concat(u, v*, v*, w) = concat(u, v*, w)
|
||||
// when adjacent children in a concat chain have congruent star bodies.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Abstract:
|
|||
|
||||
Merges equivalence classes taking into account associativity
|
||||
of concatenation and algebraic properties of strings and
|
||||
regular expressions. Implements features from ZIPT:
|
||||
regular expressions.
|
||||
|
||||
-- Concat associativity: str.++ is associative, so
|
||||
concat(a, concat(b, c)) = concat(concat(a, b), c).
|
||||
|
|
|
|||
|
|
@ -172,11 +172,9 @@ namespace euf {
|
|||
}
|
||||
|
||||
case snode_kind::s_power: {
|
||||
// s^n: nullable follows base, consistent with ZIPT's PowerToken
|
||||
// s^n: nullable follows base:
|
||||
// the exponent n is assumed to be a symbolic integer, may or may not be zero
|
||||
// NSB review: SASSERT(n->num_args() == 2); and simplify code
|
||||
// NSB review: is this the correct definition of ground what about the exponent?
|
||||
SASSERT(n->num_args() >= 1);
|
||||
SASSERT(n->num_args() == 2);
|
||||
snode const* base = n->arg(0);
|
||||
n->m_ground = base->is_ground();
|
||||
n->m_regex_free = base->is_regex_free();
|
||||
|
|
@ -307,6 +305,108 @@ namespace euf {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Regex weight estimate
|
||||
//
|
||||
// A crude estimate of the size of the automaton for a regex, used to order
|
||||
// membership constraints in regex factorization (smaller = factorized
|
||||
// first, see str_mem::operator<). All arithmetic saturates at WEIGHT_MAX
|
||||
// rather than wrapping, so a complement/intersection blow-up produces a
|
||||
// huge-but-finite weight instead of aliasing a small one.
|
||||
// ---------------------------------------------------------------------
|
||||
static const unsigned WEIGHT_MAX = UINT_MAX;
|
||||
|
||||
static unsigned sat_add(unsigned a, unsigned b) {
|
||||
return a > WEIGHT_MAX - b ? WEIGHT_MAX : a + b;
|
||||
}
|
||||
|
||||
static unsigned sat_mul(unsigned a, unsigned b) {
|
||||
if (a == 0 || b == 0)
|
||||
return 0;
|
||||
return a > WEIGHT_MAX / b ? WEIGHT_MAX : a * b;
|
||||
}
|
||||
|
||||
static unsigned sat_pow2(unsigned e) {
|
||||
// 2^e; 1u<<31 still fits, e>=32 overflows unsigned -> saturate
|
||||
return e >= 32 ? WEIGHT_MAX : (1u << e);
|
||||
}
|
||||
|
||||
// Weight recurrence (children are already weighted -> O(1)):
|
||||
// w(ε)=0 w(a)=1 w(r1·r2)=w(r1)+w(r2)
|
||||
// w(r*)=w(r)+1 w(r1|r2)=w(r1)+w(r2) w(r1&r2)=w(r1)·w(r2)
|
||||
// w(~r)=2^{w(r)} w(ite(c,r1,r2))=max(w(r1),w(r2))
|
||||
void sgraph::compute_regex_weight(snode* n) {
|
||||
unsigned w = 0;
|
||||
switch (n->m_kind) {
|
||||
case snode_kind::s_empty:
|
||||
w = 0;
|
||||
break;
|
||||
case snode_kind::s_char:
|
||||
case snode_kind::s_unit:
|
||||
case snode_kind::s_range:
|
||||
case snode_kind::s_full_char:
|
||||
// a single (possibly symbolic) character / character class
|
||||
w = 1;
|
||||
break;
|
||||
case snode_kind::s_var:
|
||||
case snode_kind::s_power:
|
||||
// not a regex proper; treated as an atom
|
||||
w = 1;
|
||||
break;
|
||||
case snode_kind::s_fail:
|
||||
case snode_kind::s_full_seq:
|
||||
// trivial one-state automata (∅ and .*)
|
||||
w = 1;
|
||||
break;
|
||||
case snode_kind::s_concat:
|
||||
w = sat_add(n->arg(0)->regex_weight(), n->arg(1)->regex_weight());
|
||||
break;
|
||||
case snode_kind::s_union:
|
||||
w = sat_add(n->arg(0)->regex_weight(), n->arg(1)->regex_weight());
|
||||
break;
|
||||
case snode_kind::s_intersect:
|
||||
// product automaton
|
||||
w = sat_mul(n->arg(0)->regex_weight(), n->arg(1)->regex_weight());
|
||||
break;
|
||||
case snode_kind::s_star:
|
||||
w = sat_add(n->arg(0)->regex_weight(), 1u);
|
||||
break;
|
||||
case snode_kind::s_plus:
|
||||
w = sat_add(sat_mul(n->arg(0)->regex_weight(), 2u), 2u);
|
||||
break;
|
||||
case snode_kind::s_complement:
|
||||
// determinization blow-up
|
||||
w = sat_pow2(n->arg(0)->regex_weight());
|
||||
break;
|
||||
case snode_kind::s_loop: {
|
||||
// r{lo,hi}: ~hi unrolled copies (bounded) or r^lo·r* (unbounded)
|
||||
const unsigned base = n->num_args() > 0 ? n->arg(0)->regex_weight() : 0;
|
||||
unsigned lo = 1, hi = 1;
|
||||
expr* body = nullptr;
|
||||
if (n->get_expr() && m_seq.re.is_loop(n->get_expr(), body, lo, hi))
|
||||
w = sat_mul(base, std::max(hi, 1u));
|
||||
else if (n->get_expr() && m_seq.re.is_loop(n->get_expr(), body, lo))
|
||||
w = sat_add(sat_mul(base, sat_add(lo, 1u)), 1u);
|
||||
else
|
||||
w = sat_add(base, 1u);
|
||||
break;
|
||||
}
|
||||
case snode_kind::s_ite:
|
||||
// ite(c, r1, r2): args 1 and 2 are the branches
|
||||
w = std::max(n->arg(1)->regex_weight(), n->arg(2)->regex_weight());
|
||||
break;
|
||||
case snode_kind::s_to_re:
|
||||
// to_re(s): one state per character of the (possibly symbolic) word
|
||||
w = n->arg(0)->length();
|
||||
break;
|
||||
case snode_kind::s_in_re:
|
||||
default:
|
||||
w = 1;
|
||||
break;
|
||||
}
|
||||
n->m_regex_weight = w;
|
||||
}
|
||||
|
||||
static constexpr unsigned HASH_BASE = 31;
|
||||
|
||||
// Compute a 2x2 polynomial hash matrix for associativity-respecting hashing.
|
||||
|
|
@ -361,6 +461,7 @@ namespace euf {
|
|||
const unsigned id = m_nodes.size();
|
||||
snode *n = snode::mk(m_region, e, k, id, num_args, args);
|
||||
compute_metadata(n);
|
||||
compute_regex_weight(n);
|
||||
compute_hash_matrix(n);
|
||||
m_nodes.push_back(n);
|
||||
SASSERT(!n->is_char_or_unit() || m_seq.str.is_unit(n->get_expr()));
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ Abstract:
|
|||
Sequence/string graph layer
|
||||
|
||||
Encapsulates string and regex expressions for the string solver.
|
||||
Implements the string graph layer from ZIPT (https://github.com/CEisenhofer/ZIPT/tree/parikh/ZIPT).
|
||||
The sgraph maps Z3 sequence/regex AST expressions to snode structures
|
||||
organized as binary concatenation trees with metadata, and owns an
|
||||
egraph with a seq_plugin for congruence closure.
|
||||
|
|
@ -29,24 +28,6 @@ Abstract:
|
|||
* re.++ identity/absorption (concat(a, epsilon) = a, concat(a, ∅) = ∅).
|
||||
-- enode registration via mk_enode(expr*).
|
||||
|
||||
ZIPT features not yet ported:
|
||||
|
||||
-- Str operations: normalisation with union-find representatives and
|
||||
cache migration, balanced tree maintenance, drop left/right with
|
||||
caching, substitution, indexed access, iteration, ToList caching,
|
||||
simplification, derivative computation, structural equality with
|
||||
associative hashing, rotation equality, expression reconstruction,
|
||||
Graphviz export.
|
||||
|
||||
-- StrToken subclasses: SymCharToken, StrAtToken, SubStrToken,
|
||||
SetToken, PostToken/PreToken.
|
||||
|
||||
-- StrToken features: Nielsen-style GetDecomposition with side
|
||||
constraints, NamedStrToken extension tracking for variable
|
||||
splitting with PowerExtension, CollectSymbols for Parikh analysis,
|
||||
MinTerms for character class analysis, token ordering, Derivable
|
||||
and BasicRegex flags.
|
||||
|
||||
Author:
|
||||
|
||||
Clemens Eisenhofer 2026-03-01
|
||||
|
|
@ -112,6 +93,7 @@ namespace euf {
|
|||
snode* mk_snode(expr* e, snode_kind k, unsigned num_args, snode const** args);
|
||||
snode_kind classify(expr* e) const;
|
||||
void compute_metadata(snode* n);
|
||||
void compute_regex_weight(snode* n);
|
||||
void compute_hash_matrix(snode* n);
|
||||
void collect_re_predicates(snode const* re, expr_ref_vector& preds);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Abstract:
|
|||
snode layer for sequence/string graph
|
||||
|
||||
Encapsulates strings in the style of euf_enode.h.
|
||||
Maps Z3 sequence expressions to a ZIPT-style representation where
|
||||
Maps Z3 sequence expressions to a representation where
|
||||
strings are composed of tokens (characters, variables, powers, regex, etc.)
|
||||
organized as a binary tree of concatenations.
|
||||
|
||||
|
|
@ -66,13 +66,15 @@ namespace euf {
|
|||
unsigned m_id = UINT_MAX;
|
||||
unsigned m_num_args = 0;
|
||||
|
||||
// metadata flags, analogous to ZIPT's Str/StrToken properties
|
||||
// metadata flags
|
||||
bool m_ground = true; // no uninterpreted string variables
|
||||
bool m_regex_free = true; // no regex constructs
|
||||
bool m_is_classical = true; // classical regular expression
|
||||
bool m_rigid = false; // defined seq op (replace/replace_all/replace_re*) — opaque to Nielsen, never substitute/split
|
||||
unsigned m_level = 0; // tree depth/level (0 for empty, 1 for singletons)
|
||||
unsigned m_length = 0; // token count, number of leaf tokens in the tree
|
||||
unsigned m_regex_weight = 0; // estimated automaton size of this regex (saturating);
|
||||
// cached at creation by sgraph::compute_regex_weight
|
||||
|
||||
// hash matrix for associativity-respecting hashing (2x2 polynomial hash matrix)
|
||||
// all zeros means not cached, non-zero means cached
|
||||
|
|
@ -222,6 +224,15 @@ namespace euf {
|
|||
unsigned length() const {
|
||||
return m_length;
|
||||
}
|
||||
// Estimated size ("weight") of the automaton for this regex, computed
|
||||
// with saturating arithmetic (see sgraph::compute_regex_weight) and
|
||||
// cached at node creation. Used to order membership constraints in
|
||||
// regex factorization (smaller weight = factorized first). Meaningful
|
||||
// for regex snodes; for pure string/word nodes it just tracks a token
|
||||
// count and is ignored by the membership heuristic.
|
||||
unsigned regex_weight() const {
|
||||
return m_regex_weight;
|
||||
}
|
||||
|
||||
// associativity-respecting hash: cached if the 2x2 matrix is non-zero.
|
||||
// M[0][0] = HASH_BASE^(num_leaves) which is always nonzero since HASH_BASE
|
||||
|
|
@ -328,7 +339,7 @@ namespace euf {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
// is this a leaf token (analogous to ZIPT's StrToken as opposed to Str)
|
||||
// is this a leaf token
|
||||
bool is_token() const {
|
||||
switch (m_kind) {
|
||||
case snode_kind::s_empty:
|
||||
|
|
@ -341,7 +352,6 @@ namespace euf {
|
|||
return m_expr ? m_expr->get_sort() : nullptr;
|
||||
}
|
||||
|
||||
// analogous to ZIPT's Str.First / Str.Last
|
||||
snode const *first() const {
|
||||
snode const* s = this;
|
||||
while (s->is_concat())
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Module Name:
|
|||
|
||||
Abstract:
|
||||
|
||||
Unit tests for seq_parikh (Parikh image filter for the ZIPT Nielsen solver).
|
||||
Unit tests for seq_parikh.
|
||||
|
||||
Tests cover:
|
||||
- compute_length_stride / get_length_stride for all regex forms
|
||||
|
|
|
|||
|
|
@ -71,227 +71,6 @@ static void test_seq_regex_is_full() {
|
|||
std::cout << " ok: re.all not recognized as empty\n";
|
||||
}
|
||||
|
||||
// Test 4: strengthened_stabilizer — null safety
|
||||
static void test_strengthened_stabilizer_null() {
|
||||
std::cout << "test_strengthened_stabilizer_null\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
|
||||
SASSERT(nr.strengthened_stabilizer(nullptr, nullptr) == nullptr);
|
||||
|
||||
seq_util su(m);
|
||||
sort* str_sort = su.str.mk_string_sort();
|
||||
sort* re_sort = su.re.mk_re(str_sort);
|
||||
const expr_ref full_e(su.re.mk_full_seq(re_sort), m);
|
||||
euf::snode const* full_re = sg.mk(full_e);
|
||||
|
||||
SASSERT(nr.strengthened_stabilizer(full_re, nullptr) == nullptr);
|
||||
SASSERT(nr.strengthened_stabilizer(nullptr, full_re) == nullptr);
|
||||
std::cout << " ok\n";
|
||||
}
|
||||
|
||||
// Test 5: strengthened_stabilizer — single char cycle on a*
|
||||
// Regex a*, history = 'a'. D('a', a*) = a* (sub-cycle back to start).
|
||||
// Stabilizer body should be to_re("a").
|
||||
static void test_strengthened_stabilizer_single_char() {
|
||||
std::cout << "test_strengthened_stabilizer_single_char\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
// Build a*
|
||||
const expr_ref star_a(su.re.mk_star(su.re.mk_to_re(su.str.mk_string("a"))), m);
|
||||
euf::snode const* re_star_a = sg.mk(star_a);
|
||||
|
||||
// Build history = char 'a' (single token, no concat needed)
|
||||
euf::snode const* tok_a = sg.mk_char('a');
|
||||
euf::snode const* history = tok_a;
|
||||
|
||||
euf::snode const* result = nr.strengthened_stabilizer(re_star_a, history);
|
||||
// Should produce a non-null stabilizer body (to_re("a"))
|
||||
SASSERT(result != nullptr);
|
||||
std::cout << " ok: a* with history 'a' -> non-null stabilizer\n";
|
||||
}
|
||||
|
||||
// Test 6: strengthened_stabilizer — two-char cycle with sub-cycle
|
||||
// Regex (ab)*, history = 'a', 'b'. D('a', (ab)*) = b(ab)*, D('b', b(ab)*) = (ab)*
|
||||
// This should detect a sub-cycle and build a stabilizer body.
|
||||
static void test_strengthened_stabilizer_two_char() {
|
||||
std::cout << "test_strengthened_stabilizer_two_char\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
// Build (ab)*
|
||||
const expr_ref ab(su.re.mk_to_re(su.str.mk_string("ab")), m);
|
||||
const expr_ref star_ab(su.re.mk_star(ab), m);
|
||||
euf::snode const* re_star_ab = sg.mk(star_ab);
|
||||
|
||||
// Build history: concat(char_a, char_b) using string concat
|
||||
euf::snode const* tok_a = sg.mk_char('a');
|
||||
euf::snode const* tok_b = sg.mk_char('b');
|
||||
euf::snode const* history = sg.mk_concat(tok_a, tok_b);
|
||||
|
||||
euf::snode const* result = nr.strengthened_stabilizer(re_star_ab, history);
|
||||
// Should produce a non-null stabilizer body
|
||||
SASSERT(result != nullptr);
|
||||
std::cout << " ok: (ab)* with history 'ab' -> non-null stabilizer\n";
|
||||
}
|
||||
|
||||
// Test 7: get_filtered_stabilizer_star — no stabilizers registered
|
||||
static void test_filtered_stabilizer_star_empty() {
|
||||
std::cout << "test_filtered_stabilizer_star_empty\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
const seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
sort* str_sort = su.str.mk_string_sort();
|
||||
sort* re_sort = su.re.mk_re(str_sort);
|
||||
const expr_ref full_e(su.re.mk_full_seq(re_sort), m);
|
||||
euf::snode const* full_re = sg.mk(full_e);
|
||||
euf::snode const* tok_a = sg.mk_char('a');
|
||||
|
||||
euf::snode const* result = nr.get_filtered_stabilizer_star(full_re, tok_a);
|
||||
SASSERT(result == nullptr);
|
||||
std::cout << " ok: no stabilizers -> nullptr\n";
|
||||
}
|
||||
|
||||
// Test 8: get_filtered_stabilizer_star — with registered stabilizer that passes filter
|
||||
static void test_filtered_stabilizer_star_with_stab() {
|
||||
std::cout << "test_filtered_stabilizer_star_with_stab\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
// Build a* as the regex state
|
||||
const expr_ref star_a(su.re.mk_star(su.re.mk_to_re(su.str.mk_string("a"))), m);
|
||||
euf::snode const* re_star_a = sg.mk(star_a);
|
||||
|
||||
// Register a stabilizer: to_re("b") — only accepts "b"
|
||||
const expr_ref stab_b(su.re.mk_to_re(su.str.mk_string("b")), m);
|
||||
euf::snode const* stab_b_sn = sg.mk(stab_b);
|
||||
nr.add_stabilizer(re_star_a, stab_b_sn);
|
||||
|
||||
// Exclude char 'a': D('a', to_re("b")) should be fail
|
||||
euf::snode const* tok_a = sg.mk_char('a');
|
||||
euf::snode const* result = nr.get_filtered_stabilizer_star(re_star_a, tok_a);
|
||||
// to_re("b") should pass the filter → result is star(to_re("b"))
|
||||
SASSERT(result != nullptr);
|
||||
SASSERT(result->is_star());
|
||||
std::cout << " ok: filter keeps to_re('b') when excluding 'a'\n";
|
||||
}
|
||||
|
||||
// Test 9: get_filtered_stabilizer_star — stabilizer filtered out
|
||||
static void test_filtered_stabilizer_star_filtered() {
|
||||
std::cout << "test_filtered_stabilizer_star_filtered\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
// Build a* as the regex state
|
||||
const expr_ref star_a(su.re.mk_star(su.re.mk_to_re(su.str.mk_string("a"))), m);
|
||||
euf::snode const* re_star_a = sg.mk(star_a);
|
||||
|
||||
// Register a stabilizer: to_re("a") — accepts "a"
|
||||
const expr_ref stab_a(su.re.mk_to_re(su.str.mk_string("a")), m);
|
||||
euf::snode const* stab_a_sn = sg.mk(stab_a);
|
||||
nr.add_stabilizer(re_star_a, stab_a_sn);
|
||||
|
||||
// Exclude char 'a': D('a', to_re("a")) is NOT fail → filtered out
|
||||
euf::snode const* tok_a = sg.mk_char('a');
|
||||
euf::snode const* result = nr.get_filtered_stabilizer_star(re_star_a, tok_a);
|
||||
SASSERT(result == nullptr);
|
||||
std::cout << " ok: filter removes to_re('a') when excluding 'a'\n";
|
||||
}
|
||||
|
||||
// Test 10: extract_cycle_history — basic extraction
|
||||
static void test_extract_cycle_history_basic() {
|
||||
std::cout << "test_extract_cycle_history_basic\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
sort* str_sort = su.str.mk_string_sort();
|
||||
sort* re_sort = su.re.mk_re(str_sort);
|
||||
const expr_ref full_e(su.re.mk_full_seq(re_sort), m);
|
||||
euf::snode const* full_re = sg.mk(full_e);
|
||||
|
||||
euf::snode const* tok_a = sg.mk_char('a');
|
||||
euf::snode const* tok_b = sg.mk_char('b');
|
||||
euf::snode const* tok_c = sg.mk_char('c');
|
||||
|
||||
// Ancestor history: just 'a' (length 1)
|
||||
euf::snode const* anc_hist = tok_a;
|
||||
|
||||
// Current history: concat(concat(a, b), c) = a,b,c (length 3)
|
||||
euf::snode const* cur_hist = sg.mk_concat(sg.mk_concat(tok_a, tok_b), tok_c);
|
||||
|
||||
euf::snode const* empty_str = sg.mk_empty_seq(str_sort);
|
||||
const seq::dep_tracker empty_dep = nullptr;
|
||||
|
||||
const seq::str_mem ancestor(m, empty_str, full_re, empty_dep);
|
||||
const seq::str_mem current(m, empty_str, full_re, empty_dep);
|
||||
|
||||
euf::snode const* cycle = nr.extract_cycle_history(current, ancestor);
|
||||
// Should return the last 2 tokens (b, c)
|
||||
SASSERT(cycle != nullptr);
|
||||
SASSERT(cycle->length() == 2);
|
||||
std::cout << " ok: extracted cycle of length 2\n";
|
||||
}
|
||||
|
||||
// Test 11: extract_cycle_history — null ancestor history
|
||||
static void test_extract_cycle_history_null_ancestor() {
|
||||
std::cout << "test_extract_cycle_history_null_ancestor\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
sort* str_sort = su.str.mk_string_sort();
|
||||
sort* re_sort = su.re.mk_re(str_sort);
|
||||
const expr_ref full_e(su.re.mk_full_seq(re_sort), m);
|
||||
euf::snode const* full_re = sg.mk(full_e);
|
||||
|
||||
euf::snode const* tok_a = sg.mk_char('a');
|
||||
euf::snode const* tok_b = sg.mk_char('b');
|
||||
euf::snode const* cur_hist = sg.mk_concat(tok_a, tok_b);
|
||||
euf::snode const* empty_str = sg.mk_empty_seq(str_sort);
|
||||
const seq::dep_tracker empty_dep = nullptr;
|
||||
|
||||
// Ancestor has no history (nullptr)
|
||||
const seq::str_mem ancestor(m, empty_str, full_re, empty_dep);
|
||||
const seq::str_mem current(m, empty_str, full_re, empty_dep);
|
||||
|
||||
euf::snode const* cycle = nr.extract_cycle_history(current, ancestor);
|
||||
// With null ancestor history, entire current history is the cycle
|
||||
SASSERT(cycle != nullptr);
|
||||
SASSERT(cycle->length() == 2);
|
||||
std::cout << " ok: null ancestor -> full history as cycle\n";
|
||||
}
|
||||
|
||||
// Test 12: BFS emptiness — re.none (empty language) is empty
|
||||
static void test_bfs_empty_none() {
|
||||
std::cout << "test_bfs_empty_none\n";
|
||||
|
|
@ -490,63 +269,6 @@ static void test_char_set_is_subset() {
|
|||
std::cout << " ok\n";
|
||||
}
|
||||
|
||||
// Test: stabilizer store basic operations
|
||||
static void test_stabilizer_store_basic() {
|
||||
std::cout << "test_stabilizer_store_basic\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
const expr_ref a_re(su.re.mk_to_re(su.str.mk_string("a")), m);
|
||||
const expr_ref b_re(su.re.mk_to_re(su.str.mk_string("b")), m);
|
||||
euf::snode const* a_sn = sg.mk(a_re);
|
||||
euf::snode const* b_sn = sg.mk(b_re);
|
||||
|
||||
SASSERT(!nr.has_stabilizers(a_sn));
|
||||
nr.add_stabilizer(a_sn, b_sn);
|
||||
SASSERT(nr.has_stabilizers(a_sn));
|
||||
SASSERT(nr.get_stabilizer_union(a_sn) == b_sn);
|
||||
|
||||
// dedup: adding same stabilizer again
|
||||
nr.add_stabilizer(a_sn, b_sn);
|
||||
auto* stabs = nr.get_stabilizers(a_sn);
|
||||
SASSERT(stabs && stabs->size() == 1);
|
||||
|
||||
// reset
|
||||
nr.reset_stabilizers();
|
||||
SASSERT(!nr.has_stabilizers(a_sn));
|
||||
|
||||
std::cout << " ok\n";
|
||||
}
|
||||
|
||||
// Test: self-stabilizing flag
|
||||
static void test_self_stabilizing() {
|
||||
std::cout << "test_self_stabilizing\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
const expr_ref a_re(su.re.mk_to_re(su.str.mk_string("a")), m);
|
||||
euf::snode const* a_sn = sg.mk(a_re);
|
||||
|
||||
SASSERT(!nr.is_self_stabilizing(a_sn));
|
||||
nr.set_self_stabilizing(a_sn);
|
||||
SASSERT(nr.is_self_stabilizing(a_sn));
|
||||
|
||||
// star should be detected as self-stabilizing
|
||||
const expr_ref star_a(su.re.mk_star(a_re), m);
|
||||
euf::snode const* star_sn = sg.mk(star_a);
|
||||
SASSERT(nr.compute_self_stabilizing(star_sn));
|
||||
|
||||
std::cout << " ok\n";
|
||||
}
|
||||
|
||||
// Test: check_intersection_emptiness — SAT case
|
||||
static void test_check_intersection_sat() {
|
||||
std::cout << "test_check_intersection_sat\n";
|
||||
|
|
@ -600,85 +322,6 @@ static void test_check_intersection_unsat() {
|
|||
std::cout << " ok: to_re(a) ∩ to_re(b) is empty\n";
|
||||
}
|
||||
|
||||
// Test: is_language_subset — true case
|
||||
static void test_is_language_subset_true() {
|
||||
std::cout << "test_is_language_subset_true\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
// a* ⊆ (a|b)* should be true
|
||||
const expr_ref a_re(su.re.mk_to_re(su.str.mk_string("a")), m);
|
||||
const expr_ref star_a(su.re.mk_star(a_re), m);
|
||||
const expr_ref b_re(su.re.mk_to_re(su.str.mk_string("b")), m);
|
||||
const expr_ref ab_union(su.re.mk_union(a_re, b_re), m);
|
||||
const expr_ref star_ab(su.re.mk_star(ab_union), m);
|
||||
|
||||
euf::snode const* subset = sg.mk(star_a);
|
||||
euf::snode const* superset = sg.mk(star_ab);
|
||||
|
||||
const lbool result = nr.is_language_subset(subset, superset);
|
||||
SASSERT(result == l_true);
|
||||
std::cout << " ok: a* ⊆ (a|b)*\n";
|
||||
}
|
||||
|
||||
// Test: is_language_subset — false case
|
||||
static void test_is_language_subset_false() {
|
||||
std::cout << "test_is_language_subset_false\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
|
||||
// (a|b)* ⊄ a* should be false (b ∈ (a|b)* but b ∉ a*)
|
||||
const expr_ref a_re(su.re.mk_to_re(su.str.mk_string("a")), m);
|
||||
const expr_ref star_a(su.re.mk_star(a_re), m);
|
||||
const expr_ref b_re(su.re.mk_to_re(su.str.mk_string("b")), m);
|
||||
const expr_ref ab_union(su.re.mk_union(a_re, b_re), m);
|
||||
const expr_ref star_ab(su.re.mk_star(ab_union), m);
|
||||
|
||||
euf::snode const* subset = sg.mk(star_ab);
|
||||
euf::snode const* superset = sg.mk(star_a);
|
||||
|
||||
const lbool result = nr.is_language_subset(subset, superset);
|
||||
SASSERT(result == l_false);
|
||||
std::cout << " ok: (a|b)* ⊄ a*\n";
|
||||
}
|
||||
|
||||
// Test: is_language_subset — trivial cases
|
||||
static void test_is_language_subset_trivial() {
|
||||
std::cout << "test_is_language_subset_trivial\n";
|
||||
ast_manager m;
|
||||
reg_decl_plugins(m);
|
||||
euf::egraph eg(m);
|
||||
euf::sgraph sg(m, eg);
|
||||
seq::seq_regex nr(sg);
|
||||
seq_util su(m);
|
||||
sort* str_sort = su.str.mk_string_sort();
|
||||
|
||||
// ∅ ⊆ anything = true
|
||||
const expr_ref none(su.re.mk_empty(su.re.mk_re(str_sort)), m);
|
||||
const expr_ref a_re(su.re.mk_to_re(su.str.mk_string("a")), m);
|
||||
euf::snode const* empty_sn = sg.mk(none);
|
||||
euf::snode const* a_sn = sg.mk(a_re);
|
||||
SASSERT(nr.is_language_subset(empty_sn, a_sn) == l_true);
|
||||
|
||||
// anything ⊆ Σ* = true
|
||||
const expr_ref full(su.re.mk_full_seq(su.re.mk_re(str_sort)), m);
|
||||
euf::snode const* full_sn = sg.mk(full);
|
||||
SASSERT(nr.is_language_subset(a_sn, full_sn) == l_true);
|
||||
|
||||
// L ⊆ L = true (same pointer)
|
||||
SASSERT(nr.is_language_subset(a_sn, a_sn) == l_true);
|
||||
|
||||
std::cout << " ok\n";
|
||||
}
|
||||
|
||||
// Regression test: representative chosen from bounds must respect accumulated excludes.
|
||||
// Example language is [A-Z] \ {"A"}, so a valid witness exists (e.g., "B") but not "A".
|
||||
static void test_some_seq_in_re_excluded_low_regression() {
|
||||
|
|
@ -815,14 +458,6 @@ void tst_seq_regex() {
|
|||
test_seq_regex_instantiation();
|
||||
test_seq_regex_is_empty();
|
||||
test_seq_regex_is_full();
|
||||
test_strengthened_stabilizer_null();
|
||||
test_strengthened_stabilizer_single_char();
|
||||
test_strengthened_stabilizer_two_char();
|
||||
test_filtered_stabilizer_star_empty();
|
||||
test_filtered_stabilizer_star_with_stab();
|
||||
test_filtered_stabilizer_star_filtered();
|
||||
test_extract_cycle_history_basic();
|
||||
test_extract_cycle_history_null_ancestor();
|
||||
test_bfs_empty_none();
|
||||
test_bfs_nonempty_full();
|
||||
test_bfs_nonempty_to_re();
|
||||
|
|
@ -832,13 +467,8 @@ void tst_seq_regex() {
|
|||
test_bfs_empty_complement_full();
|
||||
// New tests for regex membership completion
|
||||
test_char_set_is_subset();
|
||||
test_stabilizer_store_basic();
|
||||
test_self_stabilizing();
|
||||
test_check_intersection_sat();
|
||||
test_check_intersection_unsat();
|
||||
test_is_language_subset_true();
|
||||
test_is_language_subset_false();
|
||||
test_is_language_subset_trivial();
|
||||
test_some_seq_in_re_excluded_low_regression();
|
||||
test_some_seq_in_re_inter_loop_regression();
|
||||
// test_bfs_null_safety has a pre-existing failure, run it last
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ public:
|
|||
};
|
||||
|
||||
// half-open character interval [lo, hi)
|
||||
// mirrors ZIPT's CharacterRange
|
||||
struct char_range {
|
||||
unsigned m_lo;
|
||||
unsigned m_hi; // exclusive
|
||||
|
|
@ -117,7 +116,6 @@ struct char_range {
|
|||
};
|
||||
|
||||
// sorted list of non-overlapping character intervals
|
||||
// mirrors ZIPT's CharacterSet
|
||||
class char_set {
|
||||
svector<char_range> m_ranges;
|
||||
public:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue