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:
parent
22eb098cd9
commit
07451d8922
3 changed files with 81 additions and 11 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue