mirror of
https://github.com/Z3Prover/z3
synced 2026-07-17 20:45:45 +00:00
Merge remote-tracking branch 'origin/master' into c3
This commit is contained in:
commit
706f62286e
199 changed files with 11004 additions and 4584 deletions
|
|
@ -39,11 +39,16 @@ z3_add_component(rewriter
|
|||
rewriter.cpp
|
||||
seq_axioms.cpp
|
||||
seq_eq_solver.cpp
|
||||
seq_derive.cpp
|
||||
seq_subset.cpp
|
||||
seq_split.cpp
|
||||
seq_derive.cpp
|
||||
seq_range_collapse.cpp
|
||||
seq_range_predicate.cpp
|
||||
seq_rewriter.cpp
|
||||
seq_regex_bisim.cpp
|
||||
seq_skolem.cpp
|
||||
term_enumeration.cpp
|
||||
th_rewriter.cpp
|
||||
value_sweep.cpp
|
||||
var_subst.cpp
|
||||
|
|
|
|||
|
|
@ -768,9 +768,10 @@ void bit_blaster_tpl<Cfg>::mk_smod(unsigned sz, expr * const * a_bits, expr * co
|
|||
template<typename Cfg>
|
||||
void bit_blaster_tpl<Cfg>::mk_eq(unsigned sz, expr * const * a_bits, expr * const * b_bits, expr_ref & out) {
|
||||
expr_ref_vector out_bits(m());
|
||||
out_bits.resize(sz);
|
||||
for (unsigned i = 0; i < sz; ++i) {
|
||||
mk_iff(a_bits[i], b_bits[i], out);
|
||||
out_bits.push_back(out);
|
||||
out_bits[i] = out;
|
||||
}
|
||||
mk_and(out_bits.size(), out_bits.data(), out);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1196,15 +1196,15 @@ bool bool_rewriter::decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref &
|
|||
}
|
||||
for (expr *e : subterms::ground(expr_ref(r, m()))) {
|
||||
if (m().is_ite(e, cond, r1, r2)) {
|
||||
expr_safe_replace rep1(m());
|
||||
expr_safe_replace rep2(m());
|
||||
rep1.insert(e, r1);
|
||||
rep2.insert(e, r2);
|
||||
m_rep1.reset();
|
||||
m_rep2.reset();
|
||||
m_rep1.insert(e, r1);
|
||||
m_rep2.insert(e, r2);
|
||||
c = cond;
|
||||
th = r;
|
||||
el = r;
|
||||
rep1(th);
|
||||
rep2(el);
|
||||
m_rep1(th);
|
||||
m_rep2(el);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1212,6 +1212,4 @@ bool bool_rewriter::decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref &
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
template class rewriter_tpl<bool_rewriter_cfg>;
|
||||
template class rewriter_tpl<bool_rewriter_cfg>;
|
||||
|
|
@ -20,6 +20,7 @@ Notes:
|
|||
|
||||
#include "ast/ast.h"
|
||||
#include "ast/rewriter/rewriter.h"
|
||||
#include "ast/rewriter/expr_safe_replace.h"
|
||||
#include "util/params.h"
|
||||
|
||||
/**
|
||||
|
|
@ -64,6 +65,7 @@ class bool_rewriter {
|
|||
ptr_vector<expr> m_todo1, m_todo2;
|
||||
unsigned_vector m_counts1, m_counts2;
|
||||
expr_mark m_marked;
|
||||
expr_safe_replace m_rep1, m_rep2;
|
||||
|
||||
br_status mk_flat_and_core(unsigned num_args, expr * const * args, expr_ref & result);
|
||||
br_status mk_flat_or_core(unsigned num_args, expr * const * args, expr_ref & result);
|
||||
|
|
@ -87,7 +89,7 @@ class bool_rewriter {
|
|||
expr_ref simplify_eq_ite(expr* value, expr* ite);
|
||||
|
||||
public:
|
||||
bool_rewriter(ast_manager & m, params_ref const & p = params_ref()):m_manager(m), m_local_ctx_cost(0) {
|
||||
bool_rewriter(ast_manager & m, params_ref const & p = params_ref()):m_manager(m), m_local_ctx_cost(0), m_rep1(m), m_rep2(m) {
|
||||
updt_params(p);
|
||||
}
|
||||
ast_manager & m() const { return m_manager; }
|
||||
|
|
@ -243,7 +245,9 @@ public:
|
|||
void mk_nor(expr * arg1, expr * arg2, expr_ref & result);
|
||||
void mk_ge2(expr* a, expr* b, expr* c, expr_ref& result);
|
||||
|
||||
|
||||
// If r is, or contains, an if-then-else, decompose it into a top-level
|
||||
// ite by hoisting the (first) inner ite condition: returns c, th, el such
|
||||
// that r is equivalent to (ite c th el). Returns false if r has no ite.
|
||||
bool decompose_ite(expr *r, expr_ref &c, expr_ref &th, expr_ref &el);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -452,6 +452,8 @@ namespace seq {
|
|||
// |t| = 0 => |s| = 0 or indexof(t,s,offset) = -1
|
||||
// ~contains(t,s) => indexof(t,s,offset) = -1
|
||||
|
||||
add_clause(mk_ge(i, -1));
|
||||
|
||||
add_clause(cnt, i_eq_m1);
|
||||
add_clause(~t_eq_empty, s_eq_empty, i_eq_m1);
|
||||
|
||||
|
|
@ -638,8 +640,8 @@ namespace seq {
|
|||
add_clause(~i_ge_0, i_ge_len_s, mk_eq(i, len_x));
|
||||
}
|
||||
|
||||
add_clause(i_ge_0, mk_seq_eq(e, emp));
|
||||
add_clause(~i_ge_len_s, mk_seq_eq(e, emp));
|
||||
add_clause(i_ge_0, mk_eq(e, emp));
|
||||
add_clause(~i_ge_len_s, mk_eq(e, emp));
|
||||
add_clause(~i_ge_0, i_ge_len_s, mk_eq(one, len_e));
|
||||
add_clause(mk_le(len_e, 1));
|
||||
}
|
||||
|
|
@ -1066,7 +1068,7 @@ namespace seq {
|
|||
void axioms::replace_re_axiom(expr* e) {
|
||||
expr* s = nullptr, *r = nullptr, *t = nullptr;
|
||||
VERIFY(seq.str.is_replace_re(e, s, r, t));
|
||||
throw default_exception("replace-re is not supported");
|
||||
throw default_exception("no support for replace-re");
|
||||
}
|
||||
|
||||
// A basic strategy for supporting replace_all and other
|
||||
|
|
@ -1075,34 +1077,22 @@ namespace seq {
|
|||
// using iterative deepening can be re-used.
|
||||
//
|
||||
// create recursive relation 'ra' with properties:
|
||||
// ra(i, j, s, p, t, r) =
|
||||
// if len(s) = i && len(r) = j then
|
||||
// true
|
||||
// else if len(s) > i = 0 && p = "" && r = t + s then
|
||||
// true
|
||||
// else if len(s) > i && p != "" &&
|
||||
// s = extract(s, 0, i) + p + extract(s, i + len(p), len(s)) &&
|
||||
// r = extract(r, 0, i) + t + extract(r, i + len(p), len(r)) && ra(i + len(p), j + len(t), s, p, t, r)
|
||||
// else if ~prefix(p, extract(s, i, len(s)) && at(s,i) = at(r,j) then
|
||||
// ra(i + 1, j + 1, s, p, t, r)
|
||||
// else false
|
||||
// ra(i, j, s, p, t, r) <- len(s) = i && len(r) = j
|
||||
// ra(i, j, s, p, t, r) <- len(s) > i = 0 && p = "" && r = t + s
|
||||
// ra(i, j, s, p, t, r) <- len(s) > i && p != "" && s = extract(s, 0, i) + p + extract(s, i + len(p), len(s)) && r = extract(r, 0, i) + t + extract(r, i + len(p), len(r)) && ra(i + len(p), j + len(t), s, p, t, r)
|
||||
// ra(i, s, p, t, r) <- ~prefix(p, extract(s, i, len(s)) && at(s,i) = at(r,j) && ra(i + 1, j + 1, s, p, t, r)
|
||||
// which amounts to:
|
||||
//
|
||||
//
|
||||
// Then assert
|
||||
// ra(s, p, t, replace_all(s, p, t))
|
||||
//
|
||||
// ra(s, p, t, r) is a recursive predicate:
|
||||
// ra(s, p, t, r) iff replace_all(s, p, t) = r
|
||||
//
|
||||
// Base case, empty s or p: r = s
|
||||
// Match case, prefix(p, s): s = p ++ s', r = t ++ r', ra(s', p, t, r')
|
||||
// No-match case: r[0] = s[0], ra(s[1:], p, t, r[1:])
|
||||
//
|
||||
// Assert: ra(s, p, t, replace_all(s, p, t))
|
||||
//
|
||||
void axioms::replace_all_axiom(expr* r) {
|
||||
expr* s = nullptr, *p = nullptr, *t = nullptr;
|
||||
VERIFY(seq.str.is_replace_all(r, s, p, t));
|
||||
recfun::util rec(m);
|
||||
recfun::decl::plugin& plugin = rec.get_plugin();
|
||||
recfun_replace replace(m);
|
||||
sort* srt = s->get_sort();
|
||||
sort* domain[4] = { srt, srt, srt, srt };
|
||||
auto ra = rec.find_def_decl(symbol("ra"), 4, domain, m.mk_bool_sort(), true);
|
||||
|
|
@ -1146,7 +1136,7 @@ namespace seq {
|
|||
void axioms::replace_re_all_axiom(expr* e) {
|
||||
expr* s = nullptr, *p = nullptr, *t = nullptr;
|
||||
VERIFY(seq.str.is_replace_re_all(e, s, p, t));
|
||||
throw default_exception("replace_re_all is not supported");
|
||||
throw default_exception("no support for replace-re-all");
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1345,7 +1335,7 @@ namespace seq {
|
|||
|
||||
/**
|
||||
* Consider the recursive definition of negated contains:
|
||||
~contains(a, b) =
|
||||
~contains(a, b) =
|
||||
if |b| > |a| then true
|
||||
else if |b| = |a| then a != b
|
||||
else ~prefix(b, a) and ~contains(a[1:], b)
|
||||
|
|
@ -1420,9 +1410,9 @@ namespace seq {
|
|||
return bound_tracker;
|
||||
}
|
||||
|
||||
// |u| != |v| OR
|
||||
// |u| != |v| OR
|
||||
// (u = w[a]u' AND v = w[b]v' AND a != b AND |u'| = |v'|)
|
||||
void axioms::diseq_axiom(expr *u, expr *v) {
|
||||
void axioms::diseq_axiom(expr *u, expr *v) {
|
||||
expr_ref u_len(mk_len(u), m);
|
||||
expr_ref v_len(mk_len(v), m);
|
||||
expr_ref len_eq(mk_eq(u_len, v_len), m);
|
||||
|
|
|
|||
1520
src/ast/rewriter/seq_derive.cpp
Normal file
1520
src/ast/rewriter/seq_derive.cpp
Normal file
File diff suppressed because it is too large
Load diff
266
src/ast/rewriter/seq_derive.h
Normal file
266
src/ast/rewriter/seq_derive.h
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
/*++
|
||||
Copyright (c) 2026 Microsoft Corporation
|
||||
|
||||
Module Name:
|
||||
|
||||
seq_derive.h
|
||||
|
||||
Abstract:
|
||||
|
||||
Symbolic derivative computation for regular expressions.
|
||||
Produces an ITE-tree (transition regex) representation where
|
||||
the free variable is de Bruijn index 0 representing the input character.
|
||||
|
||||
Based on the theory of symbolic derivatives and transition regexes:
|
||||
- Veanes et al., "On Symbolic Derivatives and Transition Regexes" (LPAR 2024)
|
||||
- Varatalu, Veanes, Ernits, "RE#" (POPL 2025)
|
||||
- Stanford, Veanes, Bjørner, "Symbolic Boolean Derivatives" (PLDI 2021)
|
||||
|
||||
Authors:
|
||||
|
||||
Nikolaj Bjorner (nbjorner) 2025-06-03
|
||||
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ast/seq_decl_plugin.h"
|
||||
#include "ast/arith_decl_plugin.h"
|
||||
#include "ast/array_decl_plugin.h"
|
||||
#include "ast/rewriter/bool_rewriter.h"
|
||||
#include "util/obj_pair_hashtable.h"
|
||||
#include "util/obj_triple_hashtable.h"
|
||||
#include <functional>
|
||||
|
||||
class seq_rewriter;
|
||||
|
||||
namespace seq {
|
||||
|
||||
enum class derivative_kind { antimirov_t, brzozowski_t };
|
||||
/**
|
||||
* Symbolic derivative engine for regular expressions.
|
||||
*
|
||||
* Given a regex r, operator()(r) computes a symbolic derivative δ(r)
|
||||
* represented as an ITE-tree over character predicates (using de Bruijn
|
||||
* variable 0 for the character). Evaluating the ITE-tree for a concrete
|
||||
* character 'a' yields the classical Brzozowski derivative δ_a(r).
|
||||
*
|
||||
* The ITE-tree structure implicitly defines minterms (equivalence classes
|
||||
* of characters indistinguishable by the regex).
|
||||
*
|
||||
* Key properties:
|
||||
* - Results are memoized for termination on cyclic derivative graphs
|
||||
* - Union/intersection operands are sorted for ACI canonicalization
|
||||
* - Depth-bounded to prevent stack overflow
|
||||
*/
|
||||
class derive {
|
||||
ast_manager& m;
|
||||
seq_util m_util;
|
||||
arith_util m_autil;
|
||||
bool_rewriter m_br;
|
||||
seq_rewriter& m_re;
|
||||
|
||||
// Cache: maps (ele, regex) pair to its derivative
|
||||
obj_pair_map<expr, expr, expr*> m_acache, m_bcache;
|
||||
obj_pair_map<expr, expr, expr*> m_atop_cache, m_btop_cache; // post-simplify cache
|
||||
expr_ref_vector m_trail; // pin cached results
|
||||
|
||||
// Op cache for ITE-hoisting operations (union, inter, concat, complement)
|
||||
// Path-aware caches: key is (a, b, path_expr) for binary ops, (a, path_expr) for complement
|
||||
obj_triple_map<expr, expr, expr, expr *> m_aunion_cache, m_bunion_cache, m_ainter_cache, m_binter_cache, m_axor_cache, m_bxor_cache;
|
||||
obj_pair_map<expr, expr, expr*> m_aconcat_cache, m_bconcat_cache;
|
||||
obj_pair_map<expr, expr, expr*> m_acomplement_cache, m_bcomplement_cache;
|
||||
|
||||
// Depth limiting
|
||||
unsigned m_depth { 0 };
|
||||
static const unsigned m_max_depth = 512;
|
||||
|
||||
seq_util::rex& re() { return m_util.re; }
|
||||
seq_util& u() { return m_util; }
|
||||
|
||||
derivative_kind m_derivative_kind = derivative_kind::antimirov_t;
|
||||
|
||||
// The element (character) for the current derivative computation
|
||||
expr_ref m_ele;
|
||||
|
||||
// Path state for inline pruning during mk_inter/mk_union/mk_complement
|
||||
using intervals_t = svector<std::pair<unsigned, unsigned>>;
|
||||
|
||||
// Path: vector of signed atoms
|
||||
svector<std::pair<expr*, bool>> m_path;
|
||||
// Intervals: feasible character ranges under current path (append-only)
|
||||
intervals_t m_intervals;
|
||||
unsigned m_intervals_start { 0 };
|
||||
// Stack of saved states for push/pop
|
||||
struct path_save { unsigned path_sz; unsigned intervals_sz; unsigned intervals_start; expr* path_expr; };
|
||||
svector<path_save> m_path_stack;
|
||||
// Boolean expression encoding of current path (for cache keys)
|
||||
expr_ref m_path_expr;
|
||||
|
||||
// Path interface
|
||||
lbool push(expr* c, bool sign); // l_true: implied, l_undef: pushed (must pop), l_false: contradicts
|
||||
void pop(); // restore state to matching push
|
||||
expr* get_path_expr() { return m_path_expr; }
|
||||
|
||||
obj_pair_map<expr, expr, expr *> &cache() {
|
||||
return m_derivative_kind == derivative_kind::antimirov_t ? m_acache : m_bcache;
|
||||
}
|
||||
|
||||
obj_pair_map<expr, expr, expr *> &top_cache() {
|
||||
return m_derivative_kind == derivative_kind::antimirov_t ? m_atop_cache : m_btop_cache;
|
||||
}
|
||||
|
||||
obj_triple_map<expr, expr, expr, expr *> &union_cache() {
|
||||
return m_derivative_kind == derivative_kind::antimirov_t ? m_aunion_cache : m_bunion_cache;
|
||||
}
|
||||
|
||||
obj_triple_map<expr, expr, expr, expr *> &inter_cache() {
|
||||
return m_derivative_kind == derivative_kind::antimirov_t ? m_ainter_cache : m_binter_cache;
|
||||
}
|
||||
|
||||
obj_triple_map<expr, expr, expr, expr *> &xor_cache() {
|
||||
return m_derivative_kind == derivative_kind::antimirov_t ? m_axor_cache : m_bxor_cache;
|
||||
}
|
||||
|
||||
obj_pair_map<expr, expr, expr *> &concat_cache() {
|
||||
return m_derivative_kind == derivative_kind::antimirov_t ? m_aconcat_cache : m_bconcat_cache;
|
||||
}
|
||||
|
||||
obj_pair_map<expr, expr, expr *> &complement_cache() {
|
||||
return m_derivative_kind == derivative_kind::antimirov_t ? m_acomplement_cache : m_bcomplement_cache;
|
||||
}
|
||||
|
||||
// Hoist ITE: apply_op through ite(c, t, e) with path pruning
|
||||
expr_ref apply_ite(expr* c, expr* t, expr* e, expr* r, std::function<expr_ref(expr*, expr*)> apply_op);
|
||||
expr_ref apply_ite(expr* c, expr* t1, expr* e1, expr* t2, expr* e2, std::function<expr_ref(expr*, expr*)> apply_op);
|
||||
expr_ref apply_ite(expr* c, expr* t, expr* e, std::function<expr_ref(expr*)> apply_op);
|
||||
// Common ITE dispatch for binary ops (union/inter)
|
||||
expr_ref hoist_ite(expr* a, expr* b, std::function<expr_ref(expr*, expr*)> apply_op);
|
||||
|
||||
// Evaluate a condition against the current path/intervals
|
||||
lbool eval_path_cond(expr* c);
|
||||
|
||||
// Internal helpers for push
|
||||
lbool push_path_atoms(expr* c, bool sign);
|
||||
lbool push_intervals_impl(expr* c, bool sign);
|
||||
|
||||
// Core derivative computation
|
||||
expr_ref derive_rec(expr* r);
|
||||
expr_ref derive_core(expr* r);
|
||||
|
||||
// Helpers for specific regex constructs
|
||||
expr_ref derive_to_re(expr* s, sort* seq_sort);
|
||||
expr_ref derive_range(expr* lo, expr* hi, sort* seq_sort);
|
||||
expr_ref derive_of_pred(expr* pred, sort* seq_sort);
|
||||
|
||||
// Nullable check: returns a Boolean expression
|
||||
expr_ref is_nullable(expr* r);
|
||||
expr_ref is_nullable_symbolic_regex(expr* r, sort* seq_sort);
|
||||
|
||||
// Smart constructors with path-aware simplification and ACI canonicalization
|
||||
expr_ref mk_union(expr* a, expr* b);
|
||||
bool are_complements(expr* a, expr* b);
|
||||
unsigned union_id(expr* e); // complement-aware ID for sorting
|
||||
bool is_subset(expr* a, expr* b);
|
||||
expr_ref mk_union_core(expr* a, expr* b);
|
||||
void add_union_elem(expr_ref_vector& set, expr* e);
|
||||
expr_ref mk_inter(expr* a, expr* b);
|
||||
expr_ref mk_inter_core(expr* a, expr* b);
|
||||
expr_ref mk_concat(expr* a, expr* b);
|
||||
expr_ref mk_complement(expr* a);
|
||||
expr_ref mk_complement_core(expr* a);
|
||||
expr_ref mk_xor(expr *a, expr *b);
|
||||
expr_ref mk_xor_core(expr *a, expr *b);
|
||||
expr_ref mk_core(decl_kind k, expr* a, expr* b);
|
||||
expr_ref mk_ite(expr* c, expr* t, expr* e);
|
||||
|
||||
// Distribute concatenation through ITE/union in derivative
|
||||
expr_ref mk_deriv_concat(expr* d, expr* tail);
|
||||
expr_ref mk_deriv_concat_core(expr* d, expr* tail);
|
||||
|
||||
// Extract head character and tail from a sequence expression
|
||||
bool get_head_tail(expr* s1, expr* s2, expr_ref& hd, expr_ref& tl);
|
||||
|
||||
// Predicate implication for character range conditions.
|
||||
bool pred_implies(bool sign_a, expr* a, bool sign_b, expr* b);
|
||||
bool pred_implies(expr* a, expr* b);
|
||||
|
||||
// Normalize reverse(r)
|
||||
expr_ref mk_regex_reverse(expr* r);
|
||||
|
||||
// Condition evaluation helpers
|
||||
lbool eval_cond(expr* cond);
|
||||
lbool eval_range_cond(expr* c);
|
||||
void intersect_intervals(unsigned lo, unsigned hi);
|
||||
void exclude_interval(unsigned lo, unsigned hi);
|
||||
|
||||
// Cofactor enumeration over a transition regex (ITE-tree).
|
||||
void get_cofactors_rec(expr* r, expr_ref_pair_vector& result);
|
||||
|
||||
// Re-apply union/intersection simplifications bottom-up to a cofactor
|
||||
// leaf. decompose_ite substitutes ITE branch values structurally
|
||||
// (no simplification), so leaves can contain un-normalized nodes such
|
||||
// as union(R, none) or inter(R, none); this rebuilds them through
|
||||
// mk_union/mk_inter so equal states share a canonical form.
|
||||
expr_ref clean_leaf(expr* r);
|
||||
|
||||
sort* re_sort(expr* r) { return r->get_sort(); }
|
||||
sort* seq_sort(expr* r) { sort* s = nullptr; m_util.is_re(r, s); return s; }
|
||||
sort* ele_sort(expr* r) { sort* s = seq_sort(r); sort* e = nullptr; m_util.is_seq(s, e); return e; }
|
||||
|
||||
void reset();
|
||||
void reset_op_caches();
|
||||
|
||||
public:
|
||||
derive(ast_manager& m, seq_rewriter& re);
|
||||
|
||||
/**
|
||||
* Compute the derivative of regex r with respect to element ele.
|
||||
* When ele is a de Bruijn variable, produces a symbolic ITE-tree.
|
||||
* When ele is a concrete character, produces the concrete derivative.
|
||||
*/
|
||||
expr_ref operator()(derivative_kind k, expr* ele, expr* r);
|
||||
|
||||
/**
|
||||
* Convenience: symbolic derivative using de Bruijn var 0.
|
||||
*/
|
||||
expr_ref operator()(derivative_kind k, expr* r);
|
||||
|
||||
/**
|
||||
* Nullable check: returns a Boolean expression that is true iff r accepts the empty string.
|
||||
*/
|
||||
expr_ref nullable(expr* r) { return is_nullable(r); }
|
||||
|
||||
/**
|
||||
* Enumerate the cofactors (min-terms) of a transition regex r taken with
|
||||
* respect to element ele. r is an ITE-tree over character predicates on
|
||||
* ele; for every feasible path through the tree this produces a pair
|
||||
* (path_condition, leaf_regex). Infeasible character-interval
|
||||
* combinations are pruned using the same path/interval context that the
|
||||
* derivative engine uses while hoisting ITEs.
|
||||
*/
|
||||
void get_cofactors(expr* ele, expr* r, expr_ref_pair_vector& result);
|
||||
|
||||
/**
|
||||
* Compute the symbolic derivative of r and enumerate its reachable
|
||||
* leaves in fully ITE-hoisted normal form.
|
||||
*
|
||||
* Concretely this returns, for every feasible minterm (character
|
||||
* class) of δ(r), a pair (path_condition, target_regex). Every
|
||||
* if-then-else over the input character (including ones that would
|
||||
* otherwise be buried under a concat/union) is hoisted to the top
|
||||
* via the same path/interval pruning used by the derivative engine,
|
||||
* so each target_regex is free of (:var 0) and its nullability is
|
||||
* always decidable. Unions are kept intact as single leaves (a
|
||||
* union leaf denotes a single bisimulation state). Infeasible
|
||||
* minterms are pruned, so all returned leaves are reachable.
|
||||
*
|
||||
* This is the entry point the regex_bisim equivalence procedure
|
||||
* uses: it consumes the target_regex of each pair and ignores the
|
||||
* (redundant) path condition.
|
||||
*/
|
||||
void derivative_cofactors(expr* r, expr_ref_pair_vector& result);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
160
src/ast/rewriter/seq_range_collapse.cpp
Normal file
160
src/ast/rewriter/seq_range_collapse.cpp
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/*++
|
||||
Copyright (c) 2026 Microsoft Corporation
|
||||
|
||||
Module Name:
|
||||
|
||||
seq_range_collapse.cpp
|
||||
|
||||
Abstract:
|
||||
|
||||
Implementation of regex <-> range_predicate translation for the
|
||||
boolean-combination-of-ranges fragment. See header for the recognized
|
||||
grammar and the canonical regex AST emitted by materialization.
|
||||
|
||||
Authors:
|
||||
|
||||
Margus Veanes (veanes) 2026
|
||||
|
||||
--*/
|
||||
|
||||
#include "ast/rewriter/seq_range_collapse.h"
|
||||
|
||||
namespace seq {
|
||||
|
||||
bool regex_to_range_predicate(seq_util& u, expr* r, range_predicate& out) {
|
||||
// The range algebra only models sets of single characters over the
|
||||
// unsigned character domain [0, max_char]. Guard against any regex
|
||||
// whose element type is not a sequence of characters (e.g. a regex
|
||||
// over (Seq Int) or (Seq (Seq Char))): for such regexes the
|
||||
// re.range/re.union/... matchers below would silently fabricate a
|
||||
// character-class predicate and change semantics. Reject them up
|
||||
// front so callers fall back to the generic regex path.
|
||||
sort* seq_sort = nullptr;
|
||||
if (!u.is_re(r, seq_sort) || !u.is_string(seq_sort))
|
||||
return false;
|
||||
|
||||
unsigned const max_char = u.max_char();
|
||||
auto& re = u.re;
|
||||
|
||||
if (re.is_empty(r)) {
|
||||
out = range_predicate::empty(max_char);
|
||||
return true;
|
||||
}
|
||||
if (re.is_full_char(r)) {
|
||||
out = range_predicate::top(max_char);
|
||||
return true;
|
||||
}
|
||||
unsigned lo = 0, hi = 0;
|
||||
expr* lo_e = nullptr;
|
||||
expr* hi_e = nullptr;
|
||||
if (re.is_range(r, lo_e, hi_e)) {
|
||||
auto extract_char = [&](expr* e, unsigned& c) -> bool {
|
||||
if (u.is_const_char(e, c)) return true;
|
||||
expr* inner = nullptr;
|
||||
if (u.str.is_unit(e, inner) && u.is_const_char(inner, c)) return true;
|
||||
zstring s;
|
||||
if (u.str.is_string(e, s) && s.length() == 1) {
|
||||
c = s[0];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (!extract_char(lo_e, lo) || !extract_char(hi_e, hi))
|
||||
return false;
|
||||
// Empty/inverted range [lo > hi] is the empty regex.
|
||||
if (lo > hi) {
|
||||
out = range_predicate::empty(max_char);
|
||||
return true;
|
||||
}
|
||||
out = range_predicate::range(lo, hi, max_char);
|
||||
return true;
|
||||
}
|
||||
expr *a = nullptr, *b = nullptr, *c = nullptr;
|
||||
if (re.is_union(r, a, b)) {
|
||||
range_predicate pa(max_char), pb(max_char);
|
||||
if (!regex_to_range_predicate(u, a, pa)) return false;
|
||||
if (!regex_to_range_predicate(u, b, pb)) return false;
|
||||
out = pa | pb;
|
||||
return true;
|
||||
}
|
||||
auto mk_diff = [&](expr *a, expr *b) -> bool {
|
||||
range_predicate pa(max_char), pb(max_char);
|
||||
if (!regex_to_range_predicate(u, a, pa))
|
||||
return false;
|
||||
if (!regex_to_range_predicate(u, b, pb))
|
||||
return false;
|
||||
out = pa - pb;
|
||||
return true;
|
||||
};
|
||||
if (re.is_diff(r, a, b))
|
||||
return mk_diff(a, b);
|
||||
|
||||
if (re.is_intersection(r, a, b) && re.is_complement(b, c))
|
||||
return mk_diff(a, c);
|
||||
|
||||
if (re.is_intersection(r, a, b) && re.is_complement(a, c))
|
||||
return mk_diff(b, c);
|
||||
|
||||
if (re.is_intersection(r, a, b)) {
|
||||
range_predicate pa(max_char), pb(max_char);
|
||||
if (!regex_to_range_predicate(u, a, pa)) return false;
|
||||
if (!regex_to_range_predicate(u, b, pb)) return false;
|
||||
out = pa & pb;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// NOTE: re.complement is intentionally NOT handled here.
|
||||
// re.complement is the SEQUENCE-level complement: its language
|
||||
// includes the empty string, strings of length >= 2, and any
|
||||
// length-1 string outside the operand. A character-class
|
||||
// range_predicate can only describe a set of length-1 strings,
|
||||
// so collapsing re.complement(R) to ~R (character-level
|
||||
// complement) would change semantics whenever R is wrapped in
|
||||
// any sequence-level context (e.g. re.diff at the top level,
|
||||
// or membership tests). De-Morgan equivalences and the
|
||||
// special cases re.complement(re.empty) / re.complement(re.full)
|
||||
// are already handled directly in seq_rewriter::mk_re_complement.
|
||||
return false;
|
||||
}
|
||||
|
||||
static expr_ref mk_unit_string_from_char(seq_util& u, unsigned c) {
|
||||
return expr_ref(u.str.mk_string(zstring(c)), u.get_manager());
|
||||
}
|
||||
|
||||
static expr_ref mk_single_range_regex(seq_util& u, unsigned lo, unsigned hi, sort* re_sort) {
|
||||
ast_manager& m = u.get_manager();
|
||||
return expr_ref(u.re.mk_range(re_sort, lo, hi), m);
|
||||
}
|
||||
|
||||
expr_ref range_predicate_to_regex(seq_util& u, range_predicate const& p, sort* seq_sort) {
|
||||
ast_manager& m = u.get_manager();
|
||||
sort* re_sort = u.re.mk_re(seq_sort);
|
||||
if (p.is_empty())
|
||||
return expr_ref(u.re.mk_empty(re_sort), m);
|
||||
unsigned const n = p.num_ranges();
|
||||
SASSERT(n > 0);
|
||||
if (n == 1) {
|
||||
auto [lo, hi] = p[0];
|
||||
return mk_single_range_regex(u, lo, hi, re_sort);
|
||||
}
|
||||
// Build single-range AST nodes first, then sort by expression id
|
||||
// so the resulting right-associated union matches the canonical
|
||||
// id-sorted shape that seq_rewriter::merge_regex_sets expects.
|
||||
// Without this the merge algorithm produces incorrect unions
|
||||
// when it has to combine our materialized output with another
|
||||
// (id-sorted) regex set.
|
||||
expr_ref_vector ranges(m);
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
auto [lo, hi] = p[i];
|
||||
ranges.push_back(mk_single_range_regex(u, lo, hi, re_sort));
|
||||
}
|
||||
std::sort(ranges.data(), ranges.data() + ranges.size(),
|
||||
[](expr* a, expr* b) { return a->get_id() < b->get_id(); });
|
||||
expr_ref acc(ranges.get(n - 1), m);
|
||||
for (unsigned i = n - 1; i-- > 0; )
|
||||
acc = expr_ref(u.re.mk_union(ranges.get(i), acc), m);
|
||||
return acc;
|
||||
}
|
||||
|
||||
}
|
||||
71
src/ast/rewriter/seq_range_collapse.h
Normal file
71
src/ast/rewriter/seq_range_collapse.h
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/*++
|
||||
Copyright (c) 2026 Microsoft Corporation
|
||||
|
||||
Module Name:
|
||||
|
||||
seq_range_collapse.h
|
||||
|
||||
Abstract:
|
||||
|
||||
Recognize regexes that are boolean combinations of character-class
|
||||
primitives (re.empty, re.full_char, re.range with concrete chars,
|
||||
and re.union/inter/comp/diff over translatable arguments), and
|
||||
materialize a seq::range_predicate back into a canonical regex AST.
|
||||
|
||||
Together with seq_rewriter integration, this lets any boolean
|
||||
combination of character-class regexes collapse to a canonical
|
||||
multi-range form, so that equivalent character classes share AST
|
||||
identity, and downstream consumers (derivative, OneStep, caching)
|
||||
can short-circuit them as pure range predicates.
|
||||
|
||||
Authors:
|
||||
|
||||
Margus Veanes (veanes) 2026
|
||||
|
||||
--*/
|
||||
#pragma once
|
||||
|
||||
#include "ast/rewriter/seq_range_predicate.h"
|
||||
#include "ast/seq_decl_plugin.h"
|
||||
|
||||
namespace seq {
|
||||
|
||||
/**
|
||||
* If r is a boolean combination of character-class regex primitives
|
||||
* over the unsigned character domain [0, max_char], compute the
|
||||
* equivalent range_predicate and return true. Otherwise return false
|
||||
* with out untouched.
|
||||
*
|
||||
* Recognized fragment (all character-class-preserving operations):
|
||||
* re.empty -> empty
|
||||
* re.full_char_set -> top
|
||||
* re.range "c_lo" "c_hi" (concrete) -> [c_lo, c_hi]
|
||||
* re.union r1 r2 -> p1 | p2
|
||||
* re.intersection r1 r2 -> p1 & p2
|
||||
* re.diff r1 r2 -> p1 - p2
|
||||
*
|
||||
* Notably re.complement is NOT recognized: it is a SEQUENCE-level
|
||||
* complement (over all of Σ*), not a character-class complement, so
|
||||
* collapsing it would change semantics whenever the result is used
|
||||
* in any non-character-class context. Sequence-level rewrites for
|
||||
* re.complement (double-comp, deMorgan, etc.) are handled directly
|
||||
* in seq_rewriter::mk_re_complement.
|
||||
*/
|
||||
bool regex_to_range_predicate(seq_util& u, expr* r, range_predicate& out);
|
||||
|
||||
/**
|
||||
* Canonical materialization of p as a regex AST over the given
|
||||
* sequence sort. Two range_predicates with equal canonical
|
||||
* representations produce structurally identical regex ASTs:
|
||||
*
|
||||
* empty -> re.empty
|
||||
* top -> re.full_char_set
|
||||
* single range [lo, hi] -> re.range "lo" "hi"
|
||||
* multiple ranges -> right-associated re.union of single
|
||||
* ranges, in increasing order of lo
|
||||
* (matching the canonical range order
|
||||
* held by range_predicate).
|
||||
*/
|
||||
expr_ref range_predicate_to_regex(seq_util& u, range_predicate const& p, sort* seq_sort);
|
||||
|
||||
}
|
||||
292
src/ast/rewriter/seq_range_predicate.cpp
Normal file
292
src/ast/rewriter/seq_range_predicate.cpp
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
/*++
|
||||
Copyright (c) 2026 Microsoft Corporation
|
||||
|
||||
Module Name:
|
||||
|
||||
seq_range_predicate.cpp
|
||||
|
||||
Abstract:
|
||||
|
||||
Implementation of the specialized range-algebra used by symbolic
|
||||
derivative computation and regex rewriting. See seq_range_predicate.h
|
||||
for the algebraic specification.
|
||||
|
||||
All Boolean operations are implemented as single linear sweeps over
|
||||
the canonical sorted range vectors and produce canonical output
|
||||
(sorted, disjoint, non-adjacent).
|
||||
|
||||
Authors:
|
||||
|
||||
Margus Veanes (veanes) 2026
|
||||
|
||||
--*/
|
||||
|
||||
#include "ast/rewriter/seq_range_predicate.h"
|
||||
#include "util/debug.h"
|
||||
#include <algorithm>
|
||||
#include <ostream>
|
||||
|
||||
namespace seq {
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Factories
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
range_predicate range_predicate::empty(unsigned max_char) {
|
||||
return range_predicate(max_char);
|
||||
}
|
||||
|
||||
range_predicate range_predicate::top(unsigned max_char) {
|
||||
range_predicate r(max_char);
|
||||
r.m_ranges.push_back({0u, max_char});
|
||||
SASSERT(r.well_formed());
|
||||
return r;
|
||||
}
|
||||
|
||||
range_predicate range_predicate::singleton(unsigned c, unsigned max_char) {
|
||||
SASSERT(c <= max_char);
|
||||
range_predicate r(max_char);
|
||||
r.m_ranges.push_back({c, c});
|
||||
SASSERT(r.well_formed());
|
||||
return r;
|
||||
}
|
||||
|
||||
range_predicate range_predicate::range(unsigned lo, unsigned hi, unsigned max_char) {
|
||||
range_predicate r(max_char);
|
||||
if (lo <= hi && lo <= max_char) {
|
||||
unsigned clipped_hi = hi <= max_char ? hi : max_char;
|
||||
r.m_ranges.push_back({lo, clipped_hi});
|
||||
}
|
||||
SASSERT(r.well_formed());
|
||||
return r;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Invariants and observers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
bool range_predicate::well_formed() const {
|
||||
for (unsigned i = 0; i < m_ranges.size(); ++i) {
|
||||
auto [lo, hi] = m_ranges[i];
|
||||
if (lo > hi) return false;
|
||||
if (hi > m_max_char) return false;
|
||||
if (i > 0) {
|
||||
unsigned prev_hi = m_ranges[i - 1].second;
|
||||
// Non-adjacent and sorted: prev_hi + 1 < lo, with care
|
||||
// around prev_hi == UINT_MAX which we never expect because
|
||||
// hi <= m_max_char.
|
||||
if (prev_hi + 1 >= lo) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool range_predicate::contains(unsigned c) const {
|
||||
// Binary search on first element of pairs.
|
||||
unsigned lo = 0, hi = m_ranges.size();
|
||||
while (lo < hi) {
|
||||
unsigned mid = lo + (hi - lo) / 2;
|
||||
auto [a, b] = m_ranges[mid];
|
||||
if (c < a) hi = mid;
|
||||
else if (c > b) lo = mid + 1;
|
||||
else return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t range_predicate::cardinality() const {
|
||||
uint64_t n = 0;
|
||||
for (auto [lo, hi] : m_ranges)
|
||||
n += static_cast<uint64_t>(hi) - static_cast<uint64_t>(lo) + 1u;
|
||||
return n;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Equality, ordering, hashing
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
bool range_predicate::equals(range_predicate const& o) const {
|
||||
if (m_max_char != o.m_max_char) return false;
|
||||
if (m_ranges.size() != o.m_ranges.size()) return false;
|
||||
for (unsigned i = 0; i < m_ranges.size(); ++i)
|
||||
if (m_ranges[i] != o.m_ranges[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool range_predicate::operator<(range_predicate const& o) const {
|
||||
if (m_max_char != o.m_max_char)
|
||||
return m_max_char < o.m_max_char;
|
||||
unsigned n = std::min(m_ranges.size(), o.m_ranges.size());
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
auto a = m_ranges[i];
|
||||
auto b = o.m_ranges[i];
|
||||
if (a.first != b.first) return a.first < b.first;
|
||||
if (a.second != b.second) return a.second < b.second;
|
||||
}
|
||||
return m_ranges.size() < o.m_ranges.size();
|
||||
}
|
||||
|
||||
unsigned range_predicate::hash() const {
|
||||
// FNV-1a 32-bit over (max_char, then each (lo, hi)).
|
||||
uint32_t h = 2166136261u;
|
||||
auto step = [&](uint32_t x) {
|
||||
h ^= x;
|
||||
h *= 16777619u;
|
||||
};
|
||||
step(m_max_char);
|
||||
for (auto [lo, hi] : m_ranges) {
|
||||
step(lo);
|
||||
step(hi);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Boolean operations
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
// Append (lo, hi) to result, merging with the previous range if
|
||||
// adjacent or overlapping. Maintains canonical form.
|
||||
inline void append_merged(svector<std::pair<unsigned, unsigned>>& result,
|
||||
unsigned lo, unsigned hi) {
|
||||
SASSERT(lo <= hi);
|
||||
if (!result.empty() && result.back().second + 1 >= lo) {
|
||||
if (result.back().second < hi)
|
||||
result.back().second = hi;
|
||||
} else {
|
||||
result.push_back({lo, hi});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
range_predicate range_predicate::operator|(range_predicate const& o) const {
|
||||
SASSERT(m_max_char == o.m_max_char);
|
||||
range_predicate r(m_max_char);
|
||||
unsigned i = 0, j = 0;
|
||||
const unsigned n = m_ranges.size();
|
||||
const unsigned m = o.m_ranges.size();
|
||||
while (i < n && j < m) {
|
||||
auto a = m_ranges[i];
|
||||
auto b = o.m_ranges[j];
|
||||
if (a.first <= b.first) {
|
||||
append_merged(r.m_ranges, a.first, a.second);
|
||||
++i;
|
||||
} else {
|
||||
append_merged(r.m_ranges, b.first, b.second);
|
||||
++j;
|
||||
}
|
||||
}
|
||||
while (i < n) {
|
||||
auto a = m_ranges[i++];
|
||||
append_merged(r.m_ranges, a.first, a.second);
|
||||
}
|
||||
while (j < m) {
|
||||
auto b = o.m_ranges[j++];
|
||||
append_merged(r.m_ranges, b.first, b.second);
|
||||
}
|
||||
SASSERT(r.well_formed());
|
||||
return r;
|
||||
}
|
||||
|
||||
range_predicate range_predicate::operator&(range_predicate const& o) const {
|
||||
SASSERT(m_max_char == o.m_max_char);
|
||||
range_predicate r(m_max_char);
|
||||
unsigned i = 0, j = 0;
|
||||
const unsigned n = m_ranges.size();
|
||||
const unsigned m = o.m_ranges.size();
|
||||
while (i < n && j < m) {
|
||||
auto [a_lo, a_hi] = m_ranges[i];
|
||||
auto [b_lo, b_hi] = o.m_ranges[j];
|
||||
unsigned lo = std::max(a_lo, b_lo);
|
||||
unsigned hi = std::min(a_hi, b_hi);
|
||||
if (lo <= hi)
|
||||
r.m_ranges.push_back({lo, hi});
|
||||
// Advance the range that ends first.
|
||||
if (a_hi < b_hi) ++i;
|
||||
else if (b_hi < a_hi) ++j;
|
||||
else { ++i; ++j; }
|
||||
}
|
||||
SASSERT(r.well_formed());
|
||||
return r;
|
||||
}
|
||||
|
||||
range_predicate range_predicate::operator~() const {
|
||||
range_predicate r(m_max_char);
|
||||
unsigned cursor = 0;
|
||||
for (auto [lo, hi] : m_ranges) {
|
||||
if (cursor < lo)
|
||||
r.m_ranges.push_back({cursor, lo - 1});
|
||||
// Step past hi without overflow: hi <= m_max_char and we
|
||||
// only step when more characters remain.
|
||||
if (hi >= m_max_char) {
|
||||
cursor = m_max_char + 1; // sentinel: no more characters
|
||||
break;
|
||||
}
|
||||
cursor = hi + 1;
|
||||
}
|
||||
if (cursor <= m_max_char)
|
||||
r.m_ranges.push_back({cursor, m_max_char});
|
||||
SASSERT(r.well_formed());
|
||||
return r;
|
||||
}
|
||||
|
||||
range_predicate range_predicate::operator-(range_predicate const& o) const {
|
||||
SASSERT(m_max_char == o.m_max_char);
|
||||
// A - B by linear sweep: for each range of A, subtract overlapping
|
||||
// ranges of B. Both inputs are sorted so we advance j monotonically.
|
||||
range_predicate r(m_max_char);
|
||||
unsigned j = 0;
|
||||
const unsigned m = o.m_ranges.size();
|
||||
for (auto [a_lo, a_hi] : m_ranges) {
|
||||
unsigned cursor = a_lo;
|
||||
while (j < m && o.m_ranges[j].second < cursor)
|
||||
++j;
|
||||
unsigned k = j;
|
||||
while (k < m && o.m_ranges[k].first <= a_hi) {
|
||||
auto [b_lo, b_hi] = o.m_ranges[k];
|
||||
if (cursor < b_lo)
|
||||
r.m_ranges.push_back({cursor, std::min(a_hi, b_lo - 1)});
|
||||
if (b_hi >= a_hi) {
|
||||
cursor = a_hi + 1;
|
||||
break;
|
||||
}
|
||||
cursor = b_hi + 1;
|
||||
++k;
|
||||
}
|
||||
if (cursor <= a_hi)
|
||||
r.m_ranges.push_back({cursor, a_hi});
|
||||
}
|
||||
SASSERT(r.well_formed());
|
||||
return r;
|
||||
}
|
||||
|
||||
range_predicate range_predicate::operator^(range_predicate const& o) const {
|
||||
SASSERT(m_max_char == o.m_max_char);
|
||||
// (A | B) - (A & B), but implemented directly with one linear sweep
|
||||
// over the union of breakpoints.
|
||||
return (*this | o) - (*this & o);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Display
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
std::ostream& range_predicate::display(std::ostream& out) const {
|
||||
if (m_ranges.empty()) {
|
||||
return out << "[]";
|
||||
}
|
||||
out << "[";
|
||||
bool first = true;
|
||||
for (auto [lo, hi] : m_ranges) {
|
||||
if (!first) out << ",";
|
||||
first = false;
|
||||
if (lo == hi)
|
||||
out << lo;
|
||||
else
|
||||
out << lo << "-" << hi;
|
||||
}
|
||||
return out << "]";
|
||||
}
|
||||
|
||||
}
|
||||
127
src/ast/rewriter/seq_range_predicate.h
Normal file
127
src/ast/rewriter/seq_range_predicate.h
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/*++
|
||||
Copyright (c) 2026 Microsoft Corporation
|
||||
|
||||
Module Name:
|
||||
|
||||
seq_range_predicate.h
|
||||
|
||||
Abstract:
|
||||
|
||||
Specialized range-algebra over an unsigned character domain [0, max_char].
|
||||
|
||||
A range_predicate represents a subset of the character domain as a
|
||||
sorted sequence of non-overlapping, non-adjacent, non-empty ranges:
|
||||
|
||||
[(lo_0, hi_0), (lo_1, hi_1), ...] with hi_i + 1 < lo_{i+1}.
|
||||
|
||||
The representation is canonical, so two range_predicates over the same
|
||||
domain are extensionally equivalent iff their internal vectors are
|
||||
elementwise equal.
|
||||
|
||||
All Boolean operations (union, intersection, complement, difference)
|
||||
are linear in the total number of ranges and produce the canonical
|
||||
representation.
|
||||
|
||||
Intended use:
|
||||
* path conditions for symbolic derivative computation,
|
||||
* OneStep predicates capturing length-1 acceptance,
|
||||
* smart-constructor side conditions for regex rewrites such as
|
||||
R & psi --> toregex(OneStep(R) & psi).
|
||||
|
||||
The type is a pure value: no ast_manager allocation occurs in its
|
||||
construction or its Boolean operations. Conversion to and from
|
||||
expr* is the responsibility of a separate translator (see callers
|
||||
in seq_derive / seq_rewriter).
|
||||
|
||||
Authors:
|
||||
|
||||
Margus Veanes (veanes) 2026
|
||||
|
||||
--*/
|
||||
#pragma once
|
||||
|
||||
#include "util/vector.h"
|
||||
#include <iosfwd>
|
||||
#include <utility>
|
||||
|
||||
namespace seq {
|
||||
|
||||
class range_predicate {
|
||||
using range_t = std::pair<unsigned, unsigned>;
|
||||
using ranges_t = svector<range_t>;
|
||||
|
||||
// Sorted by first; ranges are disjoint and non-adjacent;
|
||||
// every range satisfies lo <= hi <= m_max_char.
|
||||
ranges_t m_ranges;
|
||||
unsigned m_max_char { 0 };
|
||||
|
||||
// Invariant check used in debug builds.
|
||||
bool well_formed() const;
|
||||
|
||||
public:
|
||||
range_predicate() = default;
|
||||
explicit range_predicate(unsigned max_char) : m_max_char(max_char) {}
|
||||
|
||||
// ---------------- Factory functions ----------------
|
||||
|
||||
static range_predicate empty(unsigned max_char);
|
||||
static range_predicate top(unsigned max_char);
|
||||
static range_predicate singleton(unsigned c, unsigned max_char);
|
||||
static range_predicate range(unsigned lo, unsigned hi, unsigned max_char);
|
||||
|
||||
// ---------------- Observers ----------------
|
||||
|
||||
unsigned max_char() const { return m_max_char; }
|
||||
unsigned num_ranges() const { return m_ranges.size(); }
|
||||
range_t operator[](unsigned i) const { return m_ranges[i]; }
|
||||
ranges_t const& ranges() const { return m_ranges; }
|
||||
|
||||
bool is_empty() const { return m_ranges.empty(); }
|
||||
bool is_top() const {
|
||||
return m_ranges.size() == 1
|
||||
&& m_ranges[0].first == 0
|
||||
&& m_ranges[0].second == m_max_char;
|
||||
}
|
||||
bool is_singleton(unsigned& c) const {
|
||||
if (m_ranges.size() != 1) return false;
|
||||
if (m_ranges[0].first != m_ranges[0].second) return false;
|
||||
c = m_ranges[0].first;
|
||||
return true;
|
||||
}
|
||||
bool contains(unsigned c) const;
|
||||
|
||||
// Number of characters in the predicate (well-defined for any domain).
|
||||
uint64_t cardinality() const;
|
||||
|
||||
// ---------------- Equality, ordering, hashing ----------------
|
||||
|
||||
bool equals(range_predicate const& o) const;
|
||||
bool operator==(range_predicate const& o) const { return equals(o); }
|
||||
bool operator!=(range_predicate const& o) const { return !equals(o); }
|
||||
|
||||
// Total order: lexicographic on the canonical range sequence,
|
||||
// with shorter sequences ordered before longer prefixes.
|
||||
// Predicates over different domains compare by max_char first.
|
||||
bool operator<(range_predicate const& o) const;
|
||||
bool less_than(range_predicate const& o) const { return *this < o; }
|
||||
|
||||
unsigned hash() const;
|
||||
|
||||
// ---------------- Boolean operations ----------------
|
||||
|
||||
range_predicate operator|(range_predicate const& o) const; // union
|
||||
range_predicate operator&(range_predicate const& o) const; // intersection
|
||||
range_predicate operator-(range_predicate const& o) const; // difference
|
||||
range_predicate operator^(range_predicate const& o) const; // symmetric diff
|
||||
range_predicate operator~() const; // complement
|
||||
|
||||
// ---------------- Display ----------------
|
||||
|
||||
std::ostream& display(std::ostream& out) const;
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& out, range_predicate const& p) {
|
||||
return p.display(out);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -85,41 +85,6 @@ namespace seq {
|
|||
return is_ground(r);
|
||||
}
|
||||
|
||||
/*
|
||||
Collect the leaves of a t-regex der (an ITE / antimirov union /
|
||||
union-tree with regex leaves) into the output vector. Empty
|
||||
(re.empty) leaves are dropped.
|
||||
|
||||
Returns false if we encountered an unexpected node (e.g. a free
|
||||
variable creeping in) — in that case the caller should bail out.
|
||||
*/
|
||||
bool regex_bisim::collect_leaves(expr* der, expr_ref_vector& leaves) {
|
||||
ptr_vector<expr> work;
|
||||
obj_hashtable<expr> seen;
|
||||
work.push_back(der);
|
||||
seen.insert(der);
|
||||
while (!work.empty()) {
|
||||
expr* e = work.back();
|
||||
work.pop_back();
|
||||
expr* c = nullptr, * t = nullptr, * f = nullptr;
|
||||
if (m.is_ite(e, c, t, f) ||
|
||||
m_util.re.is_union(e, t, f) ||
|
||||
m_util.re.is_antimirov_union(e, t, f)) {
|
||||
if (seen.insert_if_not_there(t))
|
||||
work.push_back(t);
|
||||
if (seen.insert_if_not_there(f))
|
||||
work.push_back(f);
|
||||
continue;
|
||||
}
|
||||
if (m_util.re.is_empty(e))
|
||||
continue;
|
||||
if (!m_util.is_re(e))
|
||||
return false;
|
||||
leaves.push_back(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
Fast inequivalence check based on the get_info().classical flag.
|
||||
|
||||
|
|
@ -228,15 +193,19 @@ namespace seq {
|
|||
m_worklist.pop_back();
|
||||
|
||||
// Compute the symbolic derivative wrt the canonical variable
|
||||
// (:var 0). The result is a transition regex (ITE tree) whose
|
||||
// leaves are regex expressions. We use the classical Brzozowski
|
||||
// entry point so the derivative stays as a single TRegex and
|
||||
// does not lift unions to the top via antimirov nodes — this
|
||||
// preserves the XOR-pair invariant the bisimulation relies on.
|
||||
expr_ref d(m_rw.mk_brz_derivative(r), m);
|
||||
// (:var 0) and enumerate its reachable leaves in fully
|
||||
// ITE-hoisted normal form. Every if-then-else over the input
|
||||
// character — even one that would otherwise be buried under a
|
||||
// concat or union — is hoisted to the top and infeasible
|
||||
// minterms are pruned, so each leaf is a ground regex free of
|
||||
// (:var 0) whose nullability is always decidable. Unions are
|
||||
// kept intact as single leaves (a union leaf denotes a single
|
||||
// bisimulation state, never a split into separate states).
|
||||
expr_ref_pair_vector cofs(m);
|
||||
m_rw.brz_derivative_cofactors(r, cofs);
|
||||
expr_ref_vector leaves(m);
|
||||
if (!collect_leaves(d, leaves))
|
||||
return l_undef;
|
||||
for (auto const& p : cofs)
|
||||
leaves.push_back(p.second);
|
||||
|
||||
// First pass: check for any nullable leaf (definitive
|
||||
// distinguishing empty-continuation word) or any classically
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ namespace seq {
|
|||
|
||||
unsigned node_of(expr* r);
|
||||
bool merge_leaf(expr* xor_pair);
|
||||
bool collect_leaves(expr* der, expr_ref_vector& leaves);
|
||||
lbool nullability(expr* r);
|
||||
bool is_supported(expr* r);
|
||||
// Returns true if the leaf l proves that the original pair is
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -18,7 +18,9 @@ Notes:
|
|||
--*/
|
||||
#pragma once
|
||||
|
||||
#include "seq_split.h"
|
||||
#include "ast/seq_decl_plugin.h"
|
||||
#include "ast/rewriter/seq_derive.h"
|
||||
#include "ast/ast_pp.h"
|
||||
#include "ast/arith_decl_plugin.h"
|
||||
#include "ast/rewriter/rewriter_types.h"
|
||||
|
|
@ -129,16 +131,21 @@ class seq_rewriter {
|
|||
void insert(decl_kind op, expr* a, expr* b, expr* c, expr* r);
|
||||
};
|
||||
|
||||
friend class seq::derive;
|
||||
|
||||
seq_util m_util;
|
||||
seq_subset m_subset;
|
||||
seq_split m_split;
|
||||
arith_util m_autil;
|
||||
bool_rewriter m_br;
|
||||
seq::derive m_derive;
|
||||
// re2automaton m_re2aut;
|
||||
op_cache m_op_cache;
|
||||
expr_ref_vector m_es, m_lhs, m_rhs;
|
||||
bool m_coalesce_chars;
|
||||
bool m_in_bisim { false };
|
||||
bool m_coalesce_chars = true;
|
||||
bool m_in_bisim { false };
|
||||
unsigned m_re_deriv_depth { 0 };
|
||||
static const unsigned m_max_re_deriv_depth = 512;
|
||||
|
||||
enum length_comparison {
|
||||
shorter_c,
|
||||
|
|
@ -175,50 +182,22 @@ class seq_rewriter {
|
|||
//replace b in a by c into result
|
||||
void replace_all_subvectors(expr_ref_vector const& as, expr_ref_vector const& bs, expr* c, expr_ref_vector& result);
|
||||
|
||||
// Calculate derivative, memoized and enforcing a normal form
|
||||
expr_ref is_nullable_rec(expr* r);
|
||||
expr_ref mk_derivative_rec(expr* ele, expr* r);
|
||||
expr_ref mk_der_op(decl_kind k, expr* a, expr* b);
|
||||
expr_ref mk_der_op_rec(decl_kind k, expr* a, expr* b);
|
||||
expr_ref mk_der_concat(expr* a, expr* b);
|
||||
expr_ref mk_der_union(expr* a, expr* b);
|
||||
expr_ref mk_der_inter(expr* a, expr* b);
|
||||
expr_ref mk_der_xor(expr* a, expr* b);
|
||||
expr_ref mk_der_compl(expr* a);
|
||||
expr_ref mk_der_cond(expr* cond, expr* ele, sort* seq_sort);
|
||||
expr_ref mk_der_antimirov_union(expr* r1, expr* r2);
|
||||
bool ite_bdds_compatible(expr* a, expr* b);
|
||||
/* if r has the form deriv(en..deriv(e1,to_re(s))..) returns 's = [e1..en]' else returns '() in r'*/
|
||||
expr_ref is_nullable_symbolic_regex(expr* r, sort* seq_sort);
|
||||
#ifdef Z3DEBUG
|
||||
bool check_deriv_normal_form(expr* r, int level = 3);
|
||||
#endif
|
||||
// For replace_all(x, a, b) in R: transform R so that
|
||||
// - occurrences of b_ch are replaced by union(to_re(a_str), to_re(b_str))
|
||||
// - occurrences of a_ch are replaced by empty (replace_all never outputs a)
|
||||
expr_ref re_replace_char(expr *r, unsigned a_ch, unsigned b_ch, expr *a_str, expr *b_str);
|
||||
|
||||
void mk_antimirov_deriv_rec(expr* e, expr* r, expr* path, expr_ref& result);
|
||||
|
||||
expr_ref mk_antimirov_deriv(expr* e, expr* r, expr* path);
|
||||
expr_ref mk_in_antimirov_rec(expr* s, expr* d);
|
||||
expr_ref mk_in_antimirov(expr* s, expr* d);
|
||||
|
||||
expr_ref mk_antimirov_deriv_intersection(expr* elem, expr* d1, expr* d2, expr* path);
|
||||
expr_ref mk_antimirov_deriv_concat(expr* d, expr* r);
|
||||
expr_ref mk_antimirov_deriv_negate(expr* elem, expr* d);
|
||||
expr_ref mk_antimirov_deriv_union(expr* d1, expr* d2);
|
||||
expr_ref mk_antimirov_deriv_restrict(expr* elem, expr* d1, expr* cond);
|
||||
expr_ref mk_regex_reverse(expr* r);
|
||||
expr_ref mk_regex_concat(expr* r1, expr* r2);
|
||||
|
||||
expr_ref merge_regex_sets(expr* r1, expr* r2, expr* unit, std::function<bool(expr*, expr*&, expr*&)>& decompose, std::function<expr* (expr*, expr*)>& compose);
|
||||
|
||||
// elem is (:var 0) and path a condition that may have (:var 0) as a free variable
|
||||
// simplify path, e.g., (:var 0) = 'a' & (:var 0) = 'b' is simplified to false
|
||||
expr_ref simplify_path(expr* elem, expr* path);
|
||||
// expr_ref simplify_path(expr* elem, expr* path);
|
||||
|
||||
bool lt_char(expr* ch1, expr* ch2);
|
||||
bool eq_char(expr* ch1, expr* ch2);
|
||||
bool neq_char(expr* ch1, expr* ch2);
|
||||
bool le_char(expr* ch1, expr* ch2);
|
||||
bool pred_implies(expr* a, expr* b);
|
||||
bool are_complements(expr* r1, expr* r2) const;
|
||||
bool is_subset(expr* r1, expr* r2) const;
|
||||
|
||||
|
|
@ -264,6 +243,14 @@ class seq_rewriter {
|
|||
br_status mk_re_union0(expr* a, expr* b, expr_ref& result);
|
||||
br_status mk_re_inter0(expr* a, expr* b, expr_ref& result);
|
||||
br_status mk_re_complement(expr* a, expr_ref& result);
|
||||
// Range-set collapse helpers: if the operands form a boolean
|
||||
// combination of character-class regexes, materialize the result as a
|
||||
// canonical regex over a single range_predicate. See
|
||||
// ast/rewriter/seq_range_collapse.h for the recognized fragment.
|
||||
// NOTE: re.complement is intentionally not in this set because it
|
||||
// operates at the sequence level, not the character-class level.
|
||||
bool try_collapse_re_union(expr* a, expr* b, expr_ref& result);
|
||||
bool try_collapse_re_inter(expr* a, expr* b, expr_ref& result);
|
||||
br_status mk_re_star(expr* a, expr_ref& result);
|
||||
br_status mk_re_diff(expr* a, expr* b, expr_ref& result);
|
||||
br_status mk_re_xor(expr* a, expr* b, expr_ref& result);
|
||||
|
|
@ -347,10 +334,10 @@ class seq_rewriter {
|
|||
lbool some_string_in_re(expr_mark& visited, expr* r, unsigned_vector& str);
|
||||
|
||||
public:
|
||||
seq_rewriter(ast_manager & m, params_ref const & p = params_ref()):
|
||||
m_util(m), m_subset(m_util.re), m_split(*this), m_autil(m), m_br(m, p), // m_re2aut(m),
|
||||
seq_rewriter(ast_manager & m, params_ref const & p = params_ref()) :
|
||||
m_util(m), m_subset(m_util.re), m_split(*this), m_autil(m), m_br(m, p), m_derive(m, *this), // m_re2aut(m),
|
||||
m_op_cache(m), m_es(m),
|
||||
m_lhs(m), m_rhs(m), m_coalesce_chars(true) {
|
||||
m_lhs(m), m_rhs(m) {
|
||||
}
|
||||
ast_manager & m() const { return m_util.get_manager(); }
|
||||
family_id get_fid() const { return m_util.get_family_id(); }
|
||||
|
|
@ -361,7 +348,7 @@ public:
|
|||
static void get_param_descrs(param_descrs & r);
|
||||
|
||||
|
||||
bool coalesce_chars() const { return m_coalesce_chars; }
|
||||
// bool coalesce_chars() const { return m_coalesce_chars; }
|
||||
|
||||
br_status mk_app_core(func_decl * f, unsigned num_args, expr * const * args, expr_ref & result);
|
||||
br_status mk_eq_core(expr * lhs, expr * rhs, expr_ref & result);
|
||||
|
|
@ -377,6 +364,34 @@ public:
|
|||
return result;
|
||||
}
|
||||
|
||||
expr_ref mk_xor0(expr *a, expr *b) {
|
||||
expr_ref result(m());
|
||||
if (mk_re_xor0(a, b, result) == BR_FAILED)
|
||||
result = re().mk_xor(a, b);
|
||||
return result;
|
||||
}
|
||||
|
||||
expr_ref mk_union(expr *a, expr *b) {
|
||||
expr_ref result(m());
|
||||
if (mk_re_union(a, b, result) == BR_FAILED)
|
||||
result = re().mk_union(a, b);
|
||||
return result;
|
||||
}
|
||||
|
||||
expr_ref mk_inter(expr *a, expr *b) {
|
||||
expr_ref result(m());
|
||||
if (mk_re_inter(a, b, result) == BR_FAILED)
|
||||
result = re().mk_inter(a, b);
|
||||
return result;
|
||||
}
|
||||
|
||||
expr_ref mk_complement(expr *a) {
|
||||
expr_ref result(m());
|
||||
if (mk_re_complement(a, result) == BR_FAILED)
|
||||
result = re().mk_complement(a);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* makes concat and simplifies
|
||||
*/
|
||||
|
|
@ -460,11 +475,35 @@ public:
|
|||
variable v0 = (:var 0). Unlike `mk_derivative` this entry point keeps
|
||||
the symbolic derivative as a single transition regex (TRegex): boolean
|
||||
operators are pushed into the ITE leaves rather than lifted to the top
|
||||
via _OP_RE_ANTIMIROV_UNION. Used by the regex_bisim equivalence
|
||||
as a union. Used by the regex_bisim equivalence
|
||||
procedure which relies on each leaf of D(p XOR q) being a coherent
|
||||
XOR pair (D_v p) XOR (D_v q).
|
||||
*/
|
||||
expr_ref mk_brz_derivative(expr* r);
|
||||
expr_ref mk_brz_derivative(expr *r) {
|
||||
return mk_derivative(r);
|
||||
}
|
||||
|
||||
/*
|
||||
Enumerate the cofactors (min-terms) of a transition regex r taken with
|
||||
respect to ele. Produces (path_condition, leaf_regex) pairs for every
|
||||
feasible path through the ITE-tree, pruning infeasible character ranges.
|
||||
Delegates to the derivative engine so the same path/interval context used
|
||||
while hoisting ITEs is reused for the leaf simplification.
|
||||
*/
|
||||
void get_cofactors(expr* ele, expr* r, expr_ref_pair_vector& result) {
|
||||
m_derive.get_cofactors(ele, r, result);
|
||||
}
|
||||
|
||||
/*
|
||||
Compute the symbolic derivative of r and enumerate its reachable leaves
|
||||
in fully ITE-hoisted normal form: a list of (path_condition, target)
|
||||
pairs where every target is free of (:var 0) (so nullability is always
|
||||
decidable) and unions are kept intact as single states. Used by
|
||||
regex_bisim, which consumes the targets and ignores the path conditions.
|
||||
*/
|
||||
void brz_derivative_cofactors(expr* r, expr_ref_pair_vector& result) {
|
||||
m_derive.derivative_cofactors(r, result);
|
||||
}
|
||||
|
||||
// heuristic elimination of element from condition that comes form a derivative.
|
||||
// special case optimization for conjunctions of equalities, disequalities and ranges.
|
||||
|
|
@ -475,15 +514,7 @@ public:
|
|||
/* Apply simplifications to the intersection to keep it normalized (r1 and r2 are not normalized)*/
|
||||
expr_ref mk_regex_inter_normalize(expr* r1, expr* r2);
|
||||
|
||||
/*
|
||||
* Extract some sequence that is a member of r.
|
||||
* result is set to a concrete sequence expression if l_true is returned.
|
||||
* For string-typed regexes, delegates to some_string_in_re.
|
||||
* For other sequence types, checks nullability and returns the empty
|
||||
* sequence if the regex accepts it; otherwise returns l_undef.
|
||||
* Returns l_false if the regex is known to be empty.
|
||||
*/
|
||||
lbool some_seq_in_re(expr* r, expr_ref& result);
|
||||
expr_ref mk_regex_concat(expr *r1, expr *r2);
|
||||
|
||||
/*
|
||||
* Extract some string that is a member of r.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
/*++
|
||||
Copyright (c) 2026 Microsoft Corporation
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Author:
|
|||
|
||||
bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
|
||||
while (true) {
|
||||
|
||||
|
||||
if (a == b)
|
||||
return true;
|
||||
if (m_re.is_empty(a))
|
||||
|
|
@ -30,7 +30,7 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
|
|||
return true;
|
||||
|
||||
if (depth >= m_max_depth)
|
||||
return false;
|
||||
return false;
|
||||
|
||||
expr* a1 = nullptr, * a2 = nullptr, * b1 = nullptr, * b2 = nullptr;
|
||||
unsigned la, ua, lb, ub;
|
||||
|
|
@ -39,16 +39,12 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
|
|||
if (m_re.is_dot_plus(b) && m_re.get_info(a).nullable == l_false)
|
||||
return true;
|
||||
|
||||
// a ⊆ a*
|
||||
if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth))
|
||||
return true;
|
||||
|
||||
// e ⊆ a*
|
||||
if (m_re.is_epsilon(a) && m_re.is_star(b, b1))
|
||||
return true;
|
||||
|
||||
// R ⊆ R*
|
||||
if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth + 1))
|
||||
// a ⊆ a*: if b = b1* and a ⊆ b1, then a ⊆ b1*
|
||||
if (m_re.is_star(b, b1) && is_subset_rec(a, b1, depth))
|
||||
return true;
|
||||
|
||||
// R1* ⊆ R2* if R1 ⊆ R2
|
||||
|
|
@ -112,6 +108,12 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
|
|||
if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b1) && is_subset_rec(a, b2, depth))
|
||||
return true;
|
||||
|
||||
// prefix absorption: P·R' ⊆ Σ*·R' for any prefix P (since P ⊆ Σ*).
|
||||
// Detect that a has R' (= b2) as a concatenation suffix, where b = Σ*·R'.
|
||||
// Covers contains-patterns, e.g. Σ*·a·Σ*·b·Σ* ⊆ Σ*·b·Σ*.
|
||||
if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b1) && ends_with(a, b2))
|
||||
return true;
|
||||
|
||||
// R ⊆ R'·Σ* if R ⊆ R'
|
||||
if (m_re.is_concat(b, b1, b2) && m_re.is_full_seq(b2) && is_subset_rec(a, b1, depth))
|
||||
return true;
|
||||
|
|
@ -144,3 +146,30 @@ bool seq_subset::is_subset_rec(expr* a, expr* b, unsigned depth) const {
|
|||
bool seq_subset::is_subset(expr* a, expr* b) const {
|
||||
return is_subset_rec(a, b, 0);
|
||||
}
|
||||
|
||||
bool seq_subset::ends_with(expr* a, expr* suf) const {
|
||||
if (a == suf)
|
||||
return true;
|
||||
// Flatten both regexes into their sequence of concatenation factors
|
||||
// (independent of left/right associativity) and test list-suffix equality.
|
||||
ptr_vector<expr> af, sf;
|
||||
flatten_concat(a, af);
|
||||
flatten_concat(suf, sf);
|
||||
if (sf.size() > af.size())
|
||||
return false;
|
||||
unsigned off = af.size() - sf.size();
|
||||
for (unsigned i = 0; i < sf.size(); ++i)
|
||||
if (af[off + i] != sf[i])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void seq_subset::flatten_concat(expr* a, ptr_vector<expr>& out) const {
|
||||
expr* a1 = nullptr, * a2 = nullptr;
|
||||
if (m_re.is_concat(a, a1, a2)) {
|
||||
flatten_concat(a1, out);
|
||||
flatten_concat(a2, out);
|
||||
}
|
||||
else
|
||||
out.push_back(a);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ class seq_subset {
|
|||
|
||||
bool is_subset_rec(expr* a, expr* b, unsigned depth) const;
|
||||
|
||||
// true if regex a, viewed as a flattened concatenation, has suf as a
|
||||
// structural (concatenation) suffix.
|
||||
bool ends_with(expr* a, expr* suf) const;
|
||||
|
||||
void flatten_concat(expr* a, ptr_vector<expr>& out) const;
|
||||
|
||||
public:
|
||||
explicit seq_subset(seq_util::rex& re) : m_re(re) {}
|
||||
bool is_subset(expr* a, expr* b) const;
|
||||
|
|
|
|||
681
src/ast/rewriter/term_enumeration.cpp
Normal file
681
src/ast/rewriter/term_enumeration.cpp
Normal file
|
|
@ -0,0 +1,681 @@
|
|||
/**
|
||||
* term_enumeration.cpp - Bottom-up term enumeration module for Z3
|
||||
*
|
||||
* Inspired by the Probe synthesizer (Barke et al., "Just-in-Time Learning
|
||||
* for Bottom-Up Enumerative Synthesis"). Adapted to use Z3's internal APIs.
|
||||
*
|
||||
* Key ideas:
|
||||
* - Terms are enumerated bottom-up by "cost" (calculated by tree size).
|
||||
* - A grammar describes which function symbols (operators) and leaves
|
||||
* (constants, variables) are available for enumeration.
|
||||
*/
|
||||
|
||||
#include <sstream>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include "util/vector.h"
|
||||
#include "util/scoped_ptr_vector.h"
|
||||
#include "util/obj_hashtable.h"
|
||||
#include "util/uint_set.h"
|
||||
#include "ast/ast.h"
|
||||
#include "ast/ast_ll_pp.h"
|
||||
#include "ast/ast_pp.h"
|
||||
#include "ast/rewriter/th_rewriter.h"
|
||||
#include "ast/rewriter/term_enumeration.h"
|
||||
|
||||
|
||||
namespace term_enum {
|
||||
|
||||
// ============================================================================
|
||||
// grammar production rule
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* A production describes how to construct a term from child terms.
|
||||
* - domain: the sort required for each child
|
||||
* - range: the sort of the produced term
|
||||
* - builder: given a vector of child exprs, produce the result expr
|
||||
*/
|
||||
struct production {
|
||||
std::string name;
|
||||
sort_ref range;
|
||||
sort_ref_vector domain;
|
||||
std::function<expr_ref(expr_ref_vector const&)> builder;
|
||||
|
||||
bool is_leaf() const { return domain.empty(); }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// grammar
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* A grammar groups productions into leaves (arity 0) and operators (arity > 0).
|
||||
*/
|
||||
class grammar {
|
||||
public:
|
||||
grammar(ast_manager& m) : m(m), m_pinned(m) {}
|
||||
|
||||
void add_production(production* p) {
|
||||
if (p->is_leaf())
|
||||
m_leaves.push_back(p);
|
||||
else
|
||||
m_operators.push_back(p);
|
||||
}
|
||||
|
||||
scoped_ptr_vector<production> const& leaves() const { return m_leaves; }
|
||||
scoped_ptr_vector<production> const& operators() const { return m_operators; }
|
||||
ast_manager& mgr() const { return m; }
|
||||
|
||||
void add_func_decl(func_decl *f) {
|
||||
if (m_seen.contains(f))
|
||||
return;
|
||||
m_pinned.push_back(f);
|
||||
m_seen.insert(f);
|
||||
sort_ref range(f->get_range(), m);
|
||||
sort_ref_vector dom(m);
|
||||
for (unsigned i = 0; i < f->get_arity(); ++i)
|
||||
dom.push_back(sort_ref(f->get_domain(i), m));
|
||||
add_production(alloc(production, {f->get_name().str(), range, dom, [this, f](expr_ref_vector const &args) {
|
||||
return expr_ref(m.mk_app(f, args), m);
|
||||
}}));
|
||||
}
|
||||
|
||||
void add_expr(expr *e) {
|
||||
if (m_seen.contains(e))
|
||||
return;
|
||||
m_pinned.push_back(e);
|
||||
m_seen.insert(e);
|
||||
sort_ref range(e->get_sort(), m);
|
||||
sort_ref_vector dom(m);
|
||||
std::stringstream ss;
|
||||
ss << mk_bounded_pp(e, m);
|
||||
std::string name = ss.str();
|
||||
add_production(alloc(production, {name, range, dom, [this, e](expr_ref_vector const&) { return expr_ref(e, m); }}));
|
||||
}
|
||||
|
||||
std::ostream& display(std::ostream& out) const {
|
||||
out << "Leaves:\n";
|
||||
for (auto const *p : m_leaves) {
|
||||
out << " " << p->name << " : " << mk_pp(p->range, m) << "\n";
|
||||
}
|
||||
out << "Operators:\n";
|
||||
for (auto const *p : m_operators) {
|
||||
out << " " << p->name << " : (";
|
||||
for (unsigned i = 0; i < p->domain.size(); ++i) {
|
||||
if (i > 0)
|
||||
out << ", ";
|
||||
out << mk_pp(p->domain[i], m);
|
||||
}
|
||||
out << ") -> " << mk_pp(p->range, m) << "\n";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
ast_manager& m;
|
||||
ast_ref_vector m_pinned;
|
||||
scoped_ptr_vector<production> m_leaves;
|
||||
scoped_ptr_vector<production> m_operators;
|
||||
obj_hashtable<ast> m_seen;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Term Bank - stores enumerated terms by cost and sort
|
||||
// ============================================================================
|
||||
|
||||
using cost_terms = vector<std::pair<expr*, unsigned>>;
|
||||
|
||||
class term_bank {
|
||||
using sort_term_map = obj_map<sort, ptr_vector<expr>>;
|
||||
public:
|
||||
term_bank(ast_manager& m) : m(m), m_pinned(m) {}
|
||||
|
||||
~term_bank() {
|
||||
for (auto s : m_terms)
|
||||
dealloc(s);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
m_pinned.reset();
|
||||
m_terms.clear();
|
||||
}
|
||||
|
||||
void add(expr* term, unsigned cost) {
|
||||
sort* s = term->get_sort();
|
||||
m_pinned.push_back(term);
|
||||
if (cost >= m_terms.size())
|
||||
m_terms.resize(cost + 1);
|
||||
if (!m_terms[cost])
|
||||
m_terms[cost] = alloc(sort_term_map);
|
||||
m_terms[cost]->insert_if_not_there(s, ptr_vector<expr>()).push_back(term);
|
||||
}
|
||||
|
||||
/** Get all terms of a given sort up to (and including) max_cost */
|
||||
cost_terms get_by_sort(sort* s, unsigned max_cost) const {
|
||||
cost_terms result;
|
||||
for (unsigned c = 0; c <= max_cost; ++c) {
|
||||
if (c >= m_terms.size())
|
||||
break;
|
||||
if (!m_terms[c]->contains(s))
|
||||
continue;
|
||||
for (auto t : m_terms[c]->find(s))
|
||||
result.push_back({t, c});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return true if there is at least one term at/above `cost` whose sort is
|
||||
// not in `sorts` (i.e., enumeration can still produce a new requested sort).
|
||||
bool is_productive(unsigned cost, uint_set const& sorts) {
|
||||
for (unsigned i = cost; i < m_terms.size(); ++i) {
|
||||
if (!m_terms[i])
|
||||
continue;
|
||||
for (auto const& entry : *m_terms[i]) {
|
||||
sort* term_sort = entry.m_key;
|
||||
if (!sorts.contains(term_sort->get_small_id()))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ptr_vector<expr> null_ptr_vector;
|
||||
ptr_vector<expr> const &get_by_cost_and_sort(unsigned cost, sort *s) const {
|
||||
if (cost >= m_terms.size() || !m_terms[cost] || !m_terms[cost]->contains(s))
|
||||
return null_ptr_vector;
|
||||
return m_terms[cost]->find(s);
|
||||
}
|
||||
|
||||
std::ostream& display(std::ostream& out) const {
|
||||
for (unsigned cost = 0; cost < m_terms.size(); ++cost) {
|
||||
if (!m_terms[cost])
|
||||
continue;
|
||||
out << "cost " << cost << ":\n";
|
||||
for (auto& [s, terms] : *m_terms[cost]) {
|
||||
out << " sort " << mk_pp(s, m) << ":\n";
|
||||
for (expr* e : terms) {
|
||||
out << " #" << e->get_id() << " ";
|
||||
if (cost == 0) {
|
||||
out << mk_bounded_pp(e, m);
|
||||
}
|
||||
else if (is_app(e)) {
|
||||
app* a = to_app(e);
|
||||
out << a->get_decl()->get_name() << "(";
|
||||
bool first = true;
|
||||
for (expr* arg : *a) {
|
||||
if (!first) out << ", ";
|
||||
first = false;
|
||||
out << "#" << arg->get_id();
|
||||
}
|
||||
out << ")";
|
||||
}
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
ast_manager& m;
|
||||
expr_ref_vector m_pinned;
|
||||
// cost -> sort -> terms
|
||||
ptr_vector<sort_term_map> m_terms;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Children Iterator - generates all combinations of child terms
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Iterates over all tuples (c1, c2, ..., cn) where each ci has the required
|
||||
* sort, drawn from the term bank, with at least one child at the current
|
||||
* cost - 1 (to avoid regenerating previously seen terms).
|
||||
*/
|
||||
class children_iterator {
|
||||
public:
|
||||
children_iterator(ast_manager& m, production const& prod, term_bank const& bank, unsigned current_cost)
|
||||
: m(m), m_done(false)
|
||||
{
|
||||
m_arity = prod.domain.size();
|
||||
if (m_arity == 0) {
|
||||
m_done = true;
|
||||
return;
|
||||
}
|
||||
for (unsigned i = 0; i < m_arity; ++i) {
|
||||
m_candidates.push_back(bank.get_by_sort(prod.domain[i], current_cost - 1));
|
||||
if (m_candidates.back().empty()) {
|
||||
m_done = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_indices.resize(m_arity, 0);
|
||||
}
|
||||
|
||||
bool has_next(unsigned cost) {
|
||||
while (!m_done) {
|
||||
if (m.limit().is_canceled())
|
||||
return false;
|
||||
if (has_child_at_cost(cost))
|
||||
return true;
|
||||
advance();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
expr_ref_vector next(unsigned& cost) {
|
||||
expr_ref_vector result(m);
|
||||
cost = 1;
|
||||
for (unsigned i = 0; i < m_arity; ++i) {
|
||||
auto [e, c] = m_candidates[i].get(m_indices[i]);
|
||||
cost += c;
|
||||
result.push_back(e);
|
||||
}
|
||||
advance();
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
ast_manager& m;
|
||||
unsigned m_arity;
|
||||
bool m_done;
|
||||
vector<cost_terms> m_candidates;
|
||||
svector<unsigned> m_indices;
|
||||
|
||||
bool has_child_at_cost(unsigned cost) const {
|
||||
for (unsigned i = 0; i < m_arity; ++i) {
|
||||
auto [e, c] = m_candidates[i].get(m_indices[i]);
|
||||
if (c + 1 == cost)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void advance() {
|
||||
for (auto i = m_arity; i-- > 0;) {
|
||||
m_indices[i]++;
|
||||
if (m_indices[i] < m_candidates[i].size()) return;
|
||||
m_indices[i] = 0;
|
||||
}
|
||||
m_done = true;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// bottom_up_enumerator - the main bottom-up term enumeration engine
|
||||
// ============================================================================
|
||||
|
||||
|
||||
class bottom_up_enumerator {
|
||||
public:
|
||||
bottom_up_enumerator(grammar& grammar)
|
||||
: m_grammar(grammar), m(grammar.mgr()),
|
||||
m_bank(grammar.mgr()), m_pending(grammar.mgr()), m_rewriter(grammar.mgr())
|
||||
{}
|
||||
|
||||
void set_target_sort(sort *s) {
|
||||
m_target_sort = s;
|
||||
}
|
||||
bool has_next() {
|
||||
if (m_pending) return true;
|
||||
m_pending = find_next();
|
||||
return m_pending != nullptr;
|
||||
}
|
||||
|
||||
expr_ref next() {
|
||||
if (!m_pending)
|
||||
m_pending = find_next();
|
||||
expr_ref result(m_pending, m);
|
||||
m_pending = nullptr;
|
||||
return result;
|
||||
}
|
||||
|
||||
term_bank const& bank() const { return m_bank; }
|
||||
|
||||
std::ostream& display(std::ostream& out) const {
|
||||
m_grammar.display(out);
|
||||
return m_bank.display(out);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
m_cost = 0;
|
||||
m_leaf_idx = 0;
|
||||
m_op_idx = 0;
|
||||
m_state = State::Leaves;
|
||||
m_bank.reset();
|
||||
m_pending = nullptr;
|
||||
m_rewriter.reset();
|
||||
m_seen_terms.reset();
|
||||
m_children_iter.reset();
|
||||
}
|
||||
|
||||
expr* add_term(expr_ref const& term, unsigned cost) {
|
||||
expr_ref simplified(m);
|
||||
m_rewriter(term, simplified);
|
||||
if (m_seen_terms.contains(simplified))
|
||||
return nullptr;
|
||||
IF_VERBOSE(10, verbose_stream() << "add " << simplified << "\n");
|
||||
m_seen_terms.insert(simplified);
|
||||
m_bank.add(simplified, cost);
|
||||
return simplified;
|
||||
}
|
||||
|
||||
private:
|
||||
enum class State { Leaves, Operators, Done };
|
||||
|
||||
grammar& m_grammar;
|
||||
ast_manager& m;
|
||||
term_bank m_bank;
|
||||
unsigned m_cost = 0;
|
||||
unsigned m_leaf_idx = 0;
|
||||
unsigned m_op_idx = 0;
|
||||
unsigned m_bank_idx = 0;
|
||||
unsigned m_bank_size = 0;
|
||||
bool m_made_progress = false;
|
||||
uint_set m_sorts_produced;
|
||||
State m_state = State::Leaves;
|
||||
expr_ref m_pending;
|
||||
th_rewriter m_rewriter;
|
||||
obj_hashtable<expr> m_seen_terms;
|
||||
std::unique_ptr<children_iterator> m_children_iter;
|
||||
sort *m_target_sort = nullptr;
|
||||
|
||||
bool sort_matches(expr* e) const {
|
||||
return !m_target_sort || e->get_sort() == m_target_sort;
|
||||
}
|
||||
|
||||
expr* find_next() {
|
||||
while (true) {
|
||||
if (m.limit().is_canceled()) {
|
||||
m_state = State::Done;
|
||||
return nullptr;
|
||||
}
|
||||
switch (m_state) {
|
||||
case State::Leaves:
|
||||
while (m_leaf_idx < m_grammar.leaves().size()) {
|
||||
production const &prod = *m_grammar.leaves()[m_leaf_idx];
|
||||
m_leaf_idx++;
|
||||
expr_ref_vector empty_args(m);
|
||||
expr_ref term = prod.builder(empty_args);
|
||||
expr* r = add_term(term, 0);
|
||||
if (r && sort_matches(r))
|
||||
return r;
|
||||
}
|
||||
m_state = State::Operators;
|
||||
m_cost = 1;
|
||||
m_op_idx = 0;
|
||||
m_bank_idx = 0;
|
||||
m_bank_size = get_bank_size();
|
||||
m_made_progress = false;
|
||||
m_sorts_produced.reset();
|
||||
m_children_iter.reset();
|
||||
break;
|
||||
|
||||
case State::Operators: {
|
||||
expr* result = enumerate_operators();
|
||||
if (result)
|
||||
return result;
|
||||
|
||||
m_cost++;
|
||||
m_op_idx = 0;
|
||||
m_bank_idx = 0;
|
||||
m_bank_size = get_bank_size();
|
||||
m_children_iter.reset();
|
||||
if (!m_made_progress && !m_bank.is_productive(m_cost, m_sorts_produced)) {
|
||||
m_state = State::Done;
|
||||
return nullptr;
|
||||
}
|
||||
if (m_sorts_produced.contains(m_target_sort->get_small_id()))
|
||||
m_sorts_produced.reset();
|
||||
m_made_progress = false;
|
||||
break;
|
||||
}
|
||||
case State::Done:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned get_bank_size() const {
|
||||
auto const &terms = m_bank.get_by_cost_and_sort(m_cost, m_target_sort);
|
||||
return terms.size();
|
||||
}
|
||||
|
||||
expr *enumerate_operators() {
|
||||
auto const &ops = m_grammar.operators();
|
||||
while (true) {
|
||||
if (m.limit().is_canceled())
|
||||
return nullptr;
|
||||
|
||||
// first find terms at m_cost that were already created
|
||||
if (m_bank_idx < m_bank_size) {
|
||||
auto const &terms = m_bank.get_by_cost_and_sort(m_cost, m_target_sort);
|
||||
auto t = terms.get(m_bank_idx);
|
||||
m_bank_idx++;
|
||||
SASSERT(sort_matches(t));
|
||||
return t;
|
||||
}
|
||||
|
||||
// then create new terms using children at cost below current m_cost.
|
||||
if (m_children_iter && m_children_iter->has_next(m_cost)) {
|
||||
unsigned new_cost = 0;
|
||||
expr_ref_vector children = m_children_iter->next(new_cost);
|
||||
production const &prod = *ops[m_op_idx - 1];
|
||||
expr_ref term = prod.builder(children);
|
||||
// IF_VERBOSE(0, verbose_stream() << term << "\n");
|
||||
SASSERT(new_cost >= m_cost);
|
||||
expr* r = add_term(term, new_cost);
|
||||
if (!r)
|
||||
continue;
|
||||
unsigned sort_id = r->get_sort()->get_small_id();
|
||||
if (!m_sorts_produced.contains(sort_id))
|
||||
m_made_progress = true;
|
||||
m_sorts_produced.insert(sort_id);
|
||||
if (sort_matches(r) && new_cost == m_cost) {
|
||||
return r;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_op_idx >= ops.size())
|
||||
return nullptr;
|
||||
|
||||
production const &prod = *ops[m_op_idx];
|
||||
m_op_idx++;
|
||||
m_children_iter = std::make_unique<children_iterator>(m, prod, m_bank, m_cost);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace term_enum
|
||||
|
||||
// ============================================================================
|
||||
// term_enumeration public interface implementation
|
||||
// ============================================================================
|
||||
|
||||
struct term_enumeration::imp {
|
||||
ast_manager& m;
|
||||
term_enum::grammar m_grammar;
|
||||
term_enum::bottom_up_enumerator m_bottom_up_enumerator;
|
||||
std::function<unsigned(expr*)> m_cost;
|
||||
|
||||
imp(ast_manager& m) :
|
||||
m(m), m_grammar(m), m_bottom_up_enumerator(m_grammar) {}
|
||||
|
||||
void add_production(func_decl* f) {
|
||||
m_grammar.add_func_decl(f);
|
||||
}
|
||||
|
||||
void add_production(expr* e) {
|
||||
m_grammar.add_expr(e);
|
||||
}
|
||||
|
||||
void set_cost(std::function<unsigned(expr*)> const& cost) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
std::ostream& display(std::ostream& out) const {
|
||||
return m_bottom_up_enumerator.display(out);
|
||||
}
|
||||
};
|
||||
|
||||
// -- iterator implementation --
|
||||
|
||||
struct term_enumeration::iterator::iter_imp {
|
||||
imp& m_imp;
|
||||
ast_manager & m;
|
||||
sort* m_sort;
|
||||
unsigned m_cost = 0;
|
||||
unsigned m_idx = 0;
|
||||
vector<expr_ref_vector> m_levels;
|
||||
expr_ref m_current;
|
||||
bool m_end;
|
||||
vector<expr_ref_vector> m_vars;
|
||||
vector<ptr_vector<sort>> m_decls;
|
||||
vector<vector<symbol>> m_names;
|
||||
|
||||
iter_imp(imp& i, sort* s) : m_imp(i), m(i.m), m_sort(s), m_current(i.m), m_end(false) {
|
||||
m_imp.m_bottom_up_enumerator.reset();
|
||||
init_sort();
|
||||
advance();
|
||||
}
|
||||
|
||||
// Sentinel constructor
|
||||
iter_imp(imp& i) :
|
||||
m_imp(i), m(i.m), m_sort(nullptr), m_current(i.m), m_end(true) {
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
|
||||
void init_sort() {
|
||||
array_util autil(m);
|
||||
sort *range = m_sort;
|
||||
|
||||
while (autil.is_array(range)) {
|
||||
m_vars.push_back(expr_ref_vector(m));
|
||||
m_decls.push_back(ptr_vector<sort>());
|
||||
m_names.push_back(vector<symbol>());
|
||||
for (unsigned i = 0; i < get_array_arity(range); ++i) {
|
||||
m_decls.back().push_back(get_array_domain(range, i));
|
||||
m_vars.back().push_back(nullptr);
|
||||
m_names.back().push_back(symbol());
|
||||
}
|
||||
|
||||
expr_ref_vector args(m);
|
||||
args.push_back(m.mk_const("a", range));
|
||||
for (unsigned i = 0; i < m_decls.back().size(); ++i) {
|
||||
args.push_back(m.mk_var(i, m_decls.back().get(i)));
|
||||
}
|
||||
app_ref sel(autil.mk_select(args), m);
|
||||
m_imp.m_grammar.add_func_decl(sel->get_decl());
|
||||
|
||||
range = get_array_range(range);
|
||||
}
|
||||
unsigned n = 0;
|
||||
for (unsigned i = m_decls.size(); i-- > 0;) {
|
||||
for (unsigned j = m_decls[i].size(); j-- > 0;) {
|
||||
m_vars[i][j] = m.mk_var(n, m_decls[i][j]);
|
||||
m_names[i][j] = symbol(n);
|
||||
m_imp.add_production(m_vars[i].get(j));
|
||||
n++;
|
||||
}
|
||||
}
|
||||
m_sort = range;
|
||||
m_imp.m_bottom_up_enumerator.set_target_sort(range);
|
||||
}
|
||||
|
||||
void mk_lambda() {
|
||||
if (!m_current)
|
||||
return;
|
||||
for (unsigned i = m_decls.size(); i-- > 0;)
|
||||
m_current = m.mk_lambda(m_decls[i].size(), m_decls[i].data(), m_names[i].data(), m_current);
|
||||
}
|
||||
|
||||
void advance() {
|
||||
if (m_end)
|
||||
return;
|
||||
m_current = m_imp.m_bottom_up_enumerator.next();
|
||||
SASSERT(!m_current || m_current->get_sort() == m_sort);
|
||||
mk_lambda();
|
||||
if (!m_current)
|
||||
m_end = true;
|
||||
}
|
||||
};
|
||||
|
||||
term_enumeration::iterator::iterator(imp& i, sort* s) {
|
||||
m_imp = alloc(iter_imp, i, s);
|
||||
}
|
||||
|
||||
term_enumeration::iterator::iterator(std::nullptr_t) {
|
||||
m_imp = nullptr;
|
||||
}
|
||||
|
||||
term_enumeration::iterator::~iterator() {
|
||||
dealloc(m_imp);
|
||||
}
|
||||
|
||||
expr* term_enumeration::iterator::operator*() {
|
||||
return m_imp ? m_imp->m_current.get() : nullptr;
|
||||
}
|
||||
|
||||
term_enumeration::iterator& term_enumeration::iterator::operator++() {
|
||||
if (m_imp) m_imp->advance();
|
||||
return *this;
|
||||
}
|
||||
|
||||
term_enumeration::iterator term_enumeration::iterator::operator++(int) {
|
||||
iterator tmp(*this);
|
||||
++(*this);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
bool term_enumeration::iterator::operator==(iterator const& other) const {
|
||||
if (!m_imp && !other.m_imp) return true;
|
||||
if (!m_imp) return other.m_imp->m_end;
|
||||
if (!other.m_imp) return m_imp->m_end;
|
||||
return m_imp->m_end == other.m_imp->m_end &&
|
||||
m_imp->m_current == other.m_imp->m_current;
|
||||
}
|
||||
|
||||
// -- terms implementation --
|
||||
|
||||
term_enumeration::terms::terms(imp* i, sort* s) : m_imp(i), m_sort(s) {}
|
||||
|
||||
term_enumeration::iterator term_enumeration::terms::begin() {
|
||||
return iterator(*m_imp, m_sort);
|
||||
}
|
||||
|
||||
term_enumeration::iterator term_enumeration::terms::end() {
|
||||
return iterator(nullptr);
|
||||
}
|
||||
|
||||
// -- term_enumeration implementation --
|
||||
|
||||
term_enumeration::term_enumeration(ast_manager& m) {
|
||||
m_imp = alloc(imp, m);
|
||||
}
|
||||
|
||||
term_enumeration::~term_enumeration() {
|
||||
dealloc(m_imp);
|
||||
}
|
||||
|
||||
void term_enumeration::add_production(func_decl* f) {
|
||||
m_imp->add_production(f);
|
||||
}
|
||||
|
||||
void term_enumeration::add_production(expr* e) {
|
||||
m_imp->add_production(e);
|
||||
}
|
||||
|
||||
void term_enumeration::set_cost(std::function<unsigned(expr*)> const& cost) {
|
||||
m_imp->set_cost(cost);
|
||||
}
|
||||
|
||||
term_enumeration::terms term_enumeration::enum_terms(sort* s) {
|
||||
return terms(m_imp, s);
|
||||
}
|
||||
|
||||
std::ostream& term_enumeration::display(std::ostream& out) const {
|
||||
return m_imp->display(out);
|
||||
}
|
||||
50
src/ast/rewriter/term_enumeration.h
Normal file
50
src/ast/rewriter/term_enumeration.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
|
||||
#include "ast/ast.h"
|
||||
#include <functional>
|
||||
|
||||
class term_enumeration {
|
||||
struct imp;
|
||||
imp* m_imp;
|
||||
public:
|
||||
term_enumeration(ast_manager& m);
|
||||
~term_enumeration();
|
||||
|
||||
void add_production(func_decl* f);
|
||||
void add_production(expr* e);
|
||||
// void add_production(sort *s, std::function<expr *()> g);
|
||||
|
||||
// cost function associated with expressions.
|
||||
// terms are enumerated with increasing cost.
|
||||
|
||||
void set_cost(std::function<unsigned(expr*)> const& cost);
|
||||
|
||||
class iterator {
|
||||
struct iter_imp;
|
||||
iter_imp* m_imp;
|
||||
public:
|
||||
iterator(imp& i, sort* s);
|
||||
iterator(std::nullptr_t);
|
||||
~iterator();
|
||||
expr* operator*();
|
||||
iterator operator++(int);
|
||||
iterator& operator++();
|
||||
bool operator!=(iterator const& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
bool operator==(iterator const &other) const;
|
||||
};
|
||||
|
||||
class terms {
|
||||
imp* m_imp;
|
||||
sort* m_sort;
|
||||
public:
|
||||
terms(imp* i, sort* s);
|
||||
iterator begin();
|
||||
iterator end();
|
||||
};
|
||||
|
||||
terms enum_terms(sort* s);
|
||||
|
||||
std::ostream& display(std::ostream& out) const;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue