mirror of
https://github.com/Z3Prover/z3
synced 2026-07-12 01:56:22 +00:00
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Margus Veanes <margus@microsoft.com> Co-authored-by: Margus Veanes <veanes@users.noreply.github.com>
234 lines
7.7 KiB
C++
234 lines
7.7 KiB
C++
/*++
|
|
Copyright (c) 2026 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
seq_regex_bisim.cpp
|
|
|
|
Abstract:
|
|
|
|
See seq_regex_bisim.h.
|
|
|
|
Author:
|
|
|
|
Margus Veanes (veanes)
|
|
Nikolaj Bjorner (nbjorner)
|
|
|
|
--*/
|
|
|
|
#include "ast/rewriter/seq_regex_bisim.h"
|
|
#include "ast/rewriter/seq_rewriter.h"
|
|
#include "ast/ast_pp.h"
|
|
#include "ast/ast_util.h"
|
|
#include "ast/for_each_expr.h"
|
|
|
|
namespace seq {
|
|
|
|
regex_bisim::regex_bisim(seq_rewriter& rw):
|
|
m(rw.m()),
|
|
m_rw(rw),
|
|
m_util(rw.u()),
|
|
m_pinned(m),
|
|
m_worklist(m) {
|
|
}
|
|
|
|
void regex_bisim::reset() {
|
|
m_uf.reset();
|
|
m_node_of.reset();
|
|
m_pinned.reset();
|
|
m_worklist.reset();
|
|
m_steps = 0;
|
|
}
|
|
|
|
/*
|
|
Map an expression to a union-find node, allocating a fresh node on
|
|
first encounter.
|
|
*/
|
|
unsigned regex_bisim::node_of(expr* r) {
|
|
unsigned id = 0;
|
|
if (m_node_of.find(r, id))
|
|
return id;
|
|
id = m_uf.mk_var();
|
|
m_node_of.insert(r, id);
|
|
m_pinned.push_back(r);
|
|
return id;
|
|
}
|
|
|
|
/*
|
|
Compute a definite nullability answer for r.
|
|
If the seq_rewriter is unable to produce a literal true/false (for
|
|
example because r contains an uninterpreted symbol), return l_undef.
|
|
*/
|
|
lbool regex_bisim::nullability(expr* r) {
|
|
expr_ref n = m_rw.is_nullable(r);
|
|
if (m.is_true(n))
|
|
return l_true;
|
|
if (m.is_false(n))
|
|
return l_false;
|
|
return l_undef;
|
|
}
|
|
|
|
/*
|
|
Test whether a regex expression is a kind that the bisimulation
|
|
procedure can reason about. We require it to be a syntactic ground
|
|
term (no free variables) and that its info reports min_length info
|
|
(which implies that it parses cleanly as a regex constructor).
|
|
*/
|
|
bool regex_bisim::is_supported(expr* r) {
|
|
if (!m_util.is_re(r))
|
|
return false;
|
|
if (!m_util.re.get_info(r).is_known())
|
|
return false;
|
|
// Reject regexes mentioning free variables; the symbolic
|
|
// derivative engine introduces (:var 0) only after we call it
|
|
// ourselves, so any pre-existing variable would be a free var.
|
|
return is_ground(r);
|
|
}
|
|
|
|
/*
|
|
Fast inequivalence check based on the get_info().classical flag.
|
|
|
|
Invariant: if r is well-formed and get_info(r).classical is true,
|
|
then L(r) is non-empty. The flag is set for regexes built only
|
|
from str.to_re, re.all, union, concat, star, plus, opt, loop;
|
|
it excludes complement, intersection, diff, xor, and the empty
|
|
regex.
|
|
|
|
A bare regex leaf l (i.e. not a XOR pair) represents the implicit
|
|
pair (empty XOR l). If l is classical, L(l) is non-empty so the
|
|
pair is non-empty: the original two regexes have a distinguishing
|
|
prefix and are inequivalent.
|
|
|
|
For an XOR leaf xor(a, b): if both a and b are classical and have
|
|
different min_length, then the shortest word of one is not in the
|
|
other, so the pair is non-empty and we can short-circuit. (The
|
|
case a == b syntactically is already handled by mk_re_xor0.)
|
|
|
|
Returns true if the leaf proves inequivalence; false if no
|
|
conclusion can be drawn.
|
|
*/
|
|
bool regex_bisim::classical_distinguishing(expr* l) {
|
|
expr* a = nullptr, * b = nullptr;
|
|
if (m_util.re.is_xor(l, a, b)) {
|
|
auto ia = m_util.re.get_info(a);
|
|
auto ib = m_util.re.get_info(b);
|
|
if (ia.is_known() && ib.is_known() &&
|
|
ia.classical && ib.classical &&
|
|
ia.min_length != ib.min_length)
|
|
return true;
|
|
return false;
|
|
}
|
|
if (m_util.re.is_empty(l))
|
|
return false;
|
|
auto info = m_util.re.get_info(l);
|
|
return info.is_known() && info.classical;
|
|
}
|
|
|
|
/*
|
|
Merge the two sides of the XOR pair, returning true if a fresh
|
|
merge happened (i.e. the pair must still be processed) and false
|
|
if the two sides were already in the same union-find class.
|
|
|
|
For non-XOR leaves we treat the leaf l as the pair (empty XOR l).
|
|
*/
|
|
bool regex_bisim::merge_leaf(expr* leaf) {
|
|
expr* a = nullptr, * b = nullptr;
|
|
if (!m_util.re.is_xor(leaf, a, b)) {
|
|
a = m_util.re.mk_empty(leaf->get_sort());
|
|
b = leaf;
|
|
m_pinned.push_back(a);
|
|
}
|
|
unsigned ia = node_of(a);
|
|
unsigned ib = node_of(b);
|
|
if (m_uf.find(ia) == m_uf.find(ib))
|
|
return false;
|
|
m_uf.merge(ia, ib);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
Decide equivalence by bisimulation on D(p XOR q).
|
|
*/
|
|
lbool regex_bisim::are_equivalent(expr* p, expr* q) {
|
|
return are_equivalent_core(p, q);
|
|
}
|
|
|
|
lbool regex_bisim::are_equivalent_core(expr* p, expr* q) {
|
|
if (!is_supported(p) || !is_supported(q))
|
|
return l_undef;
|
|
if (p == q)
|
|
return l_true;
|
|
|
|
reset();
|
|
|
|
// Build the initial pair r0 = p XOR q applying the structural
|
|
// XOR rewrites (r XOR r = empty, AC normalisation, etc.).
|
|
expr_ref r0 = m_rw.mk_re_xor_simplified(p, q);
|
|
|
|
// If r0 simplified to empty, the two regexes are equivalent.
|
|
if (m_util.re.is_empty(r0))
|
|
return l_true;
|
|
|
|
lbool n0 = nullability(r0);
|
|
if (n0 == l_true)
|
|
return l_false; // distinguishing empty word
|
|
if (n0 == l_undef)
|
|
return l_undef;
|
|
|
|
// Classical-leaf shortcut applied to r0 (covers the case where
|
|
// mk_re_xor_simplified collapsed p XOR q to a bare classical
|
|
// residual, e.g. when one side reduced to empty).
|
|
if (classical_distinguishing(r0))
|
|
return l_false;
|
|
|
|
if (!merge_leaf(r0))
|
|
return l_true; // already merged: trivially equivalent
|
|
m_worklist.push_back(r0);
|
|
|
|
while (!m_worklist.empty()) {
|
|
if (++m_steps > m_step_bound)
|
|
return l_undef;
|
|
|
|
expr_ref r(m_worklist.back(), m);
|
|
m_worklist.pop_back();
|
|
|
|
// Compute the symbolic derivative wrt the canonical variable
|
|
// (: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);
|
|
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
|
|
// non-empty leaf (definitive distinguishing non-empty prefix).
|
|
for (expr* l : leaves) {
|
|
lbool nl = nullability(l);
|
|
if (nl == l_true)
|
|
return l_false;
|
|
if (nl == l_undef)
|
|
return l_undef;
|
|
if (classical_distinguishing(l))
|
|
return l_false;
|
|
}
|
|
|
|
// Second pass: merge each leaf into the union-find; new
|
|
// merges go onto the worklist.
|
|
for (expr* l : leaves) {
|
|
if (merge_leaf(l)) {
|
|
m_pinned.push_back(l);
|
|
m_worklist.push_back(l);
|
|
}
|
|
}
|
|
}
|
|
return l_true;
|
|
}
|
|
}
|