mirror of
https://github.com/Z3Prover/z3
synced 2026-08-02 20:23:27 +00:00
seq_monadic: self-contained monadic-decomposition regex membership solver (generic elements, witnesses, Boolean combinations) (#10296)
## Summary
Adds `seq_monadic` (`src/ast/rewriter/seq_monadic.{h,cpp}`), a
self-contained,
rewriter-level decision procedure for regex membership of a term that is
a
concatenation of sequence variables and constant elements — e.g. `x·a·x
∈ R`,
including repeated and multiple variables. It uses a whole-language
*monadic
decomposition* plus automaton product-reachability; it is minterm-free
and does
**not** use Nielsen word-equation splitting or `seq_split`.
The component is **purely additive**: it is not wired into any solver
path, so
default behavior is unchanged. It ships with a unit test and an opt-in
benchmark
harness that is inert unless `Z3_SEQ_BENCH_DIR` is set.
## What it does
- `x·u ∈ R ⇔ ⋁_q ( x reaches q in A_R ∧ u ∈ q )` over the derivative
automaton; `reach(q)` is never materialized as a regex (avoids the
state-elimination blowup). A variable's constraint is decided by a lazy
product-reachability search over tuples of derivative states, with
transitions
= the product of `brz_derivative_cofactors` branches and
pairwise-conjoined
`seq::range_predicate` guards.
- **Generic in the element sort**: characters use the exact
`range_predicate`
algebra; any other element sort uses a candidate-basis over the element
values
the guards mention (sound and complete for the
`{true,false,=,<=,and,or,not}`
guard grammar the derivatives emit).
- **Concrete witnesses**: on sat it reconstructs a witness value (a
sequence of
concrete elements, not predicates) per variable from the accepting
product path.
- **Boolean combinations**: `solve_and` decides a conjunction of
memberships
jointly, so a variable shared across memberships is constrained
consistently.
This is the natural extension since `¬(t∈R) ≡ t∈~R`, `∨` = union of
DNFs, and
`∧` = product of DNFs.
Also de-duplicates the char-guard → `range_predicate` translator into a
single
public `seq::guard_to_range_predicate` in `seq_range_collapse` (it was
previously
duplicated there and in `seq_monadic`).
## Testing
- `tst_seq_monadic`: single / multiple / repeated variables, nested
complement,
bounded loops, per-variable constraints, a generic `(Seq Int)` section,
witness
verification (substitute the model back and re-decide membership), and
`solve_and` cases that are individually sat but jointly unsat.
- Full unit suite `test-z3 /a` passes (93/93).
## Evaluation (offline harness, not part of CI)
On a regex-membership benchmark corpus, restricted to files carrying a
genuine
`(set-info :status)`, the solver decides 318 and 316 are correct
(99.4%); the
only 2 disagreements are a length limitation (`|x|=2k`) that is outside
the
membership fragment.
## Known limitations / follow-ups
- Pathological deeply-nested, high-multiplicity regexes can overflow the
recursive derivative stack; a recursion-depth guard to degrade to
`unknown` is
a natural follow-up.
- Out-of-fragment constraints (word equations, length / Parikh) are not
handled,
by design — this decides regex membership only.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Nikolaj Bjorner <nikolaj@cs.stanford.edu>
Copilot-Session: 916db256-43c6-4067-b6f4-fa8d2cf2f37f
This commit is contained in:
parent
3c685d368b
commit
0972dd2141
8 changed files with 1118 additions and 36 deletions
|
|
@ -21,66 +21,66 @@ Authors:
|
|||
|
||||
namespace seq {
|
||||
|
||||
// Cofactor path condition `pred` (a Boolean over x = (:var 0)) -> the canonical
|
||||
// range_predicate (union of ranges) of the characters satisfying it. Returns
|
||||
// false on a construct outside {true,false,and,or,not,=,char.<=} over x.
|
||||
static bool pred_to_rp(ast_manager &m, seq_util &sq, expr *x, expr *pred,
|
||||
seq::range_predicate &out) {
|
||||
unsigned maxc = sq.max_char();
|
||||
expr *a = nullptr, *b = nullptr;
|
||||
// Cofactor guard `guard` (a Boolean over the character variable v0 = (:var 0)) ->
|
||||
// the canonical range_predicate (union of ranges) of the characters satisfying it.
|
||||
// Returns false on a construct outside {true,false,and,or,not,=,char.<=} over v0.
|
||||
bool guard_to_range_predicate(seq_util& u, expr* v0, expr* guard, range_predicate& out) {
|
||||
ast_manager& m = u.get_manager();
|
||||
unsigned maxc = u.max_char();
|
||||
expr* a = nullptr, * b = nullptr;
|
||||
unsigned c = 0;
|
||||
if (m.is_true(pred)) {
|
||||
out = seq::range_predicate::top(maxc);
|
||||
if (m.is_true(guard)) {
|
||||
out = range_predicate::top(maxc);
|
||||
return true;
|
||||
}
|
||||
if (m.is_false(pred)) {
|
||||
out = seq::range_predicate::empty(maxc);
|
||||
if (m.is_false(guard)) {
|
||||
out = range_predicate::empty(maxc);
|
||||
return true;
|
||||
}
|
||||
if (m.is_eq(pred, a, b)) {
|
||||
if (a == x && sq.is_const_char(b, c)) {
|
||||
out = seq::range_predicate::singleton(c, maxc);
|
||||
if (m.is_eq(guard, a, b)) {
|
||||
if (a == v0 && u.is_const_char(b, c)) {
|
||||
out = range_predicate::singleton(c, maxc);
|
||||
return true;
|
||||
}
|
||||
if (b == x && sq.is_const_char(a, c)) {
|
||||
out = seq::range_predicate::singleton(c, maxc);
|
||||
if (b == v0 && u.is_const_char(a, c)) {
|
||||
out = range_predicate::singleton(c, maxc);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (sq.is_char_le(pred, a, b)) {
|
||||
if (b == x && sq.is_const_char(a, c)) {
|
||||
out = seq::range_predicate::range(c, maxc, maxc);
|
||||
if (u.is_char_le(guard, a, b)) {
|
||||
if (b == v0 && u.is_const_char(a, c)) {
|
||||
out = range_predicate::range(c, maxc, maxc);
|
||||
return true;
|
||||
}
|
||||
if (a == x && sq.is_const_char(b, c)) {
|
||||
out = seq::range_predicate::range(0, c, maxc);
|
||||
if (a == v0 && u.is_const_char(b, c)) {
|
||||
out = range_predicate::range(0, c, maxc);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (m.is_not(pred, a)) {
|
||||
seq::range_predicate s(maxc);
|
||||
if (!pred_to_rp(m, sq, x, a, s))
|
||||
if (m.is_not(guard, a)) {
|
||||
range_predicate s(maxc);
|
||||
if (!guard_to_range_predicate(u, v0, a, s))
|
||||
return false;
|
||||
out = ~s;
|
||||
return true;
|
||||
}
|
||||
if (m.is_and(pred)) {
|
||||
out = seq::range_predicate::top(maxc);
|
||||
for (expr *arg : *to_app(pred)) {
|
||||
seq::range_predicate s(maxc);
|
||||
if (!pred_to_rp(m, sq, x, arg, s))
|
||||
if (m.is_and(guard)) {
|
||||
out = range_predicate::top(maxc);
|
||||
for (expr *arg : *to_app(guard)) {
|
||||
range_predicate s(maxc);
|
||||
if (!guard_to_range_predicate(u, v0, arg, s))
|
||||
return false;
|
||||
out = out & s;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (m.is_or(pred)) {
|
||||
out = seq::range_predicate::empty(maxc);
|
||||
for (expr *arg : *to_app(pred)) {
|
||||
seq::range_predicate s(maxc);
|
||||
if (!pred_to_rp(m, sq, x, arg, s))
|
||||
if (m.is_or(guard)) {
|
||||
out = range_predicate::empty(maxc);
|
||||
for (expr *arg : *to_app(guard)) {
|
||||
range_predicate s(maxc);
|
||||
if (!guard_to_range_predicate(u, v0, arg, s))
|
||||
return false;
|
||||
out = out | s;
|
||||
}
|
||||
|
|
@ -183,7 +183,7 @@ namespace seq {
|
|||
auto body = q->get_expr();
|
||||
sort *char_sort = q->get_decl_sort(0);
|
||||
expr_ref var(m.mk_var(0, char_sort), m);
|
||||
if (u.get_char_plugin().get_family_id() == char_sort->get_family_id() && pred_to_rp(m, u, var, body, out))
|
||||
if (u.get_char_plugin().get_family_id() == char_sort->get_family_id() && guard_to_range_predicate(u, var, body, out))
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue