3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-08-07 06:28:18 +00:00

guard_set: cache guard -> range predicate translation (#10361)

`guard_set::conjoin` re-translated its guard expression into a
`seq::range_predicate` on every call. It sits in the innermost loop of
`seq_monadic::product_nonempty`'s product-transition enumeration, where
the same handful of cofactor guards recurs on every branch of the
product search — a single regex benchmark was measured at **4.4M
translations of a few dozen distinct guards**.

## Change

Add an optional `guard -> range_predicate` cache to `guard_set`. It is
owned by the caller so its lifetime can be tied to the lifetime of the
guard expressions themselves; `seq_monadic` keys it off
`m_cofactor_cache` (which owns the guards) and resets both together. A
null cache entry records an unsupported guard, preserving the `m_ok =
false` behaviour without retranslating.

The cache is used only on the character-sort path, and is valid only
while every `guard_set` sharing it uses the same element variable `v0`.
That holds in `seq_monadic`: `v0` is `m.mk_var(0, m_elem_sort)`, which
is hash-consed, and the element sort is fixed for the duration of a
solve. The parameter defaults to `nullptr`, so any other caller keeps
the previous behaviour unchanged.

No behavioural change.

## Validation

`test-z3 seq_monadic`: ALL PASS (0 fail) in both `brz` and `light-ant`
modes.

1545-file regex corpus (`light-ant`, 20s timeout per file):

| | master | this PR |
|---|---|---|
| sat / unsat / undef | 1166 / 255 / 121 | 1166 / 255 / 121 |
| timeouts | 3 | 2 |
| solve time, 1421 files decided by both | 74.7 s | **8.4 s (~9x)** |

Verdicts are identical to master on every file; one benchmark that
previously hit the 20s timeout now terminates.

Full QF_S corpus (22,172 files) on the follow-up branch that builds on
this one: 0 crashes, 0 timeouts, and on the 4,089 benchmarks where the
monadic solver is complete and the expected status is known, 0
mismatches against the declared status.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a2ce3573-4e15-4a4a-afb5-21e3cb04e4a2
This commit is contained in:
Margus Veanes 2026-08-02 11:26:50 -07:00 committed by GitHub
parent 05321fbe0f
commit 690c5a4c22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 49 additions and 10 deletions

View file

@ -19,14 +19,21 @@ Author:
#include "ast/arith_decl_plugin.h"
#include "ast/bv_decl_plugin.h"
guard_set::guard_set(ast_manager& _m, seq_util& _u, sort* elem_sort, expr* v0)
guard_set::guard_set(ast_manager& _m, seq_util& _u, sort* elem_sort, expr* v0,
rp_cache* cache)
: m(_m), u(_u), m_sort(elem_sort), m_v0(v0),
m_is_char(_u.is_char(elem_sort)),
m_is_char(_u.is_char(elem_sort)), m_rp_cache(cache),
m_rp(_u.max_char()), m_guard(_m) {
if (m_is_char) m_rp = seq::range_predicate::top(u.max_char());
else m_guard = m.mk_true();
}
void guard_set::dealloc_cache(rp_cache& c) {
for (auto const& [k, v] : c)
dealloc(v);
c.reset();
}
void guard_set::collect_consts(expr* g, ptr_vector<expr>& out) const {
expr* a = nullptr, * b = nullptr;
if (m.is_and(g) || m.is_or(g)) {
@ -132,13 +139,30 @@ lbool guard_set::generic_eval(expr_ref* witness) const {
void guard_set::conjoin(expr* g) {
if (!m_ok) return;
if (m_is_char) {
seq::range_predicate s(u.max_char());
if (!seq::guard_to_range_predicate(u, m_v0, g, s)) { m_ok = false; return; }
m_rp = m_rp & s;
}
else
if (!m_is_char) {
m_guard = m.mk_and(m_guard, g);
return;
}
if (m_rp_cache) {
// Translating a guard expression into a range predicate is the inner loop of the
// product-transition enumeration; the same handful of guards recurs on every
// branch, so translate each one once.
seq::range_predicate* s = nullptr;
if (!m_rp_cache->find(g, s)) {
s = alloc(seq::range_predicate, u.max_char());
if (!seq::guard_to_range_predicate(u, m_v0, g, *s)) {
dealloc(s);
s = nullptr;
}
m_rp_cache->insert(g, s);
}
if (!s) { m_ok = false; return; }
m_rp = m_rp & *s;
return;
}
seq::range_predicate s(u.max_char());
if (!seq::guard_to_range_predicate(u, m_v0, g, s)) { m_ok = false; return; }
m_rp = m_rp & s;
}
lbool guard_set::eval(expr_ref* witness) const {

View file

@ -31,12 +31,21 @@ Author:
#include "ast/rewriter/seq_range_collapse.h"
class guard_set {
public:
// Cache of guard expression -> its range predicate (null value: unsupported guard).
// Only meaningful for the character sort, and only valid while every guard_set sharing
// it uses the same element variable v0 (v0 is hash-consed per element sort).
typedef obj_map<expr, seq::range_predicate*> rp_cache;
static void dealloc_cache(rp_cache& c);
private:
ast_manager& m;
seq_util& u;
sort* m_sort;
expr* m_v0;
bool m_is_char;
bool m_ok = true; // false: an unsupported guard was conjoined
rp_cache* m_rp_cache = nullptr;
seq::range_predicate m_rp; // char representation
expr_ref m_guard; // generic representation (conjunction over v0)
@ -54,7 +63,8 @@ class guard_set {
lbool generic_eval(expr_ref* witness) const;
public:
guard_set(ast_manager& _m, seq_util& _u, sort* elem_sort, expr* v0);
guard_set(ast_manager& _m, seq_util& _u, sort* elem_sort, expr* v0,
rp_cache* cache = nullptr);
bool ok() const { return m_ok; }

View file

@ -80,6 +80,7 @@ void seq_monadic::reset_cofactor_cache() {
for (auto const& [k, v] : m_cofactor_cache)
dealloc(v);
m_cofactor_cache.reset();
guard_set::dealloc_cache(m_rp_cache); // the guards live in the cofactor vectors
}
bool seq_monadic::live_states(expr* R, expr_ref_vector& out) {
@ -252,7 +253,7 @@ lbool seq_monadic::product_nonempty(svector<component> const& comps, expr_ref* w
if (bail) return;
}
};
guard_set top(m, u(), m_elem_sort, var0);
guard_set top(m, u(), m_elem_sort, var0, &m_rp_cache);
rec(0, top);
if (bail)
return l_undef;

View file

@ -54,6 +54,7 @@ Author:
#include "ast/rewriter/seq_rewriter.h"
#include "ast/rewriter/seq_range_predicate.h"
#include "ast/rewriter/guard_set.h"
#include "ast/rewriter/th_rewriter.h"
#include "util/lbool.h"
#include "util/obj_hashtable.h"
@ -85,6 +86,9 @@ private:
bool m_min_core = true; // whether check() minimizes the unsat core (else: all deps)
obj_map<expr, expr*> m_model; // last extracted model (var -> witness); see get_model()
obj_map<expr, expr_ref_pair_vector*> m_cofactor_cache; // memoizes derivative_cofactors per regex
guard_set::rp_cache m_rp_cache; // cofactor guard -> range predicate; the guards
// are owned by m_cofactor_cache, so both are
// reset together
using membership_vec = vector<std::tuple<expr_ref, expr_ref, void*>>;
membership_vec m_memberships; // asserted (term in regex, dep) for check()
ptr_vector<void> m_core; // dependencies of an unsat subset, filled by check() on l_false