diff --git a/src/ast/euf/euf_sgraph.cpp b/src/ast/euf/euf_sgraph.cpp index 962cc2cd81..5369c25454 100644 --- a/src/ast/euf/euf_sgraph.cpp +++ b/src/ast/euf/euf_sgraph.cpp @@ -706,6 +706,13 @@ namespace euf { expr* elem_expr = elem->get_expr(); SASSERT(re_expr); SASSERT(elem_expr); + + // Pure function of (re, elem) -- see m_deriv_cache. + snode const* cached = nullptr; + if (m_deriv_cache.find(re_expr, elem_expr, cached)) + return cached; + expr* const key_re = re_expr; + expr* const key_elem = elem_expr; // std::cout << "Derivative of " << seq::snode_label_html(re, m) << "\nwith respect to " << seq::snode_label_html(elem, m) << std::endl; // if (allowed_range) // std::cout << "using " << seq::snode_label_html(allowed_range, m) << std::endl; @@ -758,9 +765,10 @@ namespace euf { // — notably intersections like (A∩B) vs (B∩A) or (a|∅)·R vs a·R — get // distinct ids, breaking partial-DFA Q-membership and view/guard lap // detection (the multi-cycle / intersection divergence). - th_rewriter trw(m); - trw(result); - return mk(result); + m_th_rewriter(result); + snode const* res = mk(result); + m_deriv_cache.insert(key_re, key_elem, res); + return res; } bool sgraph::are_unit_distinct(snode const* a, snode const* b) const { diff --git a/src/ast/euf/euf_sgraph.h b/src/ast/euf/euf_sgraph.h index 26a81d3537..25ff8f5237 100644 --- a/src/ast/euf/euf_sgraph.h +++ b/src/ast/euf/euf_sgraph.h @@ -40,6 +40,7 @@ Author: #include "util/region.h" #include "util/statistics.h" #include "util/lbool.h" +#include "util/obj_pair_hashtable.h" #include "ast/ast.h" #include "ast/seq_decl_plugin.h" #include "ast/rewriter/seq_rewriter.h" @@ -85,6 +86,13 @@ namespace euf { // maps expression id to snode ptr_vector m_expr2snode; + + // Memo for brzozowski_deriv, keyed on the (regex, element) snode + // expressions. The derivative is a pure function of that pair, and both + // keys as well as the resulting snode are stable for the lifetime of the + // sgraph (snodes live in m_region, which is never freed, and their + // expressions are pinned in m_pin) + obj_pair_map m_deriv_cache; // trail of alias entries (string constant → decomposed snode) for pop unsigned_vector m_alias_trail; // expression ids diff --git a/src/ast/rewriter/seq_split.cpp b/src/ast/rewriter/seq_split.cpp index f12032bb77..6ed25b9669 100644 --- a/src/ast/rewriter/seq_split.cpp +++ b/src/ast/rewriter/seq_split.cpp @@ -253,6 +253,12 @@ bool seq_split::complement(sort* seq_sort, split_set const& sp, split_set& resul acc = std::move(tmp); if (acc.empty()) // intersection empty => ~S is empty break; + // Do not simplify(acc) here. It does bound |acc| (each round + // intersects acc with a 2-element set, so the fold is 2^|sp| without it), + // but merge_by collapses pairs by unioning their other component, so + // re-merging every round nests unions ~rounds*|acc| deep and the + // recursive is_subset / mk_regex_union_normalize then overflow the stack. + // |acc| is bounded by the caller's cap instead; see BOOL_CLOSURE_CAP. if (acc.size() > threshold) { ++m_stats.m_threshold_overruns; return false; @@ -663,10 +669,21 @@ expr_ref seq_split::head_normalize(expr* t, split_mode mode, unsigned threshold, ok = false; return expr_ref(m); } + // The operand split-sets are materialized eagerly, so they are capped + // rather than by the (deliberately huge) cap that bounds + // how many splits the lazy enumeration may emit + const unsigned cap = std::min(threshold, BOOL_CLOSURE_CAP); split_set sa, sb, tmp; - if (!materialize(a, mode, threshold, oracle, sa) || - !materialize(b, mode, threshold, oracle, sb) || - !intersect(sa, sb, tmp, threshold, oracle)) { + if (!materialize(a, mode, cap, oracle, sa) || + !materialize(b, mode, cap, oracle, sb)) { + ok = false; + return expr_ref(m); + } + // simplify(sa)/simplify(sb) before the cross product would cut the + // |sa|*|sb| pairs considerably, but merge_by builds unions of unbounded + // depth and the recursive seq_subset::is_subset then overflows the stack. + // Needs a depth-guarded is_subset first. + if (!intersect(sa, sb, tmp, cap, oracle)) { ok = false; return expr_ref(m); } @@ -680,9 +697,15 @@ expr_ref seq_split::head_normalize(expr* t, split_mode mode, unsigned threshold, // The body is materialized WITHOUT the oracle (its pairs are inverted, so // their N is unrelated to the output N); the oracle is re-applied in // complement(). + const unsigned cap = std::min(threshold, BOOL_CLOSURE_CAP); split_set sa, res; - if (!materialize(a, mode, threshold, split_oracle{}, sa) || - !complement(m_seq_sort, sa, res, threshold, oracle)) { + if (!materialize(a, mode, cap, split_oracle{}, sa)) { + ok = false; + return expr_ref(m); + } + // As in the inter case, simplify(sa) here would cut the number of fold + // rounds but currently risks a stack overflow; see the note above. + if (!complement(m_seq_sort, sa, res, cap, oracle)) { ok = false; return expr_ref(m); } @@ -904,7 +927,7 @@ std::pair seq_split::split_membership(expr* str, expr* regex tokens.push_back(expr_ref(cur, m)); } - expr* ch; + expr* ch = nullptr; unsigned i = 0; while (i < tokens.size() && (seq().str.is_string(tokens.get(i)) || (seq().str.is_unit(tokens.get(i), ch) && seq().is_const_char(ch)))) { diff --git a/src/ast/rewriter/seq_split.h b/src/ast/rewriter/seq_split.h index 9e5669bd0c..9f64e8b88f 100644 --- a/src/ast/rewriter/seq_split.h +++ b/src/ast/rewriter/seq_split.h @@ -194,6 +194,19 @@ class seq_split { // left (resp. right) component and unions the other component. void merge_by(split_set& pairs, bool by_left) const; + // Cap on the split-sets materialized for the *Boolean-closure* cases + // (intersection / complement), which cannot be produced lazily and are + // therefore drained in full inside head_normalize. This is a different + // quantity from the caller's `threshold`, which bounds how many splits the + // lazy enumeration may EMIT and is deliberately huge (nseq passes 2^20, so + // that the binary child-B chain may walk arbitrarily many splits). Reusing + // that value here let the De Morgan fold -- whose `acc` is intersected with a + // 2-element set per element, hence doubles -- run to 2^20 pairs before + // aborting, which is several seconds of pure waste. Overrunning this cap is + // a give-up, so the caller falls through to its other rules; that is sound + // and strictly better than the hang. + static const unsigned BOOL_CLOSURE_CAP = 256; + public: explicit seq_split(seq_rewriter& rw);