3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-15 03:25:43 +00:00

seq_split: fix derivative-complement soundness+perf via canonical range_predicate

Resolves the WIP soundness bug. Root cause: the char-classes emitted for cofactor conditions were non-canonical and unrecognized downstream (of_pred(lambda) -> opaque select(lambda,ele); raw range unions carried (and true RANGE) that is_char_const_range rejected). Fix: mk_charclass_re maps the cofactor condition (true/false/and/or/not/=/char.<=) to the canonical seq::range_predicate via its Boolean ops, then range_predicate_to_regex (seq_range_collapse.h) yields a canonical char-class regex the derivative/emptiness/primitive path handles natively. Also: derive_of_pred beta-reduces select(lambda,ele) (general of_pred soundness fix).

RESULT on gen-lb (119): 0 default disagreements, 0 nseq spurious-unsat. L15 negcount family (27) timeout->solved: e.g. l15-digit-m3-dash now sat in 0.02s (dfs-nodes 33071->55, split complement/max-split-set eliminated). No regression (L11 sat, L14 unsat). Validates the notes.md derivative-based split redesign for star-free complements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Margus Veanes 2026-07-03 23:44:09 +03:00
parent 22eb098cd9
commit 07451d8922
3 changed files with 81 additions and 11 deletions

View file

@ -427,10 +427,22 @@ namespace seq {
expr_ref empty(re().mk_empty(re_sort), m);
expr_ref eps(re().mk_to_re(u().str.mk_empty(seq_sort)), m);
// Apply predicate to the element
array_util autil(m);
expr* args[2] = { pred, m_ele };
expr_ref cond(autil.mk_select(2, args), m);
// Apply the predicate to the current element. When `pred` is a lambda,
// beta-reduce select(lambda, ele) so the resulting character condition is
// in the range/eq form the derivative's condition analysis
// (is_char_const_range / eval_path_cond) understands; a raw
// (select (lambda ..) ele) would be opaque and mishandled downstream.
expr_ref cond(m);
if (is_lambda(pred)) {
var_subst subst(m);
expr* arg = m_ele;
cond = subst(to_quantifier(pred)->get_expr(), 1, &arg);
}
else {
array_util autil(m);
expr* args[2] = { pred, m_ele };
cond = autil.mk_select(2, args);
}
return mk_ite(cond, eps, empty);
}

View file

@ -18,6 +18,7 @@ Author:
#include "ast/rewriter/seq_split.h"
#include "ast/rewriter/seq_rewriter.h"
#include "ast/rewriter/seq_range_collapse.h"
#include "ast/ast_pp.h"
#include "util/obj_hashtable.h"
#include "util/obj_pair_hashtable.h"
@ -265,10 +266,64 @@ bool seq_split::complement(sort* seq_sort, split_set const& sp, split_set& resul
// emits *suspended* split-algebra terms (from_re / lcat / rcat / inter / compl) for
// the subterms instead of recursing. `mode` is irrelevant here: weak vs. strong is
// decided when `head_normalize` reaches an inter / compl node.
namespace {
// 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,
unsigned maxc, seq::range_predicate& out) {
expr* a = nullptr, * b = nullptr; unsigned c = 0;
if (m.is_true(pred)) { out = seq::range_predicate::top(maxc); return true; }
if (m.is_false(pred)) { out = seq::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); return true; }
if (b == x && sq.is_const_char(a, c)) { out = seq::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); return true; }
if (a == x && sq.is_const_char(b, c)) { out = seq::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, maxc, 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, maxc, 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, maxc, s)) return false;
out = out | s;
}
return true;
}
return false;
}
}
// Single-character regex for a cofactor path condition `pred` (a Boolean over the
// character (:var 0)): of_pred(lambda (c) . pred).
expr_ref seq_split::mk_charclass_re(expr* pred) {
sort* cs = seq().mk_char_sort();
// character (:var 0)). Materialized via the canonical seq::range_predicate as a
// union-of-ranges regex (fully supported by the derivative / emptiness / primitive
// path, and canonical so equivalent classes share AST identity). Falls back to
// of_pred(lambda) only for predicates outside the recognized range fragment.
expr_ref seq_split::mk_charclass_re(expr* pred, sort* seq_sort) {
seq_util& sq = seq();
sort* cs = sq.mk_char_sort();
expr_ref var0(m.mk_var(0, cs), m);
seq::range_predicate rp(sq.max_char());
if (pred_to_rp(m, sq, var0, pred, sq.max_char(), rp))
return seq::range_predicate_to_regex(sq, rp, seq_sort);
symbol nm("c");
expr_ref lam(m.mk_lambda(1, &cs, &nm, pred), m);
return expr_ref(re().mk_of_pred(lam), m);
@ -435,7 +490,7 @@ expr_ref seq_split::expand_fromre(expr* r, bool& ok, obj_hashtable<expr>& deriv_
expr_ref_pair_vector cofs(m);
m_rw.brz_derivative_cofactors(r, cofs); // {(alpha_i, tgt_i)} = LF(delta(~a))
for (auto const& [cond, tgt] : cofs) {
expr_ref alpha = mk_charclass_re(cond); // single-char regex
expr_ref alpha = mk_charclass_re(cond, seq_sort); // single-char regex
expr_ref term(rex.mk_concat(alpha, tgt), m); // alpha_i . tgt_i
unfolded = expr_ref(rex.mk_union(unfolded, term), m);
}

View file

@ -136,9 +136,12 @@ class seq_split {
// One level of the sigma rules: from_re(r) -> a SplitSet term built from the
// immediate subterms. `ok` is set false on an unsupported shape.
expr_ref expand_fromre(expr* r, bool& ok, obj_hashtable<expr>& deriv_memo);
// Build the single-character regex of_pred(lambda c. pred) from a cofactor
// path condition `pred` (a Boolean over the character (:var 0)).
expr_ref mk_charclass_re(expr* pred);
// Build the single-character regex for a cofactor path condition `pred` (a
// Boolean over the character (:var 0)). Prefer a range / union-of-ranges
// (which nseq's emptiness/primitive/length path fully supports); fall back to
// of_pred(lambda) only for predicates that are not a single (possibly negated)
// range.
expr_ref mk_charclass_re(expr* pred, sort* seq_sort);
// Distribute a left/right concatenation over a head-normal split-set.
expr_ref distribute_lcat(expr* r, expr* hs);
expr_ref distribute_rcat(expr* hs, expr* r);