z3-src 503.0.0

Source distribution of the Z3 SMT solver, for use as a build dependency
Documentation
/*++
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;
    }
}