3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-08-02 20:23:27 +00:00

seq_monadic: add light Antimirov cofactor mode (#10323)

Light-weight Antimirov cofactors for seq_monadic
================================================

Overview
--------

The seq_monadic solver explores symbolic regex derivatives when
computing live
states and product transitions. Its original transition representation
used
Brzozowski cofactors.

This change adds a light-weight Antimirov representation. It first
computes the
existing Brzozowski cofactors and then decomposes targets whose outer
shape is

    s1 | ... | sn

or

    (s1 | ... | sn) . tail

into separate transitions. Concatenations are maintained in
right-associative
form, so a distributable union occurs as the head of the concatenation.
Targets are reconstructed with mk_regex_concat to preserve
normalization.

After decomposition, transitions with the same target are merged by
disjoining
their guards. The existing path-aware cofactor traversal and range
predicates
are therefore retained.

Example
-------

For

    r = .* a .{k}

the Brzozowski cofactors have the shape

    [a,  r | .{k}]
    [^a, r]

The light-weight Antimirov transformation produces

    [., r]
    [a, .{k}]

This preserves the language while avoiding the deterministic subset
states
that grow exponentially on this family.

Modes
-----

seq_monadic exposes two transition modes:

    light_antimirov   default
    brzozowski        retained as an explicit option

The implementation is seq_rewriter::light_ant_derivative_cofactors.

Correctness
-----------

The seq_monadic unit tests run in both modes. They cover character and
generic
element sequences, multiple and repeated variables, variable
constraints,
bounded loops, conjunctions of memberships, and witness construction. A
focused test checks the cofactor transformation above. The complete
94-test
unit suite used during evaluation passed. The benchmark harness is not
registered as a normal unit test; after detaching it, all 93 registered
tests
pass.

Benchmark evaluation
--------------------

The final optimized comparison used all 1,545 SMT2 files under
C:\git\bench\inputs\regexes. Each file was run in a separate process
with a
15-second timeout. There were 1,513 cases where both modes completed
without a
process failure or timeout.

                                    Brzozowski       Light-Ant
    paired solver time                 94.98 s          63.26 s
    median solver time                  1.734 ms         0.710 ms
    derivative calls                    5.70 M           3.39 M
    cofactors                           13.33 M           6.99 M
    live states                          2.74 M           0.44 M
    product states                     652.8 K         642.8 K

Light-Ant reduced paired solver time by 33.4%, derivative calls by
40.5%,
cofactors by 47.5%, and live states by 84.0%. It was faster on 1,091
cases;
Brzozowski was faster on 421 cases.

Light-Ant changed 131 Brzozowski undef results to sat. There were no
reverse
verdict changes and no mismatches against known sat/unsat statuses. Both
modes
had two timeouts. Process failures decreased from 30 to 27.

By corpus, paired solver time improved by 39.1% on ClemensRegex and by
11.9% on
MargusRegex.

Alternatives considered
-----------------------

Direct use of full Antimirov derivatives was also evaluated. It
introduced
large performance outliers, particularly around intersections, and
produced
additional undef results and timeouts. Disabling intersection-over-union
distribution improved some of these cases but remained slower and less
robust
than the light-weight transformation. The direct full-Ant mode was
therefore
removed from this change.

Copilot-Session: a2ce3573-4e15-4a4a-afb5-21e3cb04e4a2
This commit is contained in:
Margus Veanes 2026-07-31 10:34:34 -07:00 committed by GitHub
parent 214726519d
commit 683cb4ec03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 152 additions and 11 deletions

View file

@ -40,6 +40,7 @@ class seq_monadic_test {
seq_util u;
sort_ref m_str; // String sort
sort_ref m_re; // RegEx sort over m_str
seq_monadic::transition_mode m_mode;
unsigned m_fail = 0;
seq_util::rex& re() { return u.re; }
@ -51,6 +52,7 @@ class seq_monadic_test {
expr_ref star(expr* a) { return expr_ref(re().mk_star(a), m); }
expr_ref inter(expr* a, expr* b) { return expr_ref(re().mk_inter(a, b), m); }
expr_ref comp(expr* a) { return expr_ref(re().mk_complement(a), m); }
expr_ref dot() { return expr_ref(re().mk_full_char(m_re), m); }
expr_ref dotstar() { return expr_ref(re().mk_full_seq(m_re), m); }
expr_ref rng(char lo, char hi) {
char sl[2] = { lo, 0 }, sh[2] = { hi, 0 };
@ -70,6 +72,53 @@ class seq_monadic_test {
expr_ref xyx(expr* x, expr* y) { return sconcat(x, sconcat(y, x)); }
static char const* s(lbool l) { return l == l_true ? "sat" : l == l_false ? "unsat" : "undef"; }
char const* mode_name() const {
switch (m_mode) {
case seq_monadic::transition_mode::brzozowski: return "brz";
case seq_monadic::transition_mode::light_antimirov: return "light-ant";
}
UNREACHABLE();
return "";
}
bool eval_guard(expr* guard, unsigned ch) {
sort* elem_sort = nullptr;
VERIFY(u.is_seq(m_str, elem_sort));
expr_ref v0(m.mk_var(0, elem_sort), m);
expr_safe_replace rep(m);
rep.insert(v0, u.str.mk_char(ch));
expr_ref instantiated(m), simplified(m);
rep(guard, instantiated);
th_rewriter rw(m);
rw(instantiated, simplified);
return m.is_true(simplified);
}
void check_ant_cofactors() {
expr_ref a = word("a");
expr_ref any = dotstar();
expr_ref dot3 = loop(dot(), 3, 3);
expr_ref R = cat(any, cat(a, dot3));
expr_ref_pair_vector cof(m);
m_rw.light_ant_derivative_cofactors(R, cof);
bool found_R = false, found_dot3 = false, ok = cof.size() == 2;
for (auto const& [guard, target] : cof) {
if (target == R) {
found_R = eval_guard(guard, 'a') && eval_guard(guard, 'b');
}
else if (target == dot3) {
found_dot3 = eval_guard(guard, 'a') && !eval_guard(guard, 'b');
}
else {
ok = false;
}
}
ok = ok && found_R && found_dot3;
if (!ok) ++m_fail;
std::cout << (ok ? " OK " : " FAIL ")
<< mode_name() << " cofactor construction\n";
}
void check(char const* name, expr* term, expr* R, lbool expected) {
lbool got = m_mon.solve(term, R);
@ -145,12 +194,16 @@ class seq_monadic_test {
}
public:
seq_monadic_test() : m_reg(m), m_rw(m), m_mon(m_rw), u(m), m_str(m), m_re(m) {
seq_monadic_test(seq_monadic::transition_mode mode) :
m_reg(m), m_rw(m), m_mon(m_rw, mode), u(m), m_str(m), m_re(m), m_mode(mode) {
m_str = u.str.mk_string_sort();
m_re = re().mk_re(m_str);
}
void run() {
std::cout << "=== seq_monadic mode: " << mode_name() << " ===\n";
if (m_mode == seq_monadic::transition_mode::light_antimirov)
check_ant_cofactors();
expr_ref x = var("x");
expr_ref a = word("a");
expr_ref b = word("b");
@ -309,6 +362,8 @@ public:
}
void tst_seq_monadic() {
seq_monadic_test t;
t.run();
seq_monadic_test brz(seq_monadic::transition_mode::brzozowski);
brz.run();
seq_monadic_test light_ant(seq_monadic::transition_mode::light_antimirov);
light_ant.run();
}