/*++
  Module Name:

  theory_str.cpp

  Abstract:

  String Theory Plugin

  Author:

  Murphy Berzish and Yunhui Zheng

  Revision History:

  --*/
#include "ast/ast_smt2_pp.h"
#include "smt/smt_context.h"
#include "smt/theory_str.h"
#include "smt/smt_model_generator.h"
#include "ast/ast_pp.h"
#include "ast/ast_ll_pp.h"
#include<list>
#include<algorithm>
#include "smt/theory_seq_empty.h"
#include "smt/theory_arith.h"
#include "ast/ast_util.h"
#include "ast/rewriter/seq_rewriter.h"
#include "smt_kernel.h"

namespace smt {

    theory_str::theory_str(ast_manager & m, theory_str_params const & params):
        theory(m.mk_family_id("seq")),
        m_params(params),
        /* Options */
        opt_EagerStringConstantLengthAssertions(true),
        opt_VerifyFinalCheckProgress(false),
        opt_LCMUnrollStep(2),
        opt_NoQuickReturn_IntegerTheory(false),
        opt_DisableIntegerTheoryIntegration(false),
        opt_DeferEQCConsistencyCheck(false),
        opt_CheckVariableScope(true),
        opt_ConcatOverlapAvoid(true),
        /* Internal setup */
        search_started(false),
        m_autil(m),
        u(m),
        sLevel(0),
        finalCheckProgressIndicator(false),
        m_trail(m),
        m_factory(nullptr),
        m_mk_aut(m),
        m_unused_id(0),
        m_delayed_axiom_setup_terms(m),
        m_delayed_assertions_todo(m),
        m_persisted_axioms(m),
        m_persisted_axiom_todo(m),
        tmpStringVarCount(0),
        tmpXorVarCount(0),
        tmpLenTestVarCount(0),
        tmpValTestVarCount(0),
        avoidLoopCut(true),
        loopDetected(false),
        m_theoryStrOverlapAssumption_term(m),
        contains_map(m),
        string_int_conversion_terms(m),
        totalCacheAccessCount(0),
        cacheHitCount(0),
        cacheMissCount(0),
        m_fresh_id(0),
        m_trail_stack(*this),
        m_find(*this)
    {
        initialize_charset();
    }

    theory_str::~theory_str() {
        m_trail_stack.reset();
        for (eautomaton * aut : regex_automata) {
            dealloc(aut);
        }
        regex_automata.clear();
    }

    expr * theory_str::mk_string(zstring const& str) {
        if (m_params.m_StringConstantCache) {
            ++totalCacheAccessCount;
            expr * val;
            if (stringConstantCache.find(str, val)) {
                return val;
            } else {
                val = u.str.mk_string(str);
                m_trail.push_back(val);
                stringConstantCache.insert(str, val);
                return val;
            }
        } else {
            return u.str.mk_string(str);
        }
    }

    expr * theory_str::mk_string(const char * str) {
        symbol sym(str);
        return u.str.mk_string(sym);
    }

    class seq_expr_solver : public expr_solver {
        kernel m_kernel;
    public:
        seq_expr_solver(ast_manager& m, smt_params& fp):
            m_kernel(m, fp) {}
        virtual lbool check_sat(expr* e) {
            m_kernel.push();
            m_kernel.assert_expr(e);
            lbool r = m_kernel.check();
            m_kernel.pop(1);
            return r;
        }
    };

    void theory_str::init(context * ctx) {
        theory::init(ctx);
        m_mk_aut.set_solver(alloc(seq_expr_solver, get_manager(),
                                  get_context().get_fparams()));
    }

    void theory_str::initialize_charset() {
        bool defaultCharset = true;
        if (defaultCharset) {
            // valid C strings can't contain the null byte ('\0')
            charSetSize = 255;
            char_set.resize(256, 0);
            int idx = 0;
            // small letters
            for (int i = 97; i < 123; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
            // caps
            for (int i = 65; i < 91; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
            // numbers
            for (int i = 48; i < 58; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
            // printable marks - 1
            for (int i = 32; i < 48; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
            // printable marks - 2
            for (int i = 58; i < 65; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
            // printable marks - 3
            for (int i = 91; i < 97; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
            // printable marks - 4
            for (int i = 123; i < 127; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
            // non-printable - 1
            for (int i = 1; i < 32; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
            // non-printable - 2
            for (int i = 127; i < 256; i++) {
                char_set[idx] = (char) i;
                charSetLookupTable[char_set[idx]] = idx;
                idx++;
            }
        } else {
            const char setset[] = { 'a', 'b', 'c' };
            int fSize = sizeof(setset) / sizeof(char);
            char_set.resize(fSize, 0);
            charSetSize = fSize;
            for (int i = 0; i < charSetSize; i++) {
                char_set[i] = setset[i];
                charSetLookupTable[setset[i]] = i;
            }
        }
    }

    void theory_str::assert_axiom(expr * _e) {
        if (_e == nullptr) 
            return;
        if (opt_VerifyFinalCheckProgress) {
            finalCheckProgressIndicator = true;
        }

        if (get_manager().is_true(_e)) return;
        context & ctx = get_context();
        ast_manager& m = get_manager();
        TRACE("str", tout << "asserting " << mk_ismt2_pp(_e, m) << std::endl;);
        expr_ref e(_e, m);
        //th_rewriter rw(m);
        //rw(e);
        if (!ctx.b_internalized(e)) {
            ctx.internalize(e, false);
        }
        literal lit(ctx.get_literal(e));
        ctx.mark_as_relevant(lit);
        ctx.mk_th_axiom(get_id(), 1, &lit);

        // crash/error avoidance: add all axioms to the trail
        m_trail.push_back(e);

        //TRACE("str", tout << "done asserting " << mk_ismt2_pp(e, get_manager()) << std::endl;);

    }

    expr * theory_str::rewrite_implication(expr * premise, expr * conclusion) {
        ast_manager & m = get_manager();
        return m.mk_or(mk_not(m, premise), conclusion);
    }

    void theory_str::assert_implication(expr * premise, expr * conclusion) {
        ast_manager & m = get_manager();
        TRACE("str", tout << "asserting implication " << mk_ismt2_pp(premise, m) << " -> " << mk_ismt2_pp(conclusion, m) << std::endl;);
        expr_ref axiom(m.mk_or(mk_not(m, premise), conclusion), m);
        assert_axiom(axiom);
    }

    bool theory_str::internalize_atom(app * atom, bool gate_ctx) {
        return internalize_term(atom);
    }

    bool theory_str::internalize_term(app * term) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        SASSERT(term->get_family_id() == get_family_id());

        TRACE("str", tout << "internalizing term: " << mk_ismt2_pp(term, get_manager()) << std::endl;);

        // emulation of user_smt_theory::internalize_term()

        unsigned num_args = term->get_num_args();
        for (unsigned i = 0; i < num_args; ++i) {
            ctx.internalize(term->get_arg(i), false);
        }
        if (ctx.e_internalized(term)) {
            enode * e = ctx.get_enode(term);
            mk_var(e);
            return true;
        }
        // m_parents.push_back(term);
        enode * e = ctx.mk_enode(term, false, m.is_bool(term), true);
        if (m.is_bool(term)) {
            bool_var bv = ctx.mk_bool_var(term);
            ctx.set_var_theory(bv, get_id());
            ctx.set_enode_flag(bv, true);
        }
        // make sure every argument is attached to a theory variable
        for (unsigned i = 0; i < num_args; ++i) {
            enode * arg = e->get_arg(i);
            theory_var v_arg = mk_var(arg);
            TRACE("str", tout << "arg has theory var #" << v_arg << std::endl;); (void)v_arg;
        }

        theory_var v = mk_var(e);
        TRACE("str", tout << "term has theory var #" << v << std::endl;); (void)v;

        if (opt_EagerStringConstantLengthAssertions && u.str.is_string(term)) {
            TRACE("str", tout << "eagerly asserting length of string term " << mk_pp(term, m) << std::endl;);
            m_basicstr_axiom_todo.insert(e);
        }
        return true;
    }

    enode* theory_str::ensure_enode(expr* e) {
        context& ctx = get_context();
        if (!ctx.e_internalized(e)) {
            ctx.internalize(e, false);
        }
        enode* n = ctx.get_enode(e);
        ctx.mark_as_relevant(n);
        return n;
    }

    void theory_str::refresh_theory_var(expr * e) {
        ast_manager & m = get_manager();
        enode * en = ensure_enode(e);
        theory_var v = mk_var(en); (void)v;
        TRACE("str", tout << "refresh " << mk_pp(e, get_manager()) << ": v#" << v << std::endl;);
        if (m.get_sort(e) == u.str.mk_string_sort()) {
            m_basicstr_axiom_todo.push_back(en);
        }
    }

    theory_var theory_str::mk_var(enode* n) {
        TRACE("str", tout << "mk_var for " << mk_pp(n->get_owner(), get_manager()) << std::endl;);
        ast_manager & m = get_manager();
        if (!(m.get_sort(n->get_owner()) == u.str.mk_string_sort())) {
            return null_theory_var;
        }
        if (is_attached_to_var(n)) {
            TRACE("str", tout << "already attached to theory var" << std::endl;);
            return n->get_th_var(get_id());
        } else {
            theory_var v = theory::mk_var(n);
            m_find.mk_var();
            TRACE("str", tout << "new theory var v#" << v << " find " << m_find.find(v) << std::endl;);
            get_context().attach_th_var(n, this, v);
            get_context().mark_as_relevant(n);
            return v;
        }
    }

    static void cut_vars_map_copy(obj_map<expr, int> & dest, obj_map<expr, int> & src) {
        for (auto const& kv : src) {
            dest.insert(kv.m_key, 1);
        }
    }

    bool theory_str::has_self_cut(expr * n1, expr * n2) {
        if (!cut_var_map.contains(n1)) {
            return false;
        }
        if (!cut_var_map.contains(n2)) {
            return false;
        }
        if (cut_var_map[n1].empty() || cut_var_map[n2].empty()) {
            return false;
        }

        for (auto const& kv : cut_var_map[n1].top()->vars) {
            if (cut_var_map[n2].top()->vars.contains(kv.m_key)) {
                return true;
            }
        }
        return false;
    }

    void theory_str::add_cut_info_one_node(expr * baseNode, int slevel, expr * node) {
        // crash avoidance?
        m_trail.push_back(baseNode);
        m_trail.push_back(node);
        if (!cut_var_map.contains(baseNode)) {
            T_cut * varInfo = alloc(T_cut);
            m_cut_allocs.push_back(varInfo);
            varInfo->level = slevel;
            varInfo->vars.insert(node, 1);
            cut_var_map.insert(baseNode, std::stack<T_cut*>());
            cut_var_map[baseNode].push(varInfo);
            TRACE("str", tout << "add var info for baseNode=" << mk_pp(baseNode, get_manager()) << ", node=" << mk_pp(node, get_manager()) << " [" << slevel << "]" << std::endl;);
        } else {
            if (cut_var_map[baseNode].empty()) {
                T_cut * varInfo = alloc(T_cut);
                m_cut_allocs.push_back(varInfo);
                varInfo->level = slevel;
                varInfo->vars.insert(node, 1);
                cut_var_map[baseNode].push(varInfo);
                TRACE("str", tout << "add var info for baseNode=" << mk_pp(baseNode, get_manager()) << ", node=" << mk_pp(node, get_manager()) << " [" << slevel << "]" << std::endl;);
            } else {
                if (cut_var_map[baseNode].top()->level < slevel) {
                    T_cut * varInfo = alloc(T_cut);
                    m_cut_allocs.push_back(varInfo);
                    varInfo->level = slevel;
                    cut_vars_map_copy(varInfo->vars, cut_var_map[baseNode].top()->vars);
                    varInfo->vars.insert(node, 1);
                    cut_var_map[baseNode].push(varInfo);
                    TRACE("str", tout << "add var info for baseNode=" << mk_pp(baseNode, get_manager()) << ", node=" << mk_pp(node, get_manager()) << " [" << slevel << "]" << std::endl;);
                } else if (cut_var_map[baseNode].top()->level == slevel) {
                    cut_var_map[baseNode].top()->vars.insert(node, 1);
                    TRACE("str", tout << "add var info for baseNode=" << mk_pp(baseNode, get_manager()) << ", node=" << mk_pp(node, get_manager()) << " [" << slevel << "]" << std::endl;);
                } else {
                    get_manager().raise_exception("entered illegal state during add_cut_info_one_node()");
                }
            }
        }
    }

    void theory_str::add_cut_info_merge(expr * destNode, int slevel, expr * srcNode) {
        // crash avoidance?
        m_trail.push_back(destNode);
        m_trail.push_back(srcNode);
        if (!cut_var_map.contains(srcNode)) {
            get_manager().raise_exception("illegal state in add_cut_info_merge(): cut_var_map doesn't contain srcNode");
        }

        if (cut_var_map[srcNode].empty()) {
            get_manager().raise_exception("illegal state in add_cut_info_merge(): cut_var_map[srcNode] is empty");
        }

        if (!cut_var_map.contains(destNode)) {
            T_cut * varInfo = alloc(T_cut);
            m_cut_allocs.push_back(varInfo);
            varInfo->level = slevel;
            cut_vars_map_copy(varInfo->vars, cut_var_map[srcNode].top()->vars);
            cut_var_map.insert(destNode, std::stack<T_cut*>());
            cut_var_map[destNode].push(varInfo);
            TRACE("str", tout << "merge var info for destNode=" << mk_pp(destNode, get_manager()) << ", srcNode=" << mk_pp(srcNode, get_manager()) << " [" << slevel << "]" << std::endl;);
        } else {
            if (cut_var_map[destNode].empty() || cut_var_map[destNode].top()->level < slevel) {
                T_cut * varInfo = alloc(T_cut);
                m_cut_allocs.push_back(varInfo);
                varInfo->level = slevel;
                cut_vars_map_copy(varInfo->vars, cut_var_map[destNode].top()->vars);
                cut_vars_map_copy(varInfo->vars, cut_var_map[srcNode].top()->vars);
                cut_var_map[destNode].push(varInfo);
                TRACE("str", tout << "merge var info for destNode=" << mk_pp(destNode, get_manager()) << ", srcNode=" << mk_pp(srcNode, get_manager()) << " [" << slevel << "]" << std::endl;);
            } else if (cut_var_map[destNode].top()->level == slevel) {
                cut_vars_map_copy(cut_var_map[destNode].top()->vars, cut_var_map[srcNode].top()->vars);
                TRACE("str", tout << "merge var info for destNode=" << mk_pp(destNode, get_manager()) << ", srcNode=" << mk_pp(srcNode, get_manager()) << " [" << slevel << "]" << std::endl;);
            } else {
                get_manager().raise_exception("illegal state in add_cut_info_merge(): inconsistent slevels");
            }
        }
    }

    void theory_str::check_and_init_cut_var(expr * node) {
        if (cut_var_map.contains(node)) {
            return;
        } else if (!u.str.is_string(node)) {
            add_cut_info_one_node(node, -1, node);
        }
    }

    literal theory_str::mk_literal(expr* _e) {
        ast_manager & m = get_manager();
        expr_ref e(_e, m);
        context& ctx = get_context();
        ensure_enode(e);
        return ctx.get_literal(e);
    }

    app * theory_str::mk_int(int n) {
        return m_autil.mk_numeral(rational(n), true);
    }

    app * theory_str::mk_int(rational & q) {
        return m_autil.mk_numeral(q, true);
    }

    expr * theory_str::mk_internal_lenTest_var(expr * node, int lTries) {
        ast_manager & m = get_manager();

        std::stringstream ss;
        ss << "$$_len_" << mk_ismt2_pp(node, m) << "_" << lTries << "_" << tmpLenTestVarCount;
        tmpLenTestVarCount += 1;
        std::string name = ss.str();
        app * var = mk_str_var(name);
        internal_lenTest_vars.insert(var);
        m_trail.push_back(var);
        return var;
    }

    expr * theory_str::mk_internal_valTest_var(expr * node, int len, int vTries) {
        ast_manager & m = get_manager();
        std::stringstream ss;
        ss << "$$_val_" << mk_ismt2_pp(node, m) << "_" << len << "_" << vTries << "_" << tmpValTestVarCount;
        tmpValTestVarCount += 1;
        std::string name = ss.str();
        app * var = mk_str_var(name);
        internal_valTest_vars.insert(var);
        m_trail.push_back(var);
        return var;
    }

    void theory_str::track_variable_scope(expr * var) {
        if (internal_variable_scope_levels.find(sLevel) == internal_variable_scope_levels.end()) {
            internal_variable_scope_levels[sLevel] = obj_hashtable<expr>();
        }
        internal_variable_scope_levels[sLevel].insert(var);
    }

    app * theory_str::mk_internal_xor_var() {
        return mk_int_var("$$_xor");
    }

    app * theory_str::mk_fresh_const(char const* name, sort* s) {
        string_buffer<64> buffer;
        buffer << name;
        buffer << "!tmp";
        buffer << m_fresh_id;
        m_fresh_id++;
        return u.mk_skolem(symbol(buffer.c_str()), 0, nullptr, s);
    }


    app * theory_str::mk_int_var(std::string name) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        TRACE("str", tout << "creating integer variable " << name << " at scope level " << sLevel << std::endl;);

        sort * int_sort = m.mk_sort(m_autil.get_family_id(), INT_SORT);
        app * a = mk_fresh_const(name.c_str(), int_sort);

        ctx.internalize(a, false);
        SASSERT(ctx.get_enode(a) != NULL);
        SASSERT(ctx.e_internalized(a));
        ctx.mark_as_relevant(a);
        // I'm assuming that this combination will do the correct thing in the integer theory.

        //mk_var(ctx.get_enode(a));
        m_trail.push_back(a);
        //variable_set.insert(a);
        //internal_variable_set.insert(a);
        //track_variable_scope(a);

        return a;
    }

    app * theory_str::mk_unroll_bound_var() {
        return mk_int_var("unroll");
    }

    app * theory_str::mk_unroll_test_var() {
        app * v = mk_str_var("unrollTest"); // was uRt
        internal_unrollTest_vars.insert(v);
        track_variable_scope(v);
        return v;
    }

    app * theory_str::mk_str_var(std::string name) {
        context & ctx = get_context();

        TRACE("str", tout << "creating string variable " << name << " at scope level " << sLevel << std::endl;);

        sort * string_sort = u.str.mk_string_sort();
        app * a = mk_fresh_const(name.c_str(), string_sort);
        m_trail.push_back(a);

        TRACE("str", tout << "a->get_family_id() = " << a->get_family_id() << std::endl
              << "this->get_family_id() = " << this->get_family_id() << std::endl;);

        // I have a hunch that this may not get internalized for free...
        ctx.internalize(a, false);
        SASSERT(ctx.get_enode(a) != NULL);
        SASSERT(ctx.e_internalized(a));
        // this might help??
        mk_var(ctx.get_enode(a));
        m_basicstr_axiom_todo.push_back(ctx.get_enode(a));
        TRACE("str", tout << "add " << mk_pp(a, get_manager()) << " to m_basicstr_axiom_todo" << std::endl;);

        variable_set.insert(a);
        internal_variable_set.insert(a);
        track_variable_scope(a);

        return a;
    }

    app * theory_str::mk_regex_rep_var() {
        context & ctx = get_context();

        sort * string_sort = u.str.mk_string_sort();
        app * a = mk_fresh_const("regex", string_sort);
        m_trail.push_back(a);

        ctx.internalize(a, false);
        SASSERT(ctx.get_enode(a) != NULL);
        SASSERT(ctx.e_internalized(a));
        mk_var(ctx.get_enode(a));
        m_basicstr_axiom_todo.push_back(ctx.get_enode(a));
        TRACE("str", tout << "add " << mk_pp(a, get_manager()) << " to m_basicstr_axiom_todo" << std::endl;);

        variable_set.insert(a);
        //internal_variable_set.insert(a);
        regex_variable_set.insert(a);
        track_variable_scope(a);

        return a;
    }

    void theory_str::add_nonempty_constraint(expr * s) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr_ref ax1(mk_not(m, ctx.mk_eq_atom(s, mk_string(""))), m);
        assert_axiom(ax1);

        {
            // build LHS
            expr_ref len_str(mk_strlen(s), m);
            SASSERT(len_str);
            // build RHS
            expr_ref zero(m_autil.mk_numeral(rational(0), true), m);
            SASSERT(zero);
            // build LHS > RHS and assert
            // we have to build !(LHS <= RHS) instead
            expr_ref lhs_gt_rhs(mk_not(m, m_autil.mk_le(len_str, zero)), m);
            SASSERT(lhs_gt_rhs);
            assert_axiom(lhs_gt_rhs);
        }
    }

    app * theory_str::mk_nonempty_str_var() {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        std::stringstream ss;
        ss << tmpStringVarCount;
        tmpStringVarCount++;
        std::string name = "$$_str" + ss.str();

        TRACE("str", tout << "creating nonempty string variable " << name << " at scope level " << sLevel << std::endl;);

        sort * string_sort = u.str.mk_string_sort();
        app * a = mk_fresh_const(name.c_str(), string_sort);

        ctx.internalize(a, false);
        SASSERT(ctx.get_enode(a) != NULL);
        // this might help??
        mk_var(ctx.get_enode(a));

        // assert a variation of the basic string axioms that ensures this string is nonempty
        {
            // build LHS
            expr_ref len_str(mk_strlen(a), m);
            SASSERT(len_str);
            // build RHS
            expr_ref zero(m_autil.mk_numeral(rational(0), true), m);
            SASSERT(zero);
            // build LHS > RHS and assert
            // we have to build !(LHS <= RHS) instead
            expr_ref lhs_gt_rhs(mk_not(m, m_autil.mk_le(len_str, zero)), m);
            SASSERT(lhs_gt_rhs);
            assert_axiom(lhs_gt_rhs);
        }

        // add 'a' to variable sets, so we can keep track of it
        m_trail.push_back(a);
        variable_set.insert(a);
        internal_variable_set.insert(a);
        track_variable_scope(a);

        return a;
    }

    app * theory_str::mk_unroll(expr * n, expr * bound) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr * args[2] = {n, bound};
        app * unrollFunc = get_manager().mk_app(get_id(), _OP_RE_UNROLL, 0, nullptr, 2, args);
        m_trail.push_back(unrollFunc);

        expr_ref_vector items(m);
        items.push_back(ctx.mk_eq_atom(ctx.mk_eq_atom(bound, mk_int(0)), ctx.mk_eq_atom(unrollFunc, mk_string(""))));
        items.push_back(m_autil.mk_ge(bound, mk_int(0)));
        items.push_back(m_autil.mk_ge(mk_strlen(unrollFunc), mk_int(0)));

        expr_ref finalAxiom(mk_and(items), m);
        SASSERT(finalAxiom);
        assert_axiom(finalAxiom);
        return unrollFunc;
    }

    app * theory_str::mk_contains(expr * haystack, expr * needle) {
        app * contains = u.str.mk_contains(haystack, needle); // TODO double-check semantics/argument order
        m_trail.push_back(contains);
        // immediately force internalization so that axiom setup does not fail
        get_context().internalize(contains, false);
        set_up_axioms(contains);
        return contains;
    }

    // note, this invokes "special-case" handling for the start index being 0
    app * theory_str::mk_indexof(expr * haystack, expr * needle) {
        app * indexof = u.str.mk_index(haystack, needle, mk_int(0));
        m_trail.push_back(indexof);
        // immediately force internalization so that axiom setup does not fail
        get_context().internalize(indexof, false);
        set_up_axioms(indexof);
        return indexof;
    }

    app * theory_str::mk_strlen(expr * e) {
        /*if (m_strutil.is_string(e)) {*/ if (false) {
            zstring strval;
            u.str.is_string(e, strval);
            unsigned int len = strval.length();
            return m_autil.mk_numeral(rational(len), true);
        } else {
            if (false) {
                // use cache
                app * lenTerm = nullptr;
                if (!length_ast_map.find(e, lenTerm)) {
                    lenTerm = u.str.mk_length(e);
                    length_ast_map.insert(e, lenTerm);
                    m_trail.push_back(lenTerm);
                }
                return lenTerm;
            } else {
                // always regen
                return u.str.mk_length(e);
            }
        }
    }

    /*
     * Returns the simplified concatenation of two expressions,
     * where either both expressions are constant strings
     * or one expression is the empty string.
     * If this precondition does not hold, the function returns NULL.
     * (note: this function was strTheory::Concat())
     */
    expr * theory_str::mk_concat_const_str(expr * n1, expr * n2) {
        bool n1HasEqcValue = false;
        bool n2HasEqcValue = false;
        expr * v1 = get_eqc_value(n1, n1HasEqcValue);
        expr * v2 = get_eqc_value(n2, n2HasEqcValue);
        if (u.str.is_string(v1)) {
            n1HasEqcValue = true;
        }
        if (u.str.is_string(v2)) {
            n2HasEqcValue = true;
        }
        if (n1HasEqcValue && n2HasEqcValue) {
            zstring n1_str;
            u.str.is_string(v1, n1_str);
            zstring n2_str;
            u.str.is_string(v2, n2_str);
            zstring result = n1_str + n2_str;
            return mk_string(result);
        } else if (n1HasEqcValue && !n2HasEqcValue) {
            zstring n1_str;
            u.str.is_string(v1, n1_str);
            if (n1_str.empty()) {
                return n2;
            }
        } else if (!n1HasEqcValue && n2HasEqcValue) {
            zstring n2_str;
            u.str.is_string(v2, n2_str);
            if (n2_str.empty()) {
                return n1;
            }
        }
        return nullptr;
    }

    expr * theory_str::mk_concat(expr * n1, expr * n2) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        ENSURE(n1 != nullptr);
        ENSURE(n2 != nullptr);
        bool n1HasEqcValue = false;
        bool n2HasEqcValue = false;
        n1 = get_eqc_value(n1, n1HasEqcValue);
        n2 = get_eqc_value(n2, n2HasEqcValue);
        if (n1HasEqcValue && n2HasEqcValue) {
            return mk_concat_const_str(n1, n2);
        } else if (n1HasEqcValue && !n2HasEqcValue) {
            bool n2_isConcatFunc = u.str.is_concat(to_app(n2));
            zstring n1_str;
            u.str.is_string(n1, n1_str);
            if (n1_str.empty()) {
                return n2;
            }
            if (n2_isConcatFunc) {
                expr * n2_arg0 = to_app(n2)->get_arg(0);
                expr * n2_arg1 = to_app(n2)->get_arg(1);
                if (u.str.is_string(n2_arg0)) {
                    n1 = mk_concat_const_str(n1, n2_arg0); // n1 will be a constant
                    n2 = n2_arg1;
                }
            }
        } else if (!n1HasEqcValue && n2HasEqcValue) {
            zstring n2_str;
            u.str.is_string(n2, n2_str);
            if (n2_str.empty()) {
                return n1;
            }

            if (u.str.is_concat(to_app(n1))) {
                expr * n1_arg0 = to_app(n1)->get_arg(0);
                expr * n1_arg1 = to_app(n1)->get_arg(1);
                if (u.str.is_string(n1_arg1)) {
                    n1 = n1_arg0;
                    n2 = mk_concat_const_str(n1_arg1, n2); // n2 will be a constant
                }
            }
        } else {
            if (u.str.is_concat(to_app(n1)) && u.str.is_concat(to_app(n2))) {
                expr * n1_arg0 = to_app(n1)->get_arg(0);
                expr * n1_arg1 = to_app(n1)->get_arg(1);
                expr * n2_arg0 = to_app(n2)->get_arg(0);
                expr * n2_arg1 = to_app(n2)->get_arg(1);
                if (u.str.is_string(n1_arg1) && u.str.is_string(n2_arg0)) {
                    expr * tmpN1 = n1_arg0;
                    expr * tmpN2 = mk_concat_const_str(n1_arg1, n2_arg0);
                    n1 = mk_concat(tmpN1, tmpN2);
                    n2 = n2_arg1;
                }
            }
        }

        //------------------------------------------------------
        // * expr * ast1 = mk_2_arg_app(ctx, td->Concat, n1, n2);
        // * expr * ast2 = mk_2_arg_app(ctx, td->Concat, n1, n2);
        // Z3 treats (ast1) and (ast2) as two different nodes.
        //-------------------------------------------------------

        expr * concatAst = nullptr;

        if (!concat_astNode_map.find(n1, n2, concatAst)) {
            concatAst = u.str.mk_concat(n1, n2);
            m_trail.push_back(concatAst);
            concat_astNode_map.insert(n1, n2, concatAst);

            expr_ref concat_length(mk_strlen(concatAst), m);

            ptr_vector<expr> childrenVector;
            get_nodes_in_concat(concatAst, childrenVector);
            expr_ref_vector items(m);
            for (auto el : childrenVector) {
                items.push_back(mk_strlen(el));
            }
            expr_ref lenAssert(ctx.mk_eq_atom(concat_length, m_autil.mk_add(items.size(), items.c_ptr())), m);
            assert_axiom(lenAssert);
        }
        return concatAst;
    }

    bool theory_str::can_propagate() {
        return !m_basicstr_axiom_todo.empty() || !m_str_eq_todo.empty()
            || !m_concat_axiom_todo.empty() || !m_concat_eval_todo.empty()
            || !m_library_aware_axiom_todo.empty()
            || !m_delayed_axiom_setup_terms.empty()
            || !m_persisted_axiom_todo.empty()
            || (search_started && !m_delayed_assertions_todo.empty())
        ;
    }

    void theory_str::propagate() {
        context & ctx = get_context();
        while (can_propagate()) {
            TRACE("str", tout << "propagating..." << std::endl;);
            while(true) {
                // this can potentially recursively activate itself
                unsigned start_count = m_basicstr_axiom_todo.size();
                ptr_vector<enode> axioms_tmp(m_basicstr_axiom_todo);
                for (auto const& el : axioms_tmp) {
                    instantiate_basic_string_axioms(el);
                }
                unsigned end_count = m_basicstr_axiom_todo.size();
                if (end_count > start_count) {
                    TRACE("str", tout << "new basic string axiom terms added -- checking again" << std::endl;);
                    continue;
                } else {
                    break;
                }
            }
            m_basicstr_axiom_todo.reset();
            TRACE("str", tout << "reset m_basicstr_axiom_todo" << std::endl;);

            for (auto const& pair : m_str_eq_todo) {
                enode * lhs = pair.first;
                enode * rhs = pair.second;
                handle_equality(lhs->get_owner(), rhs->get_owner());
            }
            m_str_eq_todo.reset();

            for (auto const& el : m_concat_axiom_todo) {
                instantiate_concat_axiom(el);
            }
            m_concat_axiom_todo.reset();

            for (auto const& el : m_concat_eval_todo) {
                try_eval_concat(el);
            }
            m_concat_eval_todo.reset();

            while(true) {
                // Special handling: terms can recursively set up other terms
                // (e.g. indexof can instantiate other indexof terms).
                // - Copy the list so it can potentially be modified during setup.
                // - Don't clear this list if new ones are added in the process;
                //   instead, set up all the new terms before proceeding.
                // TODO see if any other propagate() worklists need this kind of handling
                // TODO we really only need to check the new ones on each pass
                unsigned start_count = m_library_aware_axiom_todo.size();
                ptr_vector<enode> axioms_tmp(m_library_aware_axiom_todo);
                for (auto const& e : axioms_tmp) {
                    app * a = e->get_owner();
                    if (u.str.is_stoi(a)) {
                        instantiate_axiom_str_to_int(e);
                    } else if (u.str.is_itos(a)) {
                        instantiate_axiom_int_to_str(e);
                    } else if (u.str.is_at(a)) {
                        instantiate_axiom_CharAt(e);
                    } else if (u.str.is_prefix(a)) {
                        instantiate_axiom_prefixof(e);
                    } else if (u.str.is_suffix(a)) {
                        instantiate_axiom_suffixof(e);
                    } else if (u.str.is_contains(a)) {
                        instantiate_axiom_Contains(e);
                    } else if (u.str.is_index(a)) {
                        instantiate_axiom_Indexof(e);
                    } else if (u.str.is_extract(a)) {
                        instantiate_axiom_Substr(e);
                    } else if (u.str.is_replace(a)) {
                        instantiate_axiom_Replace(e);
                    } else if (u.str.is_in_re(a)) {
                        instantiate_axiom_RegexIn(e);
                    } else {
                        TRACE("str", tout << "BUG: unhandled library-aware term " << mk_pp(e->get_owner(), get_manager()) << std::endl;);
                        NOT_IMPLEMENTED_YET();
                    }
                }
                unsigned end_count = m_library_aware_axiom_todo.size();
                if (end_count > start_count) {
                    TRACE("str", tout << "new library-aware terms added during axiom setup -- checking again" << std::endl;);
                    continue;
                } else {
                    break;
                }
            }
            m_library_aware_axiom_todo.reset();

            for (auto el : m_delayed_axiom_setup_terms) {
                // I think this is okay
                ctx.internalize(el, false);
                set_up_axioms(el);
            }
            m_delayed_axiom_setup_terms.reset();

            for (expr * a : m_persisted_axiom_todo) {
                assert_axiom(a);
            }
            m_persisted_axiom_todo.reset();

            if (search_started) {
                for (auto const& el : m_delayed_assertions_todo) {
                    assert_axiom(el);
                }
                m_delayed_assertions_todo.reset();
            }
        }
    }

    /*
     * Attempt to evaluate a concat over constant strings,
     * and if this is possible, assert equality between the
     * flattened string and the original term.
     */

    void theory_str::try_eval_concat(enode * cat) {
        app * a_cat = cat->get_owner();
        SASSERT(u.str.is_concat(a_cat));

        context & ctx = get_context();
        ast_manager & m = get_manager();

        TRACE("str", tout << "attempting to flatten " << mk_pp(a_cat, m) << std::endl;);

        std::stack<app*> worklist;
        zstring flattenedString("");
        bool constOK = true;

        {
            app * arg0 = to_app(a_cat->get_arg(0));
            app * arg1 = to_app(a_cat->get_arg(1));

            worklist.push(arg1);
            worklist.push(arg0);
        }

        while (constOK && !worklist.empty()) {
            app * evalArg = worklist.top(); worklist.pop();
            zstring nextStr;
            if (u.str.is_string(evalArg, nextStr)) {
                flattenedString = flattenedString + nextStr;
            } else if (u.str.is_concat(evalArg)) {
                app * arg0 = to_app(evalArg->get_arg(0));
                app * arg1 = to_app(evalArg->get_arg(1));

                worklist.push(arg1);
                worklist.push(arg0);
            } else {
                TRACE("str", tout << "non-constant term in concat -- giving up." << std::endl;);
                constOK = false;
                break;
            }
        }
        if (constOK) {
            TRACE("str", tout << "flattened to \"" << flattenedString.encode().c_str() << "\"" << std::endl;);
            expr_ref constStr(mk_string(flattenedString), m);
            expr_ref axiom(ctx.mk_eq_atom(a_cat, constStr), m);
            assert_axiom(axiom);
        }
    }

    /*
     * Instantiate an axiom of the following form:
     * Length(Concat(x, y)) = Length(x) + Length(y)
     */
    void theory_str::instantiate_concat_axiom(enode * cat) {
        app * a_cat = cat->get_owner();
        SASSERT(u.str.is_concat(a_cat));

        ast_manager & m = get_manager();

        TRACE("str", tout << "instantiating concat axiom for " << mk_ismt2_pp(a_cat, m) << std::endl;);

        // build LHS
        expr_ref len_xy(m);
        len_xy = mk_strlen(a_cat);
        SASSERT(len_xy);

        // build RHS: start by extracting x and y from Concat(x, y)
        SASSERT(a_cat->get_num_args() == 2);
        app * a_x = to_app(a_cat->get_arg(0));
        app * a_y = to_app(a_cat->get_arg(1));

        expr_ref len_x(m);
        len_x = mk_strlen(a_x);
        SASSERT(len_x);

        expr_ref len_y(m);
        len_y = mk_strlen(a_y);
        SASSERT(len_y);

        // now build len_x + len_y
        expr_ref len_x_plus_len_y(m);
        len_x_plus_len_y = m_autil.mk_add(len_x, len_y);
        SASSERT(len_x_plus_len_y);

        // finally assert equality between the two subexpressions
        app * eq = m.mk_eq(len_xy, len_x_plus_len_y);
        SASSERT(eq);
        assert_axiom(eq);
    }

    /*
     * Add axioms that are true for any string variable:
     * 1. Length(x) >= 0
     * 2. Length(x) == 0 <=> x == ""
     * If the term is a string constant, we can assert something stronger:
     *    Length(x) == strlen(x)
     */
    void theory_str::instantiate_basic_string_axioms(enode * str) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        TRACE("str", tout << "set up basic string axioms on " << mk_pp(str->get_owner(), m) << std::endl;);

        {
            sort * a_sort = m.get_sort(str->get_owner());
            sort * str_sort = u.str.mk_string_sort();
            if (a_sort != str_sort) {
                TRACE("str", tout << "WARNING: not setting up string axioms on non-string term " << mk_pp(str->get_owner(), m) << std::endl;);
                return;
            }
        }

        // TESTING: attempt to avoid a crash here when a variable goes out of scope
        if (str->get_iscope_lvl() > ctx.get_scope_level()) {
            TRACE("str", tout << "WARNING: skipping axiom setup on out-of-scope string term" << std::endl;);
            return;
        }

        // generate a stronger axiom for constant strings
        app * a_str = str->get_owner();

        if (u.str.is_string(a_str)) {
            expr_ref len_str(m);
            len_str = mk_strlen(a_str);
            SASSERT(len_str);

            zstring strconst;
            u.str.is_string(str->get_owner(), strconst);
            TRACE("str", tout << "instantiating constant string axioms for \"" << strconst.encode().c_str() << "\"" << std::endl;);
            unsigned int l = strconst.length();
            expr_ref len(m_autil.mk_numeral(rational(l), true), m);

            literal lit(mk_eq(len_str, len, false));
            ctx.mark_as_relevant(lit);
            ctx.mk_th_axiom(get_id(), 1, &lit);
        } else {
            // build axiom 1: Length(a_str) >= 0
            {
                // build LHS
                expr_ref len_str(m);
                len_str = mk_strlen(a_str);
                SASSERT(len_str);
                // build RHS
                expr_ref zero(m);
                zero = m_autil.mk_numeral(rational(0), true);
                SASSERT(zero);
                // build LHS >= RHS and assert
                app * lhs_ge_rhs = m_autil.mk_ge(len_str, zero);
                SASSERT(lhs_ge_rhs);
                TRACE("str", tout << "string axiom 1: " << mk_ismt2_pp(lhs_ge_rhs, m) << std::endl;);
                assert_axiom(lhs_ge_rhs);
            }

            // build axiom 2: Length(a_str) == 0 <=> a_str == ""
            {
                // build LHS of iff
                expr_ref len_str(m);
                len_str = mk_strlen(a_str);
                SASSERT(len_str);
                expr_ref zero(m);
                zero = m_autil.mk_numeral(rational(0), true);
                SASSERT(zero);
                expr_ref lhs(m);
                lhs = ctx.mk_eq_atom(len_str, zero);
                SASSERT(lhs);
                // build RHS of iff
                expr_ref empty_str(m);
                empty_str = mk_string("");
                SASSERT(empty_str);
                expr_ref rhs(m);
                rhs = ctx.mk_eq_atom(a_str, empty_str);
                SASSERT(rhs);
                // build LHS <=> RHS and assert
                TRACE("str", tout << "string axiom 2: " << mk_ismt2_pp(lhs, m) << " <=> " << mk_ismt2_pp(rhs, m) << std::endl;);
                literal l(mk_eq(lhs, rhs, true));
                ctx.mark_as_relevant(l);
                ctx.mk_th_axiom(get_id(), 1, &l);
            }

        }
    }

    /*
     * Add an axiom of the form:
     * (lhs == rhs) -> ( Length(lhs) == Length(rhs) )
     */
    void theory_str::instantiate_str_eq_length_axiom(enode * lhs, enode * rhs) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * a_lhs = lhs->get_owner();
        app * a_rhs = rhs->get_owner();

        // build premise: (lhs == rhs)
        expr_ref premise(ctx.mk_eq_atom(a_lhs, a_rhs), m);

        // build conclusion: ( Length(lhs) == Length(rhs) )
        expr_ref len_lhs(mk_strlen(a_lhs), m);
        SASSERT(len_lhs);
        expr_ref len_rhs(mk_strlen(a_rhs), m);
        SASSERT(len_rhs);
        expr_ref conclusion(ctx.mk_eq_atom(len_lhs, len_rhs), m);

        TRACE("str", tout << "string-eq length-eq axiom: "
              << mk_ismt2_pp(premise, m) << " -> " << mk_ismt2_pp(conclusion, m) << std::endl;);
        assert_implication(premise, conclusion);
    }

    void theory_str::instantiate_axiom_CharAt(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        expr* arg0, *arg1;
        app * expr = e->get_owner();
        if (axiomatized_terms.contains(expr)) {
            TRACE("str", tout << "already set up CharAt axiom for " << mk_pp(expr, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(expr);
        VERIFY(u.str.is_at(expr, arg0, arg1));

        TRACE("str", tout << "instantiate CharAt axiom for " << mk_pp(expr, m) << std::endl;);

        expr_ref ts0(mk_str_var("ts0"), m);
        expr_ref ts1(mk_str_var("ts1"), m);
        expr_ref ts2(mk_str_var("ts2"), m);

        expr_ref cond(m.mk_and(
                          m_autil.mk_ge(arg1, mk_int(0)),
                          m_autil.mk_lt(arg1, mk_strlen(arg0))), m);

        expr_ref_vector and_item(m);
        and_item.push_back(ctx.mk_eq_atom(arg0, mk_concat(ts0, mk_concat(ts1, ts2))));
        and_item.push_back(ctx.mk_eq_atom(arg1, mk_strlen(ts0)));
        and_item.push_back(ctx.mk_eq_atom(mk_strlen(ts1), mk_int(1)));

        expr_ref thenBranch(::mk_and(and_item));
        expr_ref elseBranch(ctx.mk_eq_atom(ts1, mk_string("")), m);
        expr_ref axiom(m.mk_ite(cond, thenBranch, elseBranch), m);
        expr_ref reductionVar(ctx.mk_eq_atom(expr, ts1), m);
        expr_ref finalAxiom(m.mk_and(axiom, reductionVar), m);
        get_context().get_rewriter()(finalAxiom);
        assert_axiom(finalAxiom);
    }

    void theory_str::instantiate_axiom_prefixof(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * expr = e->get_owner();
        if (axiomatized_terms.contains(expr)) {
            TRACE("str", tout << "already set up prefixof axiom for " << mk_pp(expr, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(expr);

        TRACE("str", tout << "instantiate prefixof axiom for " << mk_pp(expr, m) << std::endl;);

        expr_ref ts0(mk_str_var("ts0"), m);
        expr_ref ts1(mk_str_var("ts1"), m);

        expr_ref_vector innerItems(m);
        innerItems.push_back(ctx.mk_eq_atom(expr->get_arg(1), mk_concat(ts0, ts1)));
        innerItems.push_back(ctx.mk_eq_atom(mk_strlen(ts0), mk_strlen(expr->get_arg(0))));
        innerItems.push_back(m.mk_ite(ctx.mk_eq_atom(ts0, expr->get_arg(0)), expr, mk_not(m, expr)));
        expr_ref then1(m.mk_and(innerItems.size(), innerItems.c_ptr()), m);
        SASSERT(then1);

        // the top-level condition is Length(arg0) >= Length(arg1)
        expr_ref topLevelCond(
            m_autil.mk_ge(
                m_autil.mk_add(
                    mk_strlen(expr->get_arg(1)), m_autil.mk_mul(mk_int(-1), mk_strlen(expr->get_arg(0)))),
                mk_int(0))
            , m);
        SASSERT(topLevelCond);

        expr_ref finalAxiom(m.mk_ite(topLevelCond, then1, mk_not(m, expr)), m);
        SASSERT(finalAxiom);
        assert_axiom(finalAxiom);
    }

    void theory_str::instantiate_axiom_suffixof(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * expr = e->get_owner();
        if (axiomatized_terms.contains(expr)) {
            TRACE("str", tout << "already set up suffixof axiom for " << mk_pp(expr, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(expr);

        TRACE("str", tout << "instantiate suffixof axiom for " << mk_pp(expr, m) << std::endl;);

        expr_ref ts0(mk_str_var("ts0"), m);
        expr_ref ts1(mk_str_var("ts1"), m);

        expr_ref_vector innerItems(m);
        innerItems.push_back(ctx.mk_eq_atom(expr->get_arg(1), mk_concat(ts0, ts1)));
        innerItems.push_back(ctx.mk_eq_atom(mk_strlen(ts1), mk_strlen(expr->get_arg(0))));
        innerItems.push_back(m.mk_ite(ctx.mk_eq_atom(ts1, expr->get_arg(0)), expr, mk_not(m, expr)));
        expr_ref then1(m.mk_and(innerItems.size(), innerItems.c_ptr()), m);
        SASSERT(then1);

        // the top-level condition is Length(arg0) >= Length(arg1)
        expr_ref topLevelCond(
            m_autil.mk_ge(
                m_autil.mk_add(
                    mk_strlen(expr->get_arg(1)), m_autil.mk_mul(mk_int(-1), mk_strlen(expr->get_arg(0)))),
                mk_int(0))
            , m);
        SASSERT(topLevelCond);

        expr_ref finalAxiom(m.mk_ite(topLevelCond, then1, mk_not(m, expr)), m);
        SASSERT(finalAxiom);
        assert_axiom(finalAxiom);
    }

    void theory_str::instantiate_axiom_Contains(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * ex = e->get_owner();
        if (axiomatized_terms.contains(ex)) {
            TRACE("str", tout << "already set up Contains axiom for " << mk_pp(ex, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(ex);

        // quick path, because this is necessary due to rewriter behaviour
        // at minimum it should fix z3str/concat-006.smt2
        zstring haystackStr, needleStr;
        if (u.str.is_string(ex->get_arg(0), haystackStr) && u.str.is_string(ex->get_arg(1), needleStr)) {
            TRACE("str", tout << "eval constant Contains term " << mk_pp(ex, m) << std::endl;);
            if (haystackStr.contains(needleStr)) {
                assert_axiom(ex);
            } else {
                assert_axiom(mk_not(m, ex));
            }
            return;
        }

        { // register Contains()
            expr * str = ex->get_arg(0);
            expr * substr = ex->get_arg(1);
            contains_map.push_back(ex);
            std::pair<expr*, expr*> key = std::pair<expr*, expr*>(str, substr);
            contain_pair_bool_map.insert(str, substr, ex);
            if (!contain_pair_idx_map.contains(str)) {
                contain_pair_idx_map.insert(str, std::set<std::pair<expr*, expr*>>());
            }
            if (!contain_pair_idx_map.contains(substr)) {
                contain_pair_idx_map.insert(substr, std::set<std::pair<expr*, expr*>>());
            }
            contain_pair_idx_map[str].insert(key);
            contain_pair_idx_map[substr].insert(key);
        }

        TRACE("str", tout << "instantiate Contains axiom for " << mk_pp(ex, m) << std::endl;);

        expr_ref ts0(mk_str_var("ts0"), m);
        expr_ref ts1(mk_str_var("ts1"), m);

        expr_ref breakdownAssert(ctx.mk_eq_atom(ex, ctx.mk_eq_atom(ex->get_arg(0), mk_concat(ts0, mk_concat(ex->get_arg(1), ts1)))), m);
        SASSERT(breakdownAssert);
        assert_axiom(breakdownAssert);
    }

    void theory_str::instantiate_axiom_Indexof(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * ex = e->get_owner();
        if (axiomatized_terms.contains(ex)) {
            TRACE("str", tout << "already set up str.indexof axiom for " << mk_pp(ex, m) << std::endl;);
            return;
        }
        SASSERT(ex->get_num_args() == 3);
        // if the third argument is exactly the integer 0, we can use this "simple" indexof;
        // otherwise, we call the "extended" version
        expr * startingPosition = ex->get_arg(2);
        rational startingInteger;
        if (!m_autil.is_numeral(startingPosition, startingInteger) || !startingInteger.is_zero()) {
            // "extended" indexof term with prefix
            instantiate_axiom_Indexof_extended(e);
            return;
        }
        axiomatized_terms.insert(ex);

        TRACE("str", tout << "instantiate str.indexof axiom for " << mk_pp(ex, m) << std::endl;);

        expr_ref x1(mk_str_var("x1"), m);
        expr_ref x2(mk_str_var("x2"), m);
        expr_ref indexAst(mk_int_var("index"), m);

        expr_ref condAst(mk_contains(ex->get_arg(0), ex->get_arg(1)), m);
        SASSERT(condAst);

        // -----------------------
        // true branch
        expr_ref_vector thenItems(m);
        //  args[0] = x1 . args[1] . x2
        thenItems.push_back(ctx.mk_eq_atom(ex->get_arg(0), mk_concat(x1, mk_concat(ex->get_arg(1), x2))));
        //  indexAst = |x1|
        thenItems.push_back(ctx.mk_eq_atom(indexAst, mk_strlen(x1)));
        //     args[0]  = x3 . x4
        //  /\ |x3| = |x1| + |args[1]| - 1
        //  /\ ! contains(x3, args[1])
        expr_ref x3(mk_str_var("x3"), m);
        expr_ref x4(mk_str_var("x4"), m);
        expr_ref tmpLen(m_autil.mk_add(indexAst, mk_strlen(ex->get_arg(1)), mk_int(-1)), m);
        SASSERT(tmpLen);
        thenItems.push_back(ctx.mk_eq_atom(ex->get_arg(0), mk_concat(x3, x4)));
        thenItems.push_back(ctx.mk_eq_atom(mk_strlen(x3), tmpLen));
        thenItems.push_back(mk_not(m, mk_contains(x3, ex->get_arg(1))));
        expr_ref thenBranch(m.mk_and(thenItems.size(), thenItems.c_ptr()), m);
        SASSERT(thenBranch);

        // -----------------------
        // false branch
        expr_ref elseBranch(ctx.mk_eq_atom(indexAst, mk_int(-1)), m);
        SASSERT(elseBranch);

        expr_ref breakdownAssert(m.mk_ite(condAst, thenBranch, elseBranch), m);
        SASSERT(breakdownAssert);

        expr_ref reduceToIndex(ctx.mk_eq_atom(ex, indexAst), m);
        SASSERT(reduceToIndex);

        expr_ref finalAxiom(m.mk_and(breakdownAssert, reduceToIndex), m);
        SASSERT(finalAxiom);
        assert_axiom(finalAxiom);

        {
            // heuristic: integrate with str.contains information
            // (but don't introduce it if it isn't already in the instance)
            expr_ref haystack(ex->get_arg(0), m), needle(ex->get_arg(1), m), startIdx(ex->get_arg(2), m);
            expr_ref zeroAst(mk_int(0), m);
            // (H contains N) <==> (H indexof N, i) >= 0
            expr_ref premise(u.str.mk_contains(haystack, needle), m);
            ctx.internalize(premise, false);
            expr_ref conclusion(m_autil.mk_ge(ex, zeroAst), m);
            expr_ref containsAxiom(ctx.mk_eq_atom(premise, conclusion), m);
            SASSERT(containsAxiom);

            // we can't assert this during init_search as it breaks an invariant if the instance becomes inconsistent
            //m_delayed_axiom_setup_terms.push_back(containsAxiom);
        }
    }

    void theory_str::instantiate_axiom_Indexof_extended(enode * _e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * e = _e->get_owner();
        if (axiomatized_terms.contains(e)) {
            TRACE("str", tout << "already set up extended str.indexof axiom for " << mk_pp(e, m) << std::endl;);
            return;
        }
        SASSERT(e->get_num_args() == 3);
        axiomatized_terms.insert(e);

        TRACE("str", tout << "instantiate extended str.indexof axiom for " << mk_pp(e, m) << std::endl;);

        // str.indexof(H, N, i):
        // i < 0 --> -1
        // i == 0 --> str.indexof(H, N, 0)
        // i >= len(H) --> -1
        // 0 < i < len(H) -->
        //     H = hd ++ tl
        //     len(hd) = i
        //     str.indexof(tl, N, 0)

        expr * H; // "haystack"
        expr * N; // "needle"
        expr * i; // start index
        u.str.is_index(e, H, N, i);

        expr_ref minus_one(m_autil.mk_numeral(rational::minus_one(), true), m);
        expr_ref zero(m_autil.mk_numeral(rational::zero(), true), m);

        // case split

        // case 1: i < 0
        {
            expr_ref premise(m_autil.mk_le(i, minus_one), m);
            expr_ref conclusion(ctx.mk_eq_atom(e, minus_one), m);
            assert_implication(premise, conclusion);
        }

        // case 2: i = 0
        {
            expr_ref premise(ctx.mk_eq_atom(i, zero), m);
            // reduction to simpler case
            expr_ref conclusion(ctx.mk_eq_atom(e, mk_indexof(H, N)), m);
            assert_implication(premise, conclusion);
        }
        // case 3: i >= len(H)
        {
            //expr_ref _premise(m_autil.mk_ge(i, mk_strlen(H)), m);
            //expr_ref premise(_premise);
            //th_rewriter rw(m);
            //rw(premise);
            expr_ref premise(m_autil.mk_ge(m_autil.mk_add(i, m_autil.mk_mul(minus_one, mk_strlen(H))), zero), m);
            expr_ref conclusion(ctx.mk_eq_atom(e, minus_one), m);
            assert_implication(premise, conclusion);
        }
        // case 4: 0 < i < len(H)
        {
            expr_ref premise1(m_autil.mk_gt(i, zero), m);
            //expr_ref premise2(m_autil.mk_lt(i, mk_strlen(H)), m);
            expr_ref premise2(m.mk_not(m_autil.mk_ge(m_autil.mk_add(i, m_autil.mk_mul(minus_one, mk_strlen(H))), zero)), m);
            expr_ref _premise(m.mk_and(premise1, premise2), m);
            expr_ref premise(_premise);
            th_rewriter rw(m);
            rw(premise);

            expr_ref hd(mk_str_var("hd"), m);
            expr_ref tl(mk_str_var("tl"), m);

            expr_ref_vector conclusion_terms(m);
            conclusion_terms.push_back(ctx.mk_eq_atom(H, mk_concat(hd, tl)));
            conclusion_terms.push_back(ctx.mk_eq_atom(mk_strlen(hd), i));
            conclusion_terms.push_back(ctx.mk_eq_atom(e, mk_indexof(tl, N)));

            expr_ref conclusion(mk_and(conclusion_terms), m);
            assert_implication(premise, conclusion);
        }

        {
            // heuristic: integrate with str.contains information
            // (but don't introduce it if it isn't already in the instance)
            // (H contains N) <==> (H indexof N, i) >= 0
            expr_ref premise(u.str.mk_contains(H, N), m);
            ctx.internalize(premise, false);
            expr_ref conclusion(m_autil.mk_ge(e, zero), m);
            expr_ref containsAxiom(ctx.mk_eq_atom(premise, conclusion), m);
            SASSERT(containsAxiom);
            // we can't assert this during init_search as it breaks an invariant if the instance becomes inconsistent
            m_delayed_assertions_todo.push_back(containsAxiom);
        }
    }

    void theory_str::instantiate_axiom_LastIndexof(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * expr = e->get_owner();
        if (axiomatized_terms.contains(expr)) {
            TRACE("str", tout << "already set up LastIndexof axiom for " << mk_pp(expr, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(expr);

        TRACE("str", tout << "instantiate LastIndexof axiom for " << mk_pp(expr, m) << std::endl;);

        expr_ref x1(mk_str_var("x1"), m);
        expr_ref x2(mk_str_var("x2"), m);
        expr_ref indexAst(mk_int_var("index"), m);
        expr_ref_vector items(m);

        // args[0] = x1 . args[1] . x2
        expr_ref eq1(ctx.mk_eq_atom(expr->get_arg(0), mk_concat(x1, mk_concat(expr->get_arg(1), x2))), m);
        expr_ref arg0HasArg1(mk_contains(expr->get_arg(0), expr->get_arg(1)), m);  // arg0HasArg1 = Contains(args[0], args[1])
        items.push_back(ctx.mk_eq_atom(arg0HasArg1, eq1));


        expr_ref condAst(arg0HasArg1, m);
        //----------------------------
        // true branch
        expr_ref_vector thenItems(m);
        thenItems.push_back(m_autil.mk_ge(indexAst, mk_int(0)));
        //  args[0] = x1 . args[1] . x2
        //  x1 doesn't contain args[1]
        thenItems.push_back(mk_not(m, mk_contains(x2, expr->get_arg(1))));
        thenItems.push_back(ctx.mk_eq_atom(indexAst, mk_strlen(x1)));

        bool canSkip = false;
        zstring arg1Str;
        if (u.str.is_string(expr->get_arg(1), arg1Str)) {
            if (arg1Str.length() == 1) {
                canSkip = true;
            }
        }

        if (!canSkip) {
            // args[0]  = x3 . x4 /\ |x3| = |x1| + 1 /\ ! contains(x4, args[1])
            expr_ref x3(mk_str_var("x3"), m);
            expr_ref x4(mk_str_var("x4"), m);
            expr_ref tmpLen(m_autil.mk_add(indexAst, mk_int(1)), m);
            thenItems.push_back(ctx.mk_eq_atom(expr->get_arg(0), mk_concat(x3, x4)));
            thenItems.push_back(ctx.mk_eq_atom(mk_strlen(x3), tmpLen));
            thenItems.push_back(mk_not(m, mk_contains(x4, expr->get_arg(1))));
        }
        //----------------------------
        // else branch
        expr_ref_vector elseItems(m);
        elseItems.push_back(ctx.mk_eq_atom(indexAst, mk_int(-1)));

        items.push_back(m.mk_ite(condAst, m.mk_and(thenItems.size(), thenItems.c_ptr()), m.mk_and(elseItems.size(), elseItems.c_ptr())));

        expr_ref breakdownAssert(m.mk_and(items.size(), items.c_ptr()), m);
        SASSERT(breakdownAssert);

        expr_ref reduceToIndex(ctx.mk_eq_atom(expr, indexAst), m);
        SASSERT(reduceToIndex);

        expr_ref finalAxiom(m.mk_and(breakdownAssert, reduceToIndex), m);
        SASSERT(finalAxiom);
        assert_axiom(finalAxiom);
    }

    void theory_str::instantiate_axiom_Substr(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        expr* substrBase = nullptr;
        expr* substrPos = nullptr;
        expr* substrLen = nullptr;

        app * expr = e->get_owner();
        if (axiomatized_terms.contains(expr)) {
            TRACE("str", tout << "already set up Substr axiom for " << mk_pp(expr, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(expr);

        TRACE("str", tout << "instantiate Substr axiom for " << mk_pp(expr, m) << std::endl;);

        VERIFY(u.str.is_extract(expr, substrBase, substrPos, substrLen));

        expr_ref zero(m_autil.mk_numeral(rational::zero(), true), m);
        expr_ref minusOne(m_autil.mk_numeral(rational::minus_one(), true), m);
        SASSERT(zero);
        SASSERT(minusOne);

        expr_ref_vector argumentsValid_terms(m);
        // pos >= 0
        argumentsValid_terms.push_back(m_autil.mk_ge(substrPos, zero));
        // pos < strlen(base)
        // --> pos + -1*strlen(base) < 0
        argumentsValid_terms.push_back(mk_not(m, m_autil.mk_ge(
                                                    m_autil.mk_add(substrPos, m_autil.mk_mul(minusOne, mk_strlen(substrBase))),
                                                    zero)));

        // len >= 0
        argumentsValid_terms.push_back(m_autil.mk_ge(substrLen, zero));


        // (pos+len) >= strlen(base)
        // --> pos + len + -1*strlen(base) >= 0
        expr_ref lenOutOfBounds(m_autil.mk_ge(
                                    m_autil.mk_add(substrPos, substrLen, m_autil.mk_mul(minusOne, mk_strlen(substrBase))),
                                    zero), m);
        expr_ref argumentsValid = mk_and(argumentsValid_terms);

        // Case 1: pos < 0 or pos >= strlen(base) or len < 0
        // ==> (Substr ...) = ""
        expr_ref case1_premise(m.mk_not(argumentsValid), m);
        expr_ref case1_conclusion(ctx.mk_eq_atom(expr, mk_string("")), m);
        expr_ref case1(m.mk_implies(case1_premise, case1_conclusion), m);

        // Case 2: (pos >= 0 and pos < strlen(base) and len >= 0) and (pos+len) >= strlen(base)
        // ==> base = t0.t1 AND len(t0) = pos AND (Substr ...) = t1
        expr_ref t0(mk_str_var("t0"), m);
        expr_ref t1(mk_str_var("t1"), m);
        expr_ref case2_conclusion(m.mk_and(
                                      ctx.mk_eq_atom(substrBase, mk_concat(t0,t1)),
                                      ctx.mk_eq_atom(mk_strlen(t0), substrPos),
                                      ctx.mk_eq_atom(expr, t1)), m);
        expr_ref case2(m.mk_implies(m.mk_and(argumentsValid, lenOutOfBounds), case2_conclusion), m);

        // Case 3: (pos >= 0 and pos < strlen(base) and len >= 0) and (pos+len) < strlen(base)
        // ==> base = t2.t3.t4 AND len(t2) = pos AND len(t3) = len AND (Substr ...) = t3

        expr_ref t2(mk_str_var("t2"), m);
        expr_ref t3(mk_str_var("t3"), m);
        expr_ref t4(mk_str_var("t4"), m);
        expr_ref_vector case3_conclusion_terms(m);
        case3_conclusion_terms.push_back(ctx.mk_eq_atom(substrBase, mk_concat(t2, mk_concat(t3, t4))));
        case3_conclusion_terms.push_back(ctx.mk_eq_atom(mk_strlen(t2), substrPos));
        case3_conclusion_terms.push_back(ctx.mk_eq_atom(mk_strlen(t3), substrLen));
        case3_conclusion_terms.push_back(ctx.mk_eq_atom(expr, t3));
        expr_ref case3_conclusion(mk_and(case3_conclusion_terms), m);
        expr_ref case3(m.mk_implies(m.mk_and(argumentsValid, m.mk_not(lenOutOfBounds)), case3_conclusion), m);

        {
            th_rewriter rw(m);

            expr_ref case1_rw(case1, m);
            rw(case1_rw);
            assert_axiom(case1_rw);

            expr_ref case2_rw(case2, m);
            rw(case2_rw);
            assert_axiom(case2_rw);

            expr_ref case3_rw(case3, m);
            rw(case3_rw);
            assert_axiom(case3_rw);
        }
    }

    void theory_str::instantiate_axiom_Replace(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * expr = e->get_owner();
        if (axiomatized_terms.contains(expr)) {
            TRACE("str", tout << "already set up Replace axiom for " << mk_pp(expr, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(expr);

        TRACE("str", tout << "instantiate Replace axiom for " << mk_pp(expr, m) << std::endl;);

        expr_ref x1(mk_str_var("x1"), m);
        expr_ref x2(mk_str_var("x2"), m);
        expr_ref i1(mk_int_var("i1"), m);
        expr_ref result(mk_str_var("result"), m);

        // condAst = Contains(args[0], args[1])
        expr_ref condAst(mk_contains(expr->get_arg(0), expr->get_arg(1)), m);
        // -----------------------
        // true branch
        expr_ref_vector thenItems(m);
        //  args[0] = x1 . args[1] . x2
        thenItems.push_back(ctx.mk_eq_atom(expr->get_arg(0), mk_concat(x1, mk_concat(expr->get_arg(1), x2))));
        //  i1 = |x1|
        thenItems.push_back(ctx.mk_eq_atom(i1, mk_strlen(x1)));
        //  args[0]  = x3 . x4 /\ |x3| = |x1| + |args[1]| - 1 /\ ! contains(x3, args[1])
        expr_ref x3(mk_str_var("x3"), m);
        expr_ref x4(mk_str_var("x4"), m);
        expr_ref tmpLen(m_autil.mk_add(i1, mk_strlen(expr->get_arg(1)), mk_int(-1)), m);
        thenItems.push_back(ctx.mk_eq_atom(expr->get_arg(0), mk_concat(x3, x4)));
        thenItems.push_back(ctx.mk_eq_atom(mk_strlen(x3), tmpLen));
        thenItems.push_back(mk_not(m, mk_contains(x3, expr->get_arg(1))));
        thenItems.push_back(ctx.mk_eq_atom(result, mk_concat(x1, mk_concat(expr->get_arg(2), x2))));
        // -----------------------
        // false branch
        expr_ref elseBranch(ctx.mk_eq_atom(result, expr->get_arg(0)), m);

        th_rewriter rw(m);

        expr_ref breakdownAssert(m.mk_ite(condAst, m.mk_and(thenItems.size(), thenItems.c_ptr()), elseBranch), m);
        expr_ref breakdownAssert_rw(breakdownAssert, m);
        rw(breakdownAssert_rw);
        assert_axiom(breakdownAssert_rw);

        expr_ref reduceToResult(ctx.mk_eq_atom(expr, result), m);
        expr_ref reduceToResult_rw(reduceToResult, m);
        rw(reduceToResult_rw);
        assert_axiom(reduceToResult_rw);
    }

    void theory_str::instantiate_axiom_str_to_int(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * ex = e->get_owner();
        if (axiomatized_terms.contains(ex)) {
            TRACE("str", tout << "already set up str.to-int axiom for " << mk_pp(ex, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(ex);

        TRACE("str", tout << "instantiate str.to-int axiom for " << mk_pp(ex, m) << std::endl;);

        // let expr = (str.to-int S)
        // axiom 1: expr >= -1
        // axiom 2: expr = 0 <==> S = "0"
        // axiom 3: expr >= 1 ==> len(S) > 0 AND S[0] != "0"

        expr * S = ex->get_arg(0);
        {
            expr_ref axiom1(m_autil.mk_ge(ex, m_autil.mk_numeral(rational::minus_one(), true)), m);
            SASSERT(axiom1);
            assert_axiom(axiom1);
        }

        {
            expr_ref lhs(ctx.mk_eq_atom(ex, m_autil.mk_numeral(rational::zero(), true)), m);
            expr_ref rhs(ctx.mk_eq_atom(S, mk_string("0")), m);
            expr_ref axiom2(ctx.mk_eq_atom(lhs, rhs), m);
            SASSERT(axiom2);
            assert_axiom(axiom2);
        }

        {
            expr_ref premise(m_autil.mk_ge(ex, m_autil.mk_numeral(rational::one(), true)), m);
            // S >= 1 --> S in [1-9][0-9]*
            expr_ref re_positiveInteger(u.re.mk_concat(
                    u.re.mk_range(mk_string("1"), mk_string("9")),
                    u.re.mk_star(u.re.mk_range(mk_string("0"), mk_string("9")))), m);
            expr_ref conclusion(mk_RegexIn(S, re_positiveInteger), m);
            SASSERT(premise);
            SASSERT(conclusion);
            assert_implication(premise, conclusion);
        }
    }

    void theory_str::instantiate_axiom_int_to_str(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * ex = e->get_owner();
        if (axiomatized_terms.contains(ex)) {
            TRACE("str", tout << "already set up str.from-int axiom for " << mk_pp(ex, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(ex);

        TRACE("str", tout << "instantiate str.from-int axiom for " << mk_pp(ex, m) << std::endl;);

        // axiom 1: N < 0 <==> (str.from-int N) = ""
        expr * N = ex->get_arg(0);
        {
            expr_ref axiom1_lhs(mk_not(m, m_autil.mk_ge(N, m_autil.mk_numeral(rational::zero(), true))), m);
            expr_ref axiom1_rhs(ctx.mk_eq_atom(ex, mk_string("")), m);
            expr_ref axiom1(ctx.mk_eq_atom(axiom1_lhs, axiom1_rhs), m);
            SASSERT(axiom1);
            assert_axiom(axiom1);
        }
    }

    expr * theory_str::mk_RegexIn(expr * str, expr * regexp) {
        app * regexIn = u.re.mk_in_re(str, regexp);
        // immediately force internalization so that axiom setup does not fail
        get_context().internalize(regexIn, false);
        set_up_axioms(regexIn);
        return regexIn;
    }

    static zstring str2RegexStr(zstring str) {
        zstring res("");
        int len = str.length();
        for (int i = 0; i < len; i++) {
            char nc = str[i];
            // 12 special chars
            if (nc == '\\' || nc == '^' || nc == '$' || nc == '.' || nc == '|' || nc == '?'
                || nc == '*' || nc == '+' || nc == '(' || nc == ')' || nc == '[' || nc == '{') {
                res = res + zstring("\\");
            }
            char tmp[2] = {(char)str[i], '\0'};
            res = res + zstring(tmp);
        }
        return res;
    }

    zstring theory_str::get_std_regex_str(expr * regex) {
        app * a_regex = to_app(regex);
        if (u.re.is_to_re(a_regex)) {
            expr * regAst = a_regex->get_arg(0);
            zstring regAstVal;
            u.str.is_string(regAst, regAstVal);
            zstring regStr = str2RegexStr(regAstVal);
            return regStr;
        } else if (u.re.is_concat(a_regex)) {
            expr * reg1Ast = a_regex->get_arg(0);
            expr * reg2Ast = a_regex->get_arg(1);
            zstring reg1Str = get_std_regex_str(reg1Ast);
            zstring reg2Str = get_std_regex_str(reg2Ast);
            return zstring("(") + reg1Str + zstring(")(") + reg2Str + zstring(")");
        } else if (u.re.is_union(a_regex)) {
            expr * reg1Ast = a_regex->get_arg(0);
            expr * reg2Ast = a_regex->get_arg(1);
            zstring reg1Str = get_std_regex_str(reg1Ast);
            zstring reg2Str = get_std_regex_str(reg2Ast);
            return  zstring("(") + reg1Str + zstring(")|(") + reg2Str + zstring(")");
        } else if (u.re.is_star(a_regex)) {
            expr * reg1Ast = a_regex->get_arg(0);
            zstring reg1Str = get_std_regex_str(reg1Ast);
            return  zstring("(") + reg1Str + zstring(")*");
        } else if (u.re.is_range(a_regex)) {
            expr * range1 = a_regex->get_arg(0);
            expr * range2 = a_regex->get_arg(1);
            zstring range1val, range2val;
            u.str.is_string(range1, range1val);
            u.str.is_string(range2, range2val);
            return zstring("[") + range1val + zstring("-") + range2val + zstring("]");
        } else if (u.re.is_full_seq(a_regex)) {
            return zstring("(.*)");
        } else if (u.re.is_full_char(a_regex)) {
            return zstring("str.allchar");
        } else {
            TRACE("str", tout << "BUG: unrecognized regex term " << mk_pp(regex, get_manager()) << std::endl;);
            UNREACHABLE(); return zstring("");
        }
    }

    void theory_str::instantiate_axiom_RegexIn(enode * e) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        app * ex = e->get_owner();
        if (axiomatized_terms.contains(ex)) {
            TRACE("str", tout << "already set up RegexIn axiom for " << mk_pp(ex, m) << std::endl;);
            return;
        }
        axiomatized_terms.insert(ex);

        TRACE("str", tout << "instantiate RegexIn axiom for " << mk_pp(ex, m) << std::endl;);

        {
            zstring regexStr = get_std_regex_str(ex->get_arg(1));
            std::pair<expr*, zstring> key1(ex->get_arg(0), regexStr);
            // skip Z3str's map check, because we already check if we set up axioms on this term
            regex_in_bool_map[key1] = ex;
            if (!regex_in_var_reg_str_map.contains(ex->get_arg(0))) {
                regex_in_var_reg_str_map.insert(ex->get_arg(0), std::set<zstring>());
            }
            regex_in_var_reg_str_map[ex->get_arg(0)].insert(regexStr);
        }

        expr_ref str(ex->get_arg(0), m);
        app * regex = to_app(ex->get_arg(1));

        if (m_params.m_RegexAutomata) {
            regex_terms.insert(ex);
            if (!regex_terms_by_string.contains(str)) {
                regex_terms_by_string.insert(str, ptr_vector<expr>());
            }
            regex_terms_by_string[str].push_back(ex);
            // stop setting up axioms here, we do this differently
            return;
        }

        // quick reference for the following code:
        //  - ex: top-level regex membership term
        //  - str: string term appearing in ex
        //  - regex: regex term appearing in ex
        //  ex ::= (str.in.re str regex)

        if (u.re.is_to_re(regex)) {
            expr_ref rxStr(regex->get_arg(0), m);
            // want to assert 'expr IFF (str == rxStr)'
            expr_ref rhs(ctx.mk_eq_atom(str, rxStr), m);
            expr_ref finalAxiom(m.mk_iff(ex, rhs), m);
            SASSERT(finalAxiom);
            assert_axiom(finalAxiom);
            TRACE("str", tout << "set up Str2Reg: (RegexIn " << mk_pp(str, m) << " " << mk_pp(regex, m) << ")" << std::endl;);
        } else if (u.re.is_concat(regex)) {
            expr_ref var1(mk_regex_rep_var(), m);
            expr_ref var2(mk_regex_rep_var(), m);
            expr_ref rhs(mk_concat(var1, var2), m);
            expr_ref rx1(regex->get_arg(0), m);
            expr_ref rx2(regex->get_arg(1), m);
            expr_ref var1InRegex1(mk_RegexIn(var1, rx1), m);
            expr_ref var2InRegex2(mk_RegexIn(var2, rx2), m);

            expr_ref_vector items(m);
            items.push_back(var1InRegex1);
            items.push_back(var2InRegex2);
            items.push_back(ctx.mk_eq_atom(ex, ctx.mk_eq_atom(str, rhs)));

            expr_ref finalAxiom(mk_and(items), m);
            SASSERT(finalAxiom);
            assert_axiom(finalAxiom);
        } else if (u.re.is_union(regex)) {
            expr_ref var1(mk_regex_rep_var(), m);
            expr_ref var2(mk_regex_rep_var(), m);
            expr_ref orVar(m.mk_or(ctx.mk_eq_atom(str, var1), ctx.mk_eq_atom(str, var2)), m);
            expr_ref regex1(regex->get_arg(0), m);
            expr_ref regex2(regex->get_arg(1), m);
            expr_ref var1InRegex1(mk_RegexIn(var1, regex1), m);
            expr_ref var2InRegex2(mk_RegexIn(var2, regex2), m);
            expr_ref_vector items(m);
            items.push_back(var1InRegex1);
            items.push_back(var2InRegex2);
            items.push_back(ctx.mk_eq_atom(ex, orVar));
            assert_axiom(mk_and(items));
        } else if (u.re.is_star(regex)) {
            // slightly more complex due to the unrolling step.
            expr_ref regex1(regex->get_arg(0), m);
            expr_ref unrollCount(mk_unroll_bound_var(), m);
            expr_ref unrollFunc(mk_unroll(regex1, unrollCount), m);
            expr_ref_vector items(m);
            items.push_back(ctx.mk_eq_atom(ex, ctx.mk_eq_atom(str, unrollFunc)));
            items.push_back(ctx.mk_eq_atom(ctx.mk_eq_atom(unrollCount, mk_int(0)), ctx.mk_eq_atom(unrollFunc, mk_string(""))));
            expr_ref finalAxiom(mk_and(items), m);
            SASSERT(finalAxiom);
            assert_axiom(finalAxiom);
        } else if (u.re.is_range(regex)) {
            // (re.range "A" "Z") unfolds to (re.union "A" "B" ... "Z");
            // we rewrite to expr IFF (str = "A" or str = "B" or ... or str = "Z")
            expr_ref lo(regex->get_arg(0), m);
            expr_ref hi(regex->get_arg(1), m);
            zstring str_lo, str_hi;
            SASSERT(u.str.is_string(lo));
            SASSERT(u.str.is_string(hi));
            u.str.is_string(lo, str_lo);
            u.str.is_string(hi, str_hi);
            SASSERT(str_lo.length() == 1);
            SASSERT(str_hi.length() == 1);
            unsigned int c1 = str_lo[0];
            unsigned int c2 = str_hi[0];
            if (c1 > c2) {
                // exchange
                unsigned int tmp = c1;
                c1 = c2;
                c2 = tmp;
            }
            expr_ref_vector range_cases(m);
            for (unsigned int ch = c1; ch <= c2; ++ch) {
                zstring s_ch(ch);
                expr_ref rhs(ctx.mk_eq_atom(str, u.str.mk_string(s_ch)), m);
                range_cases.push_back(rhs);
            }
            expr_ref rhs(mk_or(range_cases), m);
            expr_ref finalAxiom(m.mk_iff(ex, rhs), m);
            SASSERT(finalAxiom);
            assert_axiom(finalAxiom);
        } else if (u.re.is_full_seq(regex)) {
            // trivially true for any string!
            assert_axiom(ex);
        } else if (u.re.is_full_char(regex)) {
            // any char = any string of length 1
            expr_ref rhs(ctx.mk_eq_atom(mk_strlen(str), mk_int(1)), m);
            expr_ref finalAxiom(m.mk_iff(ex, rhs), m);
            SASSERT(finalAxiom);
            assert_axiom(finalAxiom);
        } else {
            TRACE("str", tout << "ERROR: unknown regex expression " << mk_pp(regex, m) << "!" << std::endl;);
            NOT_IMPLEMENTED_YET();
        }
    }

    void theory_str::attach_new_th_var(enode * n) {
        context & ctx = get_context();
        theory_var v = mk_var(n);
        ctx.attach_th_var(n, this, v);
        TRACE("str", tout << "new theory var: " << mk_ismt2_pp(n->get_owner(), get_manager()) << " := v#" << v << std::endl;);
    }

    void theory_str::reset_eh() {
        TRACE("str", tout << "resetting" << std::endl;);
        m_trail_stack.reset();

        m_basicstr_axiom_todo.reset();
        m_str_eq_todo.reset();
        m_concat_axiom_todo.reset();
        pop_scope_eh(get_context().get_scope_level());
    }

    /*
     * Check equality among equivalence class members of LHS and RHS
     * to discover an incorrect LHS == RHS.
     * For example, if we have y2 == "str3"
     * and the equivalence classes are
     * { y2, (Concat ce m2) }
     * { "str3", (Concat abc x2) }
     * then y2 can't be equal to "str3".
     * Then add an assertion: (y2 == (Concat ce m2)) AND ("str3" == (Concat abc x2)) -> (y2 != "str3")
     */
    bool theory_str::new_eq_check(expr * lhs, expr * rhs) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        // skip this check if we defer consistency checking, as we can do it for every EQC in final check
        if (!opt_DeferEQCConsistencyCheck) {
            check_concat_len_in_eqc(lhs);
            check_concat_len_in_eqc(rhs);
        }

        // Now we iterate over all pairs of terms across both EQCs
        // and check whether we can show that any pair of distinct terms
        // cannot possibly be equal.
        // If that's the case, we assert an axiom to that effect and stop.

        expr * eqc_nn1 = lhs;
        do {
            expr * eqc_nn2 = rhs;
            do {
                TRACE("str", tout << "checking whether " << mk_pp(eqc_nn1, m) << " and " << mk_pp(eqc_nn2, m) << " can be equal" << std::endl;);
                // inconsistency check: value
                if (!can_two_nodes_eq(eqc_nn1, eqc_nn2)) {
                    TRACE("str", tout << "inconsistency detected: " << mk_pp(eqc_nn1, m) << " cannot be equal to " << mk_pp(eqc_nn2, m) << std::endl;);
                    expr_ref to_assert(mk_not(m, ctx.mk_eq_atom(eqc_nn1, eqc_nn2)), m);
                    assert_axiom(to_assert);
                    // this shouldn't use the integer theory at all, so we don't allow the option of quick-return
                    return false;
                }
                if (!check_length_consistency(eqc_nn1, eqc_nn2)) {
                    TRACE("str", tout << "inconsistency detected: " << mk_pp(eqc_nn1, m) << " and " << mk_pp(eqc_nn2, m) << " have inconsistent lengths" << std::endl;);
                    if (opt_NoQuickReturn_IntegerTheory){
                        TRACE("str", tout << "continuing in new_eq_check() due to opt_NoQuickReturn_IntegerTheory" << std::endl;);
                    } else {
                        return false;
                    }
                }
                eqc_nn2 = get_eqc_next(eqc_nn2);
            } while (eqc_nn2 != rhs);
            eqc_nn1 = get_eqc_next(eqc_nn1);
        } while (eqc_nn1 != lhs);

        if (!contains_map.empty()) {
            check_contain_in_new_eq(lhs, rhs);
        }

        if (!regex_in_bool_map.empty() && !m_params.m_RegexAutomata) {
            TRACE("str", tout << "checking regex consistency" << std::endl;);
            check_regex_in(lhs, rhs);
        }

        // okay, all checks here passed
        return true;
    }

    // support for user_smt_theory-style EQC handling

    app * theory_str::get_ast(theory_var v) {
        return get_enode(v)->get_owner();
    }

    theory_var theory_str::get_var(expr * n) const {
        if (!is_app(n)) {
            return null_theory_var;
        }
        context & ctx = get_context();
        if (ctx.e_internalized(to_app(n))) {
            enode * e = ctx.get_enode(to_app(n));
            return e->get_th_var(get_id());
        }
        return null_theory_var;
    }

    // simulate Z3_theory_get_eqc_next()
    expr * theory_str::get_eqc_next(expr * n) {
        theory_var v = get_var(n);
        if (v != null_theory_var) {
            theory_var r = m_find.next(v);
            return get_ast(r);
        }
        return n;
    }

    void theory_str::group_terms_by_eqc(expr * n, std::set<expr*> & concats, std::set<expr*> & vars, std::set<expr*> & consts) {
        expr * eqcNode = n;
        do {
            app * ast = to_app(eqcNode);
            if (u.str.is_concat(ast)) {
                expr * simConcat = simplify_concat(ast);
                if (simConcat != ast) {
                    if (u.str.is_concat(to_app(simConcat))) {
                        concats.insert(simConcat);
                    } else {
                        if (u.str.is_string(simConcat)) {
                            consts.insert(simConcat);
                        } else {
                            vars.insert(simConcat);
                        }
                    }
                } else {
                    concats.insert(simConcat);
                }
            } else if (u.str.is_string(ast)) {
                consts.insert(ast);
            } else {
                vars.insert(ast);
            }
            eqcNode = get_eqc_next(eqcNode);
        } while (eqcNode != n);
    }

    void theory_str::get_nodes_in_concat(expr * node, ptr_vector<expr> & nodeList) {
        app * a_node = to_app(node);
        if (!u.str.is_concat(a_node)) {
            nodeList.push_back(node);
            return;
        } else {
            SASSERT(a_node->get_num_args() == 2);
            expr * leftArg = a_node->get_arg(0);
            expr * rightArg = a_node->get_arg(1);
            get_nodes_in_concat(leftArg, nodeList);
            get_nodes_in_concat(rightArg, nodeList);
        }
    }

    // previously Concat() in strTheory.cpp
    // Evaluates the concatenation (n1 . n2) with respect to
    // the current equivalence classes of n1 and n2.
    // Returns a constant string expression representing this concatenation
    // if one can be determined, or NULL if this is not possible.
    expr * theory_str::eval_concat(expr * n1, expr * n2) {
        bool n1HasEqcValue = false;
        bool n2HasEqcValue = false;
        expr * v1 = get_eqc_value(n1, n1HasEqcValue);
        expr * v2 = get_eqc_value(n2, n2HasEqcValue);
        if (n1HasEqcValue && n2HasEqcValue) {
            zstring n1_str, n2_str;
            u.str.is_string(v1, n1_str);
            u.str.is_string(v2, n2_str);
            zstring result = n1_str + n2_str;
            return mk_string(result);
        } else if (n1HasEqcValue && !n2HasEqcValue) {
            zstring v1_str;
            u.str.is_string(v1, v1_str);
            if (v1_str.empty()) {
                return n2;
            }
        } else if (n2HasEqcValue && !n1HasEqcValue) {
            zstring v2_str;
            u.str.is_string(v2, v2_str);
            if (v2_str.empty()) {
                return n1;
            }
        }
        // give up
        return nullptr;
    }

    // trace code helper
    inline std::string rational_to_string_if_exists(const rational & x, bool x_exists) {
        if (x_exists) {
            return x.to_string();
        } else {
            return "?";
        }
    }

    /*
     * The inputs:
     *    ~ nn: non const node
     *    ~ eq_str: the equivalent constant string of nn
     *  Iterate the parent of all eqc nodes of nn, looking for:
     *    ~ concat node
     *  to see whether some concat nodes can be simplified.
     */
    void theory_str::simplify_parent(expr * nn, expr * eq_str) {
        ast_manager & m = get_manager();
        context & ctx = get_context();

        TRACE("str", tout << "simplifying parents of " << mk_ismt2_pp(nn, m)
              << " with respect to " << mk_ismt2_pp(eq_str, m) << std::endl;);

        ctx.internalize(nn, false);

        zstring eq_strValue;
        u.str.is_string(eq_str, eq_strValue);
        expr * n_eqNode = nn;
        do {
            enode * n_eq_enode = ctx.get_enode(n_eqNode);
            TRACE("str", tout << "considering all parents of " << mk_ismt2_pp(n_eqNode, m) << std::endl
                  << "associated n_eq_enode has " << n_eq_enode->get_num_parents() << " parents" << std::endl;);

            // the goal of this next bit is to avoid dereferencing a bogus e_parent in the following loop.
            // what I imagine is causing this bug is that, for example, we examine some parent, we add an axiom that involves it,
            // and the parent_it iterator becomes invalidated, because we indirectly modified the container that we're iterating over.

            enode_vector current_parents;
            for (enode_vector::const_iterator parent_it = n_eq_enode->begin_parents(); parent_it != n_eq_enode->end_parents(); parent_it++) {
                current_parents.insert(*parent_it);
            }

            for (enode_vector::iterator parent_it = current_parents.begin(); parent_it != current_parents.end(); ++parent_it) {
                enode * e_parent = *parent_it;
                SASSERT(e_parent != NULL);

                app * a_parent = e_parent->get_owner();
                TRACE("str", tout << "considering parent " << mk_ismt2_pp(a_parent, m) << std::endl;);

                if (u.str.is_concat(a_parent)) {
                    expr * arg0 = a_parent->get_arg(0);
                    expr * arg1 = a_parent->get_arg(1);

                    rational parentLen;
                    bool parentLen_exists = get_len_value(a_parent, parentLen);

                    if (arg0 == n_eq_enode->get_owner()) {
                        rational arg0Len, arg1Len;
                        bool arg0Len_exists = get_len_value(eq_str, arg0Len);
                        bool arg1Len_exists = get_len_value(arg1, arg1Len);

                        TRACE("str",
                              tout << "simplify_parent #1:" << std::endl
                              << "* parent = " << mk_ismt2_pp(a_parent, m) << std::endl
                              << "* |parent| = " << rational_to_string_if_exists(parentLen, parentLen_exists) << std::endl
                              << "* |arg0| = " << rational_to_string_if_exists(arg0Len, arg0Len_exists) << std::endl
                              << "* |arg1| = " << rational_to_string_if_exists(arg1Len, arg1Len_exists) << std::endl;
                              ); (void)arg0Len_exists;

                        if (parentLen_exists && !arg1Len_exists) {
                            TRACE("str", tout << "make up len for arg1" << std::endl;);
                            expr_ref implyL11(m.mk_and(ctx.mk_eq_atom(mk_strlen(a_parent), mk_int(parentLen)),
                                                       ctx.mk_eq_atom(mk_strlen(arg0), mk_int(arg0Len))), m);
                            rational makeUpLenArg1 = parentLen - arg0Len;
                            if (makeUpLenArg1.is_nonneg()) {
                                expr_ref implyR11(ctx.mk_eq_atom(mk_strlen(arg1), mk_int(makeUpLenArg1)), m);
                                assert_implication(implyL11, implyR11);
                            } else {
                                expr_ref neg(mk_not(m, implyL11), m);
                                assert_axiom(neg);
                            }
                        }

                        // (Concat n_eqNode arg1) /\ arg1 has eq const

                        expr * concatResult = eval_concat(eq_str, arg1);
                        if (concatResult != nullptr) {
                            bool arg1HasEqcValue = false;
                            expr * arg1Value = get_eqc_value(arg1, arg1HasEqcValue);
                            expr_ref implyL(m);
                            if (arg1 != arg1Value) {
                                expr_ref eq_ast1(m);
                                eq_ast1 = ctx.mk_eq_atom(n_eqNode, eq_str);
                                SASSERT(eq_ast1);

                                expr_ref eq_ast2(m);
                                eq_ast2 = ctx.mk_eq_atom(arg1, arg1Value);
                                SASSERT(eq_ast2);
                                implyL = m.mk_and(eq_ast1, eq_ast2);
                            } else {
                                implyL = ctx.mk_eq_atom(n_eqNode, eq_str);
                            }


                            if (!in_same_eqc(a_parent, concatResult)) {
                                expr_ref implyR(m);
                                implyR = ctx.mk_eq_atom(a_parent, concatResult);
                                SASSERT(implyR);

                                assert_implication(implyL, implyR);
                            }
                        } else if (u.str.is_concat(to_app(n_eqNode))) {
                            expr_ref simpleConcat(m);
                            simpleConcat = mk_concat(eq_str, arg1);
                            if (!in_same_eqc(a_parent, simpleConcat)) {
                                expr_ref implyL(m);
                                implyL = ctx.mk_eq_atom(n_eqNode, eq_str);
                                SASSERT(implyL);

                                expr_ref implyR(m);
                                implyR = ctx.mk_eq_atom(a_parent, simpleConcat);
                                SASSERT(implyR);
                                assert_implication(implyL, implyR);
                            }
                        }
                    } // if (arg0 == n_eq_enode->get_owner())

                    if (arg1 == n_eq_enode->get_owner()) {
                        rational arg0Len, arg1Len;
                        bool arg0Len_exists = get_len_value(arg0, arg0Len);
                        bool arg1Len_exists = get_len_value(eq_str, arg1Len);

                        TRACE("str",
                              tout << "simplify_parent #2:" << std::endl
                              << "* parent = " << mk_ismt2_pp(a_parent, m) << std::endl
                              << "* |parent| = " << rational_to_string_if_exists(parentLen, parentLen_exists) << std::endl
                              << "* |arg0| = " << rational_to_string_if_exists(arg0Len, arg0Len_exists) << std::endl
                              << "* |arg1| = " << rational_to_string_if_exists(arg1Len, arg1Len_exists) << std::endl;
                              ); (void)arg1Len_exists;

                        if (parentLen_exists && !arg0Len_exists) {
                            TRACE("str", tout << "make up len for arg0" << std::endl;);
                            expr_ref implyL11(m.mk_and(ctx.mk_eq_atom(mk_strlen(a_parent), mk_int(parentLen)),
                                                       ctx.mk_eq_atom(mk_strlen(arg1), mk_int(arg1Len))), m);
                            rational makeUpLenArg0 = parentLen - arg1Len;
                            if (makeUpLenArg0.is_nonneg()) {
                                expr_ref implyR11(ctx.mk_eq_atom(mk_strlen(arg0), mk_int(makeUpLenArg0)), m);
                                assert_implication(implyL11, implyR11);
                            } else {
                                expr_ref neg(mk_not(m, implyL11), m);
                                assert_axiom(neg);
                            }
                        }

                        // (Concat arg0 n_eqNode) /\ arg0 has eq const

                        expr * concatResult = eval_concat(arg0, eq_str);
                        if (concatResult != nullptr) {
                            bool arg0HasEqcValue = false;
                            expr * arg0Value = get_eqc_value(arg0, arg0HasEqcValue);
                            expr_ref implyL(m);
                            if (arg0 != arg0Value) {
                                expr_ref eq_ast1(m);
                                eq_ast1 = ctx.mk_eq_atom(n_eqNode, eq_str);
                                SASSERT(eq_ast1);
                                expr_ref eq_ast2(m);
                                eq_ast2 = ctx.mk_eq_atom(arg0, arg0Value);
                                SASSERT(eq_ast2);

                                implyL = m.mk_and(eq_ast1, eq_ast2);
                            } else {
                                implyL = ctx.mk_eq_atom(n_eqNode, eq_str);
                            }

                            if (!in_same_eqc(a_parent, concatResult)) {
                                expr_ref implyR(m);
                                implyR = ctx.mk_eq_atom(a_parent, concatResult);
                                SASSERT(implyR);

                                assert_implication(implyL, implyR);
                            }
                        } else if (u.str.is_concat(to_app(n_eqNode))) {
                            expr_ref simpleConcat(m);
                            simpleConcat = mk_concat(arg0, eq_str);
                            if (!in_same_eqc(a_parent, simpleConcat)) {
                                expr_ref implyL(m);
                                implyL = ctx.mk_eq_atom(n_eqNode, eq_str);
                                SASSERT(implyL);

                                expr_ref implyR(m);
                                implyR = ctx.mk_eq_atom(a_parent, simpleConcat);
                                SASSERT(implyR);
                                assert_implication(implyL, implyR);
                            }
                        }
                    } // if (arg1 == n_eq_enode->get_owner


                    //---------------------------------------------------------
                    // Case (2-1) begin: (Concat n_eqNode (Concat str var))
                    if (arg0 == n_eqNode && u.str.is_concat(to_app(arg1))) {
                        app * a_arg1 = to_app(arg1);
                        TRACE("str", tout << "simplify_parent #3" << std::endl;);
                        expr * r_concat_arg0 = a_arg1->get_arg(0);
                        if (u.str.is_string(r_concat_arg0)) {
                            expr * combined_str = eval_concat(eq_str, r_concat_arg0);
                            SASSERT(combined_str);
                            expr * r_concat_arg1 = a_arg1->get_arg(1);
                            expr_ref implyL(m);
                            implyL = ctx.mk_eq_atom(n_eqNode, eq_str);
                            expr * simplifiedAst = mk_concat(combined_str, r_concat_arg1);
                            if (!in_same_eqc(a_parent, simplifiedAst)) {
                                expr_ref implyR(m);
                                implyR = ctx.mk_eq_atom(a_parent, simplifiedAst);
                                assert_implication(implyL, implyR);
                            }
                        }
                    }
                    // Case (2-1) end: (Concat n_eqNode (Concat str var))
                    //---------------------------------------------------------


                    //---------------------------------------------------------
                    // Case (2-2) begin: (Concat (Concat var str) n_eqNode)
                    if (u.str.is_concat(to_app(arg0)) && arg1 == n_eqNode) {
                        app * a_arg0 = to_app(arg0);
                        TRACE("str", tout << "simplify_parent #4" << std::endl;);
                        expr * l_concat_arg1 = a_arg0->get_arg(1);
                        if (u.str.is_string(l_concat_arg1)) {
                            expr * combined_str = eval_concat(l_concat_arg1, eq_str);
                            SASSERT(combined_str);
                            expr * l_concat_arg0 = a_arg0->get_arg(0);
                            expr_ref implyL(m);
                            implyL = ctx.mk_eq_atom(n_eqNode, eq_str);
                            expr * simplifiedAst = mk_concat(l_concat_arg0, combined_str);
                            if (!in_same_eqc(a_parent, simplifiedAst)) {
                                expr_ref implyR(m);
                                implyR = ctx.mk_eq_atom(a_parent, simplifiedAst);
                                assert_implication(implyL, implyR);
                            }
                        }
                    }
                    // Case (2-2) end: (Concat (Concat var str) n_eqNode)
                    //---------------------------------------------------------

                    // Have to look up one more layer: if the parent of the concat is another concat
                    //-------------------------------------------------
                    // Case (3-1) begin: (Concat (Concat var n_eqNode) str )
                    if (arg1 == n_eqNode) {
                        for (enode_vector::iterator concat_parent_it = e_parent->begin_parents();
                             concat_parent_it != e_parent->end_parents(); concat_parent_it++) {
                            enode * e_concat_parent = *concat_parent_it;
                            app * concat_parent = e_concat_parent->get_owner();
                            if (u.str.is_concat(concat_parent)) {
                                expr * concat_parent_arg0 = concat_parent->get_arg(0);
                                expr * concat_parent_arg1 = concat_parent->get_arg(1);
                                if (concat_parent_arg0 == a_parent && u.str.is_string(concat_parent_arg1)) {
                                    TRACE("str", tout << "simplify_parent #5" << std::endl;);
                                    expr * combinedStr = eval_concat(eq_str, concat_parent_arg1);
                                    SASSERT(combinedStr);
                                    expr_ref implyL(m);
                                    implyL = ctx.mk_eq_atom(n_eqNode, eq_str);
                                    expr * simplifiedAst = mk_concat(arg0, combinedStr);
                                    if (!in_same_eqc(concat_parent, simplifiedAst)) {
                                        expr_ref implyR(m);
                                        implyR = ctx.mk_eq_atom(concat_parent, simplifiedAst);
                                        assert_implication(implyL, implyR);
                                    }
                                }
                            }
                        }
                    }
                    // Case (3-1) end: (Concat (Concat var n_eqNode) str )
                    // Case (3-2) begin: (Concat str (Concat n_eqNode var) )
                    if (arg0 == n_eqNode) {
                        for (enode_vector::iterator concat_parent_it = e_parent->begin_parents();
                             concat_parent_it != e_parent->end_parents(); concat_parent_it++) {
                            enode * e_concat_parent = *concat_parent_it;
                            app * concat_parent = e_concat_parent->get_owner();
                            if (u.str.is_concat(concat_parent)) {
                                expr * concat_parent_arg0 = concat_parent->get_arg(0);
                                expr * concat_parent_arg1 = concat_parent->get_arg(1);
                                if (concat_parent_arg1 == a_parent && u.str.is_string(concat_parent_arg0)) {
                                    TRACE("str", tout << "simplify_parent #6" << std::endl;);
                                    expr * combinedStr = eval_concat(concat_parent_arg0, eq_str);
                                    SASSERT(combinedStr);
                                    expr_ref implyL(m);
                                    implyL = ctx.mk_eq_atom(n_eqNode, eq_str);
                                    expr * simplifiedAst = mk_concat(combinedStr, arg1);
                                    if (!in_same_eqc(concat_parent, simplifiedAst)) {
                                        expr_ref implyR(m);
                                        implyR = ctx.mk_eq_atom(concat_parent, simplifiedAst);
                                        assert_implication(implyL, implyR);
                                    }
                                }
                            }
                        }
                    }
                    // Case (3-2) end: (Concat str (Concat n_eqNode var) )
                } // if is_concat(a_parent)
            } // for parent_it : n_eq_enode->begin_parents()


            // check next EQC member
            n_eqNode = get_eqc_next(n_eqNode);
        } while (n_eqNode != nn);
    }

    expr * theory_str::simplify_concat(expr * node) {
        ast_manager & m = get_manager();
        context & ctx = get_context();
        std::map<expr*, expr*> resolvedMap;
        ptr_vector<expr> argVec;
        get_nodes_in_concat(node, argVec);

        for (unsigned i = 0; i < argVec.size(); ++i) {
            bool vArgHasEqcValue = false;
            expr * vArg = get_eqc_value(argVec[i], vArgHasEqcValue);
            if (vArg != argVec[i]) {
                resolvedMap[argVec[i]] = vArg;
            }
        }

        if (resolvedMap.size() == 0) {
            // no simplification possible
            return node;
        } else {
            expr * resultAst = mk_string("");
            for (unsigned i = 0; i < argVec.size(); ++i) {
                bool vArgHasEqcValue = false;
                expr * vArg = get_eqc_value(argVec[i], vArgHasEqcValue);
                resultAst = mk_concat(resultAst, vArg);
            }
            TRACE("str", tout << mk_ismt2_pp(node, m) << " is simplified to " << mk_ismt2_pp(resultAst, m) << std::endl;);

            if (in_same_eqc(node, resultAst)) {
                TRACE("str", tout << "SKIP: both concats are already in the same equivalence class" << std::endl;);
            } else {
                expr_ref_vector items(m);
                int pos = 0;
                for (auto itor : resolvedMap) {
                    items.push_back(ctx.mk_eq_atom(itor.first, itor.second));
                    pos += 1;
                }
                expr_ref premise(mk_and(items), m);
                expr_ref conclusion(ctx.mk_eq_atom(node, resultAst), m);
                assert_implication(premise, conclusion);
            }
            return resultAst;
        }

    }

    // Modified signature of Z3str2's inferLenConcat().
    // Returns true iff nLen can be inferred by this method
    // (i.e. the equivalent of a len_exists flag in get_len_value()).

    bool theory_str::infer_len_concat(expr * n, rational & nLen) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        expr * arg0 = to_app(n)->get_arg(0);
        expr * arg1 = to_app(n)->get_arg(1);

        rational arg0_len, arg1_len;
        bool arg0_len_exists = get_len_value(arg0, arg0_len);
        bool arg1_len_exists = get_len_value(arg1, arg1_len);
        rational tmp_len;
        bool nLen_exists = get_len_value(n, tmp_len);

        if (arg0_len_exists && arg1_len_exists && !nLen_exists) {
            expr_ref_vector l_items(m);
            // if (mk_strlen(arg0) != mk_int(arg0_len)) {
            {
                l_items.push_back(ctx.mk_eq_atom(mk_strlen(arg0), mk_int(arg0_len)));
            }

            // if (mk_strlen(arg1) != mk_int(arg1_len)) {
            {
                l_items.push_back(ctx.mk_eq_atom(mk_strlen(arg1), mk_int(arg1_len)));
            }

            expr_ref axl(m.mk_and(l_items.size(), l_items.c_ptr()), m);
            rational nnLen = arg0_len + arg1_len;
            expr_ref axr(ctx.mk_eq_atom(mk_strlen(n), mk_int(nnLen)), m);
            TRACE("str", tout << "inferred (Length " << mk_pp(n, m) << ") = " << nnLen << std::endl;);
            assert_implication(axl, axr);
            nLen = nnLen;
            return true;
        } else {
            return false;
        }
    }

    void theory_str::infer_len_concat_arg(expr * n, rational len) {
        if (len.is_neg()) {
            return;
        }

        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr * arg0 = to_app(n)->get_arg(0);
        expr * arg1 = to_app(n)->get_arg(1);
        rational arg0_len, arg1_len;
        bool arg0_len_exists = get_len_value(arg0, arg0_len);
        bool arg1_len_exists = get_len_value(arg1, arg1_len);

        expr_ref_vector l_items(m);
        expr_ref axr(m);
        axr.reset();

        // if (mk_length(t, n) != mk_int(ctx, len)) {
        {
            l_items.push_back(ctx.mk_eq_atom(mk_strlen(n), mk_int(len)));
        }

        if (!arg0_len_exists && arg1_len_exists) {
            //if (mk_length(t, arg1) != mk_int(ctx, arg1_len)) {
            {
                l_items.push_back(ctx.mk_eq_atom(mk_strlen(arg1), mk_int(arg1_len)));
            }
            rational arg0Len = len - arg1_len;
            if (arg0Len.is_nonneg()) {
                axr = ctx.mk_eq_atom(mk_strlen(arg0), mk_int(arg0Len));
            } else {
                // could negate
            }
        } else if (arg0_len_exists && !arg1_len_exists) {
            //if (mk_length(t, arg0) != mk_int(ctx, arg0_len)) {
            {
                l_items.push_back(ctx.mk_eq_atom(mk_strlen(arg0), mk_int(arg0_len)));
            }
            rational arg1Len = len - arg0_len;
            if (arg1Len.is_nonneg()) {
                axr = ctx.mk_eq_atom(mk_strlen(arg1), mk_int(arg1Len));
            } else {
                // could negate
            }
        } else {

        }

        if (axr) {
            expr_ref axl(m.mk_and(l_items.size(), l_items.c_ptr()), m);
            assert_implication(axl, axr);
        }
    }

    void theory_str::infer_len_concat_equality(expr * nn1, expr * nn2) {
        rational nnLen;
        bool nnLen_exists = get_len_value(nn1, nnLen);
        if (!nnLen_exists) {
            nnLen_exists = get_len_value(nn2, nnLen);
        }

        // case 1:
        //    Known: a1_arg0 and a1_arg1
        //    Unknown: nn1

        if (u.str.is_concat(to_app(nn1))) {
            rational nn1ConcatLen;
            bool nn1ConcatLen_exists = infer_len_concat(nn1, nn1ConcatLen);
            if (nnLen_exists && nn1ConcatLen_exists) {
                nnLen = nn1ConcatLen;
            }
        }

        // case 2:
        //    Known: a1_arg0 and a1_arg1
        //    Unknown: nn1

        if (u.str.is_concat(to_app(nn2))) {
            rational nn2ConcatLen;
            bool nn2ConcatLen_exists = infer_len_concat(nn2, nn2ConcatLen);
            if (nnLen_exists && nn2ConcatLen_exists) {
                nnLen = nn2ConcatLen;
            }
        }

        if (nnLen_exists) {
            if (u.str.is_concat(to_app(nn1))) {
                infer_len_concat_arg(nn1, nnLen);
            }
            if (u.str.is_concat(to_app(nn2))) {
                infer_len_concat_arg(nn2, nnLen);
            }
        }

        /*
          if (isConcatFunc(t, nn2)) {
          int nn2ConcatLen = inferLenConcat(t, nn2);
          if (nnLen == -1 && nn2ConcatLen != -1)
          nnLen = nn2ConcatLen;
          }

          if (nnLen != -1) {
          if (isConcatFunc(t, nn1)) {
          inferLenConcatArg(t, nn1, nnLen);
          }
          if (isConcatFunc(t, nn2)) {
          inferLenConcatArg(t, nn2, nnLen);
          }
          }
        */
    }

    void theory_str::add_theory_aware_branching_info(expr * term, double priority, lbool phase) {
        context & ctx = get_context();
        ctx.internalize(term, false);
        bool_var v = ctx.get_bool_var(term);
        ctx.add_theory_aware_branching_info(v, priority, phase);
    }

    void theory_str::generate_mutual_exclusion(expr_ref_vector & terms) {
        context & ctx = get_context();
        // pull each literal out of the arrangement disjunction
        literal_vector ls;
        for (expr * e : terms) {
            literal l = ctx.get_literal(e);
            ls.push_back(l);
        }
        ctx.mk_th_case_split(ls.size(), ls.c_ptr());
    }

    void theory_str::print_cut_var(expr * node, std::ofstream & xout) {
        ast_manager & m = get_manager();
        xout << "Cut info of " << mk_pp(node, m) << std::endl;
        if (cut_var_map.contains(node)) {
            if (!cut_var_map[node].empty()) {
                xout << "[" << cut_var_map[node].top()->level << "] ";
                for (auto const& kv : cut_var_map[node].top()->vars) {
                    xout << mk_pp(kv.m_key, m) << ", ";
                }
                xout << std::endl;
            }
        }
    }

    /*
     * Handle two equivalent Concats.
     */
    void theory_str::simplify_concat_equality(expr * nn1, expr * nn2) {
        ast_manager & m = get_manager();
        context & ctx = get_context();

        app * a_nn1 = to_app(nn1);
        SASSERT(a_nn1->get_num_args() == 2);
        app * a_nn2 = to_app(nn2);
        SASSERT(a_nn2->get_num_args() == 2);

        expr * a1_arg0 = a_nn1->get_arg(0);
        expr * a1_arg1 = a_nn1->get_arg(1);
        expr * a2_arg0 = a_nn2->get_arg(0);
        expr * a2_arg1 = a_nn2->get_arg(1);

        rational a1_arg0_len, a1_arg1_len, a2_arg0_len, a2_arg1_len;

        bool a1_arg0_len_exists = get_len_value(a1_arg0, a1_arg0_len);
        bool a1_arg1_len_exists = get_len_value(a1_arg1, a1_arg1_len);
        bool a2_arg0_len_exists = get_len_value(a2_arg0, a2_arg0_len);
        bool a2_arg1_len_exists = get_len_value(a2_arg1, a2_arg1_len);

        TRACE("str", tout << "nn1 = " << mk_ismt2_pp(nn1, m) << std::endl
              << "nn2 = " << mk_ismt2_pp(nn2, m) << std::endl;);

        TRACE("str", tout
              << "len(" << mk_pp(a1_arg0, m) << ") = " << (a1_arg0_len_exists ? a1_arg0_len.to_string() : "?") << std::endl
              << "len(" << mk_pp(a1_arg1, m) << ") = " << (a1_arg1_len_exists ? a1_arg1_len.to_string() : "?") << std::endl
              << "len(" << mk_pp(a2_arg0, m) << ") = " << (a2_arg0_len_exists ? a2_arg0_len.to_string() : "?") << std::endl
              << "len(" << mk_pp(a2_arg1, m) << ") = " << (a2_arg1_len_exists ? a2_arg1_len.to_string() : "?") << std::endl
              << std::endl;);

        infer_len_concat_equality(nn1, nn2);

        if (a1_arg0 == a2_arg0) {
            if (!in_same_eqc(a1_arg1, a2_arg1)) {
                expr_ref premise(ctx.mk_eq_atom(nn1, nn2), m);
                expr_ref eq1(ctx.mk_eq_atom(a1_arg1, a2_arg1), m);
                expr_ref eq2(ctx.mk_eq_atom(mk_strlen(a1_arg1), mk_strlen(a2_arg1)), m);
                expr_ref conclusion(m.mk_and(eq1, eq2), m);
                assert_implication(premise, conclusion);
            }
            TRACE("str", tout << "SKIP: a1_arg0 == a2_arg0" << std::endl;);
            return;
        }

        if (a1_arg1 == a2_arg1) {
            if (!in_same_eqc(a1_arg0, a2_arg0)) {
                expr_ref premise(ctx.mk_eq_atom(nn1, nn2), m);
                expr_ref eq1(ctx.mk_eq_atom(a1_arg0, a2_arg0), m);
                expr_ref eq2(ctx.mk_eq_atom(mk_strlen(a1_arg0), mk_strlen(a2_arg0)), m);
                expr_ref conclusion(m.mk_and(eq1, eq2), m);
                assert_implication(premise, conclusion);
            }
            TRACE("str", tout << "SKIP: a1_arg1 == a2_arg1" << std::endl;);
            return;
        }

        // quick path

        if (in_same_eqc(a1_arg0, a2_arg0)) {
            if (in_same_eqc(a1_arg1, a2_arg1)) {
                TRACE("str", tout << "SKIP: a1_arg0 =~ a2_arg0 and a1_arg1 =~ a2_arg1" << std::endl;);
                return;
            } else {
                TRACE("str", tout << "quick path 1-1: a1_arg0 =~ a2_arg0" << std::endl;);
                expr_ref premise(m.mk_and(ctx.mk_eq_atom(nn1, nn2), ctx.mk_eq_atom(a1_arg0, a2_arg0)), m);
                expr_ref conclusion(m.mk_and(ctx.mk_eq_atom(a1_arg1, a2_arg1), ctx.mk_eq_atom(mk_strlen(a1_arg1), mk_strlen(a2_arg1))), m);
                assert_implication(premise, conclusion);
                return;
            }
        } else {
            if (in_same_eqc(a1_arg1, a2_arg1)) {
                TRACE("str", tout << "quick path 1-2: a1_arg1 =~ a2_arg1" << std::endl;);
                expr_ref premise(m.mk_and(ctx.mk_eq_atom(nn1, nn2), ctx.mk_eq_atom(a1_arg1, a2_arg1)), m);
                expr_ref conclusion(m.mk_and(ctx.mk_eq_atom(a1_arg0, a2_arg0), ctx.mk_eq_atom(mk_strlen(a1_arg0), mk_strlen(a2_arg0))), m);
                assert_implication(premise, conclusion);
                return;
            }
        }

        // quick path 2-1
        if (a1_arg0_len_exists && a2_arg0_len_exists && a1_arg0_len == a2_arg0_len) {
            if (!in_same_eqc(a1_arg0, a2_arg0)) {
                TRACE("str", tout << "quick path 2-1: len(nn1.arg0) == len(nn2.arg0)" << std::endl;);
                expr_ref ax_l1(ctx.mk_eq_atom(nn1, nn2), m);
                expr_ref ax_l2(ctx.mk_eq_atom(mk_strlen(a1_arg0), mk_strlen(a2_arg0)), m);
                expr_ref ax_r1(ctx.mk_eq_atom(a1_arg0, a2_arg0), m);
                expr_ref ax_r2(ctx.mk_eq_atom(a1_arg1, a2_arg1), m);

                expr_ref premise(m.mk_and(ax_l1, ax_l2), m);
                expr_ref conclusion(m.mk_and(ax_r1, ax_r2), m);

                assert_implication(premise, conclusion);

                if (opt_NoQuickReturn_IntegerTheory) {
                    TRACE("str", tout << "bypassing quick return from the end of this case" << std::endl;);
                } else {
                    return;
                }
            }
        }

        if (a1_arg1_len_exists && a2_arg1_len_exists && a1_arg1_len == a2_arg1_len) {
            if (!in_same_eqc(a1_arg1, a2_arg1)) {
                TRACE("str", tout << "quick path 2-2: len(nn1.arg1) == len(nn2.arg1)" << std::endl;);
                expr_ref ax_l1(ctx.mk_eq_atom(nn1, nn2), m);
                expr_ref ax_l2(ctx.mk_eq_atom(mk_strlen(a1_arg1), mk_strlen(a2_arg1)), m);
                expr_ref ax_r1(ctx.mk_eq_atom(a1_arg0, a2_arg0), m);
                expr_ref ax_r2(ctx.mk_eq_atom(a1_arg1, a2_arg1), m);

                expr_ref premise(m.mk_and(ax_l1, ax_l2), m);
                expr_ref conclusion(m.mk_and(ax_r1, ax_r2), m);

                assert_implication(premise, conclusion);
                if (opt_NoQuickReturn_IntegerTheory) {
                    TRACE("str", tout << "bypassing quick return from the end of this case" << std::endl;);
                } else {
                    return;
                }
            }
        }

        expr_ref new_nn1(simplify_concat(nn1), m);
        expr_ref new_nn2(simplify_concat(nn2), m);
        app * a_new_nn1 = to_app(new_nn1);
        app * a_new_nn2 = to_app(new_nn2);

        TRACE("str", tout << "new_nn1 = " << mk_ismt2_pp(new_nn1, m) << std::endl
              << "new_nn2 = " << mk_ismt2_pp(new_nn2, m) << std::endl;);

        if (new_nn1 == new_nn2) {
            TRACE("str", tout << "equal concats, return" << std::endl;);
            return;
        }

        if (!can_two_nodes_eq(new_nn1, new_nn2)) {
            expr_ref detected(mk_not(m, ctx.mk_eq_atom(new_nn1, new_nn2)), m);
            TRACE("str", tout << "inconsistency detected: " << mk_ismt2_pp(detected, m) << std::endl;);
            assert_axiom(detected);
            return;
        }

        // check whether new_nn1 and new_nn2 are still concats

        bool n1IsConcat = u.str.is_concat(a_new_nn1);
        bool n2IsConcat = u.str.is_concat(a_new_nn2);
        if (!n1IsConcat && n2IsConcat) {
            TRACE("str", tout << "nn1_new is not a concat" << std::endl;);
            if (u.str.is_string(a_new_nn1)) {
                simplify_parent(new_nn2, new_nn1);
            }
            return;
        } else if (n1IsConcat && !n2IsConcat) {
            TRACE("str", tout << "nn2_new is not a concat" << std::endl;);
            if (u.str.is_string(a_new_nn2)) {
                simplify_parent(new_nn1, new_nn2);
            }
            return;
        } else if (!n1IsConcat && !n2IsConcat) {
            // normally this should never happen, because group_terms_by_eqc() should have pre-simplified
            // as much as possible. however, we make a defensive check here just in case
            TRACE("str", tout << "WARNING: nn1_new and nn2_new both simplify to non-concat terms" << std::endl;);
            return;
        }

        expr * v1_arg0 = a_new_nn1->get_arg(0);
        expr * v1_arg1 = a_new_nn1->get_arg(1);
        expr * v2_arg0 = a_new_nn2->get_arg(0);
        expr * v2_arg1 = a_new_nn2->get_arg(1);

        if (!in_same_eqc(new_nn1, new_nn2) && (nn1 != new_nn1 || nn2 != new_nn2)) {
            int ii4 = 0;
            expr* item[3];
            if (nn1 != new_nn1) {
                item[ii4++] = ctx.mk_eq_atom(nn1, new_nn1);
            }
            if (nn2 != new_nn2) {
                item[ii4++] = ctx.mk_eq_atom(nn2, new_nn2);
            }
            item[ii4++] = ctx.mk_eq_atom(nn1, nn2);
            expr_ref premise(m.mk_and(ii4, item), m);
            expr_ref conclusion(ctx.mk_eq_atom(new_nn1, new_nn2), m);
            assert_implication(premise, conclusion);
        }

        // start to split both concats
        check_and_init_cut_var(v1_arg0);
        check_and_init_cut_var(v1_arg1);
        check_and_init_cut_var(v2_arg0);
        check_and_init_cut_var(v2_arg1);

        //*************************************************************
              // case 1: concat(x, y) = concat(m, n)
              //*************************************************************
                    if (is_concat_eq_type1(new_nn1, new_nn2)) {
                        process_concat_eq_type1(new_nn1, new_nn2);
                        return;
                    }

        //*************************************************************
              // case 2: concat(x, y) = concat(m, "str")
              //*************************************************************
                    if (is_concat_eq_type2(new_nn1, new_nn2)) {
                        process_concat_eq_type2(new_nn1, new_nn2);
                        return;
                    }

        //*************************************************************
              // case 3: concat(x, y) = concat("str", n)
              //*************************************************************
                    if (is_concat_eq_type3(new_nn1, new_nn2)) {
                        process_concat_eq_type3(new_nn1, new_nn2);
                        return;
                    }

        //*************************************************************
              //  case 4: concat("str1", y) = concat("str2", n)
              //*************************************************************
                    if (is_concat_eq_type4(new_nn1, new_nn2)) {
                        process_concat_eq_type4(new_nn1, new_nn2);
                        return;
                    }

        //*************************************************************
              //  case 5: concat(x, "str1") = concat(m, "str2")
              //*************************************************************
                    if (is_concat_eq_type5(new_nn1, new_nn2)) {
                        process_concat_eq_type5(new_nn1, new_nn2);
                        return;
                    }
        //*************************************************************
              //  case 6: concat("str1", y) = concat(m, "str2")
              //*************************************************************
                    if (is_concat_eq_type6(new_nn1, new_nn2)) {
                        process_concat_eq_type6(new_nn1, new_nn2);
                        return;
                    }

    }

    /*
     * Returns true if attempting to process a concat equality between lhs and rhs
     * will result in overlapping variables (false otherwise).
     */
    bool theory_str::will_result_in_overlap(expr * lhs, expr * rhs) {
        ast_manager & m = get_manager();

        expr_ref new_nn1(simplify_concat(lhs), m);
        expr_ref new_nn2(simplify_concat(rhs), m);
        app * a_new_nn1 = to_app(new_nn1);
        app * a_new_nn2 = to_app(new_nn2);

        bool n1IsConcat = u.str.is_concat(a_new_nn1);
        bool n2IsConcat = u.str.is_concat(a_new_nn2);
        if (!n1IsConcat && !n2IsConcat) {
            // we simplified both sides to non-concat expressions...
            return false;
        }

        expr * v1_arg0 = a_new_nn1->get_arg(0);
        expr * v1_arg1 = a_new_nn1->get_arg(1);
        expr * v2_arg0 = a_new_nn2->get_arg(0);
        expr * v2_arg1 = a_new_nn2->get_arg(1);

        TRACE("str", tout << "checking whether " << mk_pp(new_nn1, m) << " and " << mk_pp(new_nn1, m) << " might overlap." << std::endl;);

        check_and_init_cut_var(v1_arg0);
        check_and_init_cut_var(v1_arg1);
        check_and_init_cut_var(v2_arg0);
        check_and_init_cut_var(v2_arg1);

        //*************************************************************
              // case 1: concat(x, y) = concat(m, n)
              //*************************************************************
                    if (is_concat_eq_type1(new_nn1, new_nn2)) {
                        TRACE("str", tout << "Type 1 check." << std::endl;);
                        expr * x = to_app(new_nn1)->get_arg(0);
                        expr * y = to_app(new_nn1)->get_arg(1);
                        expr * m = to_app(new_nn2)->get_arg(0);
                        expr * n = to_app(new_nn2)->get_arg(1);

                        if (has_self_cut(m, y)) {
                            TRACE("str", tout << "Possible overlap found" << std::endl; print_cut_var(m, tout); print_cut_var(y, tout););
                            return true;
                        } else if (has_self_cut(x, n)) {
                            TRACE("str", tout << "Possible overlap found" << std::endl; print_cut_var(x, tout); print_cut_var(n, tout););
                            return true;
                        } else {
                            return false;
                        }
                    }

        //*************************************************************
              // case 2: concat(x, y) = concat(m, "str")
              //*************************************************************
                    if (is_concat_eq_type2(new_nn1, new_nn2)) {

                        expr * y = nullptr;
                        expr * m = nullptr;
                        expr * v1_arg0 = to_app(new_nn1)->get_arg(0);
                        expr * v1_arg1 = to_app(new_nn1)->get_arg(1);
                        expr * v2_arg0 = to_app(new_nn2)->get_arg(0);
                        expr * v2_arg1 = to_app(new_nn2)->get_arg(1);

                        if (u.str.is_string(v1_arg1) && !u.str.is_string(v2_arg1)) {
                            m = v1_arg0;
                            y = v2_arg1;
                        } else {
                            m = v2_arg0;
                            y = v1_arg1;
                        }

                        if (has_self_cut(m, y)) {
                            TRACE("str", tout << "Possible overlap found" << std::endl; print_cut_var(m, tout); print_cut_var(y, tout););
                            return true;
                        } else {
                            return false;
                        }
                    }

        //*************************************************************
              // case 3: concat(x, y) = concat("str", n)
              //*************************************************************
                    if (is_concat_eq_type3(new_nn1, new_nn2)) {
                        expr * v1_arg0 = to_app(new_nn1)->get_arg(0);
                        expr * v1_arg1 = to_app(new_nn1)->get_arg(1);
                        expr * v2_arg0 = to_app(new_nn2)->get_arg(0);
                        expr * v2_arg1 = to_app(new_nn2)->get_arg(1);

                        expr * x = nullptr;
                        expr * n = nullptr;

                        if (u.str.is_string(v1_arg0) && !u.str.is_string(v2_arg0)) {
                            n = v1_arg1;
                            x = v2_arg0;
                        } else {
                            n = v2_arg1;
                            x = v1_arg0;
                        }
                        if (has_self_cut(x, n)) {
                            TRACE("str", tout << "Possible overlap found" << std::endl; print_cut_var(x, tout); print_cut_var(n, tout););
                            return true;
                        } else {
                            return false;
                        }
                    }

        //*************************************************************
              //  case 4: concat("str1", y) = concat("str2", n)
              //*************************************************************
                    if (is_concat_eq_type4(new_nn1, new_nn2)) {
                        // This case can never result in an overlap.
                        return false;
                    }

        //*************************************************************
              //  case 5: concat(x, "str1") = concat(m, "str2")
              //*************************************************************
                    if (is_concat_eq_type5(new_nn1, new_nn2)) {
                        // This case can never result in an overlap.
                        return false;
                    }
        //*************************************************************
              //  case 6: concat("str1", y) = concat(m, "str2")
              //*************************************************************
                    if (is_concat_eq_type6(new_nn1, new_nn2)) {
                        expr * v1_arg0 = to_app(new_nn1)->get_arg(0);
                        expr * v1_arg1 = to_app(new_nn1)->get_arg(1);
                        expr * v2_arg0 = to_app(new_nn2)->get_arg(0);
                        expr * v2_arg1 = to_app(new_nn2)->get_arg(1);

                        expr * y = nullptr;
                        expr * m = nullptr;

                        if (u.str.is_string(v1_arg0)) {
                            y = v1_arg1;
                            m = v2_arg0;
                        } else {
                            y = v2_arg1;
                            m = v1_arg0;
                        }
                        if (has_self_cut(m, y)) {
                            TRACE("str", tout << "Possible overlap found" << std::endl; print_cut_var(m, tout); print_cut_var(y, tout););
                            return true;
                        } else {
                            return false;
                        }
                    }

        TRACE("str", tout << "warning: unrecognized concat case" << std::endl;);
        return false;
    }

    /*************************************************************
     * Type 1: concat(x, y) = concat(m, n)
     *         x, y, m and n all variables
     *************************************************************/
    bool theory_str::is_concat_eq_type1(expr * concatAst1, expr * concatAst2) {
        expr * x = to_app(concatAst1)->get_arg(0);
        expr * y = to_app(concatAst1)->get_arg(1);
        expr * m = to_app(concatAst2)->get_arg(0);
        expr * n = to_app(concatAst2)->get_arg(1);

        if (!u.str.is_string(x) && !u.str.is_string(y) && !u.str.is_string(m) && !u.str.is_string(n)) {
            return true;
        } else {
            return false;
        }
    }

    void theory_str::process_concat_eq_type1(expr * concatAst1, expr * concatAst2) {
        ast_manager & mgr = get_manager();
        context & ctx = get_context();

        bool overlapAssumptionUsed = false;

        TRACE("str", tout << "process_concat_eq TYPE 1" << std::endl
              << "concatAst1 = " << mk_ismt2_pp(concatAst1, mgr) << std::endl
              << "concatAst2 = " << mk_ismt2_pp(concatAst2, mgr) << std::endl;
              );

        if (!u.str.is_concat(to_app(concatAst1))) {
            TRACE("str", tout << "concatAst1 is not a concat function" << std::endl;);
            return;
        }
        if (!u.str.is_concat(to_app(concatAst2))) {
            TRACE("str", tout << "concatAst2 is not a concat function" << std::endl;);
            return;
        }
        expr * x = to_app(concatAst1)->get_arg(0);
        expr * y = to_app(concatAst1)->get_arg(1);
        expr * m = to_app(concatAst2)->get_arg(0);
        expr * n = to_app(concatAst2)->get_arg(1);

        rational x_len, y_len, m_len, n_len;
        bool x_len_exists = get_len_value(x, x_len);
        bool y_len_exists = get_len_value(y, y_len);
        bool m_len_exists = get_len_value(m, m_len);
        bool n_len_exists = get_len_value(n, n_len);

        int splitType = -1;
        if (x_len_exists && m_len_exists) {
            TRACE("str", tout << "length values found: x/m" << std::endl;);
            if (x_len < m_len) {
                splitType = 0;
            } else if (x_len == m_len) {
                splitType = 1;
            } else {
                splitType = 2;
            }
        }

        if (splitType == -1 && y_len_exists && n_len_exists) {
            TRACE("str", tout << "length values found: y/n" << std::endl;);
            if (y_len > n_len) {
                splitType = 0;
            } else if (y_len == n_len) {
                splitType = 1;
            } else {
                splitType = 2;
            }
        }

        TRACE("str", tout
              << "len(x) = " << (x_len_exists ? x_len.to_string() : "?") << std::endl
              << "len(y) = " << (y_len_exists ? y_len.to_string() : "?") << std::endl
              << "len(m) = " << (m_len_exists ? m_len.to_string() : "?") << std::endl
              << "len(n) = " << (n_len_exists ? n_len.to_string() : "?") << std::endl
              << "split type " << splitType << std::endl;
              );

        expr * t1 = nullptr;
        expr * t2 = nullptr;
        expr * xorFlag = nullptr;

        std::pair<expr*, expr*> key1(concatAst1, concatAst2);
        std::pair<expr*, expr*> key2(concatAst2, concatAst1);

        // check the entries in this map to make sure they're still in scope
        // before we use them.

        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry1 = varForBreakConcat.find(key1);
        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry2 = varForBreakConcat.find(key2);

        bool entry1InScope;
        if (entry1 == varForBreakConcat.end()) {
            entry1InScope = false;
        } else {
            if (internal_variable_set.find((entry1->second)[0]) == internal_variable_set.end()
                || internal_variable_set.find((entry1->second)[1]) == internal_variable_set.end()
                /*|| internal_variable_set.find((entry1->second)[2]) == internal_variable_set.end() */) {
                entry1InScope = false;
            } else {
                entry1InScope = true;
            }
        }

        bool entry2InScope;
        if (entry2 == varForBreakConcat.end()) {
            entry2InScope = false;
        } else {
            if (internal_variable_set.find((entry2->second)[0]) == internal_variable_set.end()
                || internal_variable_set.find((entry2->second)[1]) == internal_variable_set.end()
                /* || internal_variable_set.find((entry2->second)[2]) == internal_variable_set.end() */) {
                entry2InScope = false;
            } else {
                entry2InScope = true;
            }
        }

        TRACE("str", tout << "entry 1 " << (entry1InScope ? "in scope" : "not in scope") << std::endl
              << "entry 2 " << (entry2InScope ? "in scope" : "not in scope") << std::endl;);

        if (!entry1InScope && !entry2InScope) {
            t1 = mk_nonempty_str_var();
            t2 = mk_nonempty_str_var();
            xorFlag = mk_internal_xor_var();
            check_and_init_cut_var(t1);
            check_and_init_cut_var(t2);
            varForBreakConcat[key1][0] = t1;
            varForBreakConcat[key1][1] = t2;
            varForBreakConcat[key1][2] = xorFlag;
        } else {
            // match found
            if (entry1InScope) {
                t1 = varForBreakConcat[key1][0];
                t2 = varForBreakConcat[key1][1];
                xorFlag = varForBreakConcat[key1][2];
            } else {
                t1 = varForBreakConcat[key2][0];
                t2 = varForBreakConcat[key2][1];
                xorFlag = varForBreakConcat[key2][2];
            }
            refresh_theory_var(t1);
            add_nonempty_constraint(t1);
            refresh_theory_var(t2);
            add_nonempty_constraint(t2);
        }

        // For split types 0 through 2, we can get away with providing
        // fewer split options since more length information is available.
        if (splitType == 0) {
            //--------------------------------------
            // Type 0: M cuts Y.
            //   len(x) < len(m) || len(y) > len(n)
            //--------------------------------------
            expr_ref_vector ax_l_items(mgr);
            expr_ref_vector ax_r_items(mgr);

            ax_l_items.push_back(ctx.mk_eq_atom(concatAst1, concatAst2));

            expr_ref x_t1(mk_concat(x, t1), mgr);
            expr_ref t1_n(mk_concat(t1, n), mgr);

            ax_r_items.push_back(ctx.mk_eq_atom(m, x_t1));
            ax_r_items.push_back(ctx.mk_eq_atom(y, t1_n));

            if (m_len_exists && x_len_exists) {
                ax_l_items.push_back(ctx.mk_eq_atom(mk_strlen(x), mk_int(x_len)));
                ax_l_items.push_back(ctx.mk_eq_atom(mk_strlen(m), mk_int(m_len)));
                rational m_sub_x = m_len - x_len;
                ax_r_items.push_back(ctx.mk_eq_atom(mk_strlen(t1), mk_int(m_sub_x)));
            } else {
                ax_l_items.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_int(y_len)));
                ax_l_items.push_back(ctx.mk_eq_atom(mk_strlen(n), mk_int(n_len)));
                rational y_sub_n = y_len - n_len;
                ax_r_items.push_back(ctx.mk_eq_atom(mk_strlen(t1), mk_int(y_sub_n)));
            }

            expr_ref ax_l(mk_and(ax_l_items), mgr);
            expr_ref ax_r(mk_and(ax_r_items), mgr);

            if (!has_self_cut(m, y)) {
                // Cut Info
                add_cut_info_merge(t1, sLevel, m);
                add_cut_info_merge(t1, sLevel, y);

                if (m_params.m_StrongArrangements) {
                    expr_ref ax_strong(ctx.mk_eq_atom(ax_l, ax_r), mgr);
                    assert_axiom(ax_strong);
                } else {
                    assert_implication(ax_l, ax_r);
                }
            } else {
                loopDetected = true;
                if (m_params.m_FiniteOverlapModels) {
                    expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                    assert_implication(ax_l, tester);
                    add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
                } else {
                    TRACE("str", tout << "AVOID LOOP: SKIPPED" << std::endl;);
                    TRACE("str", {print_cut_var(m, tout); print_cut_var(y, tout);});

                    if (!overlapAssumptionUsed) {
                        overlapAssumptionUsed = true;
                        assert_implication(ax_l, m_theoryStrOverlapAssumption_term);
                    }
                }
            }
        } else if (splitType == 1) {
            // Type 1:
            //   len(x) = len(m) || len(y) = len(n)
            expr_ref ax_l1(ctx.mk_eq_atom(concatAst1, concatAst2), mgr);
            expr_ref ax_l2(mgr.mk_or(ctx.mk_eq_atom(mk_strlen(x), mk_strlen(m)), ctx.mk_eq_atom(mk_strlen(y), mk_strlen(n))), mgr);
            expr_ref ax_l(mgr.mk_and(ax_l1, ax_l2), mgr);
            expr_ref ax_r(mgr.mk_and(ctx.mk_eq_atom(x,m), ctx.mk_eq_atom(y,n)), mgr);
            assert_implication(ax_l, ax_r);
        } else if (splitType == 2) {
            // Type 2: X cuts N.
            //   len(x) > len(m) || len(y) < len(n)
            expr_ref m_t2(mk_concat(m, t2), mgr);
            expr_ref t2_y(mk_concat(t2, y), mgr);

            expr_ref_vector ax_l_items(mgr);
            ax_l_items.push_back(ctx.mk_eq_atom(concatAst1, concatAst2));

            expr_ref_vector ax_r_items(mgr);
            ax_r_items.push_back(ctx.mk_eq_atom(x, m_t2));
            ax_r_items.push_back(ctx.mk_eq_atom(t2_y, n));

            if (m_len_exists && x_len_exists) {
                ax_l_items.push_back(ctx.mk_eq_atom(mk_strlen(x), mk_int(x_len)));
                ax_l_items.push_back(ctx.mk_eq_atom(mk_strlen(m), mk_int(m_len)));
                rational x_sub_m = x_len - m_len;
                ax_r_items.push_back(ctx.mk_eq_atom(mk_strlen(t2), mk_int(x_sub_m)));
            } else {
                ax_l_items.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_int(y_len)));
                ax_l_items.push_back(ctx.mk_eq_atom(mk_strlen(n), mk_int(n_len)));
                rational n_sub_y = n_len - y_len;
                ax_r_items.push_back(ctx.mk_eq_atom(mk_strlen(t2), mk_int(n_sub_y)));
            }

            expr_ref ax_l(mk_and(ax_l_items), mgr);
            expr_ref ax_r(mk_and(ax_r_items), mgr);

            if (!has_self_cut(x, n)) {
                // Cut Info
                add_cut_info_merge(t2, sLevel, x);
                add_cut_info_merge(t2, sLevel, n);

                if (m_params.m_StrongArrangements) {
                    expr_ref ax_strong(ctx.mk_eq_atom(ax_l, ax_r), mgr);
                    assert_axiom(ax_strong);
                } else {
                    assert_implication(ax_l, ax_r);
                }
            } else {
                loopDetected = true;
                if (m_params.m_FiniteOverlapModels) {
                    expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                    assert_implication(ax_l, tester);
                    add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
                } else {
                    TRACE("str", tout << "AVOID LOOP: SKIPPED" << std::endl;);
                    TRACE("str", {print_cut_var(m, tout); print_cut_var(y, tout);});

                    if (!overlapAssumptionUsed) {
                        overlapAssumptionUsed = true;
                        assert_implication(ax_l, m_theoryStrOverlapAssumption_term);
                    }
                }
            }
        } else if (splitType == -1) {
            // Here we don't really have a choice. We have no length information at all...

            // This vector will eventually contain one term for each possible arrangement we explore.
            expr_ref_vector arrangement_disjunction(mgr);

            // break option 1: m cuts y
            // len(x) < len(m) || len(y) > len(n)
            if (!avoidLoopCut || !has_self_cut(m, y)) {
                expr_ref_vector and_item(mgr);
                // break down option 1-1
                expr_ref x_t1(mk_concat(x, t1), mgr);
                expr_ref t1_n(mk_concat(t1, n), mgr);

                and_item.push_back(ctx.mk_eq_atom(m, x_t1));
                and_item.push_back(ctx.mk_eq_atom(y, t1_n));

                expr_ref x_plus_t1(m_autil.mk_add(mk_strlen(x), mk_strlen(t1)), mgr);
                and_item.push_back(ctx.mk_eq_atom(mk_strlen(m), x_plus_t1));
                // These were crashing the solver because the integer theory
                // expects a constant on the right-hand side.
                // The things we want to assert here are len(m) > len(x) and len(y) > len(n).
                // We rewrite A > B as A-B > 0 and then as not(A-B <= 0),
                // and then, *because we aren't allowed to use subtraction*,
                // as not(A + -1*B <= 0)
                and_item.push_back(
                    mgr.mk_not(m_autil.mk_le(
                                   m_autil.mk_add(mk_strlen(m), m_autil.mk_mul(mk_int(-1), mk_strlen(x))),
                                   mk_int(0))) );
                and_item.push_back(
                    mgr.mk_not(m_autil.mk_le(
                                   m_autil.mk_add(mk_strlen(y),m_autil.mk_mul(mk_int(-1), mk_strlen(n))),
                                   mk_int(0))) );

                expr_ref option1(mk_and(and_item), mgr);
                arrangement_disjunction.push_back(option1);
                add_theory_aware_branching_info(option1, 0.1, l_true);

                add_cut_info_merge(t1, ctx.get_scope_level(), m);
                add_cut_info_merge(t1, ctx.get_scope_level(), y);
            } else {
                loopDetected = true;
                if (m_params.m_FiniteOverlapModels) {
                    expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                    arrangement_disjunction.push_back(tester);
                    add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
                } else {
                    TRACE("str", tout << "AVOID LOOP: SKIPPED" << std::endl;);
                    TRACE("str", {print_cut_var(m, tout); print_cut_var(y, tout);});

                    if (!overlapAssumptionUsed) {
                        overlapAssumptionUsed = true;
                        arrangement_disjunction.push_back(m_theoryStrOverlapAssumption_term);
                    }
                }
            }

            // break option 2:
            // x = m . t2
            // n = t2 . y
            if (!avoidLoopCut || !has_self_cut(x, n)) {
                expr_ref_vector and_item(mgr);
                // break down option 1-2
                expr_ref m_t2(mk_concat(m, t2), mgr);
                expr_ref t2_y(mk_concat(t2, y), mgr);

                and_item.push_back(ctx.mk_eq_atom(x, m_t2));
                and_item.push_back(ctx.mk_eq_atom(n, t2_y));


                expr_ref m_plus_t2(m_autil.mk_add(mk_strlen(m), mk_strlen(t2)), mgr);

                and_item.push_back(ctx.mk_eq_atom(mk_strlen(x), m_plus_t2));
                // want len(x) > len(m) and len(n) > len(y)
                and_item.push_back(
                    mgr.mk_not(m_autil.mk_le(
                                   m_autil.mk_add(mk_strlen(x), m_autil.mk_mul(mk_int(-1), mk_strlen(m))),
                                   mk_int(0))) );
                and_item.push_back(
                    mgr.mk_not(m_autil.mk_le(
                                   m_autil.mk_add(mk_strlen(n), m_autil.mk_mul(mk_int(-1), mk_strlen(y))),
                                   mk_int(0))) );

                expr_ref option2(mk_and(and_item), mgr);
                arrangement_disjunction.push_back(option2);
                add_theory_aware_branching_info(option2, 0.1, l_true);

                add_cut_info_merge(t2, ctx.get_scope_level(), x);
                add_cut_info_merge(t2, ctx.get_scope_level(), n);
            } else {
                loopDetected = true;
                if (m_params.m_FiniteOverlapModels) {
                    expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                    arrangement_disjunction.push_back(tester);
                    add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
                } else {
                    TRACE("str", tout << "AVOID LOOP: SKIPPED" << std::endl;);
                    TRACE("str", {print_cut_var(x, tout); print_cut_var(n, tout);});

                    if (!overlapAssumptionUsed) {
                        overlapAssumptionUsed = true;
                        arrangement_disjunction.push_back(m_theoryStrOverlapAssumption_term);
                    }
                }
            }

            // option 3:
            // x = m, y = n
            if (can_two_nodes_eq(x, m) && can_two_nodes_eq(y, n)) {
                expr_ref_vector and_item(mgr);

                and_item.push_back(ctx.mk_eq_atom(x, m));
                and_item.push_back(ctx.mk_eq_atom(y, n));
                and_item.push_back(ctx.mk_eq_atom(mk_strlen(x), mk_strlen(m)));
                and_item.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_strlen(n)));

                expr_ref option3(mk_and(and_item), mgr);
                arrangement_disjunction.push_back(option3);
                // prioritize this case, it is easier
                add_theory_aware_branching_info(option3, 0.5, l_true);
            }

            if (!arrangement_disjunction.empty()) {
                expr_ref premise(ctx.mk_eq_atom(concatAst1, concatAst2), mgr);
                expr_ref conclusion(mk_or(arrangement_disjunction), mgr);
                if (m_params.m_StrongArrangements) {
                    expr_ref ax_strong(ctx.mk_eq_atom(premise, conclusion), mgr);
                    assert_axiom(ax_strong);
                } else {
                    assert_implication(premise, conclusion);
                }
                // assert mutual exclusion between each branch of the arrangement
                generate_mutual_exclusion(arrangement_disjunction);
            } else {
                TRACE("str", tout << "STOP: no split option found for two EQ concats." << std::endl;);
            }
        } // (splitType == -1)
    }

    /*************************************************************
     * Type 2: concat(x, y) = concat(m, "str")
     *************************************************************/
    bool theory_str::is_concat_eq_type2(expr * concatAst1, expr * concatAst2) {
        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        if ((!u.str.is_string(v1_arg0)) && u.str.is_string(v1_arg1)
            && (!u.str.is_string(v2_arg0)) && (!u.str.is_string(v2_arg1))) {
            return true;
        } else if ((!u.str.is_string(v2_arg0)) && u.str.is_string(v2_arg1)
                   && (!u.str.is_string(v1_arg0)) && (!u.str.is_string(v1_arg1))) {
            return true;
        } else {
            return false;
        }
    }

    void theory_str::process_concat_eq_type2(expr * concatAst1, expr * concatAst2) {
        ast_manager & mgr = get_manager();
        context & ctx = get_context();

        bool overlapAssumptionUsed = false;

        TRACE("str", tout << "process_concat_eq TYPE 2" << std::endl
              << "concatAst1 = " << mk_ismt2_pp(concatAst1, mgr) << std::endl
              << "concatAst2 = " << mk_ismt2_pp(concatAst2, mgr) << std::endl;
              );

        if (!u.str.is_concat(to_app(concatAst1))) {
            TRACE("str", tout << "concatAst1 is not a concat function" << std::endl;);
            return;
        }
        if (!u.str.is_concat(to_app(concatAst2))) {
            TRACE("str", tout << "concatAst2 is not a concat function" << std::endl;);
            return;
        }

        expr * x = nullptr;
        expr * y = nullptr;
        expr * strAst = nullptr;
        expr * m = nullptr;

        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        if (u.str.is_string(v1_arg1) && !u.str.is_string(v2_arg1)) {
            m = v1_arg0;
            strAst = v1_arg1;
            x = v2_arg0;
            y = v2_arg1;
        } else {
            m = v2_arg0;
            strAst = v2_arg1;
            x = v1_arg0;
            y = v1_arg1;
        }

        zstring strValue;
        u.str.is_string(strAst, strValue);

        rational x_len, y_len, m_len, str_len;
        bool x_len_exists = get_len_value(x, x_len);
        bool y_len_exists = get_len_value(y, y_len);
        bool m_len_exists = get_len_value(m, m_len);
        bool str_len_exists = true;
        str_len = rational(strValue.length());

        // setup

        expr * xorFlag = nullptr;
        expr * temp1 = nullptr;
        std::pair<expr*, expr*> key1(concatAst1, concatAst2);
        std::pair<expr*, expr*> key2(concatAst2, concatAst1);

        // check the entries in this map to make sure they're still in scope
        // before we use them.

        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry1 = varForBreakConcat.find(key1);
        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry2 = varForBreakConcat.find(key2);

        // prevent checking scope for the XOR term, as it's always in the same scope as the split var

        bool entry1InScope;
        if (entry1 == varForBreakConcat.end()) {
            entry1InScope = false;
        } else {
            if (internal_variable_set.find((entry1->second)[0]) == internal_variable_set.end()
                /*|| internal_variable_set.find((entry1->second)[1]) == internal_variable_set.end()*/
                ) {
                entry1InScope = false;
            } else {
                entry1InScope = true;
            }
        }

        bool entry2InScope;
        if (entry2 == varForBreakConcat.end()) {
            entry2InScope = false;
        } else {
            if (internal_variable_set.find((entry2->second)[0]) == internal_variable_set.end()
                /*|| internal_variable_set.find((entry2->second)[1]) == internal_variable_set.end()*/
                ) {
                entry2InScope = false;
            } else {
                entry2InScope = true;
            }
        }

        TRACE("str", tout << "entry 1 " << (entry1InScope ? "in scope" : "not in scope") << std::endl
              << "entry 2 " << (entry2InScope ? "in scope" : "not in scope") << std::endl;);


        if (!entry1InScope && !entry2InScope) {
            temp1 = mk_nonempty_str_var();
            xorFlag = mk_internal_xor_var();
            varForBreakConcat[key1][0] = temp1;
            varForBreakConcat[key1][1] = xorFlag;
        } else {
            if (entry1InScope) {
                temp1 = varForBreakConcat[key1][0];
                xorFlag = varForBreakConcat[key1][1];
            } else if (entry2InScope) {
                temp1 = varForBreakConcat[key2][0];
                xorFlag = varForBreakConcat[key2][1];
            }
            refresh_theory_var(temp1);
            add_nonempty_constraint(temp1);
        }

        int splitType = -1;
        if (x_len_exists && m_len_exists) {
            if (x_len < m_len)
                splitType = 0;
            else if (x_len == m_len)
                splitType = 1;
            else
                splitType = 2;
        }
        if (splitType == -1 && y_len_exists && str_len_exists) {
            if (y_len > str_len)
                splitType = 0;
            else if (y_len == str_len)
                splitType = 1;
            else
                splitType = 2;
        }

        TRACE("str", tout << "Split type " << splitType << std::endl;);

        // Provide fewer split options when length information is available.

        if (splitType == 0) {
            // M cuts Y
            //   |  x  |      y     |
            //   |    m   |   str   |
            expr_ref temp1_strAst(mk_concat(temp1, strAst), mgr);
            if (can_two_nodes_eq(y, temp1_strAst)) {
                expr_ref_vector l_items(mgr);
                l_items.push_back(ctx.mk_eq_atom(concatAst1, concatAst2));

                expr_ref_vector r_items(mgr);
                expr_ref x_temp1(mk_concat(x, temp1), mgr);
                r_items.push_back(ctx.mk_eq_atom(m, x_temp1));
                r_items.push_back(ctx.mk_eq_atom(y, temp1_strAst));

                if (x_len_exists && m_len_exists) {
                    l_items.push_back(ctx.mk_eq_atom(mk_strlen(x), mk_int(x_len)));
                    l_items.push_back(ctx.mk_eq_atom(mk_strlen(m), mk_int(m_len)));
                    rational m_sub_x = (m_len - x_len);
                    r_items.push_back(ctx.mk_eq_atom(mk_strlen(temp1), mk_int(m_sub_x)));
                } else {
                    l_items.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_int(y_len)));
                    l_items.push_back(ctx.mk_eq_atom(mk_strlen(strAst), mk_int(str_len)));
                    rational y_sub_str = (y_len - str_len);
                    r_items.push_back(ctx.mk_eq_atom(mk_strlen(temp1), mk_int(y_sub_str)));
                }

                expr_ref ax_l(mk_and(l_items), mgr);
                expr_ref ax_r(mk_and(r_items), mgr);

                if (!avoidLoopCut || !(has_self_cut(m, y))) {
                    // break down option 2-1
                    add_cut_info_merge(temp1, sLevel, y);
                    add_cut_info_merge(temp1, sLevel, m);

                    if (m_params.m_StrongArrangements) {
                        expr_ref ax_strong(ctx.mk_eq_atom(ax_l, ax_r), mgr);
                        assert_axiom(ax_strong);
                    } else {
                        assert_implication(ax_l, ax_r);
                    }
                } else {
                    loopDetected = true;

                    if (m_params.m_FiniteOverlapModels) {
                        expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                        assert_implication(ax_l, tester);
                        add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
                    } else {
                        TRACE("str", tout << "AVOID LOOP: SKIP" << std::endl;);
                        TRACE("str", {print_cut_var(m, tout); print_cut_var(y, tout);});

                        if (!overlapAssumptionUsed) {
                            overlapAssumptionUsed = true;
                            assert_implication(ax_l, m_theoryStrOverlapAssumption_term);
                        }
                    }
                }
            }
        } else if (splitType == 1) {
            //   |   x   |    y    |
            //   |   m   |   str   |
            expr_ref ax_l1(ctx.mk_eq_atom(concatAst1, concatAst2), mgr);
            expr_ref ax_l2(mgr.mk_or(
                               ctx.mk_eq_atom(mk_strlen(x), mk_strlen(m)),
                               ctx.mk_eq_atom(mk_strlen(y), mk_strlen(strAst))), mgr);
            expr_ref ax_l(mgr.mk_and(ax_l1, ax_l2), mgr);
            expr_ref ax_r(mgr.mk_and(ctx.mk_eq_atom(x, m), ctx.mk_eq_atom(y, strAst)), mgr);
            assert_implication(ax_l, ax_r);
        } else if (splitType == 2) {
            // m cut y,
            //    |   x   |  y  |
            //    | m |   str   |
            rational lenDelta;
            expr_ref_vector l_items(mgr);
            l_items.push_back(ctx.mk_eq_atom(concatAst1, concatAst2));
            if (x_len_exists && m_len_exists) {
                l_items.push_back(ctx.mk_eq_atom(mk_strlen(x), mk_int(x_len)));
                l_items.push_back(ctx.mk_eq_atom(mk_strlen(m), mk_int(m_len)));
                lenDelta = x_len - m_len;
            } else {
                l_items.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_int(y_len)));
                lenDelta = str_len - y_len;
            }
            TRACE("str",
                  tout
                  << "xLen? " << (x_len_exists ? "yes" : "no") << std::endl
                  << "mLen? " << (m_len_exists ? "yes" : "no") << std::endl
                  << "yLen? " << (y_len_exists ? "yes" : "no") << std::endl
                  << "xLen = " << x_len.to_string() << std::endl
                  << "yLen = " << y_len.to_string() << std::endl
                  << "mLen = " << m_len.to_string() << std::endl
                  << "strLen = " << str_len.to_string() << std::endl
                  << "lenDelta = " << lenDelta.to_string() << std::endl
                  << "strValue = \"" << strValue << "\" (len=" << strValue.length() << ")" << "\n"
                  ;
                  );

            zstring part1Str = strValue.extract(0, lenDelta.get_unsigned());
            zstring part2Str = strValue.extract(lenDelta.get_unsigned(), strValue.length() - lenDelta.get_unsigned());

            expr_ref prefixStr(mk_string(part1Str), mgr);
            expr_ref x_concat(mk_concat(m, prefixStr), mgr);
            expr_ref cropStr(mk_string(part2Str), mgr);

            if (can_two_nodes_eq(x, x_concat) && can_two_nodes_eq(y, cropStr)) {
                expr_ref_vector r_items(mgr);
                r_items.push_back(ctx.mk_eq_atom(x, x_concat));
                r_items.push_back(ctx.mk_eq_atom(y, cropStr));
                expr_ref ax_l(mk_and(l_items), mgr);
                expr_ref ax_r(mk_and(r_items), mgr);

                if (m_params.m_StrongArrangements) {
                    expr_ref ax_strong(ctx.mk_eq_atom(ax_l, ax_r), mgr);
                    assert_axiom(ax_strong);
                } else {
                    assert_implication(ax_l, ax_r);
                }
            } else {
                // negate! It's impossible to split str with these lengths
                TRACE("str", tout << "CONFLICT: Impossible to split str with these lengths." << std::endl;);
                expr_ref ax_l(mk_and(l_items), mgr);
                assert_axiom(mgr.mk_not(ax_l));
            }
        } else {
            // Split type -1: no idea about the length...
            expr_ref_vector arrangement_disjunction(mgr);

            expr_ref temp1_strAst(mk_concat(temp1, strAst), mgr);

            // m cuts y
            if (can_two_nodes_eq(y, temp1_strAst)) {
                if (!avoidLoopCut || !has_self_cut(m, y)) {
                    // break down option 2-1
                    expr_ref_vector and_item(mgr);

                    expr_ref x_temp1(mk_concat(x, temp1), mgr);
                    and_item.push_back(ctx.mk_eq_atom(m, x_temp1));
                    and_item.push_back(ctx.mk_eq_atom(y, temp1_strAst));

                    and_item.push_back(ctx.mk_eq_atom(mk_strlen(m),
                                                      m_autil.mk_add(mk_strlen(x), mk_strlen(temp1))));

                    expr_ref option1(mk_and(and_item), mgr);
                    arrangement_disjunction.push_back(option1);
                    add_theory_aware_branching_info(option1, 0.1, l_true);
                    add_cut_info_merge(temp1, ctx.get_scope_level(), y);
                    add_cut_info_merge(temp1, ctx.get_scope_level(), m);
                } else {
                    loopDetected = true;
                    if (m_params.m_FiniteOverlapModels) {
                        expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                        arrangement_disjunction.push_back(tester);
                        add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
                    } else {
                        TRACE("str", tout << "AVOID LOOP: SKIPPED" << std::endl;);
                        TRACE("str", {print_cut_var(m, tout); print_cut_var(y, tout);});

                        if (!overlapAssumptionUsed) {
                            overlapAssumptionUsed = true;
                            arrangement_disjunction.push_back(m_theoryStrOverlapAssumption_term);
                        }
                    }
                }
            }

            for (unsigned int i = 0; i <= strValue.length(); ++i) {
                zstring part1Str = strValue.extract(0, i);
                zstring part2Str = strValue.extract(i, strValue.length() - i);
                expr_ref prefixStr(mk_string(part1Str), mgr);
                expr_ref x_concat(mk_concat(m, prefixStr), mgr);
                expr_ref cropStr(mk_string(part2Str), mgr);
                if (can_two_nodes_eq(x, x_concat) && can_two_nodes_eq(y, cropStr)) {
                    // break down option 2-2
                    expr_ref_vector and_item(mgr);
                    and_item.push_back(ctx.mk_eq_atom(x, x_concat));
                    and_item.push_back(ctx.mk_eq_atom(y, cropStr));
                    and_item.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_int(part2Str.length())));
                    expr_ref option2(mk_and(and_item), mgr);
                    arrangement_disjunction.push_back(option2);
                    double priority;
                    // prioritize the option where y is equal to the original string
                    if (i == 0) {
                        priority = 0.5;
                    } else {
                        priority = 0.1;
                    }
                    add_theory_aware_branching_info(option2, priority, l_true);
                }
            }

            if (!arrangement_disjunction.empty()) {
                expr_ref implyR(mk_or(arrangement_disjunction), mgr);

                if (m_params.m_StrongArrangements) {
                    expr_ref implyLHS(ctx.mk_eq_atom(concatAst1, concatAst2), mgr);
                    expr_ref ax_strong(ctx.mk_eq_atom(implyLHS, implyR), mgr);
                    assert_axiom(ax_strong);
                } else {
                    assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
                }
                generate_mutual_exclusion(arrangement_disjunction);
            } else {
                TRACE("str", tout << "STOP: Should not split two EQ concats." << std::endl;);
            }
        } // (splitType == -1)
    }

    /*************************************************************
     * Type 3: concat(x, y) = concat("str", n)
     *************************************************************/
    bool theory_str::is_concat_eq_type3(expr * concatAst1, expr * concatAst2) {
        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        if (u.str.is_string(v1_arg0) && (!u.str.is_string(v1_arg1))
            && (!u.str.is_string(v2_arg0)) && (!u.str.is_string(v2_arg1))) {
            return true;
        } else if (u.str.is_string(v2_arg0) && (!u.str.is_string(v2_arg1))
                   && (!u.str.is_string(v1_arg0)) && (!u.str.is_string(v1_arg1))) {
            return true;
        } else {
            return false;
        }
    }

    void theory_str::process_concat_eq_type3(expr * concatAst1, expr * concatAst2) {
        ast_manager & mgr = get_manager();
        context & ctx = get_context();

        bool overlapAssumptionUsed = false;

        TRACE("str", tout << "process_concat_eq TYPE 3" << std::endl
              << "concatAst1 = " << mk_ismt2_pp(concatAst1, mgr) << std::endl
              << "concatAst2 = " << mk_ismt2_pp(concatAst2, mgr) << std::endl;
              );

        if (!u.str.is_concat(to_app(concatAst1))) {
            TRACE("str", tout << "concatAst1 is not a concat function" << std::endl;);
            return;
        }
        if (!u.str.is_concat(to_app(concatAst2))) {
            TRACE("str", tout << "concatAst2 is not a concat function" << std::endl;);
            return;
        }

        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        expr * x = nullptr;
        expr * y = nullptr;
        expr * strAst = nullptr;
        expr * n = nullptr;

        if (u.str.is_string(v1_arg0) && !u.str.is_string(v2_arg0)) {
            strAst = v1_arg0;
            n = v1_arg1;
            x = v2_arg0;
            y = v2_arg1;
        } else {
            strAst = v2_arg0;
            n = v2_arg1;
            x = v1_arg0;
            y = v1_arg1;
        }

        zstring strValue;
        u.str.is_string(strAst, strValue);

        rational x_len, y_len, str_len, n_len;
        bool x_len_exists = get_len_value(x, x_len);
        bool y_len_exists = get_len_value(y, y_len);
        str_len = rational((unsigned)(strValue.length()));
        bool n_len_exists = get_len_value(n, n_len);

        expr_ref xorFlag(mgr);
        expr_ref temp1(mgr);
        std::pair<expr*, expr*> key1(concatAst1, concatAst2);
        std::pair<expr*, expr*> key2(concatAst2, concatAst1);

        // check the entries in this map to make sure they're still in scope
        // before we use them.

        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry1 = varForBreakConcat.find(key1);
        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry2 = varForBreakConcat.find(key2);

        bool entry1InScope;
        if (entry1 == varForBreakConcat.end()) {
            entry1InScope = false;
        } else {
            if (internal_variable_set.find((entry1->second)[0]) == internal_variable_set.end()
                /* || internal_variable_set.find((entry1->second)[1]) == internal_variable_set.end() */) {
                entry1InScope = false;
            } else {
                entry1InScope = true;
            }
        }

        bool entry2InScope;
        if (entry2 == varForBreakConcat.end()) {
            entry2InScope = false;
        } else {
            if (internal_variable_set.find((entry2->second)[0]) == internal_variable_set.end()
                /* || internal_variable_set.find((entry2->second)[1]) == internal_variable_set.end() */) {
                entry2InScope = false;
            } else {
                entry2InScope = true;
            }
        }

        TRACE("str", tout << "entry 1 " << (entry1InScope ? "in scope" : "not in scope") << std::endl
              << "entry 2 " << (entry2InScope ? "in scope" : "not in scope") << std::endl;);


        if (!entry1InScope && !entry2InScope) {
            temp1 = mk_nonempty_str_var();
            xorFlag = mk_internal_xor_var();

            varForBreakConcat[key1][0] = temp1;
            varForBreakConcat[key1][1] = xorFlag;
        } else {
            if (entry1InScope) {
                temp1 = varForBreakConcat[key1][0];
                xorFlag = varForBreakConcat[key1][1];
            } else if (varForBreakConcat.find(key2) != varForBreakConcat.end()) {
                temp1 = varForBreakConcat[key2][0];
                xorFlag = varForBreakConcat[key2][1];
            }
            refresh_theory_var(temp1);
            add_nonempty_constraint(temp1);
        }



        int splitType = -1;
        if (x_len_exists) {
            if (x_len < str_len)
                splitType = 0;
            else if (x_len == str_len)
                splitType = 1;
            else
                splitType = 2;
        }
        if (splitType == -1 && y_len_exists && n_len_exists) {
            if (y_len > n_len)
                splitType = 0;
            else if (y_len == n_len)
                splitType = 1;
            else
                splitType = 2;
        }

        TRACE("str", tout << "Split type " << splitType << std::endl;);

        // Provide fewer split options when length information is available.
        if (splitType == 0) {
            //   |   x   |    y     |
            //   |  str     |   n   |
            expr_ref_vector litems(mgr);
            litems.push_back(ctx.mk_eq_atom(concatAst1, concatAst2));
            rational prefixLen;
            if (!x_len_exists) {
                prefixLen = str_len - (y_len - n_len);
                litems.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_int(y_len)));
                litems.push_back(ctx.mk_eq_atom(mk_strlen(n), mk_int(n_len)));
            } else {
                prefixLen = x_len;
                litems.push_back(ctx.mk_eq_atom(mk_strlen(x), mk_int(x_len)));
            }
            zstring prefixStr = strValue.extract(0, prefixLen.get_unsigned());
            rational str_sub_prefix = str_len - prefixLen;
            zstring suffixStr = strValue.extract(prefixLen.get_unsigned(), str_sub_prefix.get_unsigned());
            expr_ref prefixAst(mk_string(prefixStr), mgr);
            expr_ref suffixAst(mk_string(suffixStr), mgr);
            expr_ref ax_l(mgr.mk_and(litems.size(), litems.c_ptr()), mgr);

            expr_ref suf_n_concat(mk_concat(suffixAst, n), mgr);
            if (can_two_nodes_eq(x, prefixAst) && can_two_nodes_eq(y, suf_n_concat)) {
                expr_ref_vector r_items(mgr);
                r_items.push_back(ctx.mk_eq_atom(x, prefixAst));
                r_items.push_back(ctx.mk_eq_atom(y, suf_n_concat));

                if (m_params.m_StrongArrangements) {
                    expr_ref ax_strong(ctx.mk_eq_atom(ax_l, mk_and(r_items)), mgr);
                    assert_axiom(ax_strong);
                } else {
                    assert_implication(ax_l, mk_and(r_items));
                }
            } else {
                // negate! It's impossible to split str with these lengths
                TRACE("str", tout << "CONFLICT: Impossible to split str with these lengths." << std::endl;);
                assert_axiom(mgr.mk_not(ax_l));
            }
        }
        else if (splitType == 1) {
            expr_ref ax_l1(ctx.mk_eq_atom(concatAst1, concatAst2), mgr);
            expr_ref ax_l2(mgr.mk_or(
                               ctx.mk_eq_atom(mk_strlen(x), mk_strlen(strAst)),
                               ctx.mk_eq_atom(mk_strlen(y), mk_strlen(n))), mgr);
            expr_ref ax_l(mgr.mk_and(ax_l1, ax_l2), mgr);
            expr_ref ax_r(mgr.mk_and(ctx.mk_eq_atom(x, strAst), ctx.mk_eq_atom(y, n)), mgr);

            if (m_params.m_StrongArrangements) {
                expr_ref ax_strong(ctx.mk_eq_atom(ax_l, ax_r), mgr);
                assert_axiom(ax_strong);
            } else {
                assert_implication(ax_l, ax_r);
            }
        }
        else if (splitType == 2) {
            //   |   x        |    y     |
            //   |  str   |       n      |
            expr_ref_vector litems(mgr);
            litems.push_back(ctx.mk_eq_atom(concatAst1, concatAst2));
            rational tmpLen;
            if (!x_len_exists) {
                tmpLen = n_len - y_len;
                litems.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_int(y_len)));
                litems.push_back(ctx.mk_eq_atom(mk_strlen(n), mk_int(n_len)));
            } else {
                tmpLen = x_len - str_len;
                litems.push_back(ctx.mk_eq_atom(mk_strlen(x), mk_int(x_len)));
            }
            expr_ref ax_l(mgr.mk_and(litems.size(), litems.c_ptr()), mgr);

            expr_ref str_temp1(mk_concat(strAst, temp1), mgr);
            expr_ref temp1_y(mk_concat(temp1, y), mgr);

            if (can_two_nodes_eq(x, str_temp1)) {
                if (!avoidLoopCut || !(has_self_cut(x, n))) {
                    expr_ref_vector r_items(mgr);
                    r_items.push_back(ctx.mk_eq_atom(x, str_temp1));
                    r_items.push_back(ctx.mk_eq_atom(n, temp1_y));
                    r_items.push_back(ctx.mk_eq_atom(mk_strlen(temp1), mk_int(tmpLen)));
                    expr_ref ax_r(mk_and(r_items), mgr);

                    //Cut Info
                    add_cut_info_merge(temp1, sLevel, x);
                    add_cut_info_merge(temp1, sLevel, n);

                    if (m_params.m_StrongArrangements) {
                        expr_ref ax_strong(ctx.mk_eq_atom(ax_l, ax_r), mgr);
                        assert_axiom(ax_strong);
                    } else {
                        assert_implication(ax_l, ax_r);
                    }
                } else {
                    loopDetected = true;
                    if (m_params.m_FiniteOverlapModels) {
                        expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                        assert_implication(ax_l, tester);
                        add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
                    } else {
                        TRACE("str", tout << "AVOID LOOP: SKIPPED" << std::endl;);
                        TRACE("str", {print_cut_var(x, tout); print_cut_var(n, tout);});

                        if (!overlapAssumptionUsed) {
                            overlapAssumptionUsed = true;
                            assert_implication(ax_l, m_theoryStrOverlapAssumption_term);
                        }
                    }
                }
            }
            //    else {
            //      // negate! It's impossible to split str with these lengths
            //      __debugPrint(logFile, "[Conflict] Negate! It's impossible to split str with these lengths @ %d.\n", __LINE__);
            //      addAxiom(t, Z3_mk_not(ctx, ax_l), __LINE__);
            //    }
        }
        else {
            // Split type -1. We know nothing about the length...

            expr_ref_vector arrangement_disjunction(mgr);

            int pos = 1;
            for (unsigned int i = 0; i <= strValue.length(); i++) {
                zstring part1Str = strValue.extract(0, i);
                zstring part2Str = strValue.extract(i, strValue.length() - i);
                expr_ref cropStr(mk_string(part1Str), mgr);
                expr_ref suffixStr(mk_string(part2Str), mgr);
                expr_ref y_concat(mk_concat(suffixStr, n), mgr);

                if (can_two_nodes_eq(x, cropStr) && can_two_nodes_eq(y, y_concat)) {
                    expr_ref_vector and_item(mgr);
                    // break down option 3-1
                    expr_ref x_eq_str(ctx.mk_eq_atom(x, cropStr), mgr);

                    and_item.push_back(x_eq_str); ++pos;
                    and_item.push_back(ctx.mk_eq_atom(y, y_concat));
                    and_item.push_back(ctx.mk_eq_atom(mk_strlen(x), mk_strlen(cropStr))); ++pos;

                    //        and_item[pos++] = Z3_mk_eq(ctx, or_item[option], Z3_mk_eq(ctx, mk_length(t, y), mk_length(t, y_concat)));
                    // adding length constraint for _ = constStr seems slowing things down.

                    expr_ref option1(mk_and(and_item), mgr);
                    arrangement_disjunction.push_back(option1);
                    double priority;
                    if (i == strValue.length()) {
                        priority = 0.5;
                    } else {
                        priority = 0.1;
                    }
                    add_theory_aware_branching_info(option1, priority, l_true);
                }
            }

            expr_ref strAst_temp1(mk_concat(strAst, temp1), mgr);


            //--------------------------------------------------------
            // x cut n
            //--------------------------------------------------------
            if (can_two_nodes_eq(x, strAst_temp1)) {
                if (!avoidLoopCut || !(has_self_cut(x, n))) {
                    // break down option 3-2
                    expr_ref_vector and_item(mgr);

                    expr_ref temp1_y(mk_concat(temp1, y), mgr);
                    and_item.push_back(ctx.mk_eq_atom(x, strAst_temp1)); ++pos;
                    and_item.push_back(ctx.mk_eq_atom(n, temp1_y)); ++pos;

                    and_item.push_back(ctx.mk_eq_atom(mk_strlen(x),
                                                      m_autil.mk_add(mk_strlen(strAst), mk_strlen(temp1)) ) ); ++pos;

                    expr_ref option2(mk_and(and_item), mgr);
                    arrangement_disjunction.push_back(option2);
                    add_theory_aware_branching_info(option2, 0.1, l_true);

                    add_cut_info_merge(temp1, sLevel, x);
                    add_cut_info_merge(temp1, sLevel, n);
                } else {
                    loopDetected = true;
                    if (m_params.m_FiniteOverlapModels) {
                        expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                        arrangement_disjunction.push_back(tester);
                        add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
                    } else {
                        TRACE("str", tout << "AVOID LOOP: SKIPPED." << std::endl;);
                        TRACE("str", {print_cut_var(x, tout); print_cut_var(n, tout);});

                        if (!overlapAssumptionUsed) {
                            overlapAssumptionUsed = true;
                            arrangement_disjunction.push_back(m_theoryStrOverlapAssumption_term);
                        }
                    }
                }
            }


            if (!arrangement_disjunction.empty()) {
                expr_ref implyR(mk_or(arrangement_disjunction), mgr);

                if (m_params.m_StrongArrangements) {
                    expr_ref ax_lhs(ctx.mk_eq_atom(concatAst1, concatAst2), mgr);
                    expr_ref ax_strong(ctx.mk_eq_atom(ax_lhs, implyR), mgr);
                    assert_axiom(ax_strong);
                } else {
                    assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
                }
                generate_mutual_exclusion(arrangement_disjunction);
            } else {
                TRACE("str", tout << "STOP: should not split two eq. concats" << std::endl;);
            }
        }

    }

    /*************************************************************
     * Type 4: concat("str1", y) = concat("str2", n)
     *************************************************************/
    bool theory_str::is_concat_eq_type4(expr * concatAst1, expr * concatAst2) {
        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        if (u.str.is_string(v1_arg0) && (!u.str.is_string(v1_arg1))
            && u.str.is_string(v2_arg0) && (!u.str.is_string(v2_arg1))) {
            return true;
        } else {
            return false;
        }
    }

    void theory_str::process_concat_eq_type4(expr * concatAst1, expr * concatAst2) {
        ast_manager & mgr = get_manager();
        context & ctx = get_context();
        TRACE("str", tout << "process_concat_eq TYPE 4" << std::endl
              << "concatAst1 = " << mk_ismt2_pp(concatAst1, mgr) << std::endl
              << "concatAst2 = " << mk_ismt2_pp(concatAst2, mgr) << std::endl;
              );

        if (!u.str.is_concat(to_app(concatAst1))) {
            TRACE("str", tout << "concatAst1 is not a concat function" << std::endl;);
            return;
        }
        if (!u.str.is_concat(to_app(concatAst2))) {
            TRACE("str", tout << "concatAst2 is not a concat function" << std::endl;);
            return;
        }

        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        expr * str1Ast = v1_arg0;
        expr * y = v1_arg1;
        expr * str2Ast = v2_arg0;
        expr * n = v2_arg1;

        zstring str1Value, str2Value;
        u.str.is_string(str1Ast, str1Value);
        u.str.is_string(str2Ast, str2Value);

        unsigned int str1Len = str1Value.length();
        unsigned int str2Len = str2Value.length();

        int commonLen = (str1Len > str2Len) ? str2Len : str1Len;
        if (str1Value.extract(0, commonLen) != str2Value.extract(0, commonLen)) {
            TRACE("str", tout << "Conflict: " << mk_ismt2_pp(concatAst1, mgr)
                  << " has no common prefix with " << mk_ismt2_pp(concatAst2, mgr) << std::endl;);
            expr_ref toNegate(mgr.mk_not(ctx.mk_eq_atom(concatAst1, concatAst2)), mgr);
            assert_axiom(toNegate);
            return;
        } else {
            if (str1Len > str2Len) {
                zstring deltaStr = str1Value.extract(str2Len, str1Len - str2Len);
                expr_ref tmpAst(mk_concat(mk_string(deltaStr), y), mgr);
                if (!in_same_eqc(tmpAst, n)) {
                    // break down option 4-1
                    expr_ref implyR(ctx.mk_eq_atom(n, tmpAst), mgr);
                    if (m_params.m_StrongArrangements) {
                        expr_ref ax_strong(ctx.mk_eq_atom( ctx.mk_eq_atom(concatAst1, concatAst2), implyR ), mgr);
                        assert_axiom(ax_strong);
                    } else {
                        assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
                    }
                }
            } else if (str1Len == str2Len) {
                if (!in_same_eqc(n, y)) {
                    //break down option 4-2
                    expr_ref implyR(ctx.mk_eq_atom(n, y), mgr);

                    if (m_params.m_StrongArrangements) {
                        expr_ref ax_strong(ctx.mk_eq_atom( ctx.mk_eq_atom(concatAst1, concatAst2), implyR ), mgr);
                        assert_axiom(ax_strong);
                    } else {
                        assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
                    }
                }
            } else {
                zstring deltaStr = str2Value.extract(str1Len, str2Len - str1Len);
                expr_ref tmpAst(mk_concat(mk_string(deltaStr), n), mgr);
                if (!in_same_eqc(y, tmpAst)) {
                    //break down option 4-3
                    expr_ref implyR(ctx.mk_eq_atom(y, tmpAst), mgr);
                    if (m_params.m_StrongArrangements) {
                        expr_ref ax_strong(ctx.mk_eq_atom( ctx.mk_eq_atom(concatAst1, concatAst2), implyR ), mgr);
                        assert_axiom(ax_strong);
                    } else {
                        assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
                    }
                }
            }
        }
    }

    /*************************************************************
     *  case 5: concat(x, "str1") = concat(m, "str2")
     *************************************************************/
    bool theory_str::is_concat_eq_type5(expr * concatAst1, expr * concatAst2) {
        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        if ((!u.str.is_string(v1_arg0)) && u.str.is_string(v1_arg1)
            && (!u.str.is_string(v2_arg0)) && u.str.is_string(v2_arg1)) {
            return true;
        } else {
            return false;
        }
    }

    void theory_str::process_concat_eq_type5(expr * concatAst1, expr * concatAst2) {
        ast_manager & mgr = get_manager();
        context & ctx = get_context();
        TRACE("str", tout << "process_concat_eq TYPE 5" << std::endl
              << "concatAst1 = " << mk_ismt2_pp(concatAst1, mgr) << std::endl
              << "concatAst2 = " << mk_ismt2_pp(concatAst2, mgr) << std::endl;
              );

        if (!u.str.is_concat(to_app(concatAst1))) {
            TRACE("str", tout << "concatAst1 is not a concat function" << std::endl;);
            return;
        }
        if (!u.str.is_concat(to_app(concatAst2))) {
            TRACE("str", tout << "concatAst2 is not a concat function" << std::endl;);
            return;
        }

        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        expr * x = v1_arg0;
        expr * str1Ast = v1_arg1;
        expr * m = v2_arg0;
        expr * str2Ast = v2_arg1;

        zstring str1Value, str2Value;
        u.str.is_string(str1Ast, str1Value);
        u.str.is_string(str2Ast, str2Value);

        unsigned int str1Len = str1Value.length();
        unsigned int str2Len = str2Value.length();

        int cLen = (str1Len > str2Len) ? str2Len : str1Len;
        if (str1Value.extract(str1Len - cLen, cLen) != str2Value.extract(str2Len - cLen, cLen)) {
            TRACE("str", tout << "Conflict: " << mk_ismt2_pp(concatAst1, mgr)
                  << " has no common suffix with " << mk_ismt2_pp(concatAst2, mgr) << std::endl;);
            expr_ref toNegate(mgr.mk_not(ctx.mk_eq_atom(concatAst1, concatAst2)), mgr);
            assert_axiom(toNegate);
            return;
        } else {
            if (str1Len > str2Len) {
                zstring deltaStr = str1Value.extract(0, str1Len - str2Len);
                expr_ref x_deltaStr(mk_concat(x, mk_string(deltaStr)), mgr);
                if (!in_same_eqc(m, x_deltaStr)) {
                    expr_ref implyR(ctx.mk_eq_atom(m, x_deltaStr), mgr);
                    if (m_params.m_StrongArrangements) {
                        expr_ref ax_strong(ctx.mk_eq_atom( ctx.mk_eq_atom(concatAst1, concatAst2), implyR ), mgr);
                        assert_axiom(ax_strong);
                    } else {
                        assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
                    }
                }
            } else if (str1Len == str2Len) {
                // test
                if (!in_same_eqc(x, m)) {
                    expr_ref implyR(ctx.mk_eq_atom(x, m), mgr);
                    if (m_params.m_StrongArrangements) {
                        expr_ref ax_strong(ctx.mk_eq_atom( ctx.mk_eq_atom(concatAst1, concatAst2), implyR ), mgr);
                        assert_axiom(ax_strong);
                    } else {
                        assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
                    }
                }
            } else {
                zstring deltaStr = str2Value.extract(0, str2Len - str1Len);
                expr_ref m_deltaStr(mk_concat(m, mk_string(deltaStr)), mgr);
                if (!in_same_eqc(x, m_deltaStr)) {
                    expr_ref implyR(ctx.mk_eq_atom(x, m_deltaStr), mgr);
                    if (m_params.m_StrongArrangements) {
                        expr_ref ax_strong(ctx.mk_eq_atom( ctx.mk_eq_atom(concatAst1, concatAst2), implyR ), mgr);
                        assert_axiom(ax_strong);
                    } else {
                        assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
                    }
                }
            }
        }
    }

    /*************************************************************
     *  case 6: concat("str1", y) = concat(m, "str2")
     *************************************************************/
    bool theory_str::is_concat_eq_type6(expr * concatAst1, expr * concatAst2) {
        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);

        if (u.str.is_string(v1_arg0) && (!u.str.is_string(v1_arg1))
            && (!u.str.is_string(v2_arg0)) && u.str.is_string(v2_arg1)) {
            return true;
        } else if (u.str.is_string(v2_arg0) && (!u.str.is_string(v2_arg1))
                   && (!u.str.is_string(v1_arg0)) && u.str.is_string(v1_arg1)) {
            return true;
        } else {
            return false;
        }
    }

    void theory_str::process_concat_eq_type6(expr * concatAst1, expr * concatAst2) {
        ast_manager & mgr = get_manager();
        context & ctx = get_context();
        TRACE("str", tout << "process_concat_eq TYPE 6" << std::endl
              << "concatAst1 = " << mk_ismt2_pp(concatAst1, mgr) << std::endl
              << "concatAst2 = " << mk_ismt2_pp(concatAst2, mgr) << std::endl;
              );

        if (!u.str.is_concat(to_app(concatAst1))) {
            TRACE("str", tout << "concatAst1 is not a concat function" << std::endl;);
            return;
        }
        if (!u.str.is_concat(to_app(concatAst2))) {
            TRACE("str", tout << "concatAst2 is not a concat function" << std::endl;);
            return;
        }

        expr * v1_arg0 = to_app(concatAst1)->get_arg(0);
        expr * v1_arg1 = to_app(concatAst1)->get_arg(1);
        expr * v2_arg0 = to_app(concatAst2)->get_arg(0);
        expr * v2_arg1 = to_app(concatAst2)->get_arg(1);


        expr * str1Ast = nullptr;
        expr * y = nullptr;
        expr * m = nullptr;
        expr * str2Ast = nullptr;

        if (u.str.is_string(v1_arg0)) {
            str1Ast = v1_arg0;
            y = v1_arg1;
            m = v2_arg0;
            str2Ast = v2_arg1;
        } else {
            str1Ast = v2_arg0;
            y = v2_arg1;
            m = v1_arg0;
            str2Ast = v1_arg1;
        }

        zstring str1Value, str2Value;
        u.str.is_string(str1Ast, str1Value);
        u.str.is_string(str2Ast, str2Value);

        unsigned int str1Len = str1Value.length();
        unsigned int str2Len = str2Value.length();

        //----------------------------------------
        //(a)  |---str1---|----y----|
        //     |--m--|-----str2-----|
        //
        //(b)  |---str1---|----y----|
        //     |-----m----|--str2---|
        //
        //(c)  |---str1---|----y----|
        //     |------m------|-str2-|
        //----------------------------------------

        std::list<unsigned int> overlapLen;
        overlapLen.push_back(0);

        for (unsigned int i = 1; i <= str1Len && i <= str2Len; i++) {
            if (str1Value.extract(str1Len - i, i) == str2Value.extract(0, i))
                overlapLen.push_back(i);
        }

        //----------------------------------------------------------------
        expr * commonVar = nullptr;
        expr * xorFlag = nullptr;
        std::pair<expr*, expr*> key1(concatAst1, concatAst2);
        std::pair<expr*, expr*> key2(concatAst2, concatAst1);

        // check the entries in this map to make sure they're still in scope
        // before we use them.

        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry1 = varForBreakConcat.find(key1);
        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry2 = varForBreakConcat.find(key2);

        bool entry1InScope;
        if (entry1 == varForBreakConcat.end()) {
            entry1InScope = false;
        } else {
            if (internal_variable_set.find((entry1->second)[0]) == internal_variable_set.end()
                /* || internal_variable_set.find((entry1->second)[1]) == internal_variable_set.end() */) {
                entry1InScope = false;
            } else {
                entry1InScope = true;
            }
        }

        bool entry2InScope;
        if (entry2 == varForBreakConcat.end()) {
            entry2InScope = false;
        } else {
            if (internal_variable_set.find((entry2->second)[0]) == internal_variable_set.end()
                /* || internal_variable_set.find((entry2->second)[1]) == internal_variable_set.end() */) {
                entry2InScope = false;
            } else {
                entry2InScope = true;
            }
        }

        TRACE("str", tout << "entry 1 " << (entry1InScope ? "in scope" : "not in scope") << std::endl
              << "entry 2 " << (entry2InScope ? "in scope" : "not in scope") << std::endl;);

        if (!entry1InScope && !entry2InScope) {
            commonVar = mk_nonempty_str_var();
            xorFlag = mk_internal_xor_var();
            varForBreakConcat[key1][0] = commonVar;
            varForBreakConcat[key1][1] = xorFlag;
        } else {
            if (entry1InScope) {
                commonVar = (entry1->second)[0];
                xorFlag = (entry1->second)[1];
            } else {
                commonVar = (entry2->second)[0];
                xorFlag = (entry2->second)[1];
            }
            refresh_theory_var(commonVar);
            add_nonempty_constraint(commonVar);
        }

        bool overlapAssumptionUsed = false;

        expr_ref_vector arrangement_disjunction(mgr);
        int pos = 1;

        if (!avoidLoopCut || !has_self_cut(m, y)) {
            expr_ref_vector and_item(mgr);

            expr_ref str1_commonVar(mk_concat(str1Ast, commonVar), mgr);
            and_item.push_back(ctx.mk_eq_atom(m, str1_commonVar));
            pos += 1;

            expr_ref commonVar_str2(mk_concat(commonVar, str2Ast), mgr);
            and_item.push_back(ctx.mk_eq_atom(y, commonVar_str2));
            pos += 1;

            and_item.push_back(ctx.mk_eq_atom(mk_strlen(m),
                                              m_autil.mk_add(mk_strlen(str1Ast), mk_strlen(commonVar)) ));
            pos += 1;

            //    addItems[0] = mk_length(t, commonVar);
            //    addItems[1] = mk_length(t, str2Ast);
            //    and_item[pos++] = Z3_mk_eq(ctx, or_item[option], Z3_mk_eq(ctx, mk_length(t, y), Z3_mk_add(ctx, 2, addItems)));

            expr_ref option1(mk_and(and_item), mgr);
            arrangement_disjunction.push_back(option1);
            add_theory_aware_branching_info(option1, 0.1, l_true);
        } else {
            loopDetected = true;

            if (m_params.m_FiniteOverlapModels) {
                expr_ref tester = set_up_finite_model_test(concatAst1, concatAst2);
                arrangement_disjunction.push_back(tester);
                add_theory_aware_branching_info(tester, m_params.m_OverlapTheoryAwarePriority, l_true);
            } else {
                TRACE("str", tout << "AVOID LOOP: SKIPPED." << std::endl;);
                TRACE("str", print_cut_var(m, tout); print_cut_var(y, tout););

                // only add the overlap assumption one time
                if (!overlapAssumptionUsed) {
                    arrangement_disjunction.push_back(m_theoryStrOverlapAssumption_term);
                    overlapAssumptionUsed = true;
                }
            }
        }

        for (unsigned int overLen : overlapLen) {
            zstring prefix = str1Value.extract(0, str1Len - overLen);
            zstring suffix = str2Value.extract(overLen, str2Len - overLen);

            expr_ref_vector and_item(mgr);

            expr_ref prefixAst(mk_string(prefix), mgr);
            expr_ref x_eq_prefix(ctx.mk_eq_atom(m, prefixAst), mgr);
            and_item.push_back(x_eq_prefix);
            pos += 1;

            and_item.push_back(
                ctx.mk_eq_atom(mk_strlen(m), mk_strlen(prefixAst)));
            pos += 1;

            // adding length constraint for _ = constStr seems slowing things down.

            expr_ref suffixAst(mk_string(suffix), mgr);
            expr_ref y_eq_suffix(ctx.mk_eq_atom(y, suffixAst), mgr);
            and_item.push_back(y_eq_suffix);
            pos += 1;

            and_item.push_back(ctx.mk_eq_atom(mk_strlen(y), mk_strlen(suffixAst)));
            pos += 1;

            expr_ref option2(mk_and(and_item), mgr);
            arrangement_disjunction.push_back(option2);
            double priority;
            // prefer the option "str1" = x
            if (prefix == str1Value) {
                priority = 0.5;
            } else {
                priority = 0.1;
            }
            add_theory_aware_branching_info(option2, priority, l_true);
        }

        //  case 6: concat("str1", y) = concat(m, "str2")

        expr_ref implyR(mk_or(arrangement_disjunction), mgr);

        if (m_params.m_StrongArrangements) {
            expr_ref ax_strong(ctx.mk_eq_atom( ctx.mk_eq_atom(concatAst1, concatAst2), implyR ), mgr);
            assert_axiom(ax_strong);
        } else {
            assert_implication(ctx.mk_eq_atom(concatAst1, concatAst2), implyR);
        }
        generate_mutual_exclusion(arrangement_disjunction);
    }

    void theory_str::process_unroll_eq_const_str(expr * unrollFunc, expr * constStr) {
        if (!u.re.is_unroll(to_app(unrollFunc))) {
            return;
        }
        if (!u.str.is_string(constStr)) {
            return;
        }

        expr * funcInUnroll = to_app(unrollFunc)->get_arg(0);
        zstring strValue;
        u.str.is_string(constStr, strValue);

        TRACE("str", tout << "unrollFunc: " << mk_pp(unrollFunc, get_manager()) << std::endl
              << "constStr: " << mk_pp(constStr, get_manager()) << std::endl;);

        if (strValue == "") {
            return;
        }

        if (u.re.is_to_re(to_app(funcInUnroll))) {
            unroll_str2reg_constStr(unrollFunc, constStr);
            return;
        }
    }

    void theory_str::process_concat_eq_unroll(expr * concat, expr * unroll) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        TRACE("str", tout << "concat = " << mk_pp(concat, mgr) << ", unroll = " << mk_pp(unroll, mgr) << std::endl;);

        expr_ref toAssert(mgr);
        expr * _toAssert;

        if (!concat_eq_unroll_ast_map.find(concat, unroll, _toAssert)) {
            expr_ref arg1(to_app(concat)->get_arg(0), mgr);
            expr_ref arg2(to_app(concat)->get_arg(1), mgr);
            expr_ref r1(to_app(unroll)->get_arg(0), mgr);
            expr_ref t1(to_app(unroll)->get_arg(1), mgr);

            expr_ref v1(mk_regex_rep_var(), mgr);
            expr_ref v2(mk_regex_rep_var(), mgr);
            expr_ref v3(mk_regex_rep_var(), mgr);
            expr_ref v4(mk_regex_rep_var(), mgr);
            expr_ref v5(mk_regex_rep_var(), mgr);

            expr_ref t2(mk_unroll_bound_var(), mgr);
            expr_ref t3(mk_unroll_bound_var(), mgr);
            expr_ref emptyStr(mk_string(""), mgr);

            expr_ref unroll1(mk_unroll(r1, t2), mgr);
            expr_ref unroll2(mk_unroll(r1, t3), mgr);

            expr_ref op0(ctx.mk_eq_atom(t1, mk_int(0)), mgr);
            expr_ref op1(m_autil.mk_ge(t1, mk_int(1)), mgr);

            expr_ref_vector op1Items(mgr);
            expr_ref_vector op2Items(mgr);

            op1Items.push_back(ctx.mk_eq_atom(arg1, emptyStr));
            op1Items.push_back(ctx.mk_eq_atom(arg2, emptyStr));
            op1Items.push_back(ctx.mk_eq_atom(mk_strlen(arg1), mk_int(0)));
            op1Items.push_back(ctx.mk_eq_atom(mk_strlen(arg2), mk_int(0)));
            expr_ref opAnd1(ctx.mk_eq_atom(op0, mk_and(op1Items)), mgr);

            expr_ref v1v2(mk_concat(v1, v2), mgr);
            op2Items.push_back(ctx.mk_eq_atom(arg1, v1v2));
            op2Items.push_back(ctx.mk_eq_atom(mk_strlen(arg1), m_autil.mk_add(mk_strlen(v1), mk_strlen(v2))));
            expr_ref v3v4(mk_concat(v3, v4), mgr);
            op2Items.push_back(ctx.mk_eq_atom(arg2, v3v4));
            op2Items.push_back(ctx.mk_eq_atom(mk_strlen(arg2), m_autil.mk_add(mk_strlen(v3), mk_strlen(v4))));

            op2Items.push_back(ctx.mk_eq_atom(v1, unroll1));
            op2Items.push_back(ctx.mk_eq_atom(mk_strlen(v1), mk_strlen(unroll1)));
            op2Items.push_back(ctx.mk_eq_atom(v4, unroll2));
            op2Items.push_back(ctx.mk_eq_atom(mk_strlen(v4), mk_strlen(unroll2)));
            expr_ref v2v3(mk_concat(v2, v3), mgr);
            op2Items.push_back(ctx.mk_eq_atom(v5, v2v3));
            reduce_virtual_regex_in(v5, r1, op2Items);
            op2Items.push_back(ctx.mk_eq_atom(mk_strlen(v5), m_autil.mk_add(mk_strlen(v2), mk_strlen(v3))));
            op2Items.push_back(ctx.mk_eq_atom(m_autil.mk_add(t2, t3), m_autil.mk_add(t1, mk_int(-1))));
            expr_ref opAnd2(ctx.mk_eq_atom(op1, mk_and(op2Items)), mgr);

            toAssert = mgr.mk_and(opAnd1, opAnd2);
            m_trail.push_back(toAssert);
            concat_eq_unroll_ast_map.insert(concat, unroll, toAssert);
        } else {
            toAssert = _toAssert;
        }

        assert_axiom(toAssert);
    }

    void theory_str::unroll_str2reg_constStr(expr * unrollFunc, expr * eqConstStr) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr * str2RegFunc = to_app(unrollFunc)->get_arg(0);
        expr * strInStr2RegFunc = to_app(str2RegFunc)->get_arg(0);
        expr * oriCnt = to_app(unrollFunc)->get_arg(1);

        zstring strValue;
        u.str.is_string(eqConstStr, strValue);
        zstring regStrValue;
        u.str.is_string(strInStr2RegFunc, regStrValue);
        unsigned int strLen = strValue.length();
        unsigned int regStrLen = regStrValue.length();
        SASSERT(regStrLen != 0); // this should never occur -- the case for empty string is handled elsewhere
        unsigned int cnt = strLen / regStrLen;

        expr_ref implyL(ctx.mk_eq_atom(unrollFunc, eqConstStr), m);
        expr_ref implyR1(ctx.mk_eq_atom(oriCnt, mk_int(cnt)), m);
        expr_ref implyR2(ctx.mk_eq_atom(mk_strlen(unrollFunc), mk_int(strLen)), m);
        expr_ref axiomRHS(m.mk_and(implyR1, implyR2), m);
        SASSERT(implyL);
        SASSERT(axiomRHS);
        assert_implication(implyL, axiomRHS);
    }

    /*
     * Look through the equivalence class of n to find a string constant.
     * Return that constant if it is found, and set hasEqcValue to true.
     * Otherwise, return n, and set hasEqcValue to false.
     */

    expr * theory_str::get_eqc_value(expr * n, bool & hasEqcValue) {
        return z3str2_get_eqc_value(n, hasEqcValue);
    }


    // Simulate the behaviour of get_eqc_value() from Z3str2.
    // We only check m_find for a string constant.

    expr * theory_str::z3str2_get_eqc_value(expr * n , bool & hasEqcValue) {
        theory_var curr = get_var(n);
        if (curr != null_theory_var) {
            curr = m_find.find(curr);
            theory_var first = curr;
            do {
                expr* a = get_ast(curr);
                if (u.str.is_string(a)) {
                    hasEqcValue = true;
                    return a;
                }
                curr = m_find.next(curr);
            } 
            while (curr != first && curr != null_theory_var);
        }
        hasEqcValue = false;
        return n;
    }

    // from Z3: theory_seq.cpp

    static theory_mi_arith* get_th_arith(context& ctx, theory_id afid, expr* e) {
        theory* th = ctx.get_theory(afid);
        if (th && ctx.e_internalized(e)) {
            return dynamic_cast<theory_mi_arith*>(th);
        }
        else {
            return nullptr;
        }
    }

    bool theory_str::get_arith_value(expr* e, rational& val) const {
        context& ctx = get_context();
        ast_manager & m = get_manager();
        // safety
        if (!ctx.e_internalized(e)) {
            return false;
        }
        // if an integer constant exists in the eqc, it should be the root
        enode * en_e = ctx.get_enode(e);
        enode * root_e = en_e->get_root();
        if (m_autil.is_numeral(root_e->get_owner(), val) && val.is_int()) {
            TRACE("str", tout << mk_pp(e, m) << " ~= " << mk_pp(root_e->get_owner(), m) << std::endl;);
            return true;
        } else {
            TRACE("str", tout << "root of eqc of " << mk_pp(e, m) << " is not a numeral" << std::endl;);
            return false;
            theory_mi_arith* tha = get_th_arith(ctx, m_autil.get_family_id(), e);
            if (!tha) return false;
            expr_ref val_e(m);
            if (tha->get_value(root_e, val_e) && m_autil.is_numeral(val_e, val) && val.is_int()) return true;
            return false;
        }
    }

    bool theory_str::lower_bound(expr* _e, rational& lo) {
        if (opt_DisableIntegerTheoryIntegration) {
            TRACE("str", tout << "WARNING: integer theory integration disabled" << std::endl;);
            return false;
        }

        context& ctx = get_context();
        ast_manager & m = get_manager();
        theory_mi_arith* tha = get_th_arith(ctx, m_autil.get_family_id(), _e);
        expr_ref _lo(m);
        if (!tha || !tha->get_lower(ctx.get_enode(_e), _lo)) return false;
        return m_autil.is_numeral(_lo, lo) && lo.is_int();
    }

    bool theory_str::upper_bound(expr* _e, rational& hi) {
        if (opt_DisableIntegerTheoryIntegration) {
            TRACE("str", tout << "WARNING: integer theory integration disabled" << std::endl;);
            return false;
        }

        context& ctx = get_context();
        ast_manager & m = get_manager();
        theory_mi_arith* tha = get_th_arith(ctx, m_autil.get_family_id(), _e);
        expr_ref _hi(m);
        if (!tha || !tha->get_upper(ctx.get_enode(_e), _hi)) return false;
        return m_autil.is_numeral(_hi, hi) && hi.is_int();
    }

    bool theory_str::get_len_value(expr* e, rational& val) {
        if (opt_DisableIntegerTheoryIntegration) {
            TRACE("str", tout << "WARNING: integer theory integration disabled" << std::endl;);
            return false;
        }

        context& ctx = get_context();
        ast_manager & m = get_manager();

        theory* th = ctx.get_theory(m_autil.get_family_id());
        if (!th) {
            TRACE("str", tout << "oops, can't get m_autil's theory" << std::endl;);
            return false;
        }
        theory_mi_arith* tha = dynamic_cast<theory_mi_arith*>(th);
        if (!tha) {
            TRACE("str", tout << "oops, can't cast to theory_mi_arith" << std::endl;);
            return false;
        }

        TRACE("str", tout << "checking len value of " << mk_ismt2_pp(e, m) << std::endl;);

        rational val1;
        expr_ref len(m), len_val(m);
        expr* e1, *e2;
        ptr_vector<expr> todo;
        todo.push_back(e);
        val.reset();
        while (!todo.empty()) {
            expr* c = todo.back();
            todo.pop_back();
            if (u.str.is_concat(to_app(c))) {
                e1 = to_app(c)->get_arg(0);
                e2 = to_app(c)->get_arg(1);
                todo.push_back(e1);
                todo.push_back(e2);
            }
            else if (u.str.is_string(to_app(c))) {
                zstring tmp;
                u.str.is_string(to_app(c), tmp);
                unsigned int sl = tmp.length();
                val += rational(sl);
            }
            else {
                len = mk_strlen(c);

                // debugging
                TRACE("str", {
                        tout << mk_pp(len, m) << ":" << std::endl
                             << (ctx.is_relevant(len.get()) ? "relevant" : "not relevant") << std::endl
                             << (ctx.e_internalized(len) ? "internalized" : "not internalized") << std::endl
                            ;
                        if (ctx.e_internalized(len)) {
                            enode * e_len = ctx.get_enode(len);
                            tout << "has " << e_len->get_num_th_vars() << " theory vars" << std::endl;

                            // eqc debugging
                            {
                                tout << "dump equivalence class of " << mk_pp(len, get_manager()) << std::endl;
                                enode * nNode = ctx.get_enode(len);
                                enode * eqcNode = nNode;
                                do {
                                    app * ast = eqcNode->get_owner();
                                    tout << mk_pp(ast, get_manager()) << std::endl;
                                    eqcNode = eqcNode->get_next();
                                } while (eqcNode != nNode);
                            }
                        }
                    });

                if (ctx.e_internalized(len) && get_arith_value(len, val1)) {
                    val += val1;
                    TRACE("str", tout << "integer theory: subexpression " << mk_ismt2_pp(len, m) << " has length " << val1 << std::endl;);
                }
                else {
                    TRACE("str", tout << "integer theory: subexpression " << mk_ismt2_pp(len, m) << " has no length assignment; bailing out" << std::endl;);
                    return false;
                }
            }
        }

        TRACE("str", tout << "length of " << mk_ismt2_pp(e, m) << " is " << val << std::endl;);
        return val.is_int();
    }

    /*
     * Decide whether n1 and n2 are already in the same equivalence class.
     * This only checks whether the core considers them to be equal;
     * they may not actually be equal.
     */
    bool theory_str::in_same_eqc(expr * n1, expr * n2) {
        if (n1 == n2) return true;
        context & ctx = get_context();

        // similar to get_eqc_value(), make absolutely sure
        // that we've set this up properly for the context

        if (!ctx.e_internalized(n1)) {
            TRACE("str", tout << "WARNING: expression " << mk_ismt2_pp(n1, get_manager()) << " was not internalized" << std::endl;);
            ctx.internalize(n1, false);
        }
        if (!ctx.e_internalized(n2)) {
            TRACE("str", tout << "WARNING: expression " << mk_ismt2_pp(n2, get_manager()) << " was not internalized" << std::endl;);
            ctx.internalize(n2, false);
        }

        expr * curr = get_eqc_next(n1);
        while (curr != n1) {
            if (curr == n2)
                return true;
            curr = get_eqc_next(curr);
        }
        return false;
    }

    expr * theory_str::collect_eq_nodes(expr * n, expr_ref_vector & eqcSet) {
        expr * constStrNode = nullptr;

        expr * ex = n;
        do {
            if (u.str.is_string(to_app(ex))) {
                constStrNode = ex;
            }
            eqcSet.push_back(ex);

            ex = get_eqc_next(ex);
        } while (ex != n);
        return constStrNode;
    }

    /*
     * Collect constant strings (from left to right) in an AST node.
     */
    void theory_str::get_const_str_asts_in_node(expr * node, expr_ref_vector & astList) {
        if (u.str.is_string(node)) {
            astList.push_back(node);
            //} else if (getNodeType(t, node) == my_Z3_Func) {
        } else if (is_app(node)) {
            app * func_app = to_app(node);
            unsigned int argCount = func_app->get_num_args();
            for (unsigned int i = 0; i < argCount; i++) {
                expr * argAst = func_app->get_arg(i);
                get_const_str_asts_in_node(argAst, astList);
            }
        }
    }

    void theory_str::check_contain_by_eqc_val(expr * varNode, expr * constNode) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        TRACE("str", tout << "varNode = " << mk_pp(varNode, m) << ", constNode = " << mk_pp(constNode, m) << std::endl;);

        expr_ref_vector litems(m);

        if (contain_pair_idx_map.contains(varNode)) {
            for (auto entry : contain_pair_idx_map[varNode]) {
                expr * strAst = entry.first;
                expr * substrAst = entry.second;

                expr * boolVar = nullptr;
                if (!contain_pair_bool_map.find(strAst, substrAst, boolVar)) {
                    TRACE("str", tout << "warning: no entry for boolVar in contain_pair_bool_map" << std::endl;);
                }

                // we only want to inspect the Contains terms where either of strAst or substrAst
                // are equal to varNode.

                TRACE("t_str_detail", tout << "considering Contains with strAst = " << mk_pp(strAst, m) << ", substrAst = " << mk_pp(substrAst, m) << "..." << std::endl;);

                if (varNode != strAst && varNode != substrAst) {
                    TRACE("str", tout << "varNode not equal to strAst or substrAst, skip" << std::endl;);
                    continue;
                }
                TRACE("str", tout << "varNode matched one of strAst or substrAst. Continuing" << std::endl;);

                // varEqcNode is str
                if (strAst == varNode) {
                    expr_ref implyR(m);
                    litems.reset();

                    if (strAst != constNode) {
                        litems.push_back(ctx.mk_eq_atom(strAst, constNode));
                    }
                    zstring strConst;
                    u.str.is_string(constNode, strConst);
                    bool subStrHasEqcValue = false;
                    expr * substrValue = get_eqc_value(substrAst, subStrHasEqcValue);
                    if (substrValue != substrAst) {
                        litems.push_back(ctx.mk_eq_atom(substrAst, substrValue));
                    }

                    if (subStrHasEqcValue) {
                        // subStr has an eqc constant value
                        zstring subStrConst;
                        u.str.is_string(substrValue, subStrConst);

                        TRACE("t_str_detail", tout << "strConst = " << strConst << ", subStrConst = " << subStrConst << "\n";);

                        if (strConst.contains(subStrConst)) {
                            //implyR = ctx.mk_eq(ctx, boolVar, Z3_mk_true(ctx));
                            implyR = boolVar;
                        } else {
                            //implyR = Z3_mk_eq(ctx, boolVar, Z3_mk_false(ctx));
                            implyR = mk_not(m, boolVar);
                        }
                    } else {
                        // ------------------------------------------------------------------------------------------------
                        // subStr doesn't have an eqc contant value
                        // however, subStr equals to some concat(arg_1, arg_2, ..., arg_n)
                        // if arg_j is a constant and is not a part of the strConst, it's sure that the contains is false
                        // ** This check is needed here because the "strConst" and "strAst" may not be in a same eqc yet
                        // ------------------------------------------------------------------------------------------------
                        // collect eqc concat
                        std::set<expr*> eqcConcats;
                        get_concats_in_eqc(substrAst, eqcConcats);
                        for (expr * aConcat : eqcConcats) {
                            expr_ref_vector constList(m);
                            bool counterEgFound = false;
                            get_const_str_asts_in_node(aConcat, constList);
                            for (auto const& cst : constList) {
                                zstring pieceStr;
                                u.str.is_string(cst, pieceStr);
                                if (!strConst.contains(pieceStr)) {
                                    counterEgFound = true;
                                    if (aConcat != substrAst) {
                                        litems.push_back(ctx.mk_eq_atom(substrAst, aConcat));
                                    }
                                    implyR = mk_not(m, boolVar);
                                    break;
                                }
                            }
                            if (counterEgFound) {
                                TRACE("str", tout << "Inconsistency found!" << std::endl;);
                                break;
                            }
                        }
                    }
                    // add assertion
                    if (implyR) {
                        expr_ref implyLHS(mk_and(litems), m);
                        assert_implication(implyLHS, implyR);
                    }
                }
                // varEqcNode is subStr
                else if (substrAst == varNode) {
                    expr_ref implyR(m);
                    litems.reset();

                    if (substrAst != constNode) {
                        litems.push_back(ctx.mk_eq_atom(substrAst, constNode));
                    }
                    bool strHasEqcValue = false;
                    expr * strValue = get_eqc_value(strAst, strHasEqcValue);
                    if (strValue != strAst) {
                        litems.push_back(ctx.mk_eq_atom(strAst, strValue));
                    }

                    if (strHasEqcValue) {
                        zstring strConst, subStrConst;
                        u.str.is_string(strValue, strConst);
                        u.str.is_string(constNode, subStrConst);
                        if (strConst.contains(subStrConst)) {
                            //implyR = Z3_mk_eq(ctx, boolVar, Z3_mk_true(ctx));
                            implyR = boolVar;
                        } else {
                            // implyR = Z3_mk_eq(ctx, boolVar, Z3_mk_false(ctx));
                            implyR = mk_not(m, boolVar);
                        }
                    }

                    // add assertion
                    if (implyR) {
                        expr_ref implyLHS(mk_and(litems), m);
                        assert_implication(implyLHS, implyR);
                    }
                }
            } // for (itor1 : contains_map)
        } // if varNode in contain_pair_idx_map
    }

    void theory_str::check_contain_by_substr(expr * varNode, expr_ref_vector & willEqClass) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        expr_ref_vector litems(m);

        if (contain_pair_idx_map.contains(varNode)) {
            for (auto entry : contain_pair_idx_map[varNode]) {
                expr * strAst = entry.first;
                expr * substrAst = entry.second;

                expr * boolVar = nullptr;
                if (!contain_pair_bool_map.find(strAst, substrAst, boolVar)) {
                    TRACE("str", tout << "warning: no entry for boolVar in contain_pair_bool_map" << std::endl;);
                }

                // we only want to inspect the Contains terms where either of strAst or substrAst
                // are equal to varNode.

                TRACE("t_str_detail", tout << "considering Contains with strAst = " << mk_pp(strAst, m) << ", substrAst = " << mk_pp(substrAst, m) << "..." << std::endl;);

                if (varNode != strAst && varNode != substrAst) {
                    TRACE("str", tout << "varNode not equal to strAst or substrAst, skip" << std::endl;);
                    continue;
                }
                TRACE("str", tout << "varNode matched one of strAst or substrAst. Continuing" << std::endl;);

                if (substrAst == varNode) {
                    bool strAstHasVal = false;
                    expr * strValue = get_eqc_value(strAst, strAstHasVal);
                    if (strAstHasVal) {
                        TRACE("str", tout << mk_pp(strAst, m) << " has constant eqc value " << mk_pp(strValue, m) << std::endl;);
                        if (strValue != strAst) {
                            litems.push_back(ctx.mk_eq_atom(strAst, strValue));
                        }
                        zstring strConst;
                        u.str.is_string(strValue, strConst);
                        // iterate eqc (also eqc-to-be) of substr
                        for (auto itAst : willEqClass) {
                            bool counterEgFound = false;
                            if (u.str.is_concat(to_app(itAst))) {
                                expr_ref_vector constList(m);
                                // get constant strings in concat
                                app * aConcat = to_app(itAst);
                                get_const_str_asts_in_node(aConcat, constList);
                                for (auto cst : constList) {
                                    zstring pieceStr;
                                    u.str.is_string(cst, pieceStr);
                                    if (!strConst.contains(pieceStr)) {
                                        TRACE("str", tout << "Inconsistency found!" << std::endl;);
                                        counterEgFound = true;
                                        if (aConcat != substrAst) {
                                            litems.push_back(ctx.mk_eq_atom(substrAst, aConcat));
                                        }
                                        expr_ref implyLHS(mk_and(litems), m);
                                        expr_ref implyR(mk_not(m, boolVar), m);
                                        assert_implication(implyLHS, implyR);
                                        break;
                                    }
                                }
                            }
                            if (counterEgFound) {
                                break;
                            }
                        }
                    }
                }
            }
        } // varNode in contain_pair_idx_map
    }

    bool theory_str::in_contain_idx_map(expr * n) {
        return contain_pair_idx_map.contains(n);
    }

    void theory_str::check_contain_by_eq_nodes(expr * n1, expr * n2) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        if (in_contain_idx_map(n1) && in_contain_idx_map(n2)) {
            for (auto const& key1 : contain_pair_idx_map[n1]) {
                // keysItor1 is on set {<.., n1>, ..., <n1, ...>, ...}
                //std::pair<expr*, expr*> key1 = *keysItor1;
                if (key1.first == n1 && key1.second == n2) {
                    expr_ref implyL(m);
                    expr_ref implyR(contain_pair_bool_map[key1], m);
                    if (n1 != n2) {
                        implyL = ctx.mk_eq_atom(n1, n2);
                        assert_implication(implyL, implyR);
                    } else {
                        assert_axiom(implyR);
                    }
                }

                //for (keysItor2 = contain_pair_idx_map[n2].begin(); keysItor2 != contain_pair_idx_map[n2].end(); keysItor2++) {
                for (auto const& key2 : contain_pair_idx_map[n2]) {
                    // keysItor2 is on set {<.., n2>, ..., <n2, ...>, ...}
                    //std::pair<expr*, expr*> key2 = *keysItor2;
                    // skip if the pair is eq
                    if (key1 == key2) {
                        continue;
                    }

                    // ***************************
                    // Case 1: Contains(m, ...) /\ Contains(n, ) /\ m = n
                    // ***************************
                    if (key1.first == n1 && key2.first == n2) {
                        expr * subAst1 = key1.second;
                        expr * subAst2 = key2.second;
                        bool subAst1HasValue = false;
                        bool subAst2HasValue = false;
                        expr * subValue1 = get_eqc_value(subAst1, subAst1HasValue);
                        expr * subValue2 = get_eqc_value(subAst2, subAst2HasValue);

                        TRACE("str",
                              tout << "(Contains " << mk_pp(n1, m) << " " << mk_pp(subAst1, m) << ")" << std::endl;
                              tout << "(Contains " << mk_pp(n2, m) << " " << mk_pp(subAst2, m) << ")" << std::endl;
                              if (subAst1 != subValue1) {
                                  tout << mk_pp(subAst1, m) << " = " << mk_pp(subValue1, m) << std::endl;
                              }
                              if (subAst2 != subValue2) {
                                  tout << mk_pp(subAst2, m) << " = " << mk_pp(subValue2, m) << std::endl;
                              }
                              );

                        if (subAst1HasValue && subAst2HasValue) {
                            expr_ref_vector litems1(m);
                            if (n1 != n2) {
                                litems1.push_back(ctx.mk_eq_atom(n1, n2));
                            }
                            if (subValue1 != subAst1) {
                                litems1.push_back(ctx.mk_eq_atom(subAst1, subValue1));
                            }
                            if (subValue2 != subAst2) {
                                litems1.push_back(ctx.mk_eq_atom(subAst2, subValue2));
                            }

                            zstring subConst1, subConst2;
                            u.str.is_string(subValue1, subConst1);
                            u.str.is_string(subValue2, subConst2);
                            expr_ref implyR(m);
                            if (subConst1 == subConst2) {
                                // key1.first = key2.first /\ key1.second = key2.second
                                // ==> (containPairBoolMap[key1] = containPairBoolMap[key2])
                                implyR = ctx.mk_eq_atom(contain_pair_bool_map[key1], contain_pair_bool_map[key2]);
                            } else if (subConst1.contains(subConst2)) {
                                // key1.first = key2.first /\ Contains(key1.second, key2.second)
                                // ==> (containPairBoolMap[key1] --> containPairBoolMap[key2])
                                implyR = rewrite_implication(contain_pair_bool_map[key1], contain_pair_bool_map[key2]);
                            } else if (subConst2.contains(subConst1)) {
                                // key1.first = key2.first /\ Contains(key2.second, key1.second)
                                // ==> (containPairBoolMap[key2] --> containPairBoolMap[key1])
                                implyR = rewrite_implication(contain_pair_bool_map[key2], contain_pair_bool_map[key1]);
                            }

                            if (implyR) {
                                if (litems1.empty()) {
                                    assert_axiom(implyR);
                                } else {
                                    assert_implication(mk_and(litems1), implyR);
                                }
                            }
                        } else {
                            expr_ref_vector subAst1Eqc(m);
                            expr_ref_vector subAst2Eqc(m);
                            collect_eq_nodes(subAst1, subAst1Eqc);
                            collect_eq_nodes(subAst2, subAst2Eqc);

                            if (subAst1Eqc.contains(subAst2)) {
                                // -----------------------------------------------------------
                                // * key1.first = key2.first /\ key1.second = key2.second
                                //   -->  containPairBoolMap[key1] = containPairBoolMap[key2]
                                // -----------------------------------------------------------
                                expr_ref_vector litems2(m);
                                if (n1 != n2) {
                                    litems2.push_back(ctx.mk_eq_atom(n1, n2));
                                }
                                if (subAst1 != subAst2) {
                                    litems2.push_back(ctx.mk_eq_atom(subAst1, subAst2));
                                }
                                expr_ref implyR(ctx.mk_eq_atom(contain_pair_bool_map[key1], contain_pair_bool_map[key2]), m);
                                if (litems2.empty()) {
                                    assert_axiom(implyR);
                                } else {
                                    assert_implication(mk_and(litems2), implyR);
                                }
                            } else {
                                // -----------------------------------------------------------
                                // * key1.first = key2.first
                                //   check eqc(key1.second) and eqc(key2.second)
                                // -----------------------------------------------------------
                                //expr_ref_vector::iterator eqItorSub1 = subAst1Eqc.begin();
                                //for (; eqItorSub1 != subAst1Eqc.end(); eqItorSub1++) {
                                for (auto eqSubVar1 : subAst1Eqc) {
                                    //expr_ref_vector::iterator eqItorSub2 = subAst2Eqc.begin();
                                    //for (; eqItorSub2 != subAst2Eqc.end(); eqItorSub2++) {
                                    for (auto eqSubVar2 : subAst2Eqc) {
                                        // ------------
                                        // key1.first = key2.first /\ containPairBoolMap[<eqc(key1.second), eqc(key2.second)>]
                                        // ==>  (containPairBoolMap[key1] --> containPairBoolMap[key2])
                                        // ------------
                                        {
                                            expr_ref_vector litems3(m);
                                            if (n1 != n2) {
                                                litems3.push_back(ctx.mk_eq_atom(n1, n2));
                                            }

                                            if (eqSubVar1 != subAst1) {
                                                litems3.push_back(ctx.mk_eq_atom(subAst1, eqSubVar1));
                                            }

                                            if (eqSubVar2 != subAst2) {
                                                litems3.push_back(ctx.mk_eq_atom(subAst2, eqSubVar2));
                                            }
                                            std::pair<expr*, expr*> tryKey1 = std::make_pair(eqSubVar1, eqSubVar2);
                                            if (contain_pair_bool_map.contains(tryKey1)) {
                                                TRACE("str", tout << "(Contains " << mk_pp(eqSubVar1, m) << " " << mk_pp(eqSubVar2, m) << ")" << std::endl;);
                                                litems3.push_back(contain_pair_bool_map[tryKey1]);
                                                expr_ref implR(rewrite_implication(contain_pair_bool_map[key1], contain_pair_bool_map[key2]), m);
                                                assert_implication(mk_and(litems3), implR);
                                            }
                                        }
                                        // ------------
                                        // key1.first = key2.first /\ containPairBoolMap[<eqc(key2.second), eqc(key1.second)>]
                                        // ==>  (containPairBoolMap[key2] --> containPairBoolMap[key1])
                                        // ------------
                                        {
                                            expr_ref_vector litems4(m);
                                            if (n1 != n2) {
                                                litems4.push_back(ctx.mk_eq_atom(n1, n2));
                                            }

                                            if (eqSubVar1 != subAst1) {
                                                litems4.push_back(ctx.mk_eq_atom(subAst1, eqSubVar1));
                                            }

                                            if (eqSubVar2 != subAst2) {
                                                litems4.push_back(ctx.mk_eq_atom(subAst2, eqSubVar2));
                                            }
                                            std::pair<expr*, expr*> tryKey2 = std::make_pair(eqSubVar2, eqSubVar1);
                                            if (contain_pair_bool_map.contains(tryKey2)) {
                                                TRACE("str", tout << "(Contains " << mk_pp(eqSubVar2, m) << " " << mk_pp(eqSubVar1, m) << ")" << std::endl;);
                                                litems4.push_back(contain_pair_bool_map[tryKey2]);
                                                expr_ref implR(rewrite_implication(contain_pair_bool_map[key2], contain_pair_bool_map[key1]), m);
                                                assert_implication(mk_and(litems4), implR);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    // ***************************
                    // Case 2: Contains(..., m) /\ Contains(... , n) /\ m = n
                    // ***************************
                    else if (key1.second == n1 && key2.second == n2) {
                        expr * str1 = key1.first;
                        expr * str2 = key2.first;
                        bool str1HasValue = false;
                        bool str2HasValue = false;
                        expr * strVal1 = get_eqc_value(str1, str1HasValue);
                        expr * strVal2 = get_eqc_value(str2, str2HasValue);

                        TRACE("str",
                              tout << "(Contains " << mk_pp(str1, m) << " " << mk_pp(n1, m) << ")" << std::endl;
                              tout << "(Contains " << mk_pp(str2, m) << " " << mk_pp(n2, m) << ")" << std::endl;
                              if (str1 != strVal1) {
                                  tout << mk_pp(str1, m) << " = " << mk_pp(strVal1, m) << std::endl;
                              }
                              if (str2 != strVal2) {
                                  tout << mk_pp(str2, m) << " = " << mk_pp(strVal2, m) << std::endl;
                              }
                              );

                        if (str1HasValue && str2HasValue) {
                            expr_ref_vector litems1(m);
                            if (n1 != n2) {
                                litems1.push_back(ctx.mk_eq_atom(n1, n2));
                            }
                            if (strVal1 != str1) {
                                litems1.push_back(ctx.mk_eq_atom(str1, strVal1));
                            }
                            if (strVal2 != str2) {
                                litems1.push_back(ctx.mk_eq_atom(str2, strVal2));
                            }

                            zstring const1, const2;
                            u.str.is_string(strVal1, const1);
                            u.str.is_string(strVal2, const2);
                            expr_ref implyR(m);

                            if (const1 == const2) {
                                // key1.second = key2.second /\ key1.first = key2.first
                                // ==> (containPairBoolMap[key1] = containPairBoolMap[key2])
                                implyR = ctx.mk_eq_atom(contain_pair_bool_map[key1], contain_pair_bool_map[key2]);
                            } else if (const1.contains(const2)) {
                                // key1.second = key2.second /\ Contains(key1.first, key2.first)
                                // ==> (containPairBoolMap[key2] --> containPairBoolMap[key1])
                                implyR = rewrite_implication(contain_pair_bool_map[key2], contain_pair_bool_map[key1]);
                            } else if (const2.contains(const1)) {
                                // key1.first = key2.first /\ Contains(key2.first, key1.first)
                                // ==> (containPairBoolMap[key1] --> containPairBoolMap[key2])
                                implyR = rewrite_implication(contain_pair_bool_map[key1], contain_pair_bool_map[key2]);
                            }

                            if (implyR) {
                                if (litems1.size() == 0) {
                                    assert_axiom(implyR);
                                } else {
                                    assert_implication(mk_and(litems1), implyR);
                                }
                            }
                        }

                        else {
                            expr_ref_vector str1Eqc(m);
                            expr_ref_vector str2Eqc(m);
                            collect_eq_nodes(str1, str1Eqc);
                            collect_eq_nodes(str2, str2Eqc);

                            if (str1Eqc.contains(str2)) {
                                // -----------------------------------------------------------
                                // * key1.first = key2.first /\ key1.second = key2.second
                                //   -->  containPairBoolMap[key1] = containPairBoolMap[key2]
                                // -----------------------------------------------------------
                                expr_ref_vector litems2(m);
                                if (n1 != n2) {
                                    litems2.push_back(ctx.mk_eq_atom(n1, n2));
                                }
                                if (str1 != str2) {
                                    litems2.push_back(ctx.mk_eq_atom(str1, str2));
                                }
                                expr_ref implyR(ctx.mk_eq_atom(contain_pair_bool_map[key1], contain_pair_bool_map[key2]), m);
                                if (litems2.empty()) {
                                    assert_axiom(implyR);
                                } else {
                                    assert_implication(mk_and(litems2), implyR);
                                }
                            } else {
                                // -----------------------------------------------------------
                                // * key1.second = key2.second
                                //   check eqc(key1.first) and eqc(key2.first)
                                // -----------------------------------------------------------
                                for (auto const& eqStrVar1 : str1Eqc) {
                                    for (auto const& eqStrVar2 : str2Eqc) {
                                        {
                                            expr_ref_vector litems3(m);
                                            if (n1 != n2) {
                                                litems3.push_back(ctx.mk_eq_atom(n1, n2));
                                            }

                                            if (eqStrVar1 != str1) {
                                                litems3.push_back(ctx.mk_eq_atom(str1, eqStrVar1));
                                            }

                                            if (eqStrVar2 != str2) {
                                                litems3.push_back(ctx.mk_eq_atom(str2, eqStrVar2));
                                            }
                                            std::pair<expr*, expr*> tryKey1 = std::make_pair(eqStrVar1, eqStrVar2);
                                            if (contain_pair_bool_map.contains(tryKey1)) {
                                                TRACE("str", tout << "(Contains " << mk_pp(eqStrVar1, m) << " " << mk_pp(eqStrVar2, m) << ")" << std::endl;);
                                                litems3.push_back(contain_pair_bool_map[tryKey1]);

                                                // ------------
                                                // key1.second = key2.second /\ containPairBoolMap[<eqc(key1.first), eqc(key2.first)>]
                                                // ==>  (containPairBoolMap[key2] --> containPairBoolMap[key1])
                                                // ------------
                                                expr_ref implR(rewrite_implication(contain_pair_bool_map[key2], contain_pair_bool_map[key1]), m);
                                                assert_implication(mk_and(litems3), implR);
                                            }
                                        }

                                        {
                                            expr_ref_vector litems4(m);
                                            if (n1 != n2) {
                                                litems4.push_back(ctx.mk_eq_atom(n1, n2));
                                            }
                                            if (eqStrVar1 != str1) {
                                                litems4.push_back(ctx.mk_eq_atom(str1, eqStrVar1));
                                            }
                                            if (eqStrVar2 != str2) {
                                                litems4.push_back(ctx.mk_eq_atom(str2, eqStrVar2));
                                            }
                                            std::pair<expr*, expr*> tryKey2 = std::make_pair(eqStrVar2, eqStrVar1);

                                            if (contain_pair_bool_map.contains(tryKey2)) {
                                                TRACE("str", tout << "(Contains " << mk_pp(eqStrVar2, m) << " " << mk_pp(eqStrVar1, m) << ")" << std::endl;);
                                                litems4.push_back(contain_pair_bool_map[tryKey2]);
                                                // ------------
                                                // key1.first = key2.first /\ containPairBoolMap[<eqc(key2.second), eqc(key1.second)>]
                                                // ==>  (containPairBoolMap[key1] --> containPairBoolMap[key2])
                                                // ------------
                                                expr_ref implR(rewrite_implication(contain_pair_bool_map[key1], contain_pair_bool_map[key2]), m);
                                                assert_implication(mk_and(litems4), implR);
                                            }
                                        }
                                    }
                                }
                            }
                        }

                    }
                }

                if (n1 == n2) {
                    break;
                }
            }
        } // (in_contain_idx_map(n1) && in_contain_idx_map(n2))
    }

    void theory_str::check_contain_in_new_eq(expr * n1, expr * n2) {
        if (contains_map.empty()) {
            return;
        }

        ast_manager & m = get_manager();
        TRACE("str", tout << "consistency check for contains wrt. " << mk_pp(n1, m) << " and " << mk_pp(n2, m) << std::endl;);

        expr_ref_vector willEqClass(m);
        expr * constStrAst_1 = collect_eq_nodes(n1, willEqClass);
        expr * constStrAst_2 = collect_eq_nodes(n2, willEqClass);
        expr * constStrAst = (constStrAst_1 != nullptr) ? constStrAst_1 : constStrAst_2;

        TRACE("str", tout << "eqc of n1 is {";
              for (expr * el : willEqClass) {
                  tout << " " << mk_pp(el, m);
              }
              tout << std::endl;
              if (constStrAst == NULL) {
                  tout << "constStrAst = NULL" << std::endl;
              } else {
                  tout << "constStrAst = " << mk_pp(constStrAst, m) << std::endl;
              }
              );

        // step 1: we may have constant values for Contains checks now
        if (constStrAst != nullptr) {
            for (auto a : willEqClass) {
                if (a == constStrAst) {
                    continue;
                }
                check_contain_by_eqc_val(a, constStrAst);
            }
        } else {
            // no concrete value to be put in eqc, solely based on context
            // Check here is used to detected the facts as follows:
            //   * known: contains(Z, Y) /\ Z = "abcdefg" /\ Y = M
            //   * new fact: M = concat(..., "jio", ...)
            // Note that in this branch, either M or concat(..., "jio", ...) has a constant value
            // So, only need to check
            //   * "EQC(M) U EQC(concat(..., "jio", ...))" as substr and
            //   * If strAst registered has an eqc constant in the context
            // -------------------------------------------------------------
            for (auto a : willEqClass) {
                check_contain_by_substr(a, willEqClass);
            }
        }

        // ------------------------------------------
        // step 2: check for b1 = contains(x, m), b2 = contains(y, n)
        //         (1) x = y /\ m = n  ==>  b1 = b2
        //         (2) x = y /\ Contains(const(m), const(n))  ==>  (b1 -> b2)
        //         (3) x = y /\ Contains(const(n), const(m))  ==>  (b2 -> b1)
        //         (4) x = y /\ containPairBoolMap[<eqc(m), eqc(n)>]  ==>  (b1 -> b2)
        //         (5) x = y /\ containPairBoolMap[<eqc(n), eqc(m)>]  ==>  (b2 -> b1)
        //         (6) Contains(const(x), const(y)) /\ m = n  ==>  (b2 -> b1)
        //         (7) Contains(const(y), const(x)) /\ m = n  ==>  (b1 -> b2)
        //         (8) containPairBoolMap[<eqc(x), eqc(y)>] /\ m = n  ==>  (b2 -> b1)
        //         (9) containPairBoolMap[<eqc(y), eqc(x)>] /\ m = n  ==>  (b1 -> b2)
        // ------------------------------------------

        for (auto varAst1 : willEqClass) {
            for (auto varAst2 : willEqClass) {
                check_contain_by_eq_nodes(varAst1, varAst2);
            }
        }
    }

    expr * theory_str::dealias_node(expr * node, std::map<expr*, expr*> & varAliasMap, std::map<expr*, expr*> & concatAliasMap) {
        if (variable_set.find(node) != variable_set.end()) {
            return get_alias_index_ast(varAliasMap, node);
        } else if (u.str.is_concat(to_app(node))) {
            return get_alias_index_ast(concatAliasMap, node);
        }
        return node;
    }

    void theory_str::get_grounded_concats(unsigned depth,
                                          expr* node, std::map<expr*, expr*> & varAliasMap,
                                          std::map<expr*, expr*> & concatAliasMap, std::map<expr*, expr*> & varConstMap,
                                          std::map<expr*, expr*> & concatConstMap, std::map<expr*, std::map<expr*, int> > & varEqConcatMap,
                                          std::map<expr*, std::map<std::vector<expr*>, std::set<expr*> > > & groundedMap) {
        if (u.re.is_unroll(to_app(node))) {
            return;
        }
        // **************************************************
        // first deAlias the node if it is a var or concat
        // **************************************************
        node = dealias_node(node, varAliasMap, concatAliasMap);

        if (groundedMap.find(node) != groundedMap.end()) {
            return;
        }
        IF_VERBOSE(100, verbose_stream() << "concats " << depth << "\n";
                   if (depth > 100) verbose_stream() << mk_pp(node, get_manager()) << "\n";
                   );

        // haven't computed grounded concats for "node" (de-aliased)
        // ---------------------------------------------------------

        context & ctx = get_context();

        // const strings: node is de-aliased
        if (u.str.is_string(node)) {
            std::vector<expr*> concatNodes;
            concatNodes.push_back(node);
            groundedMap[node][concatNodes].clear();   // no condition
        }
        // Concat functions
        else if (u.str.is_concat(to_app(node))) {
            // if "node" equals to a constant string, thenjust push the constant into the concat vector
            // Again "node" has been de-aliased at the very beginning
            if (concatConstMap.find(node) != concatConstMap.end()) {
                std::vector<expr*> concatNodes;
                concatNodes.push_back(concatConstMap[node]);
                groundedMap[node][concatNodes].clear();
                groundedMap[node][concatNodes].insert(ctx.mk_eq_atom(node, concatConstMap[node]));
            }
            // node doesn't have eq constant value. Process its children.
            else {
                // merge arg0 and arg1
                expr * arg0 = to_app(node)->get_arg(0);
                expr * arg1 = to_app(node)->get_arg(1);
                expr * arg0DeAlias = dealias_node(arg0, varAliasMap, concatAliasMap);
                expr * arg1DeAlias = dealias_node(arg1, varAliasMap, concatAliasMap);
                get_grounded_concats(depth + 1, arg0DeAlias, varAliasMap, concatAliasMap, varConstMap, concatConstMap, varEqConcatMap, groundedMap);
                get_grounded_concats(depth + 1, arg1DeAlias, varAliasMap, concatAliasMap, varConstMap, concatConstMap, varEqConcatMap, groundedMap);

                std::map<std::vector<expr*>, std::set<expr*> >::iterator arg0_grdItor = groundedMap[arg0DeAlias].begin();
                std::map<std::vector<expr*>, std::set<expr*> >::iterator arg1_grdItor;
                for (; arg0_grdItor != groundedMap[arg0DeAlias].end(); arg0_grdItor++) {
                    arg1_grdItor = groundedMap[arg1DeAlias].begin();
                    for (; arg1_grdItor != groundedMap[arg1DeAlias].end(); arg1_grdItor++) {
                        std::vector<expr*> ndVec;
                        ndVec.insert(ndVec.end(), arg0_grdItor->first.begin(), arg0_grdItor->first.end());
                        size_t arg0VecSize = arg0_grdItor->first.size();
                        size_t arg1VecSize = arg1_grdItor->first.size();
                        if (arg0VecSize > 0 && arg1VecSize > 0 && u.str.is_string(arg0_grdItor->first[arg0VecSize - 1]) && u.str.is_string(arg1_grdItor->first[0])) {
                            ndVec.pop_back();
                            ndVec.push_back(mk_concat(arg0_grdItor->first[arg0VecSize - 1], arg1_grdItor->first[0]));
                            for (size_t i = 1; i < arg1VecSize; i++) {
                                ndVec.push_back(arg1_grdItor->first[i]);
                            }
                        } else {
                            ndVec.insert(ndVec.end(), arg1_grdItor->first.begin(), arg1_grdItor->first.end());
                        }
                        // only insert if we don't know "node = concat(ndVec)" since one set of condition leads to this is enough
                        if (groundedMap[node].find(ndVec) == groundedMap[node].end()) {
                            groundedMap[node][ndVec];
                            if (arg0 != arg0DeAlias) {
                                groundedMap[node][ndVec].insert(ctx.mk_eq_atom(arg0, arg0DeAlias));
                            }
                            groundedMap[node][ndVec].insert(arg0_grdItor->second.begin(), arg0_grdItor->second.end());

                            if (arg1 != arg1DeAlias) {
                                groundedMap[node][ndVec].insert(ctx.mk_eq_atom(arg1, arg1DeAlias));
                            }
                            groundedMap[node][ndVec].insert(arg1_grdItor->second.begin(), arg1_grdItor->second.end());
                        }
                    }
                }
            }
        }
        // string variables
        else if (variable_set.find(node) != variable_set.end()) {
            // deAliasedVar = Constant
            if (varConstMap.find(node) != varConstMap.end()) {
                std::vector<expr*> concatNodes;
                concatNodes.push_back(varConstMap[node]);
                groundedMap[node][concatNodes].clear();
                groundedMap[node][concatNodes].insert(ctx.mk_eq_atom(node, varConstMap[node]));
            }
            // deAliasedVar = someConcat
            else if (varEqConcatMap.find(node) != varEqConcatMap.end()) {
                expr * eqConcat = varEqConcatMap[node].begin()->first;
                expr * deAliasedEqConcat = dealias_node(eqConcat, varAliasMap, concatAliasMap);
                get_grounded_concats(depth + 1, deAliasedEqConcat, varAliasMap, concatAliasMap, varConstMap, concatConstMap, varEqConcatMap, groundedMap);

                std::map<std::vector<expr*>, std::set<expr*> >::iterator grdItor = groundedMap[deAliasedEqConcat].begin();
                for (; grdItor != groundedMap[deAliasedEqConcat].end(); grdItor++) {
                    std::vector<expr*> ndVec;
                    ndVec.insert(ndVec.end(), grdItor->first.begin(), grdItor->first.end());
                    // only insert if we don't know "node = concat(ndVec)" since one set of condition leads to this is enough
                    if (groundedMap[node].find(ndVec) == groundedMap[node].end()) {
                        // condition: node = deAliasedEqConcat
                        groundedMap[node][ndVec].insert(ctx.mk_eq_atom(node, deAliasedEqConcat));
                        // appending conditions for "deAliasedEqConcat = CONCAT(ndVec)"
                        groundedMap[node][ndVec].insert(grdItor->second.begin(), grdItor->second.end());
                    }
                }
            }
            // node (has been de-aliased) != constant && node (has been de-aliased) != any concat
            // just push in the deAliasedVar
            else {
                std::vector<expr*> concatNodes;
                concatNodes.push_back(node);
                groundedMap[node][concatNodes];
            }
        }
    }

    void theory_str::print_grounded_concat(expr * node, std::map<expr*, std::map<std::vector<expr*>, std::set<expr*> > > & groundedMap) {
        TRACE("str", tout << mk_pp(node, get_manager()) << std::endl;);
        if (groundedMap.find(node) != groundedMap.end()) {
            std::map<std::vector<expr*>, std::set<expr*> >::iterator itor = groundedMap[node].begin();
            for (; itor != groundedMap[node].end(); ++itor) {
                TRACE("str",
                      tout << "\t[grounded] ";
                      std::vector<expr*>::const_iterator vIt = itor->first.begin();
                      for (; vIt != itor->first.end(); ++vIt) {
                          tout << mk_pp(*vIt, get_manager()) << ", ";
                      }
                      tout << std::endl;
                      tout << "\t[condition] ";
                      std::set<expr*>::iterator sIt = itor->second.begin();
                      for (; sIt != itor->second.end(); sIt++) {
                          tout << mk_pp(*sIt, get_manager()) << ", ";
                      }
                      tout << std::endl;
                      );
            }
        } else {
            TRACE("str", tout << "not found" << std::endl;);
        }
    }

    bool theory_str::is_partial_in_grounded_concat(const std::vector<expr*> & strVec, const std::vector<expr*> & subStrVec) {
        size_t strCnt = strVec.size();
        size_t subStrCnt = subStrVec.size();

        if (strCnt == 0 || subStrCnt == 0) {
            return false;
        }

        // The assumption is that all consecutive constant strings are merged into one node
        if (strCnt < subStrCnt) {
            return false;
        }

        if (subStrCnt == 1) {
            zstring subStrVal;
            if (u.str.is_string(subStrVec[0], subStrVal)) {
                for (size_t i = 0; i < strCnt; i++) {
                    zstring strVal;
                    if (u.str.is_string(strVec[i], strVal)) {
                        if (strVal.contains(subStrVal)) {
                            return true;
                        }
                    }
                }
            } else {
                for (size_t i = 0; i < strCnt; i++) {
                    if (strVec[i] == subStrVec[0]) {
                        return true;
                    }
                }
            }
            return false;
        } else {
            for (size_t i = 0; i <= (strCnt - subStrCnt); i++) {
                // The first node in subStrVect should be
                //   * constant: a suffix of a note in strVec[i]
                //   * variable:
                bool firstNodesOK = true;
                zstring subStrHeadVal;
                if (u.str.is_string(subStrVec[0], subStrHeadVal)) {
                    zstring strHeadVal;
                    if (u.str.is_string(strVec[i], strHeadVal)) {
                        if (strHeadVal.length() >= subStrHeadVal.length()) {
                            zstring suffix = strHeadVal.extract(strHeadVal.length() - subStrHeadVal.length(), subStrHeadVal.length());
                            if (suffix != subStrHeadVal) {
                                firstNodesOK = false;
                            }
                        } else {
                            firstNodesOK = false;
                        }
                    } else {
                        if (subStrVec[0] != strVec[i]) {
                            firstNodesOK = false;
                        }
                    }
                }
                if (!firstNodesOK) {
                    continue;
                }

                // middle nodes
                bool midNodesOK = true;
                for (size_t j = 1; j < subStrCnt - 1; j++) {
                    if (subStrVec[j] != strVec[i + j]) {
                        midNodesOK = false;
                        break;
                    }
                }
                if (!midNodesOK) {
                    continue;
                }

                // tail nodes
                size_t tailIdx = i + subStrCnt - 1;
                zstring subStrTailVal;
                if (u.str.is_string(subStrVec[subStrCnt - 1], subStrTailVal)) {
                    zstring strTailVal;
                    if (u.str.is_string(strVec[tailIdx], strTailVal)) {
                        if (strTailVal.length() >= subStrTailVal.length()) {
                            zstring prefix = strTailVal.extract(0, subStrTailVal.length());
                            if (prefix == subStrTailVal) {
                                return true;
                            } else {
                                continue;
                            }
                        } else {
                            continue;
                        }
                    }
                } else {
                    if (subStrVec[subStrCnt - 1] == strVec[tailIdx]) {
                        return true;
                    } else {
                        continue;
                    }
                }
            }
            return false;
        }
    }

    void theory_str::check_subsequence(expr* str, expr* strDeAlias, expr* subStr, expr* subStrDeAlias, expr* boolVar,
                                       std::map<expr*, std::map<std::vector<expr*>, std::set<expr*> > > & groundedMap) {

        context & ctx = get_context();
        ast_manager & m = get_manager();
        std::map<std::vector<expr*>, std::set<expr*> >::iterator itorStr = groundedMap[strDeAlias].begin();
        std::map<std::vector<expr*>, std::set<expr*> >::iterator itorSubStr;
        for (; itorStr != groundedMap[strDeAlias].end(); itorStr++) {
            itorSubStr = groundedMap[subStrDeAlias].begin();
            for (; itorSubStr != groundedMap[subStrDeAlias].end(); itorSubStr++) {
                bool contain = is_partial_in_grounded_concat(itorStr->first, itorSubStr->first);
                if (contain) {
                    expr_ref_vector litems(m);
                    if (str != strDeAlias) {
                        litems.push_back(ctx.mk_eq_atom(str, strDeAlias));
                    }
                    if (subStr != subStrDeAlias) {
                        litems.push_back(ctx.mk_eq_atom(subStr, subStrDeAlias));
                    }

                    //litems.insert(itorStr->second.begin(), itorStr->second.end());
                    //litems.insert(itorSubStr->second.begin(), itorSubStr->second.end());
                    for (std::set<expr*>::const_iterator i1 = itorStr->second.begin();
                         i1 != itorStr->second.end(); ++i1) {
                        litems.push_back(*i1);
                    }
                    for (std::set<expr*>::const_iterator i1 = itorSubStr->second.begin();
                         i1 != itorSubStr->second.end(); ++i1) {
                        litems.push_back(*i1);
                    }

                    expr_ref implyR(boolVar, m);

                    if (litems.empty()) {
                        assert_axiom(implyR);
                    } else {
                        expr_ref implyL(mk_and(litems), m);
                        assert_implication(implyL, implyR);
                    }

                }
            }
        }
    }

    void theory_str::compute_contains(std::map<expr*, expr*> & varAliasMap,
                                      std::map<expr*, expr*> & concatAliasMap, std::map<expr*, expr*> & varConstMap,
                                      std::map<expr*, expr*> & concatConstMap, std::map<expr*, std::map<expr*, int> > & varEqConcatMap) {
        std::map<expr*, std::map<std::vector<expr*>, std::set<expr*> > > groundedMap;
        for (auto const& kv : contain_pair_bool_map) {
            expr* containBoolVar = kv.get_value();
            expr* str = kv.get_key1();
            expr* subStr = kv.get_key2();

            expr* strDeAlias = dealias_node(str, varAliasMap, concatAliasMap);
            expr* subStrDeAlias = dealias_node(subStr, varAliasMap, concatAliasMap);

            get_grounded_concats(0, strDeAlias, varAliasMap, concatAliasMap, varConstMap, concatConstMap, varEqConcatMap, groundedMap);
            get_grounded_concats(0, subStrDeAlias, varAliasMap, concatAliasMap, varConstMap, concatConstMap, varEqConcatMap, groundedMap);

            // debugging
            print_grounded_concat(strDeAlias, groundedMap);
            print_grounded_concat(subStrDeAlias, groundedMap);

            check_subsequence(str, strDeAlias, subStr, subStrDeAlias, containBoolVar, groundedMap);
        }
    }

    bool theory_str::can_concat_eq_str(expr * concat, zstring& str) {
        unsigned int strLen = str.length();
        if (u.str.is_concat(to_app(concat))) {
            ptr_vector<expr> args;
            get_nodes_in_concat(concat, args);
            expr * ml_node = args[0];
            expr * mr_node = args[args.size() - 1];

            zstring ml_str;
            if (u.str.is_string(ml_node, ml_str)) {
                unsigned int ml_len = ml_str.length();
                if (ml_len > strLen) {
                    return false;
                }
                unsigned int cLen = ml_len;
                if (ml_str != str.extract(0, cLen)) {
                    return false;
                }
            }

            zstring mr_str;
            if (u.str.is_string(mr_node, mr_str)) {
                unsigned int mr_len = mr_str.length();
                if (mr_len > strLen) {
                    return false;
                }
                unsigned int cLen = mr_len;
                if (mr_str != str.extract(strLen - cLen, cLen)) {
                    return false;
                }
            }

            unsigned int sumLen = 0;
            for (unsigned int i = 0 ; i < args.size() ; i++) {
                expr * oneArg = args[i];
                zstring arg_str;
                if (u.str.is_string(oneArg, arg_str)) {
                    if (!str.contains(arg_str)) {
                        return false;
                    }
                    sumLen += arg_str.length();
                }
            }

            if (sumLen > strLen) {
                return false;
            }
        }
        return true;
    }

    bool theory_str::can_concat_eq_concat(expr * concat1, expr * concat2) {
        if (u.str.is_concat(to_app(concat1)) && u.str.is_concat(to_app(concat2))) {
            {
                // Suppose concat1 = (Concat X Y) and concat2 = (Concat M N).
                expr * concat1_mostL = getMostLeftNodeInConcat(concat1);
                expr * concat2_mostL = getMostLeftNodeInConcat(concat2);
                // if both X and M are constant strings, check whether they have the same prefix
                zstring concat1_mostL_str, concat2_mostL_str;
                if (u.str.is_string(concat1_mostL, concat1_mostL_str) && u.str.is_string(concat2_mostL, concat2_mostL_str)) {
                    unsigned int cLen = std::min(concat1_mostL_str.length(), concat2_mostL_str.length());
                    if (concat1_mostL_str.extract(0, cLen) != concat2_mostL_str.extract(0, cLen)) {
                        return false;
                    }
                }
            }

            {
                // Similarly, if both Y and N are constant strings, check whether they have the same suffix
                expr * concat1_mostR = getMostRightNodeInConcat(concat1);
                expr * concat2_mostR = getMostRightNodeInConcat(concat2);
                zstring concat1_mostR_str, concat2_mostR_str;
                if (u.str.is_string(concat1_mostR, concat1_mostR_str) && u.str.is_string(concat2_mostR, concat2_mostR_str)) {
                    unsigned int cLen = std::min(concat1_mostR_str.length(), concat2_mostR_str.length());
                    if (concat1_mostR_str.extract(concat1_mostR_str.length() - cLen, cLen) !=
                        concat2_mostR_str.extract(concat2_mostR_str.length() - cLen, cLen)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /*
     * Check whether n1 and n2 could be equal.
     * Returns true if n1 could equal n2 (maybe),
     * and false if n1 is definitely not equal to n2 (no).
     */
    bool theory_str::can_two_nodes_eq(expr * n1, expr * n2) {
        app * n1_curr = to_app(n1);
        app * n2_curr = to_app(n2);

        // case 0: n1_curr is const string, n2_curr is const string
        zstring n1_curr_str, n2_curr_str;
        if (u.str.is_string(n1_curr, n1_curr_str) && u.str.is_string(n2_curr, n2_curr_str)) {
            TRACE("str", tout << "checking string constants: n1=" << n1_curr_str << ", n2=" << n2_curr_str << std::endl;);
            if (n1_curr_str == n2_curr_str) {
                // TODO(mtrberzi) potential correction: if n1_curr != n2_curr,
                // assert that these two terms are in fact equal, because they ought to be
                return true;
            } else {
                return false;
            }
        }
        // case 1: n1_curr is concat, n2_curr is const string
        else if (u.str.is_concat(n1_curr) && u.str.is_string(n2_curr)) {
            zstring n2_curr_str;
            u.str.is_string(n2_curr, n2_curr_str);
            if (!can_concat_eq_str(n1_curr, n2_curr_str)) {
                return false;
            }
        }
        // case 2: n2_curr is concat, n1_curr is const string
        else if (u.str.is_concat(n2_curr) && u.str.is_string(n1_curr)) {
            zstring n1_curr_str;
            u.str.is_string(n1_curr, n1_curr_str);
            if (!can_concat_eq_str(n2_curr, n1_curr_str)) {
                return false;
            }
        }
        // case 3: both are concats
        else if (u.str.is_concat(n1_curr) && u.str.is_concat(n2_curr)) {
            if (!can_concat_eq_concat(n1_curr, n2_curr)) {
                return false;
            }
        }

        return true;
    }

    // was checkLength2ConstStr() in Z3str2
    // returns true if everything is OK, or false if inconsistency detected
    // - note that these are different from the semantics in Z3str2
    bool theory_str::check_length_const_string(expr * n1, expr * constStr) {
        ast_manager & mgr = get_manager();
        context & ctx = get_context();

        zstring tmp;
        u.str.is_string(constStr, tmp);
        rational strLen(tmp.length());

        if (u.str.is_concat(to_app(n1))) {
            ptr_vector<expr> args;
            expr_ref_vector items(mgr);

            get_nodes_in_concat(n1, args);

            rational sumLen(0);
            for (unsigned int i = 0; i < args.size(); ++i) {
                rational argLen;
                bool argLen_exists = get_len_value(args[i], argLen);
                if (argLen_exists) {
                    if (!u.str.is_string(args[i])) {
                        items.push_back(ctx.mk_eq_atom(mk_strlen(args[i]), mk_int(argLen)));
                    }
                    TRACE("str", tout << "concat arg: " << mk_pp(args[i], mgr) << " has len = " << argLen.to_string() << std::endl;);
                    sumLen += argLen;
                    if (sumLen > strLen) {
                        items.push_back(ctx.mk_eq_atom(n1, constStr));
                        expr_ref toAssert(mgr.mk_not(mk_and(items)), mgr);
                        TRACE("str", tout << "inconsistent length: concat (len = " << sumLen << ") <==> string constant (len = " << strLen << ")" << std::endl;);
                        assert_axiom(toAssert);
                        return false;
                    }
                }
            }
        } else { // !is_concat(n1)
            rational oLen;
            bool oLen_exists = get_len_value(n1, oLen);
            if (oLen_exists && oLen != strLen) {
                TRACE("str", tout << "inconsistent length: var (len = " << oLen << ") <==> string constant (len = " << strLen << ")" << std::endl;);
                expr_ref l(ctx.mk_eq_atom(n1, constStr), mgr);
                expr_ref r(ctx.mk_eq_atom(mk_strlen(n1), mk_strlen(constStr)), mgr);
                assert_implication(l, r);
                return false;
            }
        }
        rational unused;
        if (get_len_value(n1, unused) == false) {
            expr_ref l(ctx.mk_eq_atom(n1, constStr), mgr);
            expr_ref r(ctx.mk_eq_atom(mk_strlen(n1), mk_strlen(constStr)), mgr);
            assert_implication(l, r);
        }
        return true;
    }

    bool theory_str::check_length_concat_concat(expr * n1, expr * n2) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        ptr_vector<expr> concat1Args;
        ptr_vector<expr> concat2Args;
        get_nodes_in_concat(n1, concat1Args);
        get_nodes_in_concat(n2, concat2Args);

        bool concat1LenFixed = true;
        bool concat2LenFixed = true;

        expr_ref_vector items(mgr);

        rational sum1(0), sum2(0);

        for (unsigned int i = 0; i < concat1Args.size(); ++i) {
            expr * oneArg = concat1Args[i];
            rational argLen;
            bool argLen_exists = get_len_value(oneArg, argLen);
            if (argLen_exists) {
                sum1 += argLen;
                if (!u.str.is_string(oneArg)) {
                    items.push_back(ctx.mk_eq_atom(mk_strlen(oneArg), mk_int(argLen)));
                }
            } else {
                concat1LenFixed = false;
            }
        }

        for (unsigned int i = 0; i < concat2Args.size(); ++i) {
            expr * oneArg = concat2Args[i];
            rational argLen;
            bool argLen_exists = get_len_value(oneArg, argLen);
            if (argLen_exists) {
                sum2 += argLen;
                if (!u.str.is_string(oneArg)) {
                    items.push_back(ctx.mk_eq_atom(mk_strlen(oneArg), mk_int(argLen)));
                }
            } else {
                concat2LenFixed = false;
            }
        }

        items.push_back(ctx.mk_eq_atom(n1, n2));

        bool conflict = false;

        if (concat1LenFixed && concat2LenFixed) {
            if (sum1 != sum2) {
                conflict = true;
            }
        } else if (!concat1LenFixed && concat2LenFixed) {
            if (sum1 > sum2) {
                conflict = true;
            }
        } else if (concat1LenFixed && !concat2LenFixed) {
            if (sum1 < sum2) {
                conflict = true;
            }
        }

        if (conflict) {
            TRACE("str", tout << "inconsistent length detected in concat <==> concat" << std::endl;);
            expr_ref toAssert(mgr.mk_not(mk_and(items)), mgr);
            assert_axiom(toAssert);
            return false;
        }
        return true;
    }

    bool theory_str::check_length_concat_var(expr * concat, expr * var) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        rational varLen;
        bool varLen_exists = get_len_value(var, varLen);
        if (!varLen_exists) {
            return true;
        } else {
            rational sumLen(0);
            ptr_vector<expr> args;
            expr_ref_vector items(mgr);
            get_nodes_in_concat(concat, args);
            for (unsigned int i = 0; i < args.size(); ++i) {
                expr * oneArg = args[i];
                rational argLen;
                bool argLen_exists = get_len_value(oneArg, argLen);
                if (argLen_exists) {
                    if (!u.str.is_string(oneArg) && !argLen.is_zero()) {
                        items.push_back(ctx.mk_eq_atom(mk_strlen(oneArg), mk_int(argLen)));
                    }
                    sumLen += argLen;
                    if (sumLen > varLen) {
                        TRACE("str", tout << "inconsistent length detected in concat <==> var" << std::endl;);
                        items.push_back(ctx.mk_eq_atom(mk_strlen(var), mk_int(varLen)));
                        items.push_back(ctx.mk_eq_atom(concat, var));
                        expr_ref toAssert(mgr.mk_not(mk_and(items)), mgr);
                        assert_axiom(toAssert);
                        return false;
                    }
                }
            }
            return true;
        }
    }

    bool theory_str::check_length_var_var(expr * var1, expr * var2) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        rational var1Len, var2Len;
        bool var1Len_exists = get_len_value(var1, var1Len);
        bool var2Len_exists = get_len_value(var2, var2Len);

        if (var1Len_exists && var2Len_exists && var1Len != var2Len) {
            TRACE("str", tout << "inconsistent length detected in var <==> var" << std::endl;);
            expr_ref_vector items(mgr);
            items.push_back(ctx.mk_eq_atom(mk_strlen(var1), mk_int(var1Len)));
            items.push_back(ctx.mk_eq_atom(mk_strlen(var2), mk_int(var2Len)));
            items.push_back(ctx.mk_eq_atom(var1, var2));
            expr_ref toAssert(mgr.mk_not(mk_and(items)), mgr);
            assert_axiom(toAssert);
            return false;
        }
        return true;
    }

    // returns true if everything is OK, or false if inconsistency detected
    // - note that these are different from the semantics in Z3str2
    bool theory_str::check_length_eq_var_concat(expr * n1, expr * n2) {
        // n1 and n2 are not const string: either variable or concat
        bool n1Concat = u.str.is_concat(to_app(n1));
        bool n2Concat = u.str.is_concat(to_app(n2));
        if (n1Concat && n2Concat) {
            return check_length_concat_concat(n1, n2);
        }
        // n1 is concat, n2 is variable
        else if (n1Concat && (!n2Concat)) {
            return check_length_concat_var(n1, n2);
        }
        // n1 is variable, n2 is concat
        else if ((!n1Concat) && n2Concat) {
            return check_length_concat_var(n2, n1);
        }
        // n1 and n2 are both variables
        else {
            return check_length_var_var(n1, n2);
        }
        return true;
    }

    // returns false if an inconsistency is detected, or true if no inconsistencies were found
    // - note that these are different from the semantics of checkLengConsistency() in Z3str2
    bool theory_str::check_length_consistency(expr * n1, expr * n2) {
        if (u.str.is_string(n1) && u.str.is_string(n2)) {
            // consistency has already been checked in can_two_nodes_eq().
            return true;
        } else if (u.str.is_string(n1) && (!u.str.is_string(n2))) {
            return check_length_const_string(n2, n1);
        } else if (u.str.is_string(n2) && (!u.str.is_string(n1))) {
            return check_length_const_string(n1, n2);
        } else {
            // n1 and n2 are vars or concats
            return check_length_eq_var_concat(n1, n2);
        }
        return true;
    }

    // Modified signature: returns true if nothing was learned, or false if at least one axiom was asserted.
    // (This is used for deferred consistency checking)
    bool theory_str::check_concat_len_in_eqc(expr * concat) {
        bool no_assertions = true;

        expr * eqc_n = concat;
        do {
            if (u.str.is_concat(to_app(eqc_n))) {
                rational unused;
                bool status = infer_len_concat(eqc_n, unused);
                if (status) {
                    no_assertions = false;
                }
            }
            eqc_n = get_eqc_next(eqc_n);
        } while (eqc_n != concat);

        return no_assertions;
    }

    // Convert a regular expression to an e-NFA using Thompson's construction
    void nfa::convert_re(expr * e, unsigned & start, unsigned & end, seq_util & u) {
        start = next_id();
        end = next_id();
        if (u.re.is_to_re(e)) {
            app * a = to_app(e);
            expr * arg_str = a->get_arg(0);
            zstring str;
            if (u.str.is_string(arg_str, str)) {
                if (str.length() == 0) {
                    // transitioning on the empty string is handled specially
                    TRACE("str", tout << "empty string epsilon-move " << start << " --> " << end << std::endl;);
                    make_epsilon_move(start, end);
                } else {
                    TRACE("str", tout << "build NFA for '" << str << "'" << "\n";);
                    /*
                     * For an n-character string, we make (n-1) intermediate states,
                     * labelled i_(0) through i_(n-2).
                     * Then we construct the following transitions:
                     * start --str[0]--> i_(0) --str[1]--> i_(1) --...--> i_(n-2) --str[n-1]--> final
                     */
                    unsigned last = start;
                    for (int i = 0; i <= ((int)str.length()) - 2; ++i) {
                        unsigned i_state = next_id();
                        make_transition(last, str[i], i_state);
                        TRACE("str", tout << "string transition " << last << "--" << str[i] << "--> " << i_state << "\n";);
                        last = i_state;
                    }
                    make_transition(last, str[(str.length() - 1)], end);
                    TRACE("str", tout << "string transition " << last << "--" << str[(str.length() - 1)] << "--> " << end << "\n";);
                }
            } else { // ! u.str.is_string(arg_str, str)
                TRACE("str", tout << "WARNING: invalid string constant in str.to.re! Cancelling." << std::endl;);
                u.get_manager().raise_exception("invalid term in str.to.re, argument must be a string constant");
                m_valid = false;
                return;
            }
        } else if (u.re.is_concat(e)){
            app * a = to_app(e);
            expr * re1 = a->get_arg(0);
            expr * re2 = a->get_arg(1);
            unsigned start1, end1;
            convert_re(re1, start1, end1, u);
            unsigned start2, end2;
            convert_re(re2, start2, end2, u);
            // start --e--> start1 --...--> end1 --e--> start2 --...--> end2 --e--> end
            make_epsilon_move(start, start1);
            make_epsilon_move(end1, start2);
            make_epsilon_move(end2, end);
            TRACE("str", tout << "concat NFA: start = " << start << ", end = " << end << std::endl;);
        } else if (u.re.is_union(e)) {
            app * a = to_app(e);
            expr * re1 = a->get_arg(0);
            expr * re2 = a->get_arg(1);
            unsigned start1, end1;
            convert_re(re1, start1, end1, u);
            unsigned start2, end2;
            convert_re(re2, start2, end2, u);

            // start --e--> start1 ; start --e--> start2
            // end1 --e--> end ; end2 --e--> end
            make_epsilon_move(start, start1);
            make_epsilon_move(start, start2);
            make_epsilon_move(end1, end);
            make_epsilon_move(end2, end);
            TRACE("str", tout << "union NFA: start = " << start << ", end = " << end << std::endl;);
        } else if (u.re.is_star(e)) {
            app * a = to_app(e);
            expr * subex = a->get_arg(0);
            unsigned start_subex, end_subex;
            convert_re(subex, start_subex, end_subex, u);
            // start --e--> start_subex, start --e--> end
            // end_subex --e--> start_subex, end_subex --e--> end
            make_epsilon_move(start, start_subex);
            make_epsilon_move(start, end);
            make_epsilon_move(end_subex, start_subex);
            make_epsilon_move(end_subex, end);
            TRACE("str", tout << "star NFA: start = " << start << ", end = " << end << std::endl;);
        } else if (u.re.is_range(e)) {
            // range('a', 'z')
            // start --'a'--> end
            // start --'b'--> end
            // ...
            // start --'z'--> end
            app * a = to_app(e);
            expr * c1 = a->get_arg(0);
            expr * c2 = a->get_arg(1);
            zstring s_c1, s_c2;
            u.str.is_string(c1, s_c1);
            u.str.is_string(c2, s_c2);

            unsigned int id1 = s_c1[0];
            unsigned int id2 = s_c2[0];
            if (id1 > id2) {
                unsigned int tmp = id1;
                id1 = id2;
                id2 = tmp;
            }

            for (unsigned int i = id1; i <= id2; ++i) {
                char ch = (char)i;
                make_transition(start, ch, end);
            }

            TRACE("str", tout << "range NFA: start = " << start << ", end = " << end << std::endl;);
        } else if (u.re.is_full_seq(e)) {
            // effectively the same as .* where . can be any single character
            // start --e--> tmp
            // tmp --e--> end
            // tmp --C--> tmp for every character C
            unsigned tmp = next_id();
            make_epsilon_move(start, tmp);
            make_epsilon_move(tmp, end);
            for (unsigned int i = 0; i < 256; ++i) {
                char ch = (char)i;
                make_transition(tmp, ch, tmp);
            }
            TRACE("str", tout << "re.all NFA: start = " << start << ", end = " << end << std::endl;);
        } else if (u.re.is_full_char(e)) {
            // effectively . (match any one character)
            for (unsigned int i = 0; i < 256; ++i) {
                char ch = (char)i;
                make_transition(start, ch, end);
            }
            TRACE("str", tout << "re.allchar NFA: start = " << start << ", end = " << end << std::endl;);
        } else {
            TRACE("str", tout << "invalid regular expression" << std::endl;);
            m_valid = false;
            return;
        }
    }

    void nfa::epsilon_closure(unsigned start, std::set<unsigned> & closure) {
        std::deque<unsigned> worklist;
        closure.insert(start);
        worklist.push_back(start);

        while(!worklist.empty()) {
            unsigned state = worklist.front();
            worklist.pop_front();
            if (epsilon_map.find(state) != epsilon_map.end()) {
                for (std::set<unsigned>::iterator it = epsilon_map[state].begin();
                     it != epsilon_map[state].end(); ++it) {
                    unsigned new_state = *it;
                    if (closure.find(new_state) == closure.end()) {
                        closure.insert(new_state);
                        worklist.push_back(new_state);
                    }
                }
            }
        }
    }

    bool nfa::matches(zstring input) {
        /*
         * Keep a set of all states the NFA can currently be in.
         * Initially this is the e-closure of m_start_state
         * For each character A in the input string,
         * the set of next states contains
         * all states in transition_map[S][A] for each S in current_states,
         * and all states in epsilon_map[S] for each S in current_states.
         * After consuming the entire input string,
         * the match is successful iff current_states contains m_end_state.
         */
        std::set<unsigned> current_states;
        epsilon_closure(m_start_state, current_states);
        for (unsigned i = 0; i < input.length(); ++i) {
            char A = (char)input[i];
            std::set<unsigned> next_states;
            for (std::set<unsigned>::iterator it = current_states.begin();
                 it != current_states.end(); ++it) {
                unsigned S = *it;
                // check transition_map
                if (transition_map[S].find(A) != transition_map[S].end()) {
                    next_states.insert(transition_map[S][A]);
                }
            }

            // take e-closure over next_states to compute the actual next_states
            std::set<unsigned> epsilon_next_states;
            for (std::set<unsigned>::iterator it = next_states.begin(); it != next_states.end(); ++it) {
                unsigned S = *it;
                std::set<unsigned> closure;
                epsilon_closure(S, closure);
                epsilon_next_states.insert(closure.begin(), closure.end());
            }
            current_states = epsilon_next_states;
        }
        if (current_states.find(m_end_state) != current_states.end()) {
            return true;
        } else {
            return false;
        }
    }

    void theory_str::check_regex_in(expr * nn1, expr * nn2) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr_ref_vector eqNodeSet(m);

        expr * constStr_1 = collect_eq_nodes(nn1, eqNodeSet);
        expr * constStr_2 = collect_eq_nodes(nn2, eqNodeSet);
        expr * constStr = (constStr_1 != nullptr) ? constStr_1 : constStr_2;

        if (constStr == nullptr) {
            return;
        } else {
            expr_ref_vector::iterator itor = eqNodeSet.begin();
            for (; itor != eqNodeSet.end(); itor++) {
                if (regex_in_var_reg_str_map.contains(*itor)) {
                    std::set<zstring>::iterator strItor = regex_in_var_reg_str_map[*itor].begin();
                    for (; strItor != regex_in_var_reg_str_map[*itor].end(); strItor++) {
                        zstring regStr = *strItor;
                        zstring constStrValue;
                        u.str.is_string(constStr, constStrValue);
                        std::pair<expr*, zstring> key1 = std::make_pair(*itor, regStr);
                        if (regex_in_bool_map.find(key1) != regex_in_bool_map.end()) {
                            expr * boolVar = regex_in_bool_map[key1]; // actually the RegexIn term
                            app * a_regexIn = to_app(boolVar);
                            expr * regexTerm = a_regexIn->get_arg(1);

                            // TODO figure out regex NFA stuff
                            if (!regex_nfa_cache.contains(regexTerm)) {
                                TRACE("str", tout << "regex_nfa_cache: cache miss" << std::endl;);
                                regex_nfa_cache.insert(regexTerm, nfa(u, regexTerm));
                            } else {
                                TRACE("str", tout << "regex_nfa_cache: cache hit" << std::endl;);
                            }

                            nfa regexNFA = regex_nfa_cache[regexTerm];
                            ENSURE(regexNFA.is_valid());
                            bool matchRes = regexNFA.matches(constStrValue);

                            TRACE("str", tout << mk_pp(*itor, m) << " in " << regStr << " : " << (matchRes ? "yes" : "no") << std::endl;);

                            expr_ref implyL(ctx.mk_eq_atom(*itor, constStr), m);
                            if (matchRes) {
                                assert_implication(implyL, boolVar);
                            } else {
                                assert_implication(implyL, mk_not(m, boolVar));
                            }
                        }
                    }
                }
            }
        }
    }

    void theory_str::regex_inc_counter(obj_map<expr, unsigned> & counter_map, expr * key) {
        unsigned old_v;
        if (counter_map.find(key, old_v)) {
            unsigned new_v = old_v += 1;
            counter_map.insert(key, new_v);
        } else {
            counter_map.insert(key, 1);
        }
    }

    unsigned theory_str::regex_get_counter(obj_map<expr, unsigned> & counter_map, expr * key) {
        unsigned v;
        if (counter_map.find(key, v)) {
            return v;
        } else {
            counter_map.insert(key, 0);
            return 0;
        }
    }

    // saturating unsigned addition
    unsigned inline _qadd(unsigned a, unsigned b) {
        if (a == UINT_MAX || b == UINT_MAX) {
            return UINT_MAX;
        }
        unsigned result = a + b;
        if (result < a || result < b) {
            return UINT_MAX;
        }
        return result;
    }

    // saturating unsigned multiply
    unsigned inline _qmul(unsigned a, unsigned b) {
        if (a == UINT_MAX || b == UINT_MAX) {
            return UINT_MAX;
        }
        if (a == 0 || b == 0) {
            return 0;
        }
        unsigned result = a * b;
        if (result < a || result < b) {
            return UINT_MAX;
        }
        return result;
    }

    unsigned theory_str::estimate_regex_complexity(expr * re) {
        ENSURE(u.is_re(re));
        expr * sub1;
        expr * sub2;
        if (u.re.is_to_re(re, sub1)) {
            SASSERT(u.str.is_string(sub1));
            zstring str;
            u.str.is_string(sub1, str);
            return str.length();
        } else if (u.re.is_complement(re, sub1)) {
            return estimate_regex_complexity_under_complement(sub1);
        } else if (u.re.is_concat(re, sub1, sub2)) {
            unsigned cx1 = estimate_regex_complexity(sub1);
            unsigned cx2 = estimate_regex_complexity(sub2);
            return _qadd(cx1, cx2);
        } else if (u.re.is_union(re, sub1, sub2)) {
            unsigned cx1 = estimate_regex_complexity(sub1);
            unsigned cx2 = estimate_regex_complexity(sub2);
            return _qadd(cx1, cx2);
        } else if (u.re.is_star(re, sub1) || u.re.is_plus(re, sub1)) {
            unsigned cx = estimate_regex_complexity(sub1);
            return _qmul(2, cx);
        } else if (u.re.is_range(re, sub1, sub2)) {
            SASSERT(u.str.is_string(sub1));
            SASSERT(u.str.is_string(sub2));
            zstring str1, str2;
            u.str.is_string(sub1, str1);
            u.str.is_string(sub2, str2);
            SASSERT(str1.length() == 1);
            SASSERT(str2.length() == 1);
            return 1 + str2[0] - str1[0];
        } else if (u.re.is_full_char(re) || u.re.is_full_seq(re)) {
            return 1;
        } else {
            TRACE("str", tout << "WARNING: unknown regex term " << mk_pp(re, get_manager()) << std::endl;);
            return 1;
        }
    }

    unsigned theory_str::estimate_regex_complexity_under_complement(expr * re) {
        ENSURE(u.is_re(re));
        expr * sub1;
        expr * sub2;
        if (u.re.is_to_re(re, sub1)) {
            SASSERT(u.str.is_string(sub1));
            zstring str;
            u.str.is_string(sub1, str);
            return str.length();
        } else if (u.re.is_complement(re, sub1)) {
            // Why don't we return the regular complexity here?
            // We could, but this might be called from under another complemented subexpression.
            // It's better to give a worst-case complexity.
            return estimate_regex_complexity_under_complement(sub1);
        } else if (u.re.is_concat(re, sub1, sub2)) {
            unsigned cx1 = estimate_regex_complexity_under_complement(sub1);
            unsigned cx2 = estimate_regex_complexity_under_complement(sub2);
            return _qadd(_qmul(2, cx1), cx2);
        } else if (u.re.is_union(re, sub1, sub2)) {
            unsigned cx1 = estimate_regex_complexity_under_complement(sub1);
            unsigned cx2 = estimate_regex_complexity_under_complement(sub2);
            return _qmul(cx1, cx2);
        } else if (u.re.is_star(re, sub1) || u.re.is_plus(re, sub1)) {
            unsigned cx = estimate_regex_complexity_under_complement(sub1);
            return _qmul(2, cx);
        } else if (u.re.is_range(re, sub1, sub2)) {
            SASSERT(u.str.is_string(sub1));
            SASSERT(u.str.is_string(sub2));
            zstring str1, str2;
            u.str.is_string(sub1, str1);
            u.str.is_string(sub2, str2);
            SASSERT(str1.length() == 1);
            SASSERT(str2.length() == 1);
            return 1 + str2[0] - str1[0];
        } else if (u.re.is_full_char(re) || u.re.is_full_seq(re)) {
            return 1;
        } else {
            TRACE("str", tout << "WARNING: unknown regex term " << mk_pp(re, get_manager()) << std::endl;);
            return 1;
        }
    }

    unsigned theory_str::estimate_automata_intersection_difficulty(eautomaton * aut1, eautomaton * aut2) {
        ENSURE(aut1 != NULL);
        ENSURE(aut2 != NULL);
        return _qmul(aut1->num_states(), aut2->num_states());
    }

    // Check whether a regex translates well to a linear set of length constraints.
    bool theory_str::check_regex_length_linearity(expr * re) {
        return check_regex_length_linearity_helper(re, false);
    }

    bool theory_str::check_regex_length_linearity_helper(expr * re, bool already_star) {
        expr * sub1;
        expr * sub2;
        if (u.re.is_to_re(re)) {
            return true;
        } else if (u.re.is_concat(re, sub1, sub2)) {
            return check_regex_length_linearity_helper(sub1, already_star) && check_regex_length_linearity_helper(sub2, already_star);
        } else if (u.re.is_union(re, sub1, sub2)) {
            return check_regex_length_linearity_helper(sub1, already_star) && check_regex_length_linearity_helper(sub2, already_star);
        } else if (u.re.is_star(re, sub1) || u.re.is_plus(re, sub1)) {
            if (already_star) {
                return false;
            } else {
                return check_regex_length_linearity_helper(sub1, true);
            }
        } else if (u.re.is_range(re)) {
            return true;
        } else if (u.re.is_full_char(re)) {
            return true;
        } else if (u.re.is_full_seq(re)) {
            return true;
        } else if (u.re.is_complement(re)) {
            // TODO can we do better?
            return false;
        } else {
            TRACE("str", tout << "WARNING: unknown regex term " << mk_pp(re, get_manager()) << std::endl;);
            UNREACHABLE(); return false;
        }
    }

    // note: returns an empty set `lens` if something went wrong
    void theory_str::check_subterm_lengths(expr * re, integer_set & lens) {
        expr * sub1;
        expr * sub2;
        if (u.re.is_to_re(re, sub1)) {
            SASSERT(u.str.is_string(sub1));
            zstring(str);
            u.str.is_string(sub1, str);
            lens.insert(str.length());
        } else if (u.re.is_concat(re, sub1, sub2)) {
            integer_set lens_1, lens_2;
            check_subterm_lengths(sub1, lens_1);
            check_subterm_lengths(sub2, lens_2);
            if (lens_1.empty() || lens_2.empty()) {
                lens.reset();
            } else {
                // take all pairwise lengths
                for (integer_set::iterator it1 = lens_1.begin(); it1 != lens_1.end(); ++it1) {
                    for(integer_set::iterator it2 = lens_2.begin(); it2 != lens_2.end(); ++it2) {
                        int l1 = *it1;
                        int l2 = *it2;
                        lens.insert(l1 + l2);
                    }
                }
            }
        } else if (u.re.is_union(re, sub1, sub2)) {
            integer_set lens_1, lens_2;
            check_subterm_lengths(sub1, lens_1);
            check_subterm_lengths(sub2, lens_2);
            if (lens_1.empty() || lens_2.empty()) {
                lens.reset();
            } else {
                // take all possibilities from either side
                for (integer_set::iterator it1 = lens_1.begin(); it1 != lens_1.end(); ++it1) {
                    lens.insert(*it1);
                }
                for (integer_set::iterator it2 = lens_2.begin(); it2 != lens_2.end(); ++it2) {
                    lens.insert(*it2);
                }
            }
        } else if (u.re.is_star(re, sub1) || u.re.is_plus(re, sub1)) {
            // this is bad -- term generation requires this not to appear
            lens.reset();
        } else if (u.re.is_range(re, sub1, sub2)) {
            SASSERT(u.str.is_string(sub1));
            SASSERT(u.str.is_string(sub2));
            zstring str1, str2;
            u.str.is_string(sub1, str1);
            u.str.is_string(sub2, str2);
            SASSERT(str1.length() == 1);
            SASSERT(str2.length() == 1);
            lens.insert(1);
        } else if (u.re.is_full_char(re)) {
            lens.insert(1);
        } else if (u.re.is_full_seq(re)) {
            lens.reset();
        } else if (u.re.is_complement(re)) {
            lens.reset();
        } else {
            TRACE("str", tout << "WARNING: unknown regex term " << mk_pp(re, get_manager()) << std::endl;);
            lens.reset();
        }
    }

    /*
     * Infer all length constraints implied by the given regular expression `re`
     * in order to constrain `lenVar` (which must be of sort Int).
     * This assumes that `re` appears in a positive context.
     * Returns a Boolean formula expressing the appropriate constraints over `lenVar`.
     * In some cases, the returned formula requires one or more free integer variables to be created.
     * These variables are returned in the reference parameter `freeVariables`.
     * Extra assertions should be made for these free variables constraining them to be non-negative.
     */
    expr_ref theory_str::infer_all_regex_lengths(expr * lenVar, expr * re, expr_ref_vector & freeVariables) {
        ENSURE(u.is_re(re));
        context & ctx = get_context();
        ast_manager & m = get_manager();
        expr * sub1;
        expr * sub2;
        if (u.re.is_to_re(re, sub1)) {
            SASSERT(u.str.is_string(sub1));
            zstring str;
            u.str.is_string(sub1, str);
            rational strlen(str.length());
            expr_ref retval(ctx.mk_eq_atom(lenVar, m_autil.mk_numeral(strlen, true)), m);
            return retval;
        } else if (u.re.is_union(re, sub1, sub2)) {
            expr_ref r1 = infer_all_regex_lengths(lenVar, sub1, freeVariables);
            expr_ref r2 = infer_all_regex_lengths(lenVar, sub2, freeVariables);
            expr_ref retval(m.mk_or(r1, r2), m);
            return retval;
        } else if (u.re.is_concat(re, sub1, sub2)) {
            expr * v1 = mk_int_var("rlen1");
            expr * v2 = mk_int_var("rlen2");
            freeVariables.push_back(v1);
            freeVariables.push_back(v2);
            expr_ref r1 = infer_all_regex_lengths(v1, sub1, freeVariables);
            expr_ref r2 = infer_all_regex_lengths(v2, sub2, freeVariables);
            expr_ref_vector finalResult(m);
            finalResult.push_back(ctx.mk_eq_atom(lenVar, m_autil.mk_add(v1, v2)));
            finalResult.push_back(r1);
            finalResult.push_back(r2);
            expr_ref retval(mk_and(finalResult), m);
            return retval;
        } else if (u.re.is_star(re, sub1) || u.re.is_plus(re, sub1)) {
            // stars are generated as a linear combination of all possible subterm lengths;
            // this requires that there are no stars under this one
            /*
            expr * v = mk_int_var("rlen");
            expr * n = mk_int_var("rstar");
            freeVariables.push_back(v);
            freeVariables.push_back(n);
            expr_ref rsub = infer_all_regex_lengths(v, sub1, freeVariables);
            expr_ref_vector finalResult(m);
            finalResult.push_back(rsub);
            finalResult.push_back(ctx.mk_eq_atom(lenVar, m_autil.mk_mul(v, n)));
            expr_ref retval(mk_and(finalResult), m);
            return retval;
            */
            integer_set subterm_lens;
            check_subterm_lengths(sub1, subterm_lens);
            if (subterm_lens.empty()) {
                // somehow generation was impossible
                expr_ref retval(m_autil.mk_ge(lenVar, m_autil.mk_numeral(rational::zero(), true)), m);
                return retval;
            } else {
                TRACE("str", tout << "subterm lengths:";
                for(integer_set::iterator it = subterm_lens.begin(); it != subterm_lens.end(); ++it) {
                    tout << " " << *it;
                }
                tout << std::endl;);
                expr_ref_vector sum_terms(m);
                for (integer_set::iterator it = subterm_lens.begin(); it != subterm_lens.end(); ++it) {
                    rational lenOption(*it);
                    expr * n = mk_int_var("rstar");
                    freeVariables.push_back(n);
                    expr_ref term(m_autil.mk_mul(m_autil.mk_numeral(lenOption, true), n), m);
                    expr_ref term2(term, m);
                    if (u.re.is_plus(re)) {
                        // n effectively starts at 1
                        term2 = m_autil.mk_add(m_autil.mk_numeral(lenOption, true), term);
                    }
                    sum_terms.push_back(term2);
                }
                expr_ref retval(ctx.mk_eq_atom(lenVar, m_autil.mk_add_simplify(sum_terms)), m);
                return retval;
            }
        } else if (u.re.is_range(re, sub1, sub2)) {
            SASSERT(u.str.is_string(sub1));
            SASSERT(u.str.is_string(sub2));
            zstring str1, str2;
            u.str.is_string(sub1, str1);
            u.str.is_string(sub2, str2);
            SASSERT(str1.length() == 1);
            SASSERT(str2.length() == 1);
            expr_ref retval(ctx.mk_eq_atom(lenVar, m_autil.mk_numeral(rational::one(), true)), m);
            return retval;
        } else if (u.re.is_full_char(re)) {
            expr_ref retval(ctx.mk_eq_atom(lenVar, m_autil.mk_numeral(rational::one(), true)), m);
            return retval;
        } else if (u.re.is_full_seq(re)) {
            // match any unbounded string
            expr_ref retval(m_autil.mk_ge(lenVar, m_autil.mk_numeral(rational::zero(), true)), m);
            return retval;
        } else if (u.re.is_complement(re)) {
            // skip complement for now, in general this is difficult to predict
            expr_ref retval(m_autil.mk_ge(lenVar, m_autil.mk_numeral(rational::zero(), true)), m);
            return retval;
        } else {
            TRACE("str", tout << "WARNING: unknown regex term " << mk_pp(re, m) << std::endl;);
            expr_ref retval(m_autil.mk_ge(lenVar, m_autil.mk_numeral(rational::zero(), true)), m);
            return retval;
        }
    }

    /*
     * Assert initial lower and upper bounds for the positive constraint (str in re) corresponding
     * to the automaton `aut`.
     * This asserts a constraint of the form:
     *   str_in_re --> (len(str) ?= 0 OR len(str) >= lb) AND len(str) <= ub
     * where the upper bound clause is omitted if the upper bound doesn't exist
     * and the equality with 0 is based on whether solutions of length 0 are allowed.
     */
    void theory_str::find_automaton_initial_bounds(expr * str_in_re, eautomaton * aut) {
        ENSURE(aut != NULL);
        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr_ref_vector rhs(m);
        expr * str;
        expr * re;
        u.str.is_in_re(str_in_re, str, re);
        expr_ref strlen(mk_strlen(str), m);

        // lower bound first
        rational nonzero_lower_bound;
        bool zero_sol_exists = refine_automaton_lower_bound(aut, rational::zero(), nonzero_lower_bound);
        if (zero_sol_exists) {
            regex_last_lower_bound.insert(str, rational::zero());
            // solution at 0
            if (!nonzero_lower_bound.is_minus_one()) {
                expr_ref rhs1(ctx.mk_eq_atom(strlen, m_autil.mk_numeral(rational::zero(), true)), m);
                expr_ref rhs2(m_autil.mk_ge(strlen, m_autil.mk_numeral(nonzero_lower_bound, true)), m);
                rhs.push_back(m.mk_or(rhs1, rhs2));
            } else {
                // shouldn't happen
                UNREACHABLE();
            }
        } else {
            // no solution at 0
            if (!nonzero_lower_bound.is_minus_one()) {
                regex_last_lower_bound.insert(str, nonzero_lower_bound);
                expr_ref rhs2(m_autil.mk_ge(strlen, m_autil.mk_numeral(nonzero_lower_bound, true)), m);
                rhs.push_back(rhs2);
            } else {
                // shouldn't happen
                UNREACHABLE();
            }
        }
        // TODO upper bound check

        if (!rhs.empty()) {
            expr_ref lhs(str_in_re, m);
            expr_ref _rhs(mk_and(rhs), m);
            assert_implication(lhs, _rhs);
        }
    }

    /*
     * Refine the lower bound on the length of a solution to a given automaton.
     * The method returns TRUE if a solution of length `current_lower_bound` exists,
     * and FALSE otherwise. In addition, the reference parameter `refined_lower_bound`
     * is assigned the length of the shortest solution longer than `current_lower_bound`
     * if it exists, or -1 otherwise.
     */
    bool theory_str::refine_automaton_lower_bound(eautomaton * aut, rational current_lower_bound, rational & refined_lower_bound) {
        ENSURE(aut != NULL);

        if (aut->final_states().size() < 1) {
            // no solutions at all
            refined_lower_bound = rational::minus_one();
            return false;
        }

        // from here we assume that there is a final state reachable from the initial state

        unsigned_vector search_queue;
        // populate search_queue with all states reachable from the epsilon-closure of start state
        aut->get_epsilon_closure(aut->init(), search_queue);

        unsigned search_depth = 0;
        hashtable<unsigned, unsigned_hash, default_eq<unsigned>> next_states;
        unsigned_vector next_search_queue;

        bool found_solution_at_lower_bound = false;

        while (!search_queue.empty()) {
            // if we are at the lower bound, check for final states
            if (search_depth == current_lower_bound.get_unsigned()) {
                for (unsigned_vector::iterator it = search_queue.begin(); it != search_queue.end(); ++it) {
                    unsigned state = *it;
                    if (aut->is_final_state(state)) {
                        found_solution_at_lower_bound = true;
                        break;
                    }
                }
                // end phase 1
                break;
            }
            next_states.reset();
            next_search_queue.clear();
            // move one step along all states
            for (unsigned_vector::iterator it = search_queue.begin(); it != search_queue.end(); ++it) {
                unsigned src = *it;
                eautomaton::moves next_moves;
                aut->get_moves_from(src, next_moves, true);
                for (eautomaton::moves::iterator move_it = next_moves.begin();
                        move_it != next_moves.end(); ++move_it) {
                    unsigned dst = move_it->dst();
                    if (!next_states.contains(dst)) {
                        next_states.insert(dst);
                        next_search_queue.push_back(dst);
                    }
                }
            }
            search_queue.clear();
            search_queue.append(next_search_queue);
            search_depth += 1;
        } // !search_queue.empty()

        // if we got here before reaching the lower bound,
        // there aren't any solutions at or above it, so stop
        if (search_depth < current_lower_bound.get_unsigned()) {
            refined_lower_bound = rational::minus_one();
            return false;
        }

        // phase 2: continue exploring the automaton above the lower bound
        SASSERT(search_depth == current_lower_bound.get_unsigned());

        while (!search_queue.empty()) {
            if (search_depth > current_lower_bound.get_unsigned()) {
                // check if we have found a solution above the lower bound
                for (unsigned_vector::iterator it = search_queue.begin(); it != search_queue.end(); ++it) {
                    unsigned state = *it;
                    if (aut->is_final_state(state)) {
                        // this is a solution at a depth higher than the lower bound
                        refined_lower_bound = rational(search_depth);
                        return found_solution_at_lower_bound;
                    }
                }
            }
            next_states.reset();
            next_search_queue.clear();
            // move one step along all states
            for (unsigned_vector::iterator it = search_queue.begin(); it != search_queue.end(); ++it) {
                unsigned src = *it;
                eautomaton::moves next_moves;
                aut->get_moves_from(src, next_moves, true);
                for (eautomaton::moves::iterator move_it = next_moves.begin();
                        move_it != next_moves.end(); ++move_it) {
                    unsigned dst = move_it->dst();
                    if (!next_states.contains(dst)) {
                        next_states.insert(dst);
                        next_search_queue.push_back(dst);
                    }
                }
            }
            search_queue.clear();
            search_queue.append(next_search_queue);
            search_depth += 1;
        }
        // if we reached this point, we explored the whole automaton and didn't find any
        // solutions above the lower bound
        refined_lower_bound = rational::minus_one();
        return found_solution_at_lower_bound;
    }

    /*
     * Refine the upper bound on the length of a solution to a given automaton.
     * The method returns TRUE if a solution of length `current_upper_bound` exists,
     * and FALSE otherwise. In addition, the reference parameter `refined_upper_bound`
     * is assigned the length of the longest solution shorter than `current_upper_bound`,
     * if a shorter solution exists, or -1 otherwise.
     */
    bool theory_str::refine_automaton_upper_bound(eautomaton * aut, rational current_upper_bound, rational & refined_upper_bound) {
        ENSURE(aut != NULL);

        if (aut->final_states().empty()) {
            // no solutions at all!
            refined_upper_bound = rational::minus_one();
            return false;
        }

        // from here we assume there is a final state reachable from the initial state
        unsigned_vector search_queue;
        // populate search queue with all states reachable from the epsilon-closure of the start state
        aut->get_epsilon_closure(aut->init(), search_queue);

        rational last_solution_depth = rational::minus_one();
        bool found_solution_at_upper_bound = false;

        unsigned search_depth = 0;
        hashtable<unsigned, unsigned_hash, default_eq<unsigned> > next_states;
        unsigned_vector next_search_queue;

        while(!search_queue.empty()) {
            // see if any of the current states are final
            for (unsigned_vector::iterator it = search_queue.begin(); it != search_queue.end(); ++it) {
                unsigned src = *it;
                if (aut->is_final_state(src)) {
                    if (search_depth == current_upper_bound.get_unsigned()) {
                        found_solution_at_upper_bound = true;
                    } else {
                        last_solution_depth = rational(search_depth);
                    }
                    break;
                }
            }

            if (search_depth == current_upper_bound.get_unsigned()) {
                break;
            }

            next_states.reset();
            next_search_queue.clear();
            // move one step along all states
            for (unsigned_vector::iterator it = search_queue.begin(); it != search_queue.end(); ++it) {
                unsigned src = *it;
                eautomaton::moves next_moves;
                aut->get_moves_from(src, next_moves, true);
                for (eautomaton::moves::iterator moves_it = next_moves.begin();
                        moves_it != next_moves.end(); ++moves_it) {
                    unsigned dst = moves_it->dst();
                    if (!next_states.contains(dst)) {
                        next_states.insert(dst);
                        next_search_queue.push_back(dst);
                    }
                }
            }
            search_queue.clear();
            search_queue.append(next_search_queue);
            search_depth += 1;
        } //!search_queue.empty()

        refined_upper_bound = last_solution_depth;
        return found_solution_at_upper_bound;
    }

    void theory_str::aut_path_add_next(u_map<expr*>& next, expr_ref_vector& trail, unsigned idx, expr* cond) {
        expr* acc;
        if (!get_manager().is_true(cond) && next.find(idx, acc)) {
            expr* args[2] = { cond, acc };
            cond = mk_or(get_manager(), 2, args);
        }
        trail.push_back(cond);
        next.insert(idx, cond);
    }

    expr_ref theory_str::aut_path_rewrite_constraint(expr * cond, expr * ch_var) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        bv_util bvu(m);

        expr_ref retval(m);

        rational char_val;
        unsigned int bv_width;

        expr * lhs;
        expr * rhs;

        if (bvu.is_numeral(cond, char_val, bv_width)) {
            SASSERT(char_val.is_nonneg() && char_val.get_unsigned() < 256);
            TRACE("str", tout << "rewrite character constant " << char_val << std::endl;);
            zstring str_const(char_val.get_unsigned());
            retval = u.str.mk_string(str_const);
            return retval;
        } else if (is_var(cond)) {
            TRACE("str", tout << "substitute var" << std::endl;);
            retval = ch_var;
            return retval;
        } else if (m.is_eq(cond, lhs, rhs)) {
            // handle this specially because the sort of the equality will change
            expr_ref new_lhs(aut_path_rewrite_constraint(lhs, ch_var), m);
            SASSERT(new_lhs);
            expr_ref new_rhs(aut_path_rewrite_constraint(rhs, ch_var), m);
            SASSERT(new_rhs);
            retval = ctx.mk_eq_atom(new_lhs, new_rhs);
            return retval;
        } else if (m.is_bool(cond)) {
            TRACE("str", tout << "rewrite boolean term " << mk_pp(cond, m) << std::endl;);
            app * a_cond = to_app(cond);
            expr_ref_vector rewritten_args(m);
            for (unsigned i = 0; i < a_cond->get_num_args(); ++i) {
                expr * argI = a_cond->get_arg(i);
                expr_ref new_arg(aut_path_rewrite_constraint(argI, ch_var), m);
                SASSERT(new_arg);
                rewritten_args.push_back(new_arg);
            }
            retval = m.mk_app(a_cond->get_decl(), rewritten_args.c_ptr());
            TRACE("str", tout << "final rewritten term is " << mk_pp(retval, m) << std::endl;);
            return retval;
        } else {
            TRACE("str", tout << "ERROR: unrecognized automaton path constraint " << mk_pp(cond, m) << ", cannot translate" << std::endl;);
            retval = NULL;
            return retval;
        }
    }

    /*
     * Create finite path constraints for the string variable `str` with respect to the automaton `aut`.
     * The returned expression is the right-hand side of a constraint of the form
     * (str in re) AND (|str| = len) AND (any applicable length assumptions on aut) -> (rhs AND character constraints).
     * The character constraints, which are (str = c0 . c1 . (...) . cn) and (|c0| = 1, ...),
     * are returned in `characterConstraints`.
     */
    expr_ref theory_str::generate_regex_path_constraints(expr * stringTerm, eautomaton * aut, rational lenVal, expr_ref & characterConstraints) {
        ENSURE(aut != NULL);
        context & ctx = get_context();
        ast_manager & m = get_manager();

        if (lenVal.is_zero()) {
            // if any state in the epsilon-closure of the start state is accepting,
            // then the empty string is in this language
            unsigned_vector states;
            bool has_final = false;
            aut->get_epsilon_closure(aut->init(), states);
            for (unsigned i = 0; i < states.size() && !has_final; ++i) {
                has_final = aut->is_final_state(states[i]);
            }
            if (has_final) {
                // empty string is OK, assert axiom
                expr_ref rhs(ctx.mk_eq_atom(stringTerm, mk_string("")), m);
                SASSERT(rhs);
                //regex_automata_assertions.insert(stringTerm, final_axiom);
                //m_trail_stack.push(insert_obj_map<theory_str, expr, expr* >(regex_automata_assertions, stringTerm) );
                return rhs;
            } else {
                // negate -- the empty string isn't in the language
                //expr_ref conflict(m.mk_not(mk_and(toplevel_lhs)), m);
                //assert_axiom(conflict);
                expr_ref conflict(m.mk_false(), m);
                return conflict;
            }
        } // lenVal.is_zero()

        expr_ref_vector pathChars(m);
        expr_ref_vector pathChars_len_constraints(m);

        // reuse character terms over the same string
        if (string_chars.contains(stringTerm)) {
            // find out whether we have enough characters already
            ptr_vector<expr> old_chars;
            string_chars.find(stringTerm, old_chars);
            if (old_chars.size() < lenVal.get_unsigned()) {
                for (unsigned i = old_chars.size(); i < lenVal.get_unsigned(); ++i) {
                    std::stringstream ss;
                    ss << "ch" << i;
                    expr_ref ch(mk_str_var(ss.str()), m);
                    m_trail.push_back(ch);
                    old_chars.push_back(ch);
                }
            }
            string_chars.insert(stringTerm, old_chars);
            // now we're guaranteed to have at least the right number of characters in old_chars
            for (unsigned i = 0; i < lenVal.get_unsigned(); ++i) {
                expr_ref ch(old_chars.get(i), m);
                refresh_theory_var(ch);
                pathChars.push_back(ch);
                pathChars_len_constraints.push_back(ctx.mk_eq_atom(mk_strlen(ch), m_autil.mk_numeral(rational::one(), true)));
            }
        } else {
            ptr_vector<expr> new_chars;
            for (unsigned i = 0; i < lenVal.get_unsigned(); ++i) {
                std::stringstream ss;
                ss << "ch" << i;
                expr_ref ch(mk_str_var(ss.str()), m);
                pathChars.push_back(ch);
                pathChars_len_constraints.push_back(ctx.mk_eq_atom(mk_strlen(ch), m_autil.mk_numeral(rational::one(), true)));
                new_chars.push_back(ch);
            }
            string_chars.insert(stringTerm, new_chars);
        }

        // modification of code in seq_rewriter::mk_str_in_regexp()
        expr_ref_vector trail(m);
        u_map<expr*> maps[2];
        bool select_map = false;
        expr_ref ch(m), cond(m);
        eautomaton::moves mvs;
        maps[0].insert(aut->init(), m.mk_true());
        // is_accepted(a, aut) & some state in frontier is final.
        for (unsigned i = 0; i < lenVal.get_unsigned(); ++i) {
            u_map<expr*>& frontier = maps[select_map];
            u_map<expr*>& next = maps[!select_map];
            select_map = !select_map;
            ch = pathChars.get(i);
            next.reset();
            u_map<expr*>::iterator it = frontier.begin(), end = frontier.end();
            for (; it != end; ++it) {
                mvs.reset();
                unsigned state = it->m_key;
                expr*    acc  = it->m_value;
                aut->get_moves_from(state, mvs, false);
                for (unsigned j = 0; j < mvs.size(); ++j) {
                    eautomaton::move const& mv = mvs[j];
                    SASSERT(mv.t());
                    if (mv.t()->is_char() && m.is_value(mv.t()->get_char())) {
                        // change this to a string constraint
                        expr_ref cond_rhs = aut_path_rewrite_constraint(mv.t()->get_char(), ch);
                        SASSERT(cond_rhs);
                        cond = ctx.mk_eq_atom(ch, cond_rhs);
                        SASSERT(cond);
                        expr * args[2] = {cond, acc};
                        cond = mk_and(m, 2, args);
                        aut_path_add_next(next, trail, mv.dst(), cond);
                    } else if (mv.t()->is_range()) {
                        expr_ref range_lo(mv.t()->get_lo(), m);
                        expr_ref range_hi(mv.t()->get_hi(), m);
                        bv_util bvu(m);

                        rational lo_val, hi_val;
                        unsigned int bv_width;

                        if (bvu.is_numeral(range_lo, lo_val, bv_width) && bvu.is_numeral(range_hi, hi_val, bv_width)) {
                            TRACE("str", tout << "make range predicate from " << lo_val << " to " << hi_val << std::endl;);
                            expr_ref cond_rhs(m);

                            if (hi_val < lo_val) {
                                rational tmp = lo_val;
                                lo_val = hi_val;
                                hi_val = tmp;
                            }

                            expr_ref_vector cond_rhs_terms(m);
                            for (unsigned i = lo_val.get_unsigned(); i <= hi_val.get_unsigned(); ++i) {
                                zstring str_const(i);
                                expr_ref str_expr(u.str.mk_string(str_const), m);
                                cond_rhs_terms.push_back(ctx.mk_eq_atom(ch, str_expr));
                            }
                            cond_rhs = mk_or(cond_rhs_terms);
                            SASSERT(cond_rhs);
                            expr * args[2] = {cond_rhs, acc};
                            cond = mk_and(m, 2, args);
                            aut_path_add_next(next, trail, mv.dst(), cond);
                        } else {
                            TRACE("str", tout << "warning: non-bitvectors in automaton range predicate" << std::endl;);
                            UNREACHABLE();
                        }
                    } else if (mv.t()->is_pred()) {
                        // rewrite this constraint over string terms
                        expr_ref cond_rhs = aut_path_rewrite_constraint(mv.t()->get_pred(), ch);
                        SASSERT(cond_rhs);

                        if (m.is_false(cond_rhs)) {
                            continue;
                        } else if (m.is_true(cond_rhs)) {
                            aut_path_add_next(next, trail, mv.dst(), acc);
                            continue;
                        }
                        expr * args[2] = {cond_rhs, acc};
                        cond = mk_and(m, 2, args);
                        aut_path_add_next(next, trail, mv.dst(), cond);
                    }
                }
            }
        }
        u_map<expr*> const& frontier = maps[select_map];
        u_map<expr*>::iterator it = frontier.begin(), end = frontier.end();
        expr_ref_vector ors(m);
        for (; it != end; ++it) {
            unsigned_vector states;
            bool has_final = false;
            aut->get_epsilon_closure(it->m_key, states);
            for (unsigned i = 0; i < states.size() && !has_final; ++i) {
                has_final = aut->is_final_state(states[i]);
            }
            if (has_final) {
                ors.push_back(it->m_value);
            }
        }
        expr_ref result(mk_or(ors));
        TRACE("str", tout << "regex path constraint: " << mk_pp(result, m) << "\n";);

        expr_ref concat_rhs(m);
        if (pathChars.size() == 1) {
            concat_rhs = ctx.mk_eq_atom(stringTerm, pathChars.get(0));
        } else {
            expr_ref acc(pathChars.get(0), m);
            for (unsigned i = 1; i < pathChars.size(); ++i) {
                acc = mk_concat(acc, pathChars.get(i));
            }
            concat_rhs = ctx.mk_eq_atom(stringTerm, acc);
        }

        //expr_ref toplevel_rhs(m.mk_and(result, mk_and(pathChars_len_constraints), concat_rhs), m);
        characterConstraints = m.mk_and(mk_and(pathChars_len_constraints), concat_rhs);
        //expr_ref final_axiom(rewrite_implication(mk_and(toplevel_lhs), toplevel_rhs), m);
        //regex_automata_assertions.insert(stringTerm, final_axiom);
        //m_trail_stack.push(insert_obj_map<theory_str, expr, expr* >(regex_automata_assertions, stringTerm) );
        return result;
    }

    /*
     * strArgmt::solve_concat_eq_str()
     * Solve concatenations of the form:
     *   const == Concat(const, X)
     *   const == Concat(X, const)
     */
    void theory_str::solve_concat_eq_str(expr * concat, expr * str) {
        ast_manager & m = get_manager();
        context & ctx = get_context();

        TRACE("str", tout << mk_ismt2_pp(concat, m) << " == " << mk_ismt2_pp(str, m) << std::endl;);

        zstring const_str;
        if (u.str.is_concat(to_app(concat)) && u.str.is_string(to_app(str), const_str)) {
            app * a_concat = to_app(concat);
            SASSERT(a_concat->get_num_args() == 2);
            expr * a1 = a_concat->get_arg(0);
            expr * a2 = a_concat->get_arg(1);

            if (const_str.empty()) {
                TRACE("str", tout << "quick path: concat == \"\"" << std::endl;);
                // assert the following axiom:
                // ( (Concat a1 a2) == "" ) -> ( (a1 == "") AND (a2 == "") )


                expr_ref premise(ctx.mk_eq_atom(concat, str), m);
                expr_ref c1(ctx.mk_eq_atom(a1, str), m);
                expr_ref c2(ctx.mk_eq_atom(a2, str), m);
                expr_ref conclusion(m.mk_and(c1, c2), m);
                assert_implication(premise, conclusion);

                return;
            }
            bool arg1_has_eqc_value = false;
            bool arg2_has_eqc_value = false;
            expr * arg1 = get_eqc_value(a1, arg1_has_eqc_value);
            expr * arg2 = get_eqc_value(a2, arg2_has_eqc_value);
            expr_ref newConcat(m);
            if (arg1 != a1 || arg2 != a2) {
                TRACE("str", tout << "resolved concat argument(s) to eqc string constants" << std::endl;);
                int iPos = 0;
                expr_ref_vector item1(m);
                if (a1 != arg1) {
                    item1.push_back(ctx.mk_eq_atom(a1, arg1));
                    iPos += 1;
                }
                if (a2 != arg2) {
                    item1.push_back(ctx.mk_eq_atom(a2, arg2));
                    iPos += 1;
                }
                expr_ref implyL1(mk_and(item1), m);
                newConcat = mk_concat(arg1, arg2);
                if (newConcat != str) {
                    expr_ref implyR1(ctx.mk_eq_atom(concat, newConcat), m);
                    assert_implication(implyL1, implyR1);
                }
            } else {
                newConcat = concat;
            }
            if (newConcat == str) {
                return;
            }
            if (!u.str.is_concat(to_app(newConcat))) {
                return;
            }
            if (arg1_has_eqc_value && arg2_has_eqc_value) {
                // Case 1: Concat(const, const) == const
                TRACE("str", tout << "Case 1: Concat(const, const) == const" << std::endl;);
                zstring arg1_str, arg2_str;
                u.str.is_string(arg1, arg1_str);
                u.str.is_string(arg2, arg2_str);

                zstring result_str = arg1_str + arg2_str;
                if (result_str != const_str) {
                    // Inconsistency
                    TRACE("str", tout << "inconsistency detected: \""
                          << arg1_str << "\" + \"" << arg2_str <<
                          "\" != \"" << const_str << "\"" << "\n";);
                    expr_ref equality(ctx.mk_eq_atom(concat, str), m);
                    expr_ref diseq(mk_not(m, equality), m);
                    assert_axiom(diseq);
                    return;
                }
            } else if (!arg1_has_eqc_value && arg2_has_eqc_value) {
                // Case 2: Concat(var, const) == const
                TRACE("str", tout << "Case 2: Concat(var, const) == const" << std::endl;);
                zstring arg2_str;
                u.str.is_string(arg2, arg2_str);
                unsigned int resultStrLen = const_str.length();
                unsigned int arg2StrLen = arg2_str.length();
                if (resultStrLen < arg2StrLen) {
                    // Inconsistency
                    TRACE("str", tout << "inconsistency detected: \""
                          << arg2_str <<
                          "\" is longer than \"" << const_str << "\","
                          << " so cannot be concatenated with anything to form it" << "\n";);
                    expr_ref equality(ctx.mk_eq_atom(newConcat, str), m);
                    expr_ref diseq(mk_not(m, equality), m);
                    assert_axiom(diseq);
                    return;
                } else {
                    int varStrLen = resultStrLen - arg2StrLen;
                    zstring firstPart = const_str.extract(0, varStrLen);
                    zstring secondPart = const_str.extract(varStrLen, arg2StrLen);
                    if (arg2_str != secondPart) {
                        // Inconsistency
                        TRACE("str", tout << "inconsistency detected: "
                              << "suffix of concatenation result expected \"" << secondPart << "\", "
                              << "actually \"" << arg2_str << "\""
                              << "\n";);
                        expr_ref equality(ctx.mk_eq_atom(newConcat, str), m);
                        expr_ref diseq(mk_not(m, equality), m);
                        assert_axiom(diseq);
                        return;
                    } else {
                        expr_ref tmpStrConst(mk_string(firstPart), m);
                        expr_ref premise(ctx.mk_eq_atom(newConcat, str), m);
                        expr_ref conclusion(ctx.mk_eq_atom(arg1, tmpStrConst), m);
                        assert_implication(premise, conclusion);
                        return;
                    }
                }
            } else if (arg1_has_eqc_value && !arg2_has_eqc_value) {
                // Case 3: Concat(const, var) == const
                TRACE("str", tout << "Case 3: Concat(const, var) == const" << std::endl;);
                zstring arg1_str;
                u.str.is_string(arg1, arg1_str);
                unsigned int resultStrLen = const_str.length();
                unsigned int arg1StrLen = arg1_str.length();
                if (resultStrLen < arg1StrLen) {
                    // Inconsistency
                    TRACE("str", tout << "inconsistency detected: \""
                          << arg1_str <<
                          "\" is longer than \"" << const_str << "\","
                          << " so cannot be concatenated with anything to form it" << "\n";);
                    expr_ref equality(ctx.mk_eq_atom(newConcat, str), m);
                    expr_ref diseq(m.mk_not(equality), m);
                    assert_axiom(diseq);
                    return;
                } else {
                    int varStrLen = resultStrLen - arg1StrLen;
                    zstring firstPart = const_str.extract(0, arg1StrLen);
                    zstring secondPart = const_str.extract(arg1StrLen, varStrLen);
                    if (arg1_str != firstPart) {
                        // Inconsistency
                        TRACE("str", tout << "inconsistency detected: "
                              << "prefix of concatenation result expected \"" << secondPart << "\", "
                              << "actually \"" << arg1_str << "\""
                              << "\n";);
                        expr_ref equality(ctx.mk_eq_atom(newConcat, str), m);
                        expr_ref diseq(m.mk_not(equality), m);
                        assert_axiom(diseq);
                        return;
                    } else {
                        expr_ref tmpStrConst(mk_string(secondPart), m);
                        expr_ref premise(ctx.mk_eq_atom(newConcat, str), m);
                        expr_ref conclusion(ctx.mk_eq_atom(arg2, tmpStrConst), m);
                        assert_implication(premise, conclusion);
                        return;
                    }
                }
            } else {
                // Case 4: Concat(var, var) == const
                TRACE("str", tout << "Case 4: Concat(var, var) == const" << std::endl;);
                if (eval_concat(arg1, arg2) == nullptr) {
                    rational arg1Len, arg2Len;
                    bool arg1Len_exists = get_len_value(arg1, arg1Len);
                    bool arg2Len_exists = get_len_value(arg2, arg2Len);
                    rational concatStrLen((unsigned)const_str.length());
                    if (arg1Len_exists || arg2Len_exists) {
                        expr_ref ax_l1(ctx.mk_eq_atom(concat, str), m);
                        expr_ref ax_l2(m);
                        zstring prefixStr, suffixStr;
                        if (arg1Len_exists) {
                            if (arg1Len.is_neg()) {
                                TRACE("str", tout << "length conflict: arg1Len = " << arg1Len << ", concatStrLen = " << concatStrLen << std::endl;);
                                expr_ref toAssert(m_autil.mk_ge(mk_strlen(arg1), mk_int(0)), m);
                                assert_axiom(toAssert);
                                return;
                            } else if (arg1Len > concatStrLen) {
                                TRACE("str", tout << "length conflict: arg1Len = " << arg1Len << ", concatStrLen = " << concatStrLen << std::endl;);
                                expr_ref ax_r1(m_autil.mk_le(mk_strlen(arg1), mk_int(concatStrLen)), m);
                                assert_implication(ax_l1, ax_r1);
                                return;
                            }

                            prefixStr = const_str.extract(0, arg1Len.get_unsigned());
                            rational concat_minus_arg1 = concatStrLen - arg1Len;
                            suffixStr = const_str.extract(arg1Len.get_unsigned(), concat_minus_arg1.get_unsigned());
                            ax_l2 = ctx.mk_eq_atom(mk_strlen(arg1), mk_int(arg1Len));
                        } else {
                            // arg2's length is available
                            if (arg2Len.is_neg()) {
                                TRACE("str", tout << "length conflict: arg2Len = " << arg2Len << ", concatStrLen = " << concatStrLen << std::endl;);
                                expr_ref toAssert(m_autil.mk_ge(mk_strlen(arg2), mk_int(0)), m);
                                assert_axiom(toAssert);
                                return;
                            } else if (arg2Len > concatStrLen) {
                                TRACE("str", tout << "length conflict: arg2Len = " << arg2Len << ", concatStrLen = " << concatStrLen << std::endl;);
                                expr_ref ax_r1(m_autil.mk_le(mk_strlen(arg2), mk_int(concatStrLen)), m);
                                assert_implication(ax_l1, ax_r1);
                                return;
                            }

                            rational concat_minus_arg2 = concatStrLen - arg2Len;
                            prefixStr = const_str.extract(0, concat_minus_arg2.get_unsigned());
                            suffixStr = const_str.extract(concat_minus_arg2.get_unsigned(), arg2Len.get_unsigned());
                            ax_l2 = ctx.mk_eq_atom(mk_strlen(arg2), mk_int(arg2Len));
                        }
                        // consistency check
                        if (u.str.is_concat(to_app(arg1)) && !can_concat_eq_str(arg1, prefixStr)) {
                            expr_ref ax_r(m.mk_not(ax_l2), m);
                            assert_implication(ax_l1, ax_r);
                            return;
                        }
                        if (u.str.is_concat(to_app(arg2)) && !can_concat_eq_str(arg2, suffixStr)) {
                            expr_ref ax_r(m.mk_not(ax_l2), m);
                            assert_implication(ax_l1, ax_r);
                            return;
                        }
                        expr_ref_vector r_items(m);
                        r_items.push_back(ctx.mk_eq_atom(arg1, mk_string(prefixStr)));
                        r_items.push_back(ctx.mk_eq_atom(arg2, mk_string(suffixStr)));
                        if (!arg1Len_exists) {
                            r_items.push_back(ctx.mk_eq_atom(mk_strlen(arg1), mk_int(prefixStr.length())));
                        }
                        if (!arg2Len_exists) {
                            r_items.push_back(ctx.mk_eq_atom(mk_strlen(arg2), mk_int(suffixStr.length())));
                        }
                        expr_ref lhs(m.mk_and(ax_l1, ax_l2), m);
                        expr_ref rhs(mk_and(r_items), m);
                        assert_implication(lhs, rhs);
                    } else { /* ! (arg1Len != 1 || arg2Len != 1) */
                        expr_ref xorFlag(m);
                        std::pair<expr*, expr*> key1(arg1, arg2);
                        std::pair<expr*, expr*> key2(arg2, arg1);

                        // check the entries in this map to make sure they're still in scope
                        // before we use them.

                        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry1 = varForBreakConcat.find(key1);
                        std::map<std::pair<expr*,expr*>, std::map<int, expr*> >::iterator entry2 = varForBreakConcat.find(key2);

                        bool entry1InScope;
                        if (entry1 == varForBreakConcat.end()) {
                            TRACE("str", tout << "key1 no entry" << std::endl;);
                            entry1InScope = false;
                        } else {
                            // OVERRIDE.
                            entry1InScope = true;
                            TRACE("str", tout << "key1 entry" << std::endl;);
                            /*
                              if (internal_variable_set.find((entry1->second)[0]) == internal_variable_set.end()) {
                              TRACE("str", tout << "key1 entry not in scope" << std::endl;);
                              entry1InScope = false;
                              } else {
                              TRACE("str", tout << "key1 entry in scope" << std::endl;);
                              entry1InScope = true;
                              }
                            */
                        }

                        bool entry2InScope;
                        if (entry2 == varForBreakConcat.end()) {
                            TRACE("str", tout << "key2 no entry" << std::endl;);
                            entry2InScope = false;
                        } else {
                            // OVERRIDE.
                            entry2InScope = true;
                            TRACE("str", tout << "key2 entry" << std::endl;);
                            /*
                              if (internal_variable_set.find((entry2->second)[0]) == internal_variable_set.end()) {
                              TRACE("str", tout << "key2 entry not in scope" << std::endl;);
                              entry2InScope = false;
                              } else {
                              TRACE("str", tout << "key2 entry in scope" << std::endl;);
                              entry2InScope = true;
                              }
                            */
                        }

                        TRACE("str", tout << "entry 1 " << (entry1InScope ? "in scope" : "not in scope") << std::endl
                              << "entry 2 " << (entry2InScope ? "in scope" : "not in scope") << std::endl;);

                        if (!entry1InScope && !entry2InScope) {
                            xorFlag = mk_internal_xor_var();
                            varForBreakConcat[key1][0] = xorFlag;
                        } else if (entry1InScope) {
                            xorFlag = varForBreakConcat[key1][0];
                        } else { // entry2InScope
                            xorFlag = varForBreakConcat[key2][0];
                        }

                        int concatStrLen = const_str.length();
                        int and_count = 1;

                        expr_ref_vector arrangement_disjunction(m);

                        for (int i = 0; i < concatStrLen + 1; ++i) {
                            expr_ref_vector and_items(m);
                            zstring prefixStr = const_str.extract(0, i);
                            zstring suffixStr = const_str.extract(i, concatStrLen - i);
                            // skip invalid options
                            if (u.str.is_concat(to_app(arg1)) && !can_concat_eq_str(arg1, prefixStr)) {
                                continue;
                            }
                            if (u.str.is_concat(to_app(arg2)) && !can_concat_eq_str(arg2, suffixStr)) {
                                continue;
                            }

                            expr_ref prefixAst(mk_string(prefixStr), m);
                            expr_ref arg1_eq (ctx.mk_eq_atom(arg1, prefixAst), m);
                            and_items.push_back(arg1_eq);
                            and_count += 1;

                            expr_ref suffixAst(mk_string(suffixStr), m);
                            expr_ref arg2_eq (ctx.mk_eq_atom(arg2, suffixAst), m);
                            and_items.push_back(arg2_eq);
                            and_count += 1;

                            arrangement_disjunction.push_back(mk_and(and_items));
                        }

                        expr_ref implyL(ctx.mk_eq_atom(concat, str), m);
                        expr_ref implyR1(m);
                        if (arrangement_disjunction.empty()) {
                            // negate
                            expr_ref concat_eq_str(ctx.mk_eq_atom(concat, str), m);
                            expr_ref negate_ast(m.mk_not(concat_eq_str), m);
                            assert_axiom(negate_ast);
                        } else {
                            implyR1 = mk_or(arrangement_disjunction);
                            if (m_params.m_StrongArrangements) {
                                expr_ref ax_strong(ctx.mk_eq_atom(implyL, implyR1), m);
                                assert_axiom(ax_strong);
                            } else {
                                assert_implication(implyL, implyR1);
                            }
                            generate_mutual_exclusion(arrangement_disjunction);
                        }
                    } /* (arg1Len != 1 || arg2Len != 1) */
                } /* if (Concat(arg1, arg2) == NULL) */
            }
        }
    }

    expr_ref theory_str::set_up_finite_model_test(expr * lhs, expr * rhs) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        TRACE("str", tout << "activating finite model testing for overlapping concats "
              << mk_pp(lhs, m) << " and " << mk_pp(rhs, m) << std::endl;);
        std::map<expr*, int> concatMap;
        std::map<expr*, int> unrollMap;
        std::map<expr*, int> varMap;
        classify_ast_by_type(lhs, varMap, concatMap, unrollMap);
        classify_ast_by_type(rhs, varMap, concatMap, unrollMap);
        TRACE("str", tout << "found vars:";
              for (std::map<expr*,int>::iterator it = varMap.begin(); it != varMap.end(); ++it) {
                  tout << " " << mk_pp(it->first, m);
              }
              tout << std::endl;
              );

        expr_ref testvar(mk_str_var("finiteModelTest"), m);
        m_trail.push_back(testvar);
        ptr_vector<expr> varlist;

        for (std::map<expr*, int>::iterator it = varMap.begin(); it != varMap.end(); ++it) {
            expr * v = it->first;
            varlist.push_back(v);
        }

        // make things easy for the core wrt. testvar
        expr_ref t1(ctx.mk_eq_atom(testvar, mk_string("")), m);
        expr_ref t_yes(ctx.mk_eq_atom(testvar, mk_string("yes")), m);
        expr_ref testvaraxiom(m.mk_or(t1, t_yes), m);
        assert_axiom(testvaraxiom);

        finite_model_test_varlists.insert(testvar, varlist);
        m_trail_stack.push(insert_obj_map<theory_str, expr, ptr_vector<expr> >(finite_model_test_varlists, testvar) );
        return t_yes;
    }

    void theory_str::finite_model_test(expr * testvar, expr * str) {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        zstring s;
        if (!u.str.is_string(str, s)) return;
        if (s == "yes") {
            TRACE("str", tout << "start finite model test for " << mk_pp(testvar, m) << std::endl;);
            ptr_vector<expr> & vars = finite_model_test_varlists[testvar];
            for (ptr_vector<expr>::iterator it = vars.begin(); it != vars.end(); ++it) {
                expr * v = *it;
                bool v_has_eqc = false;
                get_eqc_value(v, v_has_eqc);
                if (v_has_eqc) {
                    TRACE("str", tout << "variable " << mk_pp(v,m) << " already equivalent to a string constant" << std::endl;);
                    continue;
                }
                // check for any sort of existing length tester we might interfere with
                if (m_params.m_UseBinarySearch) {
                    if (binary_search_len_tester_stack.contains(v) && !binary_search_len_tester_stack[v].empty()) {
                        TRACE("str", tout << "already found existing length testers for " << mk_pp(v, m) << std::endl;);
                        continue;
                    } else {
                        // start binary search as normal
                        expr_ref implLhs(ctx.mk_eq_atom(testvar, str), m);
                        expr_ref implRhs(binary_search_length_test(v, nullptr, ""), m);
                        assert_implication(implLhs, implRhs);
                    }
                } else {
                    bool map_effectively_empty = false;
                    if (!fvar_len_count_map.contains(v)) {
                        map_effectively_empty = true;
                    }

                    if (!map_effectively_empty) {
                        map_effectively_empty = true;
                        if (fvar_lenTester_map.contains(v)) {
                            for (expr * indicator : fvar_lenTester_map[v]) {
                                if (internal_variable_set.contains(indicator)) {
                                    map_effectively_empty = false;
                                    break;
                                }
                            }
                        }
                    }

                    if (map_effectively_empty) {
                        TRACE("str", tout << "no existing length testers for " << mk_pp(v, m) << std::endl;);
                        rational v_len;
                        rational v_lower_bound;
                        rational v_upper_bound;
                        expr_ref vLengthExpr(mk_strlen(v), m);
                        if (get_len_value(v, v_len)) {
                            TRACE("str", tout << "length = " << v_len.to_string() << std::endl;);
                            v_lower_bound = v_len;
                            v_upper_bound = v_len;
                        } else {
                            bool lower_bound_exists = lower_bound(vLengthExpr, v_lower_bound);
                            bool upper_bound_exists = upper_bound(vLengthExpr, v_upper_bound);
                            TRACE("str", tout << "bounds = [" << (lower_bound_exists?v_lower_bound.to_string():"?")
                                  << ".." << (upper_bound_exists?v_upper_bound.to_string():"?") << "]" << std::endl;);

                            // make sure the bounds are non-negative
                            if (lower_bound_exists && v_lower_bound.is_neg()) {
                                v_lower_bound = rational::zero();
                            }
                            if (upper_bound_exists && v_upper_bound.is_neg()) {
                                v_upper_bound = rational::zero();
                            }

                            if (lower_bound_exists && upper_bound_exists) {
                                // easiest case. we will search within these bounds
                            } else if (upper_bound_exists && !lower_bound_exists) {
                                // search between 0 and the upper bound
                                v_lower_bound = rational::zero();
                            } else if (lower_bound_exists && !upper_bound_exists) {
                                // check some finite portion of the search space
                                v_upper_bound = v_lower_bound + rational(10);
                            } else {
                                // no bounds information
                                v_lower_bound = rational::zero();
                                v_upper_bound = v_lower_bound + rational(10);
                            }
                        }
                        // now create a fake length tester over this finite disjunction of lengths

                        fvar_len_count_map.insert(v, 1);
                        unsigned int testNum = fvar_len_count_map[v];

                        expr_ref indicator(mk_internal_lenTest_var(v, testNum), m);
                        SASSERT(indicator);
                        m_trail.push_back(indicator);

                        if (!fvar_lenTester_map.contains(v)) {
                            fvar_lenTester_map.insert(v, ptr_vector<expr>());
                        }
                        fvar_lenTester_map[v].shrink(0);
                        fvar_lenTester_map[v].push_back(indicator);
                        lenTester_fvar_map.insert(indicator, v);

                        expr_ref_vector orList(m);
                        expr_ref_vector andList(m);

                        for (rational l = v_lower_bound; l <= v_upper_bound; l += rational::one()) {
                            zstring lStr = zstring(l.to_string().c_str());
                            expr_ref str_indicator(mk_string(lStr), m);
                            expr_ref or_expr(ctx.mk_eq_atom(indicator, str_indicator), m);
                            orList.push_back(or_expr);
                            expr_ref and_expr(ctx.mk_eq_atom(or_expr, ctx.mk_eq_atom(vLengthExpr, m_autil.mk_numeral(l, true))), m);
                            andList.push_back(and_expr);
                        }
                        andList.push_back(mk_or(orList));
                        expr_ref implLhs(ctx.mk_eq_atom(testvar, str), m);
                        expr_ref implRhs(mk_and(andList), m);
                        assert_implication(implLhs, implRhs);
                    } else {
                        TRACE("str", tout << "already found existing length testers for " << mk_pp(v, m) << std::endl;);
                        continue;
                    }
                }
            } // foreach (v in vars)
        } // (s == "yes")
    }

    void theory_str::more_len_tests(expr * lenTester, zstring lenTesterValue) {
        ast_manager & m = get_manager();
        if (lenTester_fvar_map.contains(lenTester)) {
            expr * fVar = lenTester_fvar_map[lenTester];
            expr_ref toAssert(gen_len_val_options_for_free_var(fVar, lenTester, lenTesterValue), m);
            TRACE("str", tout << "asserting more length tests for free variable " << mk_ismt2_pp(fVar, m) << std::endl;);
            if (toAssert) {
                assert_axiom(toAssert);
            }
        }
    }

    void theory_str::more_value_tests(expr * valTester, zstring valTesterValue) {
        ast_manager & m = get_manager(); (void)m;

        expr * fVar = valueTester_fvar_map[valTester];
        if (m_params.m_UseBinarySearch) {
            if (!binary_search_len_tester_stack.contains(fVar) || binary_search_len_tester_stack[fVar].empty()) {
                TRACE("str", tout << "WARNING: no active length testers for " << mk_pp(fVar, m) << std::endl;);
                NOT_IMPLEMENTED_YET();
            }
            expr * effectiveLenInd = binary_search_len_tester_stack[fVar].back();
            bool hasEqcValue;
            expr * len_indicator_value = get_eqc_value(effectiveLenInd, hasEqcValue);
            if (!hasEqcValue) {
                TRACE("str", tout << "WARNING: length tester " << mk_pp(effectiveLenInd, m) << " at top of stack for " << mk_pp(fVar, m) << " has no EQC value" << std::endl;);
            } else {
                // safety check
                zstring effectiveLenIndiStr;
                u.str.is_string(len_indicator_value, effectiveLenIndiStr);
                if (effectiveLenIndiStr == "more" || effectiveLenIndiStr == "less") {
                    TRACE("str", tout << "ERROR: illegal state -- requesting 'more value tests' but a length tester is not yet concrete!" << std::endl;);
                    UNREACHABLE();
                }
                expr * valueAssert = gen_free_var_options(fVar, effectiveLenInd, effectiveLenIndiStr, valTester, valTesterValue);
                TRACE("str", tout << "asserting more value tests for free variable " << mk_ismt2_pp(fVar, m) << std::endl;);
                if (valueAssert != nullptr) {
                    assert_axiom(valueAssert);
                }
            }
        } else {
            int lenTesterCount;
            if (fvar_lenTester_map.contains(fVar)) {
                lenTesterCount = fvar_lenTester_map[fVar].size();
            } else {
                lenTesterCount = 0;
            }

            expr * effectiveLenInd = nullptr;
            zstring effectiveLenIndiStr = "";
            for (int i = 0; i < lenTesterCount; ++i) {
                expr * len_indicator_pre = fvar_lenTester_map[fVar][i];
                bool indicatorHasEqcValue = false;
                expr * len_indicator_value = get_eqc_value(len_indicator_pre, indicatorHasEqcValue);
                if (indicatorHasEqcValue) {
                    zstring len_pIndiStr;
                    u.str.is_string(len_indicator_value, len_pIndiStr);
                    if (len_pIndiStr != "more") {
                        effectiveLenInd = len_indicator_pre;
                        effectiveLenIndiStr = len_pIndiStr;
                        break;
                    }
                }
            }
            expr * valueAssert = gen_free_var_options(fVar, effectiveLenInd, effectiveLenIndiStr, valTester, valTesterValue);
            TRACE("str", tout << "asserting more value tests for free variable " << mk_ismt2_pp(fVar, m) << std::endl;);
            if (valueAssert != nullptr) {
                assert_axiom(valueAssert);
            }
        }
    }

    bool theory_str::free_var_attempt(expr * nn1, expr * nn2) {
        zstring nn2_str;
        if (internal_lenTest_vars.contains(nn1) && u.str.is_string(nn2, nn2_str)) {
            TRACE("str", tout << "acting on equivalence between length tester var " << mk_pp(nn1, get_manager())
                  << " and constant " << mk_pp(nn2, get_manager()) << std::endl;);
            more_len_tests(nn1, nn2_str);
            return true;
        } else if (internal_valTest_vars.contains(nn1) && u.str.is_string(nn2, nn2_str)) {
            if (nn2_str == "more") {
                TRACE("str", tout << "acting on equivalence between value var " << mk_pp(nn1, get_manager())
                      << " and constant " << mk_pp(nn2, get_manager()) << std::endl;);
                more_value_tests(nn1, nn2_str);
            }
            return true;
        } else if (internal_unrollTest_vars.contains(nn1)) {
            return true;
        } else {
            return false;
        }
    }

    void theory_str::handle_equality(expr * lhs, expr * rhs) {
        ast_manager & m = get_manager();
        context & ctx = get_context();
        // both terms must be of sort String
        sort * lhs_sort = m.get_sort(lhs);
        sort * rhs_sort = m.get_sort(rhs);
        sort * str_sort = u.str.mk_string_sort();

        if (lhs_sort != str_sort || rhs_sort != str_sort) {
            TRACE("str", tout << "skip equality: not String sort" << std::endl;);
            return;
        }

        /* // temporarily disabled, we are borrowing these testers for something else
           if (m_params.m_FiniteOverlapModels && !finite_model_test_varlists.empty()) {
           if (finite_model_test_varlists.contains(lhs)) {
           finite_model_test(lhs, rhs); return;
           } else if (finite_model_test_varlists.contains(rhs)) {
           finite_model_test(rhs, lhs); return;
           }
           }
        */

        if (free_var_attempt(lhs, rhs) || free_var_attempt(rhs, lhs)) {
            return;
        }

        if (u.str.is_concat(to_app(lhs)) && u.str.is_concat(to_app(rhs))) {
            bool nn1HasEqcValue = false;
            bool nn2HasEqcValue = false;
            expr * nn1_value = get_eqc_value(lhs, nn1HasEqcValue);
            expr * nn2_value = get_eqc_value(rhs, nn2HasEqcValue);
            if (nn1HasEqcValue && !nn2HasEqcValue) {
                simplify_parent(rhs, nn1_value);
            }
            if (!nn1HasEqcValue && nn2HasEqcValue) {
                simplify_parent(lhs, nn2_value);
            }

            expr * nn1_arg0 = to_app(lhs)->get_arg(0);
            expr * nn1_arg1 = to_app(lhs)->get_arg(1);
            expr * nn2_arg0 = to_app(rhs)->get_arg(0);
            expr * nn2_arg1 = to_app(rhs)->get_arg(1);
            if (nn1_arg0 == nn2_arg0 && in_same_eqc(nn1_arg1, nn2_arg1)) {
                TRACE("str", tout << "skip: lhs arg0 == rhs arg0" << std::endl;);
                return;
            }

            if (nn1_arg1 == nn2_arg1 && in_same_eqc(nn1_arg0, nn2_arg0)) {
                TRACE("str", tout << "skip: lhs arg1 == rhs arg1" << std::endl;);
                return;
            }
        }

        if (opt_DeferEQCConsistencyCheck) {
            TRACE("str", tout << "opt_DeferEQCConsistencyCheck is set; deferring new_eq_check call" << std::endl;);
        } else {
            // newEqCheck() -- check consistency wrt. existing equivalence classes
            if (!new_eq_check(lhs, rhs)) {
                return;
            }
        }

        // BEGIN new_eq_handler() in strTheory

        {
            rational nn1Len, nn2Len;
            bool nn1Len_exists = get_len_value(lhs, nn1Len);
            bool nn2Len_exists = get_len_value(rhs, nn2Len);
            expr_ref emptyStr(mk_string(""), m);

            if (nn1Len_exists && nn1Len.is_zero()) {
                if (!in_same_eqc(lhs, emptyStr) && rhs != emptyStr) {
                    expr_ref eql(ctx.mk_eq_atom(mk_strlen(lhs), mk_int(0)), m);
                    expr_ref eqr(ctx.mk_eq_atom(lhs, emptyStr), m);
                    expr_ref toAssert(ctx.mk_eq_atom(eql, eqr), m);
                    assert_axiom(toAssert);
                }
            }

            if (nn2Len_exists && nn2Len.is_zero()) {
                if (!in_same_eqc(rhs, emptyStr) && lhs != emptyStr) {
                    expr_ref eql(ctx.mk_eq_atom(mk_strlen(rhs), mk_int(0)), m);
                    expr_ref eqr(ctx.mk_eq_atom(rhs, emptyStr), m);
                    expr_ref toAssert(ctx.mk_eq_atom(eql, eqr), m);
                    assert_axiom(toAssert);
                }
            }
        }

        instantiate_str_eq_length_axiom(ctx.get_enode(lhs), ctx.get_enode(rhs));

        // group terms by equivalence class (groupNodeInEqc())

        std::set<expr*> eqc_concat_lhs;
        std::set<expr*> eqc_var_lhs;
        std::set<expr*> eqc_const_lhs;
        group_terms_by_eqc(lhs, eqc_concat_lhs, eqc_var_lhs, eqc_const_lhs);

        std::set<expr*> eqc_concat_rhs;
        std::set<expr*> eqc_var_rhs;
        std::set<expr*> eqc_const_rhs;
        group_terms_by_eqc(rhs, eqc_concat_rhs, eqc_var_rhs, eqc_const_rhs);

        TRACE("str",
              tout << "lhs eqc:" << std::endl;
              tout << "Concats:" << std::endl;
              for (std::set<expr*>::iterator it = eqc_concat_lhs.begin(); it != eqc_concat_lhs.end(); ++it) {
                  expr * ex = *it;
                  tout << mk_ismt2_pp(ex, get_manager()) << std::endl;
              }
              tout << "Variables:" << std::endl;
              for (std::set<expr*>::iterator it = eqc_var_lhs.begin(); it != eqc_var_lhs.end(); ++it) {
                  expr * ex = *it;
                  tout << mk_ismt2_pp(ex, get_manager()) << std::endl;
              }
              tout << "Constants:" << std::endl;
              for (std::set<expr*>::iterator it = eqc_const_lhs.begin(); it != eqc_const_lhs.end(); ++it) {
                  expr * ex = *it;
                  tout << mk_ismt2_pp(ex, get_manager()) << std::endl;
              }

              tout << "rhs eqc:" << std::endl;
              tout << "Concats:" << std::endl;
              for (std::set<expr*>::iterator it = eqc_concat_rhs.begin(); it != eqc_concat_rhs.end(); ++it) {
                  expr * ex = *it;
                  tout << mk_ismt2_pp(ex, get_manager()) << std::endl;
              }
              tout << "Variables:" << std::endl;
              for (std::set<expr*>::iterator it = eqc_var_rhs.begin(); it != eqc_var_rhs.end(); ++it) {
                  expr * ex = *it;
                  tout << mk_ismt2_pp(ex, get_manager()) << std::endl;
              }
              tout << "Constants:" << std::endl;
              for (std::set<expr*>::iterator it = eqc_const_rhs.begin(); it != eqc_const_rhs.end(); ++it) {
                  expr * ex = *it;
                  tout << mk_ismt2_pp(ex, get_manager()) << std::endl;
              }
              );

        // step 1: Concat == Concat
        int hasCommon = 0;
        if (eqc_concat_lhs.size() != 0 && eqc_concat_rhs.size() != 0) {
            std::set<expr*>::iterator itor1 = eqc_concat_lhs.begin();
            std::set<expr*>::iterator itor2 = eqc_concat_rhs.begin();
            for (; itor1 != eqc_concat_lhs.end(); itor1++) {
                if (eqc_concat_rhs.find(*itor1) != eqc_concat_rhs.end()) {
                    hasCommon = 1;
                    break;
                }
            }
            for (; itor2 != eqc_concat_rhs.end(); itor2++) {
                if (eqc_concat_lhs.find(*itor2) != eqc_concat_lhs.end()) {
                    hasCommon = 1;
                    break;
                }
            }
            if (hasCommon == 0) {
                if (opt_ConcatOverlapAvoid) {
                    bool found = false;
                    // check each pair and take the first ones that won't immediately overlap
                    for (itor1 = eqc_concat_lhs.begin(); itor1 != eqc_concat_lhs.end() && !found; ++itor1) {
                        expr * concat_lhs = *itor1;
                        for (itor2 = eqc_concat_rhs.begin(); itor2 != eqc_concat_rhs.end() && !found; ++itor2) {
                            expr * concat_rhs = *itor2;
                            if (will_result_in_overlap(concat_lhs, concat_rhs)) {
                                TRACE("str", tout << "Concats " << mk_pp(concat_lhs, m) << " and "
                                      << mk_pp(concat_rhs, m) << " will result in overlap; skipping." << std::endl;);
                            } else {
                                TRACE("str", tout << "Concats " << mk_pp(concat_lhs, m) << " and "
                                      << mk_pp(concat_rhs, m) << " won't overlap. Simplifying here." << std::endl;);
                                simplify_concat_equality(concat_lhs, concat_rhs);
                                found = true;
                                break;
                            }
                        }
                    }
                    if (!found) {
                        TRACE("str", tout << "All pairs of concats expected to overlap, falling back." << std::endl;);
                        simplify_concat_equality(*(eqc_concat_lhs.begin()), *(eqc_concat_rhs.begin()));
                    }
                } else {
                    // default behaviour
                    simplify_concat_equality(*(eqc_concat_lhs.begin()), *(eqc_concat_rhs.begin()));
                }
            }
        }

        // step 2: Concat == Constant

        if (eqc_const_lhs.size() != 0) {
            expr * conStr = *(eqc_const_lhs.begin());
            std::set<expr*>::iterator itor2 = eqc_concat_rhs.begin();
            for (; itor2 != eqc_concat_rhs.end(); itor2++) {
                solve_concat_eq_str(*itor2, conStr);
            }
        } else if (eqc_const_rhs.size() != 0) {
            expr* conStr = *(eqc_const_rhs.begin());
            std::set<expr*>::iterator itor1 = eqc_concat_lhs.begin();
            for (; itor1 != eqc_concat_lhs.end(); itor1++) {
                solve_concat_eq_str(*itor1, conStr);
            }
        }

        // simplify parents wrt. the equivalence class of both sides
        bool nn1HasEqcValue = false;
        bool nn2HasEqcValue = false;
        // we want the Z3str2 eqc check here...
        expr * nn1_value = z3str2_get_eqc_value(lhs, nn1HasEqcValue);
        expr * nn2_value = z3str2_get_eqc_value(rhs, nn2HasEqcValue);
        if (nn1HasEqcValue && !nn2HasEqcValue) {
            simplify_parent(rhs, nn1_value);
        }

        if (!nn1HasEqcValue && nn2HasEqcValue) {
            simplify_parent(lhs, nn2_value);
        }

        expr * nn1EqConst = nullptr;
        std::set<expr*> nn1EqUnrollFuncs;
        get_eqc_allUnroll(lhs, nn1EqConst, nn1EqUnrollFuncs);
        expr * nn2EqConst = nullptr;
        std::set<expr*> nn2EqUnrollFuncs;
        get_eqc_allUnroll(rhs, nn2EqConst, nn2EqUnrollFuncs);

        if (nn2EqConst != nullptr) {
            for (std::set<expr*>::iterator itor1 = nn1EqUnrollFuncs.begin(); itor1 != nn1EqUnrollFuncs.end(); itor1++) {
                process_unroll_eq_const_str(*itor1, nn2EqConst);
            }
        }

        if (nn1EqConst != nullptr) {
            for (std::set<expr*>::iterator itor2 = nn2EqUnrollFuncs.begin(); itor2 != nn2EqUnrollFuncs.end(); itor2++) {
                process_unroll_eq_const_str(*itor2, nn1EqConst);
            }
        }

    }

    void theory_str::set_up_axioms(expr * ex) {
        ast_manager & m = get_manager();
        context & ctx = get_context();

        sort * ex_sort = m.get_sort(ex);
        sort * str_sort = u.str.mk_string_sort();
        sort * bool_sort = m.mk_bool_sort();

        family_id m_arith_fid = m.mk_family_id("arith");
        sort * int_sort = m.mk_sort(m_arith_fid, INT_SORT);

        if (ex_sort == str_sort) {
            TRACE("str", tout << "setting up axioms for " << mk_ismt2_pp(ex, get_manager()) <<
                  ": expr is of sort String" << std::endl;);
            // set up basic string axioms
            enode * n = ctx.get_enode(ex);
            SASSERT(n);
            m_basicstr_axiom_todo.push_back(n);
            TRACE("str", tout << "add " << mk_pp(ex, m) << " to m_basicstr_axiom_todo" << std::endl;);


            if (is_app(ex)) {
                app * ap = to_app(ex);
                if (u.str.is_concat(ap)) {
                    // if ex is a concat, set up concat axioms later
                    m_concat_axiom_todo.push_back(n);
                    // we also want to check whether we can eval this concat,
                    // in case the rewriter did not totally finish with this term
                    m_concat_eval_todo.push_back(n);
                } else if (u.str.is_length(ap)) {
                    // if the argument is a variable,
                    // keep track of this for later, we'll need it during model gen
                    expr * var = ap->get_arg(0);
                    app * aVar = to_app(var);
                    if (aVar->get_num_args() == 0 && !u.str.is_string(aVar)) {
                        input_var_in_len.insert(var);
                    }
                } else if (u.str.is_at(ap) || u.str.is_extract(ap) || u.str.is_replace(ap)) {
                    m_library_aware_axiom_todo.push_back(n);
                } else if (u.str.is_itos(ap)) {
                    TRACE("str", tout << "found string-integer conversion term: " << mk_pp(ex, get_manager()) << std::endl;);
                    string_int_conversion_terms.push_back(ap);
                    m_library_aware_axiom_todo.push_back(n);
                } else if (ap->get_num_args() == 0 && !u.str.is_string(ap)) {
                    // if ex is a variable, add it to our list of variables
                    TRACE("str", tout << "tracking variable " << mk_ismt2_pp(ap, get_manager()) << std::endl;);
                    variable_set.insert(ex);
                    ctx.mark_as_relevant(ex);
                    // this might help??
                    theory_var v = mk_var(n);
                    TRACE("str", tout << "variable " << mk_ismt2_pp(ap, get_manager()) << " is #" << v << std::endl;);
                    (void)v;
                }
            }
        } else if (ex_sort == bool_sort && !is_quantifier(ex)) {
            TRACE("str", tout << "setting up axioms for " << mk_ismt2_pp(ex, get_manager()) <<
                  ": expr is of sort Bool" << std::endl;);
            // set up axioms for boolean terms

            ensure_enode(ex);
            if (ctx.e_internalized(ex)) {
                enode * n = ctx.get_enode(ex);
                SASSERT(n);

                if (is_app(ex)) {
                    app * ap = to_app(ex);
                    if (u.str.is_prefix(ap) || u.str.is_suffix(ap) || u.str.is_contains(ap) || u.str.is_in_re(ap)) {
                        m_library_aware_axiom_todo.push_back(n);
                    }
                }
            } else {
                TRACE("str", tout << "WARNING: Bool term " << mk_ismt2_pp(ex, get_manager()) << " not internalized. Delaying axiom setup to prevent a crash." << std::endl;);
                ENSURE(!search_started); // infinite loop prevention
                m_delayed_axiom_setup_terms.push_back(ex);
                return;
            }
        } else if (ex_sort == int_sort) {
            TRACE("str", tout << "setting up axioms for " << mk_ismt2_pp(ex, get_manager()) <<
                  ": expr is of sort Int" << std::endl;);
            // set up axioms for integer terms
            enode * n = ensure_enode(ex);
            SASSERT(n);

            if (is_app(ex)) {
                app * ap = to_app(ex);
                if (u.str.is_index(ap)) {
                    m_library_aware_axiom_todo.push_back(n);
                } else if (u.str.is_stoi(ap)) {
                    TRACE("str", tout << "found string-integer conversion term: " << mk_pp(ex, get_manager()) << std::endl;);
                    string_int_conversion_terms.push_back(ap);
                    m_library_aware_axiom_todo.push_back(n);
                }
            }
        } else {
            TRACE("str", tout << "setting up axioms for " << mk_ismt2_pp(ex, get_manager()) <<
                  ": expr is of wrong sort, ignoring" << std::endl;);
        }

        // if expr is an application, recursively inspect all arguments
        if (is_app(ex)) {
            app * term = to_app(ex);
            unsigned num_args = term->get_num_args();
            for (unsigned i = 0; i < num_args; i++) {
                set_up_axioms(term->get_arg(i));
            }
        }
    }

    void theory_str::add_theory_assumptions(expr_ref_vector & assumptions) {
        TRACE("str", tout << "add overlap assumption for theory_str" << std::endl;);
        const char* strOverlap = "!!TheoryStrOverlapAssumption!!";
        seq_util m_sequtil(get_manager());
        sort * s = get_manager().mk_bool_sort();
        m_theoryStrOverlapAssumption_term = expr_ref(mk_fresh_const(strOverlap, s), get_manager());
        assumptions.push_back(get_manager().mk_not(m_theoryStrOverlapAssumption_term));
    }

    lbool theory_str::validate_unsat_core(expr_ref_vector & unsat_core) {
        app * target_term = to_app(get_manager().mk_not(m_theoryStrOverlapAssumption_term));
        get_context().internalize(target_term, false);
        for (unsigned i = 0; i < unsat_core.size(); ++i) {
            app * core_term = to_app(unsat_core.get(i));
            // not sure if this is the correct way to compare terms in this context
            enode * e1;
            enode * e2;
            e1 = get_context().get_enode(target_term);
            e2 = get_context().get_enode(core_term);
            if (e1 == e2) {
                TRACE("str", tout << "overlap detected in unsat core, changing UNSAT to UNKNOWN" << std::endl;);
                return l_undef;
            }
        }

        return l_false;
    }

    void theory_str::init_search_eh() {
        context & ctx = get_context();

        TRACE("str",
              tout << "dumping all asserted formulas:" << std::endl;
              unsigned nFormulas = ctx.get_num_asserted_formulas();
              for (unsigned i = 0; i < nFormulas; ++i) {
                  expr * ex = ctx.get_asserted_formula(i);
                  tout << mk_pp(ex, get_manager()) << (ctx.is_relevant(ex) ? " (rel)" : " (NOT REL)") << std::endl;
              }
              );
        /*
         * Recursive descent through all asserted formulas to set up axioms.
         * Note that this is just the input structure and not necessarily things
         * that we know to be true or false. We're just doing this to see
         * which terms are explicitly mentioned.
         */
        unsigned nFormulas = ctx.get_num_asserted_formulas();
        for (unsigned i = 0; i < nFormulas; ++i) {
            expr * ex = ctx.get_asserted_formula(i);
            set_up_axioms(ex);
        }

        // this might be cheating but we need to make sure that certain maps are populated
        // before the first call to new_eq_eh()
        propagate();

        TRACE("str", tout << "search started" << std::endl;);
        search_started = true;
    }

    void theory_str::new_eq_eh(theory_var x, theory_var y) {
        //TRACE("str", tout << "new eq: v#" << x << " = v#" << y << std::endl;);
        TRACE("str", tout << "new eq: " << mk_ismt2_pp(get_enode(x)->get_owner(), get_manager()) << " = " <<
              mk_ismt2_pp(get_enode(y)->get_owner(), get_manager()) << std::endl;);

        /*
          if (m_find.find(x) == m_find.find(y)) {
          return;
          }
        */
        handle_equality(get_enode(x)->get_owner(), get_enode(y)->get_owner());

        // replicate Z3str2 behaviour: merge eqc **AFTER** handle_equality
        m_find.merge(x, y);
    }

    void theory_str::new_diseq_eh(theory_var x, theory_var y) {
        //TRACE("str", tout << "new diseq: v#" << x << " != v#" << y << std::endl;);
        TRACE("str", tout << "new diseq: " << mk_ismt2_pp(get_enode(x)->get_owner(), get_manager()) << " != " <<
              mk_ismt2_pp(get_enode(y)->get_owner(), get_manager()) << std::endl;);
    }

    void theory_str::relevant_eh(app * n) {
        TRACE("str", tout << "relevant: " << mk_ismt2_pp(n, get_manager()) << std::endl;);
    }

    void theory_str::assign_eh(bool_var v, bool is_true) {
        TRACE("str", tout << "assert: v" << v << " #" << get_context().bool_var2expr(v)->get_id() << " is_true: " << is_true << std::endl;);
    }

    void theory_str::push_scope_eh() {
        theory::push_scope_eh();
        m_trail_stack.push_scope();

        sLevel += 1;
        TRACE("str", tout << "push to " << sLevel << std::endl;);
        TRACE_CODE(if (is_trace_enabled("t_str_dump_assign_on_scope_change")) { dump_assignments(); });
    }

    void theory_str::recursive_check_variable_scope(expr * ex) {
        ast_manager & m = get_manager();

        if (is_app(ex)) {
            app * a = to_app(ex);
            if (a->get_num_args() == 0) {
                // we only care about string variables
                sort * s = m.get_sort(ex);
                sort * string_sort = u.str.mk_string_sort();
                if (s != string_sort) {
                    return;
                }
                // base case: string constant / var
                if (u.str.is_string(a)) {
                    return;
                } else {
                    // assume var
                    if (variable_set.find(ex) == variable_set.end()
                        && internal_variable_set.find(ex) == internal_variable_set.end()) {
                        TRACE("str", tout << "WARNING: possible reference to out-of-scope variable " << mk_pp(ex, m) << std::endl;);
                    }
                }
            } else {
                for (unsigned i = 0; i < a->get_num_args(); ++i) {
                    recursive_check_variable_scope(a->get_arg(i));
                }
            }
        }
    }

    void theory_str::check_variable_scope() {
        if (!opt_CheckVariableScope) {
            return;
        }

        if (!is_trace_enabled("t_str_detail")) {
            return;
        }

        TRACE("str", tout << "checking scopes of variables in the current assignment" << std::endl;);

        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr_ref_vector assignments(m);
        ctx.get_assignments(assignments);
        for (expr_ref_vector::iterator i = assignments.begin(); i != assignments.end(); ++i) {
            expr * ex = *i;
            recursive_check_variable_scope(ex);
        }
    }

    void theory_str::add_persisted_axiom(expr * a) {
        m_persisted_axioms.push_back(a);
    }

    void theory_str::pop_scope_eh(unsigned num_scopes) {
        sLevel -= num_scopes;
        TRACE("str", tout << "pop " << num_scopes << " to " << sLevel << std::endl;);

        TRACE_CODE(if (is_trace_enabled("t_str_dump_assign_on_scope_change")) { dump_assignments(); });

        // list of expr* to remove from cut_var_map
        ptr_vector<expr> cutvarmap_removes;

        obj_map<expr, std::stack<T_cut *> >::iterator varItor = cut_var_map.begin();
        while (varItor != cut_var_map.end()) {
            std::stack<T_cut*> & val = cut_var_map[varItor->m_key];
            while ((val.size() > 0) && (val.top()->level != 0) && (val.top()->level >= sLevel)) {
                // TRACE("str", tout << "remove cut info for " << mk_pp(e, get_manager()) << std::endl; print_cut_var(e, tout););
                // T_cut * aCut = val.top();
                val.pop();
                // dealloc(aCut);
            }
            if (val.size() == 0) {
                cutvarmap_removes.insert(varItor->m_key);
            }
            varItor++;
        }

        if (!cutvarmap_removes.empty()) {
            ptr_vector<expr>::iterator it = cutvarmap_removes.begin();
            for (; it != cutvarmap_removes.end(); ++it) {
                expr * ex = *it;
                cut_var_map.remove(ex);
            }
        }

        ptr_vector<enode> new_m_basicstr;
        for (ptr_vector<enode>::iterator it = m_basicstr_axiom_todo.begin(); it != m_basicstr_axiom_todo.end(); ++it) {
            enode * e = *it;
            TRACE("str", tout << "consider deleting " << mk_pp(e->get_owner(), get_manager())
                  << ", enode scope level is " << e->get_iscope_lvl()
                  << std::endl;);
            if (e->get_iscope_lvl() <= (unsigned)sLevel) {
                new_m_basicstr.push_back(e);
            }
        }
        m_basicstr_axiom_todo.reset();
        m_basicstr_axiom_todo = new_m_basicstr;

        for (expr * e : m_persisted_axioms) {
            TRACE("str", tout << "persist axiom: " << mk_pp(e, get_manager()) << std::endl;);
            m_persisted_axiom_todo.push_back(e);
        }

        m_trail_stack.pop_scope(num_scopes);
        theory::pop_scope_eh(num_scopes);

        //check_variable_scope();
    }

    void theory_str::dump_assignments() {
        TRACE_CODE(
            ast_manager & m = get_manager();
            context & ctx = get_context();
            tout << "dumping all assignments:" << std::endl;
            expr_ref_vector assignments(m);
            ctx.get_assignments(assignments);
            for (expr_ref_vector::iterator i = assignments.begin(); i != assignments.end(); ++i) {
                expr * ex = *i;
                tout << mk_ismt2_pp(ex, m) << (ctx.is_relevant(ex) ? "" : " (NOT REL)") << std::endl;
            }
                   );
    }

    // returns true if needle appears as a subterm anywhere under haystack,
    // or if needle appears in the same EQC as a subterm anywhere under haystack
    bool theory_str::term_appears_as_subterm(expr * needle, expr * haystack) {
        if (in_same_eqc(needle, haystack)) {
            return true;
        }

        if (is_app(haystack)) {
            app * a_haystack = to_app(haystack);
            for (unsigned i = 0; i < a_haystack->get_num_args(); ++i) {
                expr * subterm = a_haystack->get_arg(i);
                if (term_appears_as_subterm(needle, subterm)) {
                    return true;
                }
            }
        }

        // not found
        return false;
    }

    void theory_str::classify_ast_by_type(expr * node, std::map<expr*, int> & varMap,
                                          std::map<expr*, int> & concatMap, std::map<expr*, int> & unrollMap) {

        // check whether the node is a string variable;
        // testing set membership here bypasses several expensive checks.
        // note that internal variables don't count if they're only length tester / value tester vars.
        if (variable_set.find(node) != variable_set.end()
            && internal_lenTest_vars.find(node) == internal_lenTest_vars.end()
            && internal_valTest_vars.find(node) == internal_valTest_vars.end()
            && internal_unrollTest_vars.find(node) == internal_unrollTest_vars.end()) {
            if (varMap[node] != 1) {
                TRACE("str", tout << "new variable: " << mk_pp(node, get_manager()) << std::endl;);
            }
            varMap[node] = 1;
        }
        // check whether the node is a function that we want to inspect
        else if (is_app(node)) {
            app * aNode = to_app(node);
            if (u.str.is_length(aNode)) {
                // Length
                return;
            } else if (u.str.is_concat(aNode)) {
                expr * arg0 = aNode->get_arg(0);
                expr * arg1 = aNode->get_arg(1);
                bool arg0HasEq = false;
                bool arg1HasEq = false;
                expr * arg0Val = get_eqc_value(arg0, arg0HasEq);
                expr * arg1Val = get_eqc_value(arg1, arg1HasEq);

                int canskip = 0;
                zstring tmp;
                u.str.is_string(arg0Val, tmp);
                if (arg0HasEq && tmp.empty()) {
                    canskip = 1;
                }
                u.str.is_string(arg1Val, tmp);
                if (canskip == 0 && arg1HasEq && tmp.empty()) {
                    canskip = 1;
                }
                if (canskip == 0 && concatMap.find(node) == concatMap.end()) {
                    concatMap[node] = 1;
                }
            } else if (u.re.is_unroll(aNode)) {
                // Unroll
                if (unrollMap.find(node) == unrollMap.end()) {
                    unrollMap[node] = 1;
                }
            }
            // recursively visit all arguments
            for (unsigned i = 0; i < aNode->get_num_args(); ++i) {
                expr * arg = aNode->get_arg(i);
                classify_ast_by_type(arg, varMap, concatMap, unrollMap);
            }
        }
    }

    // NOTE: this function used to take an argument `Z3_ast node`;
    // it was not used and so was removed from the signature
    void theory_str::classify_ast_by_type_in_positive_context(std::map<expr*, int> & varMap,
                                                              std::map<expr*, int> & concatMap, std::map<expr*, int> & unrollMap) {

        context & ctx = get_context();
        ast_manager & m = get_manager();
        expr_ref_vector assignments(m);
        ctx.get_assignments(assignments);

        for (expr_ref_vector::iterator it = assignments.begin(); it != assignments.end(); ++it) {
            expr * argAst = *it;
            // the original code jumped through some hoops to check whether the AST node
            // is a function, then checked whether that function is "interesting".
            // however, the only thing that's considered "interesting" is an equality predicate.
            // so we bypass a huge amount of work by doing the following...

            if (m.is_eq(argAst)) {
                TRACE("str", tout
                      << "eq ast " << mk_pp(argAst, m) << " is between args of sort "
                      << m.get_sort(to_app(argAst)->get_arg(0))->get_name()
                      << std::endl;);
                classify_ast_by_type(argAst, varMap, concatMap, unrollMap);
            }
        }
    }

    inline expr * theory_str::get_alias_index_ast(std::map<expr*, expr*> & aliasIndexMap, expr * node) {
        if (aliasIndexMap.find(node) != aliasIndexMap.end())
            return aliasIndexMap[node];
        else
            return node;
    }

    inline expr * theory_str::getMostLeftNodeInConcat(expr * node) {
        app * aNode = to_app(node);
        if (!u.str.is_concat(aNode)) {
            return node;
        } else {
            expr * concatArgL = aNode->get_arg(0);
            return getMostLeftNodeInConcat(concatArgL);
        }
    }

    inline expr * theory_str::getMostRightNodeInConcat(expr * node) {
        app * aNode = to_app(node);
        if (!u.str.is_concat(aNode)) {
            return node;
        } else {
            expr * concatArgR = aNode->get_arg(1);
            return getMostRightNodeInConcat(concatArgR);
        }
    }

    void theory_str::trace_ctx_dep(std::ofstream & tout,
                                   std::map<expr*, expr*> & aliasIndexMap,
                                   std::map<expr*, expr*> & var_eq_constStr_map,
                                   std::map<expr*, std::map<expr*, int> > & var_eq_concat_map,
                                   std::map<expr*, std::map<expr*, int> > & var_eq_unroll_map,
                                   std::map<expr*, expr*> & concat_eq_constStr_map,
                                   std::map<expr*, std::map<expr*, int> > & concat_eq_concat_map,
                                   std::map<expr*, std::set<expr*> > & unrollGroupMap) {
#ifdef _TRACE
        context & ctx = get_context();
        ast_manager & mgr = get_manager();
        {
            tout << "(0) alias: variables" << std::endl;
            std::map<expr*, std::map<expr*, int> > aliasSumMap;
            std::map<expr*, expr*>::iterator itor0 = aliasIndexMap.begin();
            for (; itor0 != aliasIndexMap.end(); itor0++) {
                aliasSumMap[itor0->second][itor0->first] = 1;
            }
            std::map<expr*, std::map<expr*, int> >::iterator keyItor = aliasSumMap.begin();
            for (; keyItor != aliasSumMap.end(); keyItor++) {
                tout << "    * ";
                tout << mk_pp(keyItor->first, mgr);
                tout << " : ";
                std::map<expr*, int>::iterator innerItor = keyItor->second.begin();
                for (; innerItor != keyItor->second.end(); innerItor++) {
                    tout << mk_pp(innerItor->first, mgr);
                    tout << ", ";
                }
                tout << std::endl;
            }
            tout << std::endl;
        }

        {
            tout << "(1) var = constStr:" << std::endl;
            std::map<expr*, expr*>::iterator itor1 = var_eq_constStr_map.begin();
            for (; itor1 != var_eq_constStr_map.end(); itor1++) {
                tout << "    * ";
                tout << mk_pp(itor1->first, mgr);
                tout << " = ";
                tout << mk_pp(itor1->second, mgr);
                if (!in_same_eqc(itor1->first, itor1->second)) {
                    tout << "   (not true in ctx)";
                }
                tout << std::endl;
            }
            tout << std::endl;
        }

        {
            tout << "(2) var = concat:" << std::endl;
            std::map<expr*, std::map<expr*, int> >::iterator itor2 = var_eq_concat_map.begin();
            for (; itor2 != var_eq_concat_map.end(); itor2++) {
                tout << "    * ";
                tout << mk_pp(itor2->first, mgr);
                tout << " = { ";
                std::map<expr*, int>::iterator i_itor = itor2->second.begin();
                for (; i_itor != itor2->second.end(); i_itor++) {
                    tout << mk_pp(i_itor->first, mgr);
                    tout << ", ";
                }
                tout << std::endl;
            }
            tout << std::endl;
        }

        {
            tout << "(3) var = unrollFunc:" << std::endl;
            std::map<expr*, std::map<expr*, int> >::iterator itor2 = var_eq_unroll_map.begin();
            for (; itor2 != var_eq_unroll_map.end(); itor2++) {
                tout << "    * " << mk_pp(itor2->first, mgr) << " = { ";
                std::map<expr*, int>::iterator i_itor = itor2->second.begin();
                for (; i_itor != itor2->second.end(); i_itor++) {
                    tout << mk_pp(i_itor->first, mgr) << ", ";
                }
                tout << " }" << std::endl;
            }
            tout << std::endl;
        }

        {
            tout << "(4) concat = constStr:" << std::endl;
            std::map<expr*, expr*>::iterator itor3 = concat_eq_constStr_map.begin();
            for (; itor3 != concat_eq_constStr_map.end(); itor3++) {
                tout << "    * ";
                tout << mk_pp(itor3->first, mgr);
                tout << " = ";
                tout << mk_pp(itor3->second, mgr);
                tout << std::endl;

            }
            tout << std::endl;
        }

        {
            tout << "(5) eq concats:" << std::endl;
            std::map<expr*, std::map<expr*, int> >::iterator itor4 = concat_eq_concat_map.begin();
            for (; itor4 != concat_eq_concat_map.end(); itor4++) {
                if (itor4->second.size() > 1) {
                    std::map<expr*, int>::iterator i_itor = itor4->second.begin();
                    tout << "    * ";
                    for (; i_itor != itor4->second.end(); i_itor++) {
                        tout << mk_pp(i_itor->first, mgr);
                        tout << " , ";
                    }
                    tout << std::endl;
                }
            }
            tout << std::endl;
        }

        {
            tout << "(6) eq unrolls:" << std::endl;
            std::map<expr*, std::set<expr*> >::iterator itor5 = unrollGroupMap.begin();
            for (; itor5 != unrollGroupMap.end(); itor5++) {
                tout << "    * ";
                std::set<expr*>::iterator i_itor = itor5->second.begin();
                for (; i_itor != itor5->second.end(); i_itor++) {
                    tout << mk_pp(*i_itor, mgr) << ",  ";
                }
                tout << std::endl;
            }
            tout << std::endl;
        }

        {
            tout << "(7) unroll = concats:" << std::endl;
            std::map<expr*, std::set<expr*> >::iterator itor5 = unrollGroupMap.begin();
            for (; itor5 != unrollGroupMap.end(); itor5++) {
                tout << "    * ";
                expr * unroll = itor5->first;
                tout << mk_pp(unroll, mgr) << std::endl;
                enode * e_curr = ctx.get_enode(unroll);
                enode * e_curr_end = e_curr;
                do {
                    app * curr = e_curr->get_owner();
                    if (u.str.is_concat(curr)) {
                        tout << "      >>> " << mk_pp(curr, mgr) << std::endl;
                    }
                    e_curr = e_curr->get_next();
                } while (e_curr != e_curr_end);
                tout << std::endl;
            }
            tout << std::endl;
        }
#else
        return;
#endif // _TRACE
    }


    /*
     * Dependence analysis from current context assignment
     * - "freeVarMap" contains a set of variables that doesn't constrained by Concats.
     *    But it's possible that it's bounded by unrolls
     *    For the case of
     *    (1) var1 = unroll(r1, t1)
     *        var1 is in the freeVarMap
     *        > should unroll r1 for var1
     *    (2) var1 = unroll(r1, t1) /\ var1 = Concat(var2, var3)
     *        var2, var3 are all in freeVar
     *        > should split the unroll function so that var2 and var3 are bounded by new unrolls
     */
    int theory_str::ctx_dep_analysis(std::map<expr*, int> & strVarMap, std::map<expr*, int> & freeVarMap,
                                     std::map<expr*, std::set<expr*> > & unrollGroupMap, std::map<expr*, std::map<expr*, int> > & var_eq_concat_map) {
        std::map<expr*, int> concatMap;
        std::map<expr*, int> unrollMap;
        std::map<expr*, expr*> aliasIndexMap;
        std::map<expr*, expr*> var_eq_constStr_map;
        std::map<expr*, expr*> concat_eq_constStr_map;
        std::map<expr*, std::map<expr*, int> > var_eq_unroll_map;
        std::map<expr*, std::map<expr*, int> > concat_eq_concat_map;
        std::map<expr*, std::map<expr*, int> > depMap;

        context & ctx = get_context();
        ast_manager & m = get_manager();

        // note that the old API concatenated these assignments into
        // a massive conjunction; we may have the opportunity to avoid that here
        expr_ref_vector assignments(m);
        ctx.get_assignments(assignments);

        // Step 1: get variables / concat AST appearing in the context
        // the thing we iterate over should just be variable_set - internal_variable_set
        // so we avoid computing the set difference (but this might be slower)
        for (expr* var : variable_set) {
        //for(obj_hashtable<expr>::iterator it = variable_set.begin(); it != variable_set.end(); ++it) {
            //expr* var = *it;
            if (internal_variable_set.find(var) == internal_variable_set.end()) {
                TRACE("str", tout << "new variable: " << mk_pp(var, m) << std::endl;);
                strVarMap[var] = 1;
            }
        }
        classify_ast_by_type_in_positive_context(strVarMap, concatMap, unrollMap);

        std::map<expr*, expr*> aliasUnrollSet;
        std::map<expr*, int>::iterator unrollItor = unrollMap.begin();
        for (; unrollItor != unrollMap.end(); ++unrollItor) {
            if (aliasUnrollSet.find(unrollItor->first) != aliasUnrollSet.end()) {
                continue;
            }
            expr * aRoot = nullptr;
            enode * e_currEqc = ctx.get_enode(unrollItor->first);
            enode * e_curr = e_currEqc;
            do {
                app * curr = e_currEqc->get_owner();
                if (u.re.is_unroll(curr)) {
                    if (aRoot == nullptr) {
                        aRoot = curr;
                    }
                    aliasUnrollSet[curr] = aRoot;
                }
                e_currEqc = e_currEqc->get_next();
            } while (e_currEqc != e_curr);
        }

        for (unrollItor = unrollMap.begin(); unrollItor != unrollMap.end(); unrollItor++) {
            expr * unrFunc = unrollItor->first;
            expr * urKey = aliasUnrollSet[unrFunc];
            unrollGroupMap[urKey].insert(unrFunc);
        }

        // Step 2: collect alias relation
        // e.g. suppose we have the equivalence class {x, y, z};
        // then we set aliasIndexMap[y] = x
        // and aliasIndexMap[z] = x

        std::map<expr*, int>::iterator varItor = strVarMap.begin();
        for (; varItor != strVarMap.end(); ++varItor) {
            if (aliasIndexMap.find(varItor->first) != aliasIndexMap.end()) {
                continue;
            }
            expr * aRoot = nullptr;
            expr * curr = varItor->first;
            do {
                if (variable_set.find(curr) != variable_set.end()) {
                    if (aRoot == nullptr) {
                        aRoot = curr;
                    } else {
                        aliasIndexMap[curr] = aRoot;
                    }
                }
                curr = get_eqc_next(curr);
            } while (curr != varItor->first);
        }

        // Step 3: Collect interested cases

        varItor = strVarMap.begin();
        for (; varItor != strVarMap.end(); ++varItor) {
            expr * deAliasNode = get_alias_index_ast(aliasIndexMap, varItor->first);
            // Case 1: variable = string constant
            // e.g. z = "str1" ::= var_eq_constStr_map[z] = "str1"

            if (var_eq_constStr_map.find(deAliasNode) == var_eq_constStr_map.end()) {
                bool nodeHasEqcValue = false;
                expr * nodeValue = get_eqc_value(deAliasNode, nodeHasEqcValue);
                if (nodeHasEqcValue) {
                    var_eq_constStr_map[deAliasNode] = nodeValue;
                }
            }

            // Case 2: var_eq_concat
            // e.g. z = concat("str1", b) ::= var_eq_concat[z][concat(c, "str2")] = 1
            // var_eq_unroll
            // e.g. z = unroll(...) ::= var_eq_unroll[z][unroll(...)] = 1

            if (var_eq_concat_map.find(deAliasNode) == var_eq_concat_map.end()) {
                expr * curr = get_eqc_next(deAliasNode);
                while (curr != deAliasNode) {
                    app * aCurr = to_app(curr);
                    // collect concat
                    if (u.str.is_concat(aCurr)) {
                        expr * arg0 = aCurr->get_arg(0);
                        expr * arg1 = aCurr->get_arg(1);
                        bool arg0HasEqcValue = false;
                        bool arg1HasEqcValue = false;
                        expr * arg0_value = get_eqc_value(arg0, arg0HasEqcValue);
                        expr * arg1_value = get_eqc_value(arg1, arg1HasEqcValue);

                        bool is_arg0_emptyStr = false;
                        if (arg0HasEqcValue) {
                            zstring strval;
                            u.str.is_string(arg0_value, strval);
                            if (strval.empty()) {
                                is_arg0_emptyStr = true;
                            }
                        }

                        bool is_arg1_emptyStr = false;
                        if (arg1HasEqcValue) {
                            zstring strval;
                            u.str.is_string(arg1_value, strval);
                            if (strval.empty()) {
                                is_arg1_emptyStr = true;
                            }
                        }

                        if (!is_arg0_emptyStr && !is_arg1_emptyStr) {
                            var_eq_concat_map[deAliasNode][curr] = 1;
                        }
                    } else if (u.re.is_unroll(to_app(curr))) {
                        var_eq_unroll_map[deAliasNode][curr] = 1;
                    }

                    curr = get_eqc_next(curr);
                }
            }

        } // for(varItor in strVarMap)

        // --------------------------------------------------
        // * collect aliasing relation among eq concats
        //   e.g EQC={concat1, concat2, concat3}
        //       concats_eq_Index_map[concat2] = concat1
        //       concats_eq_Index_map[concat3] = concat1
        // --------------------------------------------------

        std::map<expr*, expr*> concats_eq_index_map;
        std::map<expr*, int>::iterator concatItor = concatMap.begin();
        for(; concatItor != concatMap.end(); ++concatItor) {
            if (concats_eq_index_map.find(concatItor->first) != concats_eq_index_map.end()) {
                continue;
            }
            expr * aRoot = nullptr;
            expr * curr = concatItor->first;
            do {
                if (u.str.is_concat(to_app(curr))) {
                    if (aRoot == nullptr) {
                        aRoot = curr;
                    } else {
                        concats_eq_index_map[curr] = aRoot;
                    }
                }
                curr = get_eqc_next(curr);
            } while (curr != concatItor->first);
        }

        concatItor = concatMap.begin();
        for(; concatItor != concatMap.end(); ++concatItor) {
            expr * deAliasConcat = nullptr;
            if (concats_eq_index_map.find(concatItor->first) != concats_eq_index_map.end()) {
                deAliasConcat = concats_eq_index_map[concatItor->first];
            } else {
                deAliasConcat = concatItor->first;
            }

            // (3) concat_eq_conststr, e.g. concat(a,b) = "str1"
            if (concat_eq_constStr_map.find(deAliasConcat) == concat_eq_constStr_map.end()) {
                bool nodeHasEqcValue = false;
                expr * nodeValue = get_eqc_value(deAliasConcat, nodeHasEqcValue);
                if (nodeHasEqcValue) {
                    concat_eq_constStr_map[deAliasConcat] = nodeValue;
                }
            }

            // (4) concat_eq_concat, e.g.
            // concat(a,b) = concat("str1", c) AND z = concat(a,b) AND z = concat(e,f)
            if (concat_eq_concat_map.find(deAliasConcat) == concat_eq_concat_map.end()) {
                expr * curr = deAliasConcat;
                do {
                    if (u.str.is_concat(to_app(curr))) {
                        // curr cannot be reduced
                        if (concatMap.find(curr) != concatMap.end()) {
                            concat_eq_concat_map[deAliasConcat][curr] = 1;
                        }
                    }
                    curr = get_eqc_next(curr);
                } while (curr != deAliasConcat);
            }
        }

        // print some debugging info
        TRACE("str", trace_ctx_dep(tout, aliasIndexMap, var_eq_constStr_map,
                                   var_eq_concat_map, var_eq_unroll_map,
                                   concat_eq_constStr_map, concat_eq_concat_map, unrollGroupMap););

        if (!contain_pair_bool_map.empty()) {
            compute_contains(aliasIndexMap, concats_eq_index_map, var_eq_constStr_map, concat_eq_constStr_map, var_eq_concat_map);
        }

        // step 4: dependence analysis

        // (1) var = string constant
        for (std::map<expr*, expr*>::iterator itor = var_eq_constStr_map.begin();
             itor != var_eq_constStr_map.end(); ++itor) {
            expr * var = get_alias_index_ast(aliasIndexMap, itor->first);
            expr * strAst = itor->second;
            depMap[var][strAst] = 1;
        }

        // (2) var = concat
        for (std::map<expr*, std::map<expr*, int> >::iterator itor = var_eq_concat_map.begin();
             itor != var_eq_concat_map.end(); ++itor) {
            expr * var = get_alias_index_ast(aliasIndexMap, itor->first);
            for (std::map<expr*, int>::iterator itor1 = itor->second.begin(); itor1 != itor->second.end(); ++itor1) {
                expr * concat = itor1->first;
                std::map<expr*, int> inVarMap;
                std::map<expr*, int> inConcatMap;
                std::map<expr*, int> inUnrollMap;
                classify_ast_by_type(concat, inVarMap, inConcatMap, inUnrollMap);
                for (std::map<expr*, int>::iterator itor2 = inVarMap.begin(); itor2 != inVarMap.end(); ++itor2) {
                    expr * varInConcat = get_alias_index_ast(aliasIndexMap, itor2->first);
                    if (!(depMap[var].find(varInConcat) != depMap[var].end() && depMap[var][varInConcat] == 1)) {
                        depMap[var][varInConcat] = 2;
                    }
                }
            }
        }

        for (std::map<expr*, std::map<expr*, int> >::iterator itor = var_eq_unroll_map.begin();
             itor != var_eq_unroll_map.end(); itor++) {
            expr * var = get_alias_index_ast(aliasIndexMap, itor->first);
            for (std::map<expr*, int>::iterator itor1 = itor->second.begin(); itor1 != itor->second.end(); itor1++) {
                expr * unrollFunc = itor1->first;
                std::map<expr*, int> inVarMap;
                std::map<expr*, int> inConcatMap;
                std::map<expr*, int> inUnrollMap;
                classify_ast_by_type(unrollFunc, inVarMap, inConcatMap, inUnrollMap);
                for (std::map<expr*, int>::iterator itor2 = inVarMap.begin(); itor2 != inVarMap.end(); itor2++) {
                    expr * varInFunc = get_alias_index_ast(aliasIndexMap, itor2->first);

                    TRACE("str", tout << "var in unroll = " <<
                          mk_ismt2_pp(itor2->first, m) << std::endl
                          << "dealiased var = " << mk_ismt2_pp(varInFunc, m) << std::endl;);

                    // it's possible that we have both (Unroll $$_regVar_0 $$_unr_0) /\ (Unroll abcd $$_unr_0),
                    // while $$_regVar_0 = "abcd"
                    // have to exclude such cases
                    bool varHasValue = false;
                    get_eqc_value(varInFunc, varHasValue);
                    if (varHasValue)
                        continue;

                    if (depMap[var].find(varInFunc) == depMap[var].end()) {
                        depMap[var][varInFunc] = 6;
                    }
                }
            }
        }

        // (3) concat = string constant
        for (std::map<expr*, expr*>::iterator itor = concat_eq_constStr_map.begin();
             itor != concat_eq_constStr_map.end(); itor++) {
            expr * concatAst = itor->first;
            expr * constStr = itor->second;
            std::map<expr*, int> inVarMap;
            std::map<expr*, int> inConcatMap;
            std::map<expr*, int> inUnrollMap;
            classify_ast_by_type(concatAst, inVarMap, inConcatMap, inUnrollMap);
            for (std::map<expr*, int>::iterator itor2 = inVarMap.begin(); itor2 != inVarMap.end(); itor2++) {
                expr * varInConcat = get_alias_index_ast(aliasIndexMap, itor2->first);
                if (!(depMap[varInConcat].find(constStr) != depMap[varInConcat].end() && depMap[varInConcat][constStr] == 1))
                    depMap[varInConcat][constStr] = 3;
            }
        }

        // (4) equivalent concats
        //     - possibility 1 : concat("str", v1) = concat(concat(v2, v3), v4) = concat(v5, v6)
        //         ==> v2, v5 are constrained by "str"
        //     - possibility 2 : concat(v1, "str") = concat(v2, v3) = concat(v4, v5)
        //         ==> v2, v4 are constrained by "str"
        //--------------------------------------------------------------

        std::map<expr*, expr*> mostLeftNodes;
        std::map<expr*, expr*> mostRightNodes;

        std::map<expr*, int> mLIdxMap;
        std::map<int, std::set<expr*> > mLMap;
        std::map<expr*, int> mRIdxMap;
        std::map<int, std::set<expr*> > mRMap;
        std::set<expr*> nSet;

        for (std::map<expr*, std::map<expr*, int> >::iterator itor = concat_eq_concat_map.begin();
             itor != concat_eq_concat_map.end(); itor++) {
            mostLeftNodes.clear();
            mostRightNodes.clear();

            expr * mLConst = nullptr;
            expr * mRConst = nullptr;

            for (std::map<expr*, int>::iterator itor1 = itor->second.begin(); itor1 != itor->second.end(); itor1++) {
                expr * concatNode = itor1->first;
                expr * mLNode = getMostLeftNodeInConcat(concatNode);
                zstring strval;
                if (u.str.is_string(to_app(mLNode), strval)) {
                    if (mLConst == nullptr && strval.empty()) {
                        mLConst = mLNode;
                    }
                } else {
                    mostLeftNodes[mLNode] = concatNode;
                }

                expr * mRNode = getMostRightNodeInConcat(concatNode);
                if (u.str.is_string(to_app(mRNode), strval)) {
                    if (mRConst == nullptr && strval.empty()) {
                        mRConst = mRNode;
                    }
                } else {
                    mostRightNodes[mRNode] = concatNode;
                }
            }

            if (mLConst != nullptr) {
                // -------------------------------------------------------------------------------------
                // The left most variable in a concat is constrained by a constant string in eqc concat
                // -------------------------------------------------------------------------------------
                // e.g. Concat(x, ...) = Concat("abc", ...)
                // -------------------------------------------------------------------------------------
                for (std::map<expr*, expr*>::iterator itor1 = mostLeftNodes.begin();
                     itor1 != mostLeftNodes.end(); itor1++) {
                    expr * deVar = get_alias_index_ast(aliasIndexMap, itor1->first);
                    if (depMap[deVar].find(mLConst) == depMap[deVar].end() || depMap[deVar][mLConst] != 1) {
                        depMap[deVar][mLConst] = 4;
                    }
                }
            }

            {
                // -------------------------------------------------------------------------------------
                // The left most variables in eqc concats are constrained by each other
                // -------------------------------------------------------------------------------------
                // e.g. concat(x, ...) = concat(u, ...) = ...
                //      x and u are constrained by each other
                // -------------------------------------------------------------------------------------
                nSet.clear();
                std::map<expr*, expr*>::iterator itl = mostLeftNodes.begin();
                for (; itl != mostLeftNodes.end(); itl++) {
                    bool lfHasEqcValue = false;
                    get_eqc_value(itl->first, lfHasEqcValue);
                    if (lfHasEqcValue)
                        continue;
                    expr * deVar = get_alias_index_ast(aliasIndexMap, itl->first);
                    nSet.insert(deVar);
                }

                if (nSet.size() > 1) {
                    int lId = -1;
                    for (std::set<expr*>::iterator itor2 = nSet.begin(); itor2 != nSet.end(); itor2++) {
                        if (mLIdxMap.find(*itor2) != mLIdxMap.end()) {
                            lId = mLIdxMap[*itor2];
                            break;
                        }
                    }
                    if (lId == -1)
                        lId = static_cast<int>(mLMap.size());
                    for (std::set<expr*>::iterator itor2 = nSet.begin(); itor2 != nSet.end(); itor2++) {
                        bool itorHasEqcValue = false;
                        get_eqc_value(*itor2, itorHasEqcValue);
                        if (itorHasEqcValue)
                            continue;
                        mLIdxMap[*itor2] = lId;
                        mLMap[lId].insert(*itor2);
                    }
                }
            }

            if (mRConst != nullptr) {
                for (std::map<expr*, expr*>::iterator itor1 = mostRightNodes.begin();
                     itor1 != mostRightNodes.end(); itor1++) {
                    expr * deVar = get_alias_index_ast(aliasIndexMap, itor1->first);
                    if (depMap[deVar].find(mRConst) == depMap[deVar].end() || depMap[deVar][mRConst] != 1) {
                        depMap[deVar][mRConst] = 5;
                    }
                }
            }

            {
                nSet.clear();
                std::map<expr*, expr*>::iterator itr = mostRightNodes.begin();
                for (; itr != mostRightNodes.end(); itr++) {
                    expr * deVar = get_alias_index_ast(aliasIndexMap, itr->first);
                    nSet.insert(deVar);
                }
                if (nSet.size() > 1) {
                    int rId = -1;
                    std::set<expr*>::iterator itor2 = nSet.begin();
                    for (; itor2 != nSet.end(); itor2++) {
                        if (mRIdxMap.find(*itor2) != mRIdxMap.end()) {
                            rId = mRIdxMap[*itor2];
                            break;
                        }
                    }
                    if (rId == -1)
                        rId = static_cast<int>(mRMap.size());
                    for (itor2 = nSet.begin(); itor2 != nSet.end(); itor2++) {
                        bool rHasEqcValue = false;
                        get_eqc_value(*itor2, rHasEqcValue);
                        if (rHasEqcValue)
                            continue;
                        mRIdxMap[*itor2] = rId;
                        mRMap[rId].insert(*itor2);
                    }
                }
            }
        }

        // print the dependence map
        TRACE("str",
              tout << "Dependence Map" << std::endl;
              for(std::map<expr*, std::map<expr*, int> >::iterator itor = depMap.begin(); itor != depMap.end(); itor++) {
                  tout << mk_pp(itor->first, m);
                  rational nnLen;
                  bool nnLen_exists = get_len_value(itor->first, nnLen);
                  tout << "  [len = " << (nnLen_exists ? nnLen.to_string() : "?") << "] \t-->\t";
                  for (std::map<expr*, int>::iterator itor1 = itor->second.begin(); itor1 != itor->second.end(); itor1++) {
                      tout << mk_pp(itor1->first, m) << "(" << itor1->second << "), ";
                  }
                  tout << std::endl;
              }
              );

        // step, errr, 5: compute free variables based on the dependence map

        // the case dependence map is empty, every var in VarMap is free
        //---------------------------------------------------------------
        // remove L/R most var in eq concat since they are constrained with each other
        std::map<expr*, std::map<expr*, int> > lrConstrainedMap;
        for (std::map<int, std::set<expr*> >::iterator itor = mLMap.begin(); itor != mLMap.end(); itor++) {
            for (std::set<expr*>::iterator it1 = itor->second.begin(); it1 != itor->second.end(); it1++) {
                std::set<expr*>::iterator it2 = it1;
                it2++;
                for (; it2 != itor->second.end(); it2++) {
                    expr * n1 = *it1;
                    expr * n2 = *it2;
                    lrConstrainedMap[n1][n2] = 1;
                    lrConstrainedMap[n2][n1] = 1;
                }
            }
        }
        for (std::map<int, std::set<expr*> >::iterator itor = mRMap.begin(); itor != mRMap.end(); itor++) {
            for (std::set<expr*>::iterator it1 = itor->second.begin(); it1 != itor->second.end(); it1++) {
                std::set<expr*>::iterator it2 = it1;
                it2++;
                for (; it2 != itor->second.end(); it2++) {
                    expr * n1 = *it1;
                    expr * n2 = *it2;
                    lrConstrainedMap[n1][n2] = 1;
                    lrConstrainedMap[n2][n1] = 1;
                }
            }
        }

        if (depMap.size() == 0) {
            std::map<expr*, int>::iterator itor = strVarMap.begin();
            for (; itor != strVarMap.end(); itor++) {
                expr * var = get_alias_index_ast(aliasIndexMap, itor->first);
                if (lrConstrainedMap.find(var) == lrConstrainedMap.end()) {
                    freeVarMap[var] = 1;
                } else {
                    int lrConstainted = 0;
                    std::map<expr*, int>::iterator lrit = freeVarMap.begin();
                    for (; lrit != freeVarMap.end(); lrit++) {
                        if (lrConstrainedMap[var].find(lrit->first) != lrConstrainedMap[var].end()) {
                            lrConstainted = 1;
                            break;
                        }
                    }
                    if (lrConstainted == 0) {
                        freeVarMap[var] = 1;
                    }
                }
            }
        } else {
            // if the keys in aliasIndexMap are not contained in keys in depMap, they are free
            // e.g.,  x= y /\ x = z /\ t = "abc"
            //        aliasIndexMap[y]= x, aliasIndexMap[z] = x
            //        depMap        t ~ "abc"(1)
            //        x should be free
            std::map<expr*, int>::iterator itor2 = strVarMap.begin();
            for (; itor2 != strVarMap.end(); itor2++) {
                if (aliasIndexMap.find(itor2->first) != aliasIndexMap.end()) {
                    expr * var = aliasIndexMap[itor2->first];
                    if (depMap.find(var) == depMap.end()) {
                        if (lrConstrainedMap.find(var) == lrConstrainedMap.end()) {
                            freeVarMap[var] = 1;
                        } else {
                            int lrConstainted = 0;
                            std::map<expr*, int>::iterator lrit = freeVarMap.begin();
                            for (; lrit != freeVarMap.end(); lrit++) {
                                if (lrConstrainedMap[var].find(lrit->first) != lrConstrainedMap[var].end()) {
                                    lrConstainted = 1;
                                    break;
                                }
                            }
                            if (lrConstainted == 0) {
                                freeVarMap[var] = 1;
                            }
                        }
                    }
                } else if (aliasIndexMap.find(itor2->first) == aliasIndexMap.end()) {
                    // if a variable is not in aliasIndexMap and not in depMap, it's free
                    if (depMap.find(itor2->first) == depMap.end()) {
                        expr * var = itor2->first;
                        if (lrConstrainedMap.find(var) == lrConstrainedMap.end()) {
                            freeVarMap[var] = 1;
                        } else {
                            int lrConstainted = 0;
                            std::map<expr*, int>::iterator lrit = freeVarMap.begin();
                            for (; lrit != freeVarMap.end(); lrit++) {
                                if (lrConstrainedMap[var].find(lrit->first) != lrConstrainedMap[var].end()) {
                                    lrConstainted = 1;
                                    break;
                                }
                            }
                            if (lrConstainted == 0) {
                                freeVarMap[var] = 1;
                            }
                        }
                    }
                }
            }

            std::map<expr*, std::map<expr*, int> >::iterator itor = depMap.begin();
            for (; itor != depMap.end(); itor++) {
                for (std::map<expr*, int>::iterator itor1 = itor->second.begin(); itor1 != itor->second.end(); itor1++) {
                    if (variable_set.find(itor1->first) != variable_set.end()) { // expr type = var
                        expr * var = get_alias_index_ast(aliasIndexMap, itor1->first);
                        // if a var is dep on itself and all dependence are type 2, it's a free variable
                        // e.g {y --> x(2), y(2), m --> m(2), n(2)} y,m are free
                        {
                            if (depMap.find(var) == depMap.end()) {
                                if (freeVarMap.find(var) == freeVarMap.end()) {
                                    if (lrConstrainedMap.find(var) == lrConstrainedMap.end()) {
                                        freeVarMap[var] = 1;
                                    } else {
                                        int lrConstainted = 0;
                                        std::map<expr*, int>::iterator lrit = freeVarMap.begin();
                                        for (; lrit != freeVarMap.end(); lrit++) {
                                            if (lrConstrainedMap[var].find(lrit->first) != lrConstrainedMap[var].end()) {
                                                lrConstainted = 1;
                                                break;
                                            }
                                        }
                                        if (lrConstainted == 0) {
                                            freeVarMap[var] = 1;
                                        }
                                    }

                                } else {
                                    freeVarMap[var] = freeVarMap[var] + 1;
                                }
                            }
                        }
                    }
                }
            }
        }

        return 0;
    }

    // Check agreement between integer and string theories for the term a = (str.to-int S).
    // Returns true if axioms were added, and false otherwise.
    bool theory_str::finalcheck_str2int(app * a) {
        SASSERT(u.str.is_stoi(a));
        bool axiomAdd = false;
        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr * S = a->get_arg(0);

        // check integer theory
        rational Ival;
        bool Ival_exists = get_arith_value(a, Ival);
        if (Ival_exists) {
            TRACE("str", tout << "integer theory assigns " << mk_pp(a, m) << " = " << Ival.to_string() << std::endl;);
            // if that value is not -1, we can assert (str.to.int S) = Ival --> S = "Ival"
            if (!Ival.is_minus_one()) {
                zstring Ival_str(Ival.to_string().c_str());
                expr_ref premise(ctx.mk_eq_atom(a, m_autil.mk_numeral(Ival, true)), m);
                expr_ref conclusion(ctx.mk_eq_atom(S, mk_string(Ival_str)), m);
                expr_ref axiom(rewrite_implication(premise, conclusion), m);
                if (!string_int_axioms.contains(axiom)) {
                    string_int_axioms.insert(axiom);
                    assert_axiom(axiom);
                    m_trail_stack.push(insert_obj_trail<theory_str, expr>(string_int_axioms, axiom));
                    axiomAdd = true;
                }
            }
        } else {
            TRACE("str", tout << "integer theory has no assignment for " << mk_pp(a, m) << std::endl;);
            expr_ref is_zero(ctx.mk_eq_atom(a, m_autil.mk_int(0)), m);
            /* literal is_zero_l = */ mk_literal(is_zero);
            axiomAdd = true;
            TRACE("str", ctx.display(tout););
            // NOT_IMPLEMENTED_YET();
        }

        bool S_hasEqcValue;
        expr * S_str = get_eqc_value(S, S_hasEqcValue);
        if (S_hasEqcValue) {
            zstring str;
            u.str.is_string(S_str, str);
            bool valid = true;
            rational convertedRepresentation(0);
            rational ten(10);
            if (str.length() == 0) {
                valid = false;
            } else {
                for (unsigned i = 0; i < str.length(); ++i) {
                    if (!('0' <= str[i] && str[i] <= '9')) {
                        valid = false;
                        break;
                    } else {
                        // accumulate
                        char digit = (int)str[i];
                        std::string sDigit(1, digit);
                        int val = atoi(sDigit.c_str());
                        convertedRepresentation = (ten * convertedRepresentation) + rational(val);
                    }
                }
            }
            // TODO this duplicates code a bit, we can simplify the branch on "conclusion" only
            if (valid) {
                expr_ref premise(ctx.mk_eq_atom(S, mk_string(str)), m);
                expr_ref conclusion(ctx.mk_eq_atom(a, m_autil.mk_numeral(convertedRepresentation, true)), m);
                expr_ref axiom(rewrite_implication(premise, conclusion), m);
                if (!string_int_axioms.contains(axiom)) {
                    string_int_axioms.insert(axiom);
                    assert_axiom(axiom);
                    m_trail_stack.push(insert_obj_trail<theory_str, expr>(string_int_axioms, axiom));
                    axiomAdd = true;
                }
            } else {
                expr_ref premise(ctx.mk_eq_atom(S, mk_string(str)), m);
                expr_ref conclusion(ctx.mk_eq_atom(a, m_autil.mk_numeral(rational::minus_one(), true)), m);
                expr_ref axiom(rewrite_implication(premise, conclusion), m);
                if (!string_int_axioms.contains(axiom)) {
                    string_int_axioms.insert(axiom);
                    assert_axiom(axiom);
                    m_trail_stack.push(insert_obj_trail<theory_str, expr>(string_int_axioms, axiom));
                    axiomAdd = true;
                }
            }
        }

        return axiomAdd;
    }

    bool theory_str::finalcheck_int2str(app * a) {
        bool axiomAdd = false;
        context & ctx = get_context();
        ast_manager & m = get_manager();

        expr * N = a->get_arg(0);

        // check string theory
        bool Sval_expr_exists;
        expr * Sval_expr = get_eqc_value(a, Sval_expr_exists);
        if (Sval_expr_exists) {
            zstring Sval;
            u.str.is_string(Sval_expr, Sval);
            TRACE("str", tout << "string theory assigns \"" << mk_pp(a, m) << " = " << Sval << "\n";);
            // empty string --> integer value < 0
            if (Sval.empty()) {
                // ignore this. we should already assert the axiom for what happens when the string is ""
            } else {
                // nonempty string --> convert to correct integer value, or disallow it
                rational convertedRepresentation(0);
                rational ten(10);
                bool conversionOK = true;
                for (unsigned i = 0; i < Sval.length(); ++i) {
                    char digit = (int)Sval[i];
                    if (isdigit((int)digit)) {
                        std::string sDigit(1, digit);
                        int val = atoi(sDigit.c_str());
                        convertedRepresentation = (ten * convertedRepresentation) + rational(val);
                    } else {
                        // not a digit, invalid
                        TRACE("str", tout << "str.to-int argument contains non-digit character '" << digit << "'" << std::endl;);
                        conversionOK = false;
                        break;
                    }
                }
                if (conversionOK) {
                    expr_ref premise(ctx.mk_eq_atom(a, mk_string(Sval)), m);
                    expr_ref conclusion(ctx.mk_eq_atom(N, m_autil.mk_numeral(convertedRepresentation, true)), m);
                    expr_ref axiom(rewrite_implication(premise, conclusion), m);
                    if (!string_int_axioms.contains(axiom)) {
                        string_int_axioms.insert(axiom);
                        assert_axiom(axiom);
                        m_trail_stack.push(insert_obj_trail<theory_str, expr>(string_int_axioms, axiom));
                        axiomAdd = true;
                    }
                } else {
                    expr_ref axiom(m.mk_not(ctx.mk_eq_atom(a, mk_string(Sval))), m);
                    // always assert this axiom because this is a conflict clause
                    assert_axiom(axiom);
                    axiomAdd = true;
                }
            }
        } else {
            TRACE("str", tout << "string theory has no assignment for " << mk_pp(a, m) << std::endl;);
            NOT_IMPLEMENTED_YET();
        }
        return axiomAdd;
    }

    void theory_str::collect_var_concat(expr * node, std::set<expr*> & varSet, std::set<expr*> & concatSet) {
        if (variable_set.find(node) != variable_set.end()) {
            if (internal_lenTest_vars.find(node) == internal_lenTest_vars.end()) {
                varSet.insert(node);
            }
        }
        else if (is_app(node)) {
            app * aNode = to_app(node);
            if (u.str.is_length(aNode)) {
                // Length
                return;
            }
            if (u.str.is_concat(aNode)) {
                if (concatSet.find(node) == concatSet.end()) {
                    concatSet.insert(node);
                }
            }
            // recursively visit all arguments
            for (unsigned i = 0; i < aNode->get_num_args(); ++i) {
                expr * arg = aNode->get_arg(i);
                collect_var_concat(arg, varSet, concatSet);
            }
        }
    }

    bool theory_str::propagate_length_within_eqc(expr * var) {
        bool res = false;
        ast_manager & m = get_manager();
        context & ctx = get_context();

        TRACE("str", tout << "propagate_length_within_eqc: " << mk_ismt2_pp(var, m) << std::endl ;);

        rational varLen;
        if (! get_len_value(var, varLen)) {
            bool hasLen = false;
            expr * nodeWithLen= var;
            do {
                if (get_len_value(nodeWithLen, varLen)) {
                    hasLen = true;
                    break;
                }
                nodeWithLen = get_eqc_next(nodeWithLen);
            } while (nodeWithLen != var);

            if (hasLen) {
                // var = nodeWithLen --> |var| = |nodeWithLen|
                expr_ref_vector l_items(m);
                expr_ref varEqNode(ctx.mk_eq_atom(var, nodeWithLen), m);
                l_items.push_back(varEqNode);

                expr_ref nodeWithLenExpr (mk_strlen(nodeWithLen), m);
                expr_ref varLenExpr (mk_int(varLen), m);
                expr_ref lenEqNum(ctx.mk_eq_atom(nodeWithLenExpr, varLenExpr), m);
                l_items.push_back(lenEqNum);

                expr_ref axl(m.mk_and(l_items.size(), l_items.c_ptr()), m);
                expr_ref varLen(mk_strlen(var), m);
                expr_ref axr(ctx.mk_eq_atom(varLen, mk_int(varLen)), m);
                assert_implication(axl, axr);
                TRACE("str", tout <<  mk_ismt2_pp(axl, m) << std::endl << "  --->  " << std::endl <<  mk_ismt2_pp(axr, m););
                res = true;
            }
        }
        return res;
    }

    bool theory_str::propagate_length(std::set<expr*> & varSet, std::set<expr*> & concatSet, std::map<expr*, int> & exprLenMap) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        expr_ref_vector assignments(m);
        ctx.get_assignments(assignments);
        bool axiomAdded = false;
        // collect all concats in context
        for (expr_ref_vector::iterator it = assignments.begin(); it != assignments.end(); ++it) {
            if (! ctx.is_relevant(*it)) {
                continue;
            }
            if (m.is_eq(*it)) {
                collect_var_concat(*it, varSet, concatSet);
            }
        }
        // iterate each concat
        // if a concat doesn't have length info, check if the length of all leaf nodes can be resolved
        for (std::set<expr*>::iterator it = concatSet.begin(); it != concatSet.end(); it++) {
            expr * concat = *it;
            rational lenValue;
            expr_ref concatlenExpr (mk_strlen(concat), m) ;
            bool allLeafResolved = true;
            if (! get_arith_value(concatlenExpr, lenValue)) {
                // the length fo concat is unresolved yet
                if (get_len_value(concat, lenValue)) {
                    // but all leaf nodes have length information
                    TRACE("str", tout << "* length pop-up: " <<  mk_ismt2_pp(concat, m) << "| = " << lenValue << std::endl;);
                    std::set<expr*> leafNodes;
                    get_unique_non_concat_nodes(concat, leafNodes);
                    expr_ref_vector l_items(m);
                    for (std::set<expr*>::iterator leafIt = leafNodes.begin(); leafIt != leafNodes.end(); ++leafIt) {
                        rational leafLenValue;
                        if (get_len_value(*leafIt, leafLenValue)) {
                            expr_ref leafItLenExpr (mk_strlen(*leafIt), m);
                            expr_ref leafLenValueExpr (mk_int(leafLenValue), m);
                            expr_ref lcExpr (ctx.mk_eq_atom(leafItLenExpr, leafLenValueExpr), m);
                            l_items.push_back(lcExpr);
                        } else {
                            allLeafResolved = false;
                            break;
                        }
                    }
                    if (allLeafResolved) {
                        expr_ref axl(m.mk_and(l_items.size(), l_items.c_ptr()), m);
                        expr_ref lenValueExpr (mk_int(lenValue), m);
                        expr_ref axr(ctx.mk_eq_atom(concatlenExpr, lenValueExpr), m);
                        assert_implication(axl, axr);
                        TRACE("str", tout <<  mk_ismt2_pp(axl, m) << std::endl << "  --->  " << std::endl <<  mk_ismt2_pp(axr, m)<< std::endl;);
                        axiomAdded = true;
                    }
                }
            }
        }
        // if no concat length is propagated, check the length of variables.
        if (! axiomAdded) {
            for (std::set<expr*>::iterator it = varSet.begin(); it != varSet.end(); it++) {
                expr * var = *it;
                rational lenValue;
                expr_ref varlen (mk_strlen(var), m) ;
                if (! get_arith_value(varlen, lenValue)) {
                    if (propagate_length_within_eqc(var)) {
                        axiomAdded = true;
                    }
                }
            }

        }
        return axiomAdded;
    }

    void theory_str::get_unique_non_concat_nodes(expr * node, std::set<expr*> & argSet) {
        app * a_node = to_app(node);
        if (!u.str.is_concat(a_node)) {
            argSet.insert(node);
            return;
        } else {
            SASSERT(a_node->get_num_args() == 2);
            expr * leftArg = a_node->get_arg(0);
            expr * rightArg = a_node->get_arg(1);
            get_unique_non_concat_nodes(leftArg, argSet);
            get_unique_non_concat_nodes(rightArg, argSet);
        }
    }

    final_check_status theory_str::final_check_eh() {
        context & ctx = get_context();
        ast_manager & m = get_manager();

        //expr_ref_vector assignments(m);
        //ctx.get_assignments(assignments);

        if (opt_VerifyFinalCheckProgress) {
            finalCheckProgressIndicator = false;
        }

        TRACE("str", tout << "final check" << std::endl;);
        TRACE_CODE(if (is_trace_enabled("t_str_dump_assign")) { dump_assignments(); });
        check_variable_scope();

        if (opt_DeferEQCConsistencyCheck) {
            TRACE("str", tout << "performing deferred EQC consistency check" << std::endl;);
            std::set<enode*> eqc_roots;
            for (ptr_vector<enode>::const_iterator it = ctx.begin_enodes(); it != ctx.end_enodes(); ++it) {
                enode * e = *it;
                enode * root = e->get_root();
                eqc_roots.insert(root);
            }

            bool found_inconsistency = false;

            for (std::set<enode*>::iterator it = eqc_roots.begin(); it != eqc_roots.end(); ++it) {
                enode * e = *it;
                app * a = e->get_owner();
                if (!(m.get_sort(a) == u.str.mk_string_sort())) {
                    TRACE("str", tout << "EQC root " << mk_pp(a, m) << " not a string term; skipping" << std::endl;);
                } else {
                    TRACE("str", tout << "EQC root " << mk_pp(a, m) << " is a string term. Checking this EQC" << std::endl;);
                    // first call check_concat_len_in_eqc() on each member of the eqc
                    enode * e_it = e;
                    enode * e_root = e_it;
                    do {
                        bool status = check_concat_len_in_eqc(e_it->get_owner());
                        if (!status) {
                            TRACE("str", tout << "concat-len check asserted an axiom on " << mk_pp(e_it->get_owner(), m) << std::endl;);
                            found_inconsistency = true;
                        }
                        e_it = e_it->get_next();
                    } while (e_it != e_root);

                    // now grab any two distinct elements from the EQC and call new_eq_check() on them
                    enode * e1 = e;
                    enode * e2 = e1->get_next();
                    if (e1 != e2) {
                        TRACE("str", tout << "deferred new_eq_check() over EQC of " << mk_pp(e1->get_owner(), m) << " and " << mk_pp(e2->get_owner(), m) << std::endl;);
                        bool result = new_eq_check(e1->get_owner(), e2->get_owner());
                        if (!result) {
                            TRACE("str", tout << "new_eq_check found inconsistencies" << std::endl;);
                            found_inconsistency = true;
                        }
                    }
                }
            }

            if (found_inconsistency) {
                TRACE("str", tout << "Found inconsistency in final check! Returning to search." << std::endl;);
                return FC_CONTINUE;
            } else {
                TRACE("str", tout << "Deferred consistency check passed. Continuing in final check." << std::endl;);
            }
        }

        // run dependence analysis to find free string variables
        std::map<expr*, int> varAppearInAssign;
        std::map<expr*, int> freeVar_map;
        std::map<expr*, std::set<expr*> > unrollGroup_map;
        std::map<expr*, std::map<expr*, int> > var_eq_concat_map;
        int conflictInDep = ctx_dep_analysis(varAppearInAssign, freeVar_map, unrollGroup_map, var_eq_concat_map);
        if (conflictInDep == -1) {
            // return Z3_TRUE;
            return FC_DONE;
        }

        // enhancement: improved backpropagation of string constants into var=concat terms
        bool backpropagation_occurred = false;
        for (std::map<expr*, std::map<expr*, int> >::iterator veqc_map_it = var_eq_concat_map.begin();
             veqc_map_it != var_eq_concat_map.end(); ++veqc_map_it) {
            expr * var = veqc_map_it->first;
            for (std::map<expr*, int>::iterator concat_map_it = veqc_map_it->second.begin();
                 concat_map_it != veqc_map_it->second.end(); ++concat_map_it) {
                app * concat = to_app(concat_map_it->first);
                expr * concat_lhs = concat->get_arg(0);
                expr * concat_rhs = concat->get_arg(1);
                // If the concat LHS and RHS both have a string constant in their EQC,
                // but the var does not, then we assert an axiom of the form
                // (lhs = "lhs" AND rhs = "rhs") --> (Concat lhs rhs) = "lhsrhs"
                bool concat_lhs_haseqc, concat_rhs_haseqc, var_haseqc;
                expr * concat_lhs_str = get_eqc_value(concat_lhs, concat_lhs_haseqc);
                expr * concat_rhs_str = get_eqc_value(concat_rhs, concat_rhs_haseqc);
                get_eqc_value(var, var_haseqc);
                if (concat_lhs_haseqc && concat_rhs_haseqc && !var_haseqc) {
                    TRACE("str", tout << "backpropagate into " << mk_pp(var, m) << " = " << mk_pp(concat, m) << std::endl
                          << "LHS ~= " << mk_pp(concat_lhs_str, m) << " RHS ~= " << mk_pp(concat_rhs_str, m) << std::endl;);

                    zstring lhsString, rhsString;
                    u.str.is_string(concat_lhs_str, lhsString);
                    u.str.is_string(concat_rhs_str, rhsString);
                    zstring concatString = lhsString + rhsString;

                    // special handling: don't assert that string constants are equal to themselves
                    expr_ref_vector lhs_terms(m);
                    if (!u.str.is_string(concat_lhs)) {
                        lhs_terms.push_back(ctx.mk_eq_atom(concat_lhs, concat_lhs_str));
                    }
                    if (!u.str.is_string(concat_rhs)) {
                        lhs_terms.push_back(ctx.mk_eq_atom(concat_rhs, concat_rhs_str));
                    }

                    if (lhs_terms.empty()) {
                        // no assumptions on LHS
                        expr_ref rhs(ctx.mk_eq_atom(concat, mk_string(concatString)), m);
                        assert_axiom(rhs);
                    } else {
                        expr_ref lhs(mk_and(lhs_terms), m);
                        expr_ref rhs(ctx.mk_eq_atom(concat, mk_string(concatString)), m);
                        assert_implication(lhs, rhs);
                    }
                    backpropagation_occurred = true;
                }
            }
        }

        if (backpropagation_occurred) {
            TRACE("str", tout << "Resuming search due to axioms added by backpropagation." << std::endl;);
            return FC_CONTINUE;
        }

        // enhancement: improved backpropagation of length information
        {
            std::set<expr*> varSet;
            std::set<expr*> concatSet;
            std::map<expr*, int> exprLenMap;

            bool length_propagation_occurred = propagate_length(varSet, concatSet, exprLenMap);
            if (length_propagation_occurred) {
                TRACE("str", tout << "Resuming search due to axioms added by length propagation." << std::endl;);
                return FC_CONTINUE;
            }
        }

        // regex automata
        if (m_params.m_RegexAutomata) {
            // TODO since heuristics might fail, the "no progress" flag might need to be handled specially here
            bool regex_axiom_add = false;
            for (obj_hashtable<expr>::iterator it = regex_terms.begin(); it != regex_terms.end(); ++it) {
                expr * str_in_re = *it;
                expr * str;
                expr * re;
                u.str.is_in_re(str_in_re, str, re);
                lbool current_assignment = ctx.get_assignment(str_in_re);
                TRACE("str", tout << "regex term: " << mk_pp(str, m) << " in " << mk_pp(re, m) << " : " << current_assignment << std::endl;);
                if (current_assignment == l_undef) {
                    continue;
                }

                if (!regex_terms_with_length_constraints.contains(str_in_re)) {
                    if (current_assignment == l_true && check_regex_length_linearity(re)) {
                        TRACE("str", tout << "regex length constraints expected to be linear -- generating and asserting them" << std::endl;);

                        if (regex_term_to_length_constraint.contains(str_in_re)) {
                            // use existing length constraint
                            expr * top_level_length_constraint;
                            regex_term_to_length_constraint.find(str_in_re, top_level_length_constraint);

                            ptr_vector<expr> extra_length_vars;
                            regex_term_to_extra_length_vars.find(str_in_re, extra_length_vars);

                            assert_axiom(top_level_length_constraint);
                            for(ptr_vector<expr>::iterator it = extra_length_vars.begin(); it != extra_length_vars.end(); ++it) {
                                expr * v = *it;
                                refresh_theory_var(v);
                                expr_ref len_constraint(m_autil.mk_ge(v, m_autil.mk_numeral(rational::zero(), true)), m);
                                assert_axiom(len_constraint);
                            }
                        } else {
                            // generate new length constraint
                            expr_ref_vector extra_length_vars(m);
                            expr_ref _top_level_length_constraint = infer_all_regex_lengths(mk_strlen(str), re, extra_length_vars);
                            expr_ref top_level_length_constraint(_top_level_length_constraint, m);
                            th_rewriter rw(m);
                            rw(top_level_length_constraint);
                            TRACE("str", tout << "top-level length constraint: " << mk_pp(top_level_length_constraint, m) << std::endl;);
                            // assert and track length constraint
                            assert_axiom(top_level_length_constraint);
                            for(expr_ref_vector::iterator it = extra_length_vars.begin(); it != extra_length_vars.end(); ++it) {
                                expr * v = *it;
                                expr_ref len_constraint(m_autil.mk_ge(v, m_autil.mk_numeral(rational::zero(), true)), m);
                                assert_axiom(len_constraint);
                            }

                            regex_term_to_length_constraint.insert(str_in_re, top_level_length_constraint);
                            ptr_vector<expr> vtmp;
                            for(expr_ref_vector::iterator it = extra_length_vars.begin(); it != extra_length_vars.end(); ++it) {
                                vtmp.push_back(*it);
                            }
                            regex_term_to_extra_length_vars.insert(str_in_re, vtmp);
                        }

                        regex_terms_with_length_constraints.insert(str_in_re);
                        m_trail_stack.push(insert_obj_trail<theory_str, expr>(regex_terms_with_length_constraints, str_in_re));
                        regex_axiom_add = true;
                    }
                } // re not in regex_terms_with_length_constraints

                rational exact_length_value;
                if (get_len_value(str, exact_length_value)) {
                    TRACE("str", tout << "exact length of " << mk_pp(str, m) << " is " << exact_length_value << std::endl;);

                    if (regex_terms_with_path_constraints.contains(str_in_re)) {
                        TRACE("str", tout << "term " << mk_pp(str_in_re, m) << " already has path constraints set up" << std::endl;);
                        continue;
                    }

                    // find a consistent automaton for this term
                    bool found = false;
                    regex_automaton_under_assumptions assumption;
                    if (regex_automaton_assumptions.contains(re) &&
                            !regex_automaton_assumptions[re].empty()){
                        for (svector<regex_automaton_under_assumptions>::iterator it = regex_automaton_assumptions[re].begin();
                                it != regex_automaton_assumptions[re].end(); ++it) {
                            regex_automaton_under_assumptions autA = *it;
                            rational assumed_upper_bound, assumed_lower_bound;
                            bool assumes_upper_bound = autA.get_upper_bound(assumed_upper_bound);
                            bool assumes_lower_bound = autA.get_lower_bound(assumed_lower_bound);
                            if (!assumes_upper_bound && !assumes_lower_bound) {
                                // automaton with no assumptions is always usable
                                assumption = autA;
                                found = true;
                                break;
                            }
                            // TODO check consistency of bounds assumptions
                        } // foreach(a in regex_automaton_assumptions)
                    }
                    if (found) {
                        if (exact_length_value.is_zero()) {
                            // check consistency of 0-length solution with automaton
                            eautomaton * aut = assumption.get_automaton();
                            bool zero_solution = false;
                            unsigned initial_state = aut->init();
                            if (aut->is_final_state(initial_state)) {
                                zero_solution = true;
                            } else {
                                unsigned_vector eps_states;
                                aut->get_epsilon_closure(initial_state, eps_states);
                                for (unsigned_vector::iterator it = eps_states.begin(); it != eps_states.end(); ++it) {
                                    unsigned state = *it;
                                    if (aut->is_final_state(state)) {
                                        zero_solution = true;
                                        break;
                                    }
                                }
                            }

                            // now check polarity of automaton wrt. original term
                            if ( (current_assignment == l_true && !assumption.get_polarity())
                                    || (current_assignment == l_false && assumption.get_polarity())) {
                                // invert sense
                                zero_solution = !zero_solution;
                            }

                            if (zero_solution) {
                                TRACE("str", tout << "zero-length solution OK -- asserting empty path constraint" << std::endl;);
                                expr_ref_vector lhs_terms(m);
                                if (current_assignment == l_true) {
                                    lhs_terms.push_back(str_in_re);
                                } else {
                                    lhs_terms.push_back(m.mk_not(str_in_re));
                                }
                                lhs_terms.push_back(ctx.mk_eq_atom(mk_strlen(str), m_autil.mk_numeral(exact_length_value, true)));
                                expr_ref lhs(mk_and(lhs_terms), m);
                                expr_ref rhs(ctx.mk_eq_atom(str, mk_string("")), m);
                                assert_implication(lhs, rhs);
                                regex_terms_with_path_constraints.insert(str_in_re);
                                m_trail_stack.push(insert_obj_trail<theory_str, expr>(regex_terms_with_path_constraints, str_in_re));
                            } else {
                                TRACE("str", tout << "zero-length solution not admitted by this automaton -- asserting conflict clause" << std::endl;);
                                expr_ref_vector lhs_terms(m);
                                if (current_assignment == l_true) {
                                    lhs_terms.push_back(str_in_re);
                                } else {
                                    lhs_terms.push_back(m.mk_not(str_in_re));
                                }
                                lhs_terms.push_back(ctx.mk_eq_atom(mk_strlen(str), m_autil.mk_numeral(exact_length_value, true)));
                                expr_ref lhs(mk_and(lhs_terms), m);
                                expr_ref conflict(m.mk_not(lhs), m);
                                assert_axiom(conflict);
                            }
                            regex_axiom_add = true;
                            regex_inc_counter(regex_length_attempt_count, re);
                            continue;
                        } else {
                            expr_ref pathConstraint(m);
                            expr_ref characterConstraints(m);
                            pathConstraint = generate_regex_path_constraints(str, assumption.get_automaton(), exact_length_value, characterConstraints);
                            TRACE("str", tout << "generated regex path constraint " << mk_pp(pathConstraint, m) << std::endl;);
                            TRACE("str", tout << "character constraints are " << mk_pp(characterConstraints, m) << std::endl;);

                            expr_ref_vector lhs_terms(m);
                            if (current_assignment == l_true) {
                                lhs_terms.push_back(str_in_re);
                            } else {
                                lhs_terms.push_back(m.mk_not(str_in_re));
                            }
                            lhs_terms.push_back(ctx.mk_eq_atom(mk_strlen(str), m_autil.mk_numeral(exact_length_value, true)));
                            expr_ref lhs(mk_and(lhs_terms), m);

                            // If the path constraint comes out as "false", this means there are no paths of that length
                            // in the automaton. If the polarity is the same, we can assert a conflict clause.
                            // If the polarity is opposite, we ignore the path constraint.

                            if (m.is_false(pathConstraint)) {
                                if ( (current_assignment == l_true && assumption.get_polarity())
                                        || (current_assignment == l_false && !assumption.get_polarity())) {
                                    // automaton and constraint have same polarity -- assert conflict clause
                                    TRACE("str", tout << "path constraint is false with matching polarity; asserting conflict clause" << std::endl;);
                                    expr_ref conflict(m.mk_not(mk_and(lhs_terms)), m);
                                    assert_axiom(conflict);
                                    // don't set up "regex_terms_with_path_constraints" as a conflict clause is not a path constraint
                                } else {
                                    // automaton and constraint have opposite polarity -- ignore path constraint
                                    TRACE("str", tout << "path constraint is false with opposite polarity; ignoring path constraint" << std::endl;);
                                    assert_implication(lhs, characterConstraints);
                                    regex_terms_with_path_constraints.insert(str_in_re);
                                    m_trail_stack.push(insert_obj_trail<theory_str, expr>(regex_terms_with_path_constraints, str_in_re));
                                }
                                regex_axiom_add = true;
                            } else {
                                // If the automaton was built with the same polarity as the constraint,
                                // assert directly. Otherwise, negate the path constraint
                                if ( (current_assignment == l_true && assumption.get_polarity())
                                        || (current_assignment == l_false && !assumption.get_polarity())) {
                                    TRACE("str", tout << "automaton and regex term have same polarity" << std::endl;);
                                    expr_ref rhs(m.mk_and(pathConstraint, characterConstraints), m);
                                    assert_implication(lhs, rhs);
                                } else {
                                    TRACE("str", tout << "automaton and regex term have opposite polarity" << std::endl;);
                                    expr_ref rhs(m.mk_and(m.mk_not(pathConstraint), characterConstraints), m);
                                    assert_implication(lhs, rhs);
                                }
                                regex_terms_with_path_constraints.insert(str_in_re);
                                m_trail_stack.push(insert_obj_trail<theory_str, expr>(regex_terms_with_path_constraints, str_in_re));
                                regex_axiom_add = true;
                            }

                            // increment LengthAttemptCount
                            regex_inc_counter(regex_length_attempt_count, re);

                            TRACE("str", 
                                  {
                                      unsigned v = regex_get_counter(regex_length_attempt_count, re);
                                      tout << "length attempt count for " << mk_pp(re, m) << " is " << v << std::endl;
                                  });                            

                            continue;
                        }
                    } else {
                        // no automata available, or else all bounds assumptions are invalid
                        unsigned expected_complexity = estimate_regex_complexity(re);
                        if (expected_complexity <= m_params.m_RegexAutomata_DifficultyThreshold || regex_get_counter(regex_fail_count, str_in_re) >= m_params.m_RegexAutomata_FailedAutomatonThreshold) {
                            CTRACE("str", regex_get_counter(regex_fail_count, str_in_re) >= m_params.m_RegexAutomata_FailedAutomatonThreshold,
                                    tout << "failed automaton threshold reached for " << mk_pp(str_in_re, m) << " -- automatically constructing full automaton" << std::endl;);
                            eautomaton * aut = m_mk_aut(re);
                            aut->compress();
                            regex_automata.push_back(aut);
                            regex_automaton_under_assumptions new_aut(re, aut, true);
                            if (!regex_automaton_assumptions.contains(re)) {
                                regex_automaton_assumptions.insert(re, svector<regex_automaton_under_assumptions>());
                            }
                            regex_automaton_assumptions[re].push_back(new_aut);
                            TRACE("str", tout << "add new automaton for " << mk_pp(re, m) << ": no assumptions" << std::endl;);
                            regex_axiom_add = true;
                            find_automaton_initial_bounds(str_in_re, aut);
                        } else {
                            regex_inc_counter(regex_fail_count, str_in_re);
                        }
                        continue;
                    }
                } // get_len_value()
                expr_ref str_len(mk_strlen(str), m);
                rational lower_bound_value;
                rational upper_bound_value;
                bool lower_bound_exists = lower_bound(str_len, lower_bound_value);
                bool upper_bound_exists = upper_bound(str_len, upper_bound_value);
                CTRACE("str", lower_bound_exists, tout << "lower bound of " << mk_pp(str, m) << " is " << lower_bound_value << std::endl;);
                CTRACE("str", upper_bound_exists, tout << "upper bound of " << mk_pp(str, m) << " is " << upper_bound_value << std::endl;);

                bool new_lower_bound_info = true;
                bool new_upper_bound_info = true;
                // check last seen lower/upper bound to avoid performing duplicate work
                if (regex_last_lower_bound.contains(str)) {
                    rational last_lb_value;
                    regex_last_lower_bound.find(str, last_lb_value);
                    if (last_lb_value == lower_bound_value) {
                        new_lower_bound_info = false;
                    }
                }
                if (regex_last_upper_bound.contains(str)) {
                    rational last_ub_value;
                    regex_last_upper_bound.find(str, last_ub_value);
                    if (last_ub_value == upper_bound_value) {
                        new_upper_bound_info = false;
                    }
                }

                if (new_lower_bound_info) {
                    regex_last_lower_bound.insert(str, lower_bound_value);
                }
                if (new_upper_bound_info) {
                    regex_last_upper_bound.insert(str, upper_bound_value);
                }

                if (upper_bound_exists && new_upper_bound_info) {
                    // check current assumptions
                    if (regex_automaton_assumptions.contains(re) &&
                            !regex_automaton_assumptions[re].empty()){
                        // one or more existing assumptions.
                        // see if the (current best) upper bound can be refined
                        // (note that if we have an automaton with no assumption,
                        // this automatically counts as best)
                        bool need_assumption = true;
                        regex_automaton_under_assumptions last_assumption;
                        rational last_ub = rational::minus_one();
                        for (svector<regex_automaton_under_assumptions>::iterator it = regex_automaton_assumptions[re].begin();
                                it != regex_automaton_assumptions[re].end(); ++it) {
                            regex_automaton_under_assumptions autA = *it;
                            if ((current_assignment == l_true && autA.get_polarity() == false)
                                    || (current_assignment == l_false && autA.get_polarity() == true)) {
                                // automaton uses incorrect polarity
                                continue;
                            }
                            rational this_ub;
                            if (autA.get_upper_bound(this_ub)) {
                                if (last_ub == rational::minus_one() || this_ub < last_ub) {
                                    last_ub = this_ub;
                                    last_assumption = autA;
                                }
                            } else {
                                need_assumption = false;
                                last_assumption = autA;
                                break;
                            }
                        }
                        if (!last_ub.is_minus_one() || !need_assumption) {
                            CTRACE("str", !need_assumption, tout << "using automaton with full length information" << std::endl;);
                            CTRACE("str", need_assumption, tout << "using automaton with assumed upper bound of " << last_ub << std::endl;);

                            rational refined_upper_bound;
                            bool solution_at_upper_bound = refine_automaton_upper_bound(last_assumption.get_automaton(),
                                    upper_bound_value, refined_upper_bound);
                            TRACE("str", tout << "refined upper bound is " << refined_upper_bound <<
                                    (solution_at_upper_bound?", solution at upper bound":", no solution at upper bound") << std::endl;);

                            expr_ref_vector lhs(m);
                            if (current_assignment == l_false) {
                                lhs.push_back(m.mk_not(str_in_re));
                            } else {
                                lhs.push_back(str_in_re);
                            }
                            if (need_assumption) {
                                lhs.push_back(m_autil.mk_le(str_len, m_autil.mk_numeral(last_ub, true)));
                            }
                            lhs.push_back(m_autil.mk_le(str_len, m_autil.mk_numeral(upper_bound_value, true)));

                            expr_ref_vector rhs(m);

                            if (solution_at_upper_bound) {
                                if (refined_upper_bound.is_minus_one()) {
                                    // If there are solutions at the upper bound but not below it, make the bound exact.
                                    rhs.push_back(ctx.mk_eq_atom(str_len, m_autil.mk_numeral(upper_bound_value, true)));
                                } else {
                                    // If there are solutions at and below the upper bound, add an additional bound.
                                    rhs.push_back(m.mk_or(
                                            ctx.mk_eq_atom(str_len, m_autil.mk_numeral(upper_bound_value, true)),
                                            m_autil.mk_le(str_len, m_autil.mk_numeral(refined_upper_bound, true))
                                            ));
                                }
                            } else {
                                if (refined_upper_bound.is_minus_one()) {
                                    // If there are no solutions at or below the upper bound, assert a conflict clause.
                                    rhs.push_back(m.mk_not(m_autil.mk_le(str_len, m_autil.mk_numeral(upper_bound_value, true))));
                                } else {
                                    // If there are solutions below the upper bound but not at it, refine the bound.
                                    rhs.push_back(m_autil.mk_le(str_len, m_autil.mk_numeral(refined_upper_bound, true)));
                                }
                            }

                            if (!rhs.empty()) {
                                expr_ref lhs_terms(mk_and(lhs), m);
                                expr_ref rhs_terms(mk_and(rhs), m);
                                assert_implication(lhs_terms, rhs_terms);
                                regex_axiom_add = true;
                            }
                        }
                    } else {
                        // no existing automata/assumptions.
                        // if it's easy to construct a full automaton for R, do so
                        unsigned expected_complexity = estimate_regex_complexity(re);
                        bool failureThresholdExceeded = (regex_get_counter(regex_fail_count, str_in_re) >= m_params.m_RegexAutomata_FailedAutomatonThreshold);
                        if (expected_complexity <= m_params.m_RegexAutomata_DifficultyThreshold || failureThresholdExceeded) {
                            eautomaton * aut = m_mk_aut(re);
                            aut->compress();
                            regex_automata.push_back(aut);
                            regex_automaton_under_assumptions new_aut(re, aut, true);
                            if (!regex_automaton_assumptions.contains(re)) {
                                regex_automaton_assumptions.insert(re, svector<regex_automaton_under_assumptions>());
                            }
                            regex_automaton_assumptions[re].push_back(new_aut);
                            TRACE("str", tout << "add new automaton for " << mk_pp(re, m) << ": no assumptions" << std::endl;);
                            regex_axiom_add = true;
                            find_automaton_initial_bounds(str_in_re, aut);
                        } else {
                            // TODO check negation?
                            // TODO construct a partial automaton for R to the given upper bound?
                            if (false) {

                            } else {
                                regex_inc_counter(regex_fail_count, str_in_re);
                            }
                        }
                        continue;
                    }
                    // if we have *any* automaton for R, and the upper bound is not too large,
                    // finitize the automaton (if we have not already done so) and assert all solutions
                    if (upper_bound_value < 50) { // TODO better metric for threshold
                        // NOT_IMPLEMENTED_YET(); // TODO(mtrberzi)
                    }
                } else { // !upper_bound_exists
                    // no upper bound information
                    if (lower_bound_exists && !lower_bound_value.is_zero() && new_lower_bound_info) {
                        // nonzero lower bound, no upper bound

                        // check current assumptions
                        if (regex_automaton_assumptions.contains(re) &&
                                !regex_automaton_assumptions[re].empty()){
                            // one or more existing assumptions.
                            // see if the (current best) lower bound can be refined
                            // (note that if we have an automaton with no assumption,
                            // this automatically counts as best)
                            bool need_assumption = true;
                            regex_automaton_under_assumptions last_assumption;
                            rational last_lb = rational::zero(); // the default
                            for (svector<regex_automaton_under_assumptions>::iterator it = regex_automaton_assumptions[re].begin();
                                    it != regex_automaton_assumptions[re].end(); ++it) {
                                regex_automaton_under_assumptions autA = *it;
                                if ((current_assignment == l_true && autA.get_polarity() == false)
                                        || (current_assignment == l_false && autA.get_polarity() == true)) {
                                    // automaton uses incorrect polarity
                                    continue;
                                }
                                rational this_lb;
                                if (autA.get_lower_bound(this_lb)) {
                                    if (this_lb > last_lb) {
                                        last_lb = this_lb;
                                        last_assumption = autA;
                                    }
                                } else {
                                    need_assumption = false;
                                    last_assumption = autA;
                                    break;
                                }
                            }
                            if (!last_lb.is_zero() || !need_assumption) {
                                CTRACE("str", !need_assumption, tout << "using automaton with full length information" << std::endl;);
                                CTRACE("str", need_assumption, tout << "using automaton with assumed lower bound of " << last_lb << std::endl;);
                                rational refined_lower_bound;
                                bool solution_at_lower_bound = refine_automaton_lower_bound(last_assumption.get_automaton(),
                                        lower_bound_value, refined_lower_bound);
                                TRACE("str", tout << "refined lower bound is " << refined_lower_bound <<
                                        (solution_at_lower_bound?", solution at lower bound":", no solution at lower bound") << std::endl;);

                                expr_ref_vector lhs(m);
                                if (current_assignment == l_false) {
                                    lhs.push_back(m.mk_not(str_in_re));
                                } else {
                                    lhs.push_back(str_in_re);
                                }
                                if (need_assumption) {
                                    lhs.push_back(m_autil.mk_ge(str_len, m_autil.mk_numeral(last_lb, true)));
                                }
                                lhs.push_back(m_autil.mk_ge(str_len, m_autil.mk_numeral(lower_bound_value, true)));

                                expr_ref_vector rhs(m);

                                if (solution_at_lower_bound) {
                                    if (refined_lower_bound.is_minus_one()) {
                                        // If there are solutions at the lower bound but not above it, make the bound exact.
                                        rhs.push_back(ctx.mk_eq_atom(str_len, m_autil.mk_numeral(lower_bound_value, true)));
                                    } else {
                                        // If there are solutions at and above the lower bound, add an additional bound.
                                        // DISABLED as this is causing non-termination in the integer solver. --mtrberzi
                                        /*
                                        rhs.push_back(m.mk_or(
                                                ctx.mk_eq_atom(str_len, m_autil.mk_numeral(lower_bound_value, true)),
                                                m_autil.mk_ge(str_len, m_autil.mk_numeral(refined_lower_bound, true))
                                        ));
                                        */
                                    }
                                } else {
                                    if (refined_lower_bound.is_minus_one()) {
                                        // If there are no solutions at or above the lower bound, assert a conflict clause.
                                        rhs.push_back(m.mk_not(m_autil.mk_ge(str_len, m_autil.mk_numeral(lower_bound_value, true))));
                                    } else {
                                        // If there are solutions above the lower bound but not at it, refine the bound.
                                        rhs.push_back(m_autil.mk_ge(str_len, m_autil.mk_numeral(refined_lower_bound, true)));
                                    }
                                }

                                if (!rhs.empty()) {
                                    expr_ref lhs_terms(mk_and(lhs), m);
                                    expr_ref rhs_terms(mk_and(rhs), m);
                                    assert_implication(lhs_terms, rhs_terms);
                                    regex_axiom_add = true;
                                }
                            }
                        } else {
                            // no existing automata/assumptions.
                            // if it's easy to construct a full automaton for R, do so
                            unsigned expected_complexity = estimate_regex_complexity(re);
                            bool failureThresholdExceeded = (regex_get_counter(regex_fail_count, str_in_re) >= m_params.m_RegexAutomata_FailedAutomatonThreshold);
                            if (expected_complexity <= m_params.m_RegexAutomata_DifficultyThreshold || failureThresholdExceeded) {
                                eautomaton * aut = m_mk_aut(re);
                                aut->compress();
                                regex_automata.push_back(aut);
                                regex_automaton_under_assumptions new_aut(re, aut, true);
                                if (!regex_automaton_assumptions.contains(re)) {
                                    regex_automaton_assumptions.insert(re, svector<regex_automaton_under_assumptions>());
                                }
                                regex_automaton_assumptions[re].push_back(new_aut);
                                TRACE("str", tout << "add new automaton for " << mk_pp(re, m) << ": no assumptions" << std::endl;);
                                regex_axiom_add = true;
                                find_automaton_initial_bounds(str_in_re, aut);
                            } else {
                                // TODO check negation?
                                // TODO construct a partial automaton for R to the given lower bound?
                                if (false) {

                                } else {
                                    regex_inc_counter(regex_fail_count, str_in_re);
                                }
                            }
                            continue;
                        }
                    } else { // !lower_bound_exists
                        // no bounds information
                        // check for existing automata;
                        // try to construct an automaton if we don't have one yet
                        // and doing so without bounds is not difficult
                        bool existingAutomata = (regex_automaton_assumptions.contains(re) && !regex_automaton_assumptions[re].empty());
                        bool failureThresholdExceeded = (regex_get_counter(regex_fail_count, str_in_re) >= m_params.m_RegexAutomata_FailedAutomatonThreshold);
                        if (!existingAutomata) {
                            unsigned expected_complexity = estimate_regex_complexity(re);
                            if (expected_complexity <= m_params.m_RegexAutomata_DifficultyThreshold
                                    || failureThresholdExceeded) {
                                eautomaton * aut = m_mk_aut(re);
                                aut->compress();
                                regex_automata.push_back(aut);
                                regex_automaton_under_assumptions new_aut(re, aut, true);
                                if (!regex_automaton_assumptions.contains(re)) {
                                    regex_automaton_assumptions.insert(re, svector<regex_automaton_under_assumptions>());
                                }
                                regex_automaton_assumptions[re].push_back(new_aut);
                                TRACE("str", tout << "add new automaton for " << mk_pp(re, m) << ": no assumptions" << std::endl;);
                                regex_axiom_add = true;
                                find_automaton_initial_bounds(str_in_re, aut);
                            } else {
                                regex_inc_counter(regex_fail_count, str_in_re);
                            }
                        } else {
                            regex_inc_counter(regex_fail_count, str_in_re);
                        }
                    }
                }
            } // foreach (entry in regex_terms)

            for (obj_map<expr, ptr_vector<expr> >::iterator it = regex_terms_by_string.begin();
                    it != regex_terms_by_string.end(); ++it) {
                // TODO do we need to check equivalence classes of strings here?

                expr * str = it->m_key;
                ptr_vector<expr> str_in_re_terms = it->m_value;

                svector<regex_automaton_under_assumptions> intersect_constraints;
                // we may find empty intersection before checking every constraint;
                // this vector keeps track of which ones actually take part in intersection
                svector<regex_automaton_under_assumptions> used_intersect_constraints;

                // choose an automaton/assumption for each assigned (str.in.re)
                // that's consistent with the current length information
                for (ptr_vector<expr>::iterator term_it = str_in_re_terms.begin();
                        term_it != str_in_re_terms.end(); ++term_it) {
                    expr * _unused;
                    expr * re;
                    SASSERT(u.str.is_in_re(*term_it));
                    u.str.is_in_re(*term_it, _unused, re);

                    rational exact_len;
                    bool has_exact_len = get_len_value(str, exact_len);

                    rational lb, ub;
                    bool has_lower_bound = lower_bound(mk_strlen(str), lb);
                    bool has_upper_bound = upper_bound(mk_strlen(str), ub);

                    if (regex_automaton_assumptions.contains(re) &&
                                                    !regex_automaton_assumptions[re].empty()){
                        for (svector<regex_automaton_under_assumptions>::iterator aut_it = regex_automaton_assumptions[re].begin();
                                                           aut_it != regex_automaton_assumptions[re].end(); ++aut_it) {
                            regex_automaton_under_assumptions aut = *aut_it;
                            rational aut_ub;
                            bool assume_ub = aut.get_upper_bound(aut_ub);
                            rational aut_lb;
                            bool assume_lb = aut.get_lower_bound(aut_lb);
                            bool consistent = true;

                            if (assume_ub) {
                                // check consistency of assumed upper bound
                                if (has_exact_len) {
                                    if (exact_len > aut_ub) {
                                        consistent = false;
                                    }
                                } else {
                                    if (has_upper_bound && ub > aut_ub) {
                                        consistent = false;
                                    }
                                }
                            }

                            if (assume_lb) {
                                // check consistency of assumed lower bound
                                if (has_exact_len) {
                                    if (exact_len < aut_lb) {
                                        consistent = false;
                                    }
                                } else {
                                    if (has_lower_bound && lb < aut_lb) {
                                        consistent = false;
                                    }
                                }
                            }

                            if (consistent) {
                                intersect_constraints.push_back(aut);
                                break;
                            }
                        }
                    }
                } // foreach(term in str_in_re_terms)

                eautomaton * aut_inter = NULL;
                CTRACE("str", !intersect_constraints.empty(), tout << "check intersection of automata constraints for " << mk_pp(str, m) << std::endl;);
                for (svector<regex_automaton_under_assumptions>::iterator aut_it = intersect_constraints.begin();
                        aut_it != intersect_constraints.end(); ++aut_it) {
                    regex_automaton_under_assumptions aut = *aut_it;
                    if (aut_inter == NULL) {
                        // start somewhere
                        aut_inter = aut.get_automaton();
                        used_intersect_constraints.push_back(aut);
                        continue;
                    }

                    TRACE("str", 
                          {
                              unsigned v = regex_get_counter(regex_length_attempt_count, aut.get_regex_term());
                              tout << "length attempt count of " << mk_pp(aut.get_regex_term(), m) << " is " << v
                                   << ", threshold is " << m_params.m_RegexAutomata_LengthAttemptThreshold << std::endl;
                          });

                    if (regex_get_counter(regex_length_attempt_count, aut.get_regex_term()) >= m_params.m_RegexAutomata_LengthAttemptThreshold) {
                        unsigned intersectionDifficulty = estimate_automata_intersection_difficulty(aut_inter, aut.get_automaton());
                        TRACE("str", tout << "intersection difficulty is " << intersectionDifficulty << std::endl;);
                        if (intersectionDifficulty <= m_params.m_RegexAutomata_IntersectionDifficultyThreshold
                                || regex_get_counter(regex_intersection_fail_count, aut.get_regex_term()) >= m_params.m_RegexAutomata_FailedIntersectionThreshold) {

                            expr * str_in_re_term(u.re.mk_in_re(str, aut.get_regex_term()));
                            lbool current_assignment = ctx.get_assignment(str_in_re_term);
                            // if the assignment is consistent with our assumption, use the automaton directly;
                            // otherwise, complement it (and save that automaton for next time)
                            // TODO we should cache these intermediate results
                            // TODO do we need to push the intermediates into a vector for deletion anyway?
                            if ( (current_assignment == l_true && aut.get_polarity())
                                    || (current_assignment == l_false && !aut.get_polarity())) {
                                aut_inter = m_mk_aut.mk_product(aut_inter, aut.get_automaton());
                                m_automata.push_back(aut_inter);
                            } else {
                                // need to complement first
                                expr_ref rc(u.re.mk_complement(aut.get_regex_term()), m);
                                eautomaton * aut_c = m_mk_aut(rc);
                                regex_automata.push_back(aut_c);
                                // TODO is there any way to build a complement automaton from an existing one?
                                // this discards length information
                                aut_inter = m_mk_aut.mk_product(aut_inter, aut_c);
                                m_automata.push_back(aut_inter);
                            }
                            used_intersect_constraints.push_back(aut);
                            if (aut_inter->is_empty()) {
                                break;
                            }
                        } else {
                            // failed intersection
                            regex_inc_counter(regex_intersection_fail_count, aut.get_regex_term());
                        }
                    }
                } // foreach(entry in intersect_constraints)
                if (aut_inter != NULL) {
                    aut_inter->compress();
                }
                TRACE("str", tout << "intersected " << used_intersect_constraints.size() << " constraints" << std::endl;);

                expr_ref_vector conflict_terms(m);
                expr_ref conflict_lhs(m);
                for (svector<regex_automaton_under_assumptions>::iterator aut_it = used_intersect_constraints.begin();
                        aut_it != used_intersect_constraints.end(); ++aut_it) {
                    regex_automaton_under_assumptions aut = *aut_it;
                    expr * str_in_re_term(u.re.mk_in_re(str, aut.get_regex_term()));
                    lbool current_assignment = ctx.get_assignment(str_in_re_term);
                    if (current_assignment == l_true) {
                        conflict_terms.push_back(str_in_re_term);
                    } else if (current_assignment == l_false) {
                        conflict_terms.push_back(m.mk_not(str_in_re_term));
                    }
                    // add length assumptions, if any
                    rational ub;
                    if (aut.get_upper_bound(ub)) {
                        expr_ref ub_term(m_autil.mk_le(mk_strlen(str), m_autil.mk_numeral(ub, true)), m);
                        conflict_terms.push_back(ub_term);
                    }
                    rational lb;
                    if (aut.get_lower_bound(lb)) {
                        expr_ref lb_term(m_autil.mk_ge(mk_strlen(str), m_autil.mk_numeral(lb, true)), m);
                        conflict_terms.push_back(lb_term);
                    }
                }
                conflict_lhs = mk_and(conflict_terms);

                if (used_intersect_constraints.size() > 1 && aut_inter != NULL) {
                    // check whether the intersection is only the empty string
                    unsigned initial_state = aut_inter->init();
                    if (aut_inter->final_states().size() == 1 && aut_inter->is_final_state(initial_state)) {
                        // initial state is final and it is the only final state
                        // if there are no moves from the initial state,
                        // the only solution is the empty string
                        if (aut_inter->get_moves_from(initial_state).empty()) {
                            TRACE("str", tout << "product automaton only accepts empty string" << std::endl;);
                            expr_ref rhs1(ctx.mk_eq_atom(str, mk_string("")), m);
                            expr_ref rhs2(ctx.mk_eq_atom(mk_strlen(str), m_autil.mk_numeral(rational::zero(), true)), m);
                            expr_ref rhs(m.mk_and(rhs1, rhs2), m);
                            assert_implication(conflict_lhs, rhs);
                            regex_axiom_add = true;
                        }
                    }
                }

                if (aut_inter != NULL && aut_inter->is_empty()) {
                    TRACE("str", tout << "product automaton is empty; asserting conflict clause" << std::endl;);
                    expr_ref conflict_clause(m.mk_not(mk_and(conflict_terms)), m);
                    assert_axiom(conflict_clause);
                    add_persisted_axiom(conflict_clause);
                    regex_axiom_add = true;
                }
            } // foreach (entry in regex_terms_by_string)
            if (regex_axiom_add) {
                //return FC_CONTINUE;
            }
        } // RegexAutomata

        bool needToAssignFreeVars = false;
        std::set<expr*> free_variables;
        std::set<expr*> unused_internal_variables;
        { // Z3str2 free variables check
            std::map<expr*, int>::iterator itor = varAppearInAssign.begin();
            for (; itor != varAppearInAssign.end(); ++itor) {
                /*
                  std::string vName = std::string(Z3_ast_to_string(ctx, itor->first));
                  if (vName.length() >= 3 && vName.substr(0, 3) == "$$_")
                  continue;
                */
                if (internal_variable_set.find(itor->first) != internal_variable_set.end()
                    || regex_variable_set.find(itor->first) != regex_variable_set.end()) {
                    // this can be ignored, I think
                    TRACE("str", tout << "free internal variable " << mk_pp(itor->first, m) << " ignored" << std::endl;);
                    continue;
                }
                bool hasEqcValue = false;
                get_eqc_value(itor->first, hasEqcValue);
                if (!hasEqcValue) {
                    TRACE("str", tout << "found free variable " << mk_pp(itor->first, m) << std::endl;);
                    needToAssignFreeVars = true;
                    free_variables.insert(itor->first);
                    // break;
                } else {
                    // debug
                    // TRACE("str", tout << "variable " << mk_pp(itor->first, m) << " = " << mk_pp(eqcString, m) << std::endl;);
                }
            }
        }

        if (!needToAssignFreeVars) {

            // check string-int terms
            bool addedStrIntAxioms = false;
            for (unsigned i = 0; i < string_int_conversion_terms.size(); ++i) {
                app * ex = to_app(string_int_conversion_terms[i].get());
                if (u.str.is_stoi(ex)) {
                    bool axiomAdd = finalcheck_str2int(ex);
                    if (axiomAdd) {
                        addedStrIntAxioms = true;
                    }
                } else if (u.str.is_itos(ex)) {
                    bool axiomAdd = finalcheck_int2str(ex);
                    if (axiomAdd) {
                        addedStrIntAxioms = true;
                    }
                } else {
                    UNREACHABLE();
                }
            }
            if (addedStrIntAxioms) {
                TRACE("str", tout << "Resuming search due to addition of string-integer conversion axioms." << std::endl;);
                return FC_CONTINUE;
            }

            // We must be be 100% certain that if there are any regex constraints,
            // the string assignment for each variable is consistent with the automaton.
            // The (probably) easiest way to do this is to ensure
            // that we have path constraints set up for every assigned regex term.
            if (m_params.m_RegexAutomata && !regex_terms.empty()) {
                for (obj_hashtable<expr>::iterator it = regex_terms.begin(); it != regex_terms.end(); ++it) {
                    expr * str_in_re = *it;
                    expr * str;
                    expr * re;
                    u.str.is_in_re(str_in_re, str, re);
                    lbool current_assignment = ctx.get_assignment(str_in_re);
                    if (current_assignment == l_undef) {
                        continue;
                    }
                    if (!regex_terms_with_path_constraints.contains(str_in_re)) {
                        TRACE("str", tout << "assigned regex term " << mk_pp(str_in_re, m) << " has no path constraints -- continuing search" << std::endl;);
                        return FC_CONTINUE;
                    }
                } // foreach (str.in.re in regex_terms)
            }

            if (unused_internal_variables.empty()) {
                TRACE("str", tout << "All variables are assigned. Done!" << std::endl;);
                return FC_DONE;
            } else {
                TRACE("str", tout << "Assigning decoy values to free internal variables." << std::endl;);
                for (std::set<expr*>::iterator it = unused_internal_variables.begin(); it != unused_internal_variables.end(); ++it) {
                    expr * var = *it;
                    expr_ref assignment(m.mk_eq(var, mk_string("**unused**")), m);
                    assert_axiom(assignment);
                }
                return FC_CONTINUE;
            }
        }

        CTRACE("str", needToAssignFreeVars,
               tout << "Need to assign values to the following free variables:" << std::endl;
               for (expr* v : free_variables) {
                   tout << mk_ismt2_pp(v, m) << std::endl;
               }
               tout << "freeVar_map has the following entries:" << std::endl;
               for (auto const& kv : freeVar_map) {
                   expr * var = kv.first;
                   tout << mk_ismt2_pp(var, m) << std::endl;
               }
               );

        // -----------------------------------------------------------
        // variables in freeVar are those not bounded by Concats
        // classify variables in freeVarMap:
        // (1) freeVar = unroll(r1, t1)
        // (2) vars are not bounded by either concat or unroll
        // -----------------------------------------------------------
        std::map<expr*, std::set<expr*> > fv_unrolls_map;
        std::set<expr*> tmpSet;
        expr * constValue = nullptr;
        for (std::map<expr*, int>::iterator fvIt2 = freeVar_map.begin(); fvIt2 != freeVar_map.end(); fvIt2++) {
            expr * var = fvIt2->first;
            tmpSet.clear();
            get_eqc_allUnroll(var, constValue, tmpSet);
            if (tmpSet.size() > 0) {
                fv_unrolls_map[var] = tmpSet;
            }
        }
        // erase var bounded by an unroll function from freeVar_map
        for (std::map<expr*, std::set<expr*> >::iterator fvIt3 = fv_unrolls_map.begin();
             fvIt3 != fv_unrolls_map.end(); fvIt3++) {
            expr * var = fvIt3->first;
            TRACE("str", tout << "erase free variable " << mk_pp(var, m) << " from freeVar_map, it is bounded by an Unroll" << std::endl;);
            freeVar_map.erase(var);
        }

        // collect the case:
        //   * Concat(X, Y) = unroll(r1, t1) /\ Concat(X, Y) = unroll(r2, t2)
        //     concatEqUnrollsMap[Concat(X, Y)] = {unroll(r1, t1), unroll(r2, t2)}

        std::map<expr*, std::set<expr*> > concatEqUnrollsMap;
        for (std::map<expr*, std::set<expr*> >::iterator urItor = unrollGroup_map.begin();
             urItor != unrollGroup_map.end(); urItor++) {
            expr * unroll = urItor->first;
            expr * curr = unroll;
            do {
                if (u.str.is_concat(to_app(curr))) {
                    concatEqUnrollsMap[curr].insert(unroll);
                    concatEqUnrollsMap[curr].insert(unrollGroup_map[unroll].begin(), unrollGroup_map[unroll].end());
                }
                enode * e_curr = ctx.get_enode(curr);
                curr = e_curr->get_next()->get_owner();
                // curr = get_eqc_next(curr);
            } while (curr != unroll);
        }

        std::map<expr*, std::set<expr*> > concatFreeArgsEqUnrollsMap;
        std::set<expr*> fvUnrollSet;
        for (std::map<expr*, std::set<expr*> >::iterator concatItor = concatEqUnrollsMap.begin();
             concatItor != concatEqUnrollsMap.end(); concatItor++) {
            expr * concat = concatItor->first;
            expr * concatArg1 = to_app(concat)->get_arg(0);
            expr * concatArg2 = to_app(concat)->get_arg(1);
            bool arg1Bounded = false;
            bool arg2Bounded = false;
            // arg1
            if (variable_set.find(concatArg1) != variable_set.end()) {
                if (freeVar_map.find(concatArg1) == freeVar_map.end()) {
                    arg1Bounded = true;
                } else {
                    fvUnrollSet.insert(concatArg1);
                }
            } else if (u.str.is_concat(to_app(concatArg1))) {
                if (concatEqUnrollsMap.find(concatArg1) == concatEqUnrollsMap.end()) {
                    arg1Bounded = true;
                }
            }
            // arg2
            if (variable_set.find(concatArg2) != variable_set.end()) {
                if (freeVar_map.find(concatArg2) == freeVar_map.end()) {
                    arg2Bounded = true;
                } else {
                    fvUnrollSet.insert(concatArg2);
                }
            } else if (u.str.is_concat(to_app(concatArg2))) {
                if (concatEqUnrollsMap.find(concatArg2) == concatEqUnrollsMap.end()) {
                    arg2Bounded = true;
                }
            }
            if (!arg1Bounded && !arg2Bounded) {
                concatFreeArgsEqUnrollsMap[concat].insert(
                    concatEqUnrollsMap[concat].begin(),
                    concatEqUnrollsMap[concat].end());
            }
        }
        for (std::set<expr*>::iterator vItor = fvUnrollSet.begin(); vItor != fvUnrollSet.end(); vItor++) {
            TRACE("str", tout << "remove " << mk_pp(*vItor, m) << " from freeVar_map" << std::endl;);
            freeVar_map.erase(*vItor);
        }

        // Assign free variables
        std::set<expr*> fSimpUnroll;

        constValue = nullptr;

        {
            TRACE("str", tout << "free var map (#" << freeVar_map.size() << "):" << std::endl;
                  for (std::map<expr*, int>::iterator freeVarItor1 = freeVar_map.begin(); freeVarItor1 != freeVar_map.end(); freeVarItor1++) {
                      expr * freeVar = freeVarItor1->first;
                      rational lenValue;
                      bool lenValue_exists = get_len_value(freeVar, lenValue);
                      tout << mk_pp(freeVar, m) << " [depCnt = " << freeVarItor1->second << ", length = "
                           << (lenValue_exists ? lenValue.to_string() : "?")
                           << "]" << std::endl;
                  }
                  );
        }

        for (std::map<expr*, std::set<expr*> >::iterator fvIt2 = concatFreeArgsEqUnrollsMap.begin();
             fvIt2 != concatFreeArgsEqUnrollsMap.end(); fvIt2++) {
            expr * concat = fvIt2->first;
            for (std::set<expr*>::iterator urItor = fvIt2->second.begin(); urItor != fvIt2->second.end(); urItor++) {
                expr * unroll = *urItor;
                process_concat_eq_unroll(concat, unroll);
            }
        }

        // --------
        // experimental free variable assignment - begin
        //   * special handling for variables that are not used in concat
        // --------
        bool testAssign = true;
        if (!testAssign) {
            for (std::map<expr*, int>::iterator fvIt = freeVar_map.begin(); fvIt != freeVar_map.end(); fvIt++) {
                expr * freeVar = fvIt->first;
                /*
                  std::string vName = std::string(Z3_ast_to_string(ctx, freeVar));
                  if (vName.length() >= 9 && vName.substr(0, 9) == "$$_regVar") {
                  continue;
                  }
                */
                expr * toAssert = gen_len_val_options_for_free_var(freeVar, nullptr, "");
                if (toAssert != nullptr) {
                    assert_axiom(toAssert);
                }
            }
        } else {
            process_free_var(freeVar_map);
        }
        // experimental free variable assignment - end

        // now deal with removed free variables that are bounded by an unroll
        TRACE("str", tout << "fv_unrolls_map (#" << fv_unrolls_map.size() << "):" << std::endl;);
        for (std::map<expr*, std::set<expr*> >::iterator fvIt1 = fv_unrolls_map.begin();
             fvIt1 != fv_unrolls_map.end(); fvIt1++) {
            expr * var = fvIt1->first;
            fSimpUnroll.clear();
            get_eqc_simpleUnroll(var, constValue, fSimpUnroll);
            if (fSimpUnroll.size() == 0) {
                gen_assign_unroll_reg(fv_unrolls_map[var]);
            } else {
                expr * toAssert = gen_assign_unroll_Str2Reg(var, fSimpUnroll);
                if (toAssert != nullptr) {
                    assert_axiom(toAssert);
                }
            }
        }

        if (opt_VerifyFinalCheckProgress && !finalCheckProgressIndicator) {
            TRACE("str", tout << "BUG: no progress in final check, giving up!!" << std::endl;);
            m.raise_exception("no progress in theory_str final check");
        }

        return FC_CONTINUE; // since by this point we've added axioms
    }

    inline zstring int_to_string(int i) {
        std::stringstream ss;
        ss << i;
        std::string str = ss.str();
        return zstring(str.c_str());
    }

    inline std::string longlong_to_string(long long i) {
        std::stringstream ss;
        ss << i;
        return ss.str();
    }

    void theory_str::print_value_tester_list(svector<std::pair<int, expr*> > & testerList) {
        TRACE("str",
              int ss = testerList.size();
              tout << "valueTesterList = {";
              for (int i = 0; i < ss; ++i) {
                  if (i % 4 == 0) {
                      tout << std::endl;
                  }
                  tout << "(" << testerList[i].first << ", ";
                  tout << mk_pp(testerList[i].second, get_manager());
                  tout << "), ";
              }
              tout << std::endl << "}" << std::endl;
              );
    }

    zstring theory_str::gen_val_string(int len, int_vector & encoding) {
        SASSERT(charSetSize > 0);
        SASSERT(!char_set.empty());

        std::string re(len, char_set[0]);
        for (int i = 0; i < (int) encoding.size() - 1; i++) {
            int idx = encoding[i];
            re[len - 1 - i] = char_set[idx];
        }
        return zstring(re.c_str());
    }

    /*
     * The return value indicates whether we covered the search space.
     *   - If the next encoding is valid, return false
     *   - Otherwise, return true
     */
    bool theory_str::get_next_val_encode(int_vector & base, int_vector & next) {
        SASSERT(charSetSize > 0);

        TRACE("str", tout << "base vector: [ ";
              for (unsigned i = 0; i < base.size(); ++i) {
                  tout << base[i] << " ";
              }
              tout << "]" << std::endl;
              );

        int s = 0;
        int carry = 0;
        next.reset();

        for (int i = 0; i < (int) base.size(); i++) {
            if (i == 0) {
                s = base[i] + 1;
                carry = s / charSetSize;
                s = s % charSetSize;
                next.push_back(s);
            } else {
                s = base[i] + carry;
                carry = s / charSetSize;
                s = s % charSetSize;
                next.push_back(s);
            }
        }
        if (next[next.size() - 1] > 0) {
            next.reset();
            return true;
        } else {
            return false;
        }
    }

    expr* theory_str::gen_val_options(expr * freeVar, expr * len_indicator, expr * val_indicator,
                                       zstring lenStr, int tries) {
        ast_manager & m = get_manager();
        context & ctx = get_context();

        int distance = 32;

        // ----------------------------------------------------------------------------------------
        // generate value options encoding
        // encoding is a vector of size (len + 1)
        // e.g, len = 2,
        //      encoding {1, 2, 0} means the value option is "charSet[2]"."charSet[1]"
        //      the last item in the encoding indicates whether the whole space is covered
        //      for example, if the charSet = {a, b}. All valid encodings are
        //        {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {1, 1, 0}
        //      if add 1 to the last one, we get
        //        {0, 0, 1}
        //      the last item "1" shows this is not a valid encoding, and we have covered all space
        // ----------------------------------------------------------------------------------------
        int len = atoi(lenStr.encode().c_str());
        bool coverAll = false;
        vector<int_vector, true, size_t> options;
        int_vector base;

        TRACE("str", tout
              << "freeVar = " << mk_ismt2_pp(freeVar, m) << std::endl
              << "len_indicator = " << mk_ismt2_pp(len_indicator, m) << std::endl
              << "val_indicator = " << mk_ismt2_pp(val_indicator, m) << std::endl
              << "lenstr = " << lenStr << "\n"
              << "tries = " << tries << "\n";
              if (m_params.m_AggressiveValueTesting) {
                  tout << "note: aggressive value testing is enabled" << std::endl;
              }
              );

        if (tries == 0) {
            base = int_vector(len + 1, 0);
            coverAll = false;
        } else {
            expr * lastestValIndi = fvar_valueTester_map[freeVar][len][tries - 1].second;
            TRACE("str", tout << "last value tester = " << mk_ismt2_pp(lastestValIndi, m) << std::endl;);
            coverAll = get_next_val_encode(val_range_map[lastestValIndi], base);
        }

        size_t l = (tries) * distance;
        size_t h = l;
        for (int i = 0; i < distance; i++) {
            if (coverAll)
                break;
            options.push_back(base);
            h++;
            coverAll = get_next_val_encode(options[options.size() - 1], base);
        }
        val_range_map.insert(val_indicator, options[options.size() - 1]);

        TRACE("str",
              tout << "value tester encoding " << "{" << std::endl;
              int_vector vec = val_range_map[val_indicator];

              for (int_vector::iterator it = vec.begin(); it != vec.end(); ++it) {
                  tout << *it << std::endl;
              }
              tout << "}" << std::endl;
              );

        // ----------------------------------------------------------------------------------------

        expr_ref_vector orList(m), andList(m);

        for (size_t i = l; i < h; i++) {
            orList.push_back(m.mk_eq(val_indicator, mk_string(longlong_to_string(i).c_str()) ));
            if (m_params.m_AggressiveValueTesting) {
                literal lit = mk_eq(val_indicator, mk_string(longlong_to_string(i).c_str()), false);
                ctx.mark_as_relevant(lit);
                ctx.force_phase(lit);
            }

            zstring aStr = gen_val_string(len, options[i - l]);
            expr * strAst;
            if (m_params.m_UseFastValueTesterCache) {
                if (!valueTesterCache.find(aStr, strAst)) {
                    strAst = mk_string(aStr);
                    valueTesterCache.insert(aStr, strAst);
                    m_trail.push_back(strAst);
                }
            } else {
                strAst = mk_string(aStr);
            }
            andList.push_back(m.mk_eq(orList[orList.size() - 1].get(), m.mk_eq(freeVar, strAst)));
        }
        if (!coverAll) {
            orList.push_back(m.mk_eq(val_indicator, mk_string("more")));
            if (m_params.m_AggressiveValueTesting) {
                literal l = mk_eq(val_indicator, mk_string("more"), false);
                ctx.mark_as_relevant(l);
                ctx.force_phase(~l);
            }
        }

        andList.push_back(mk_or(orList));
        expr_ref valTestAssert = mk_and(andList);

        // ---------------------------------------
        // If the new value tester is $$_val_x_16_i
        // Should add ($$_len_x_j = 16) /\ ($$_val_x_16_i = "more")
        // ---------------------------------------
        andList.reset();
        andList.push_back(m.mk_eq(len_indicator, mk_string(lenStr)));
        for (int i = 0; i < tries; i++) {
            expr * vTester = fvar_valueTester_map[freeVar][len][i].second;
            if (vTester != val_indicator)
                andList.push_back(m.mk_eq(vTester, mk_string("more")));
        }
        expr_ref assertL = mk_and(andList);
        // (assertL => valTestAssert) <=> (!assertL OR valTestAssert)
        return m.mk_or(m.mk_not(assertL), valTestAssert);
    }

    expr * theory_str::gen_free_var_options(expr * freeVar, expr * len_indicator,
                                            zstring len_valueStr, expr * valTesterInCbEq, zstring valTesterValueStr) {
        ast_manager & m = get_manager();

        int len = atoi(len_valueStr.encode().c_str());

        // check whether any value tester is actually in scope
        TRACE("str", tout << "checking scope of previous value testers" << std::endl;);
        bool map_effectively_empty = true;
        if (fvar_valueTester_map.contains(freeVar) &&
            fvar_valueTester_map[freeVar].find(len) != fvar_valueTester_map[freeVar].end()) {
            // there's *something* in the map, but check its scope
            for (auto const& entry : fvar_valueTester_map[freeVar][len]) {
                expr * aTester = entry.second;
                if (!internal_variable_set.contains(aTester)) {
                    TRACE("str", tout << mk_pp(aTester, m) << " out of scope" << std::endl;);
                } else {
                    TRACE("str", tout << mk_pp(aTester, m) << " in scope" << std::endl;);
                    map_effectively_empty = false;
                    break;
                }
            }
        }

        if (map_effectively_empty) {
            TRACE("str", tout << "no previous value testers, or none of them were in scope" << std::endl;);
            int tries = 0;
            expr * val_indicator = mk_internal_valTest_var(freeVar, len, tries);
            valueTester_fvar_map.insert(val_indicator, freeVar);
            if (!fvar_valueTester_map.contains(freeVar)) {
                fvar_valueTester_map.insert(freeVar, std::map<int, svector<std::pair<int, expr*> > >());
            }
            fvar_valueTester_map[freeVar][len].push_back(std::make_pair(sLevel, val_indicator));
            print_value_tester_list(fvar_valueTester_map[freeVar][len]);
            return gen_val_options(freeVar, len_indicator, val_indicator, len_valueStr, tries);
        } else {
            TRACE("str", tout << "checking previous value testers" << std::endl;);
            print_value_tester_list(fvar_valueTester_map[freeVar][len]);

            // go through all previous value testers
            // If some doesn't have an eqc value, add its assertion again.
            int testerTotal = fvar_valueTester_map[freeVar][len].size();
            int i = 0;
            for (; i < testerTotal; i++) {
                expr * aTester = fvar_valueTester_map[freeVar][len][i].second;

                // it's probably worth checking scope here, actually
                if (!internal_variable_set.contains(aTester)) {
                    TRACE("str", tout << "value tester " << mk_pp(aTester, m) << " out of scope, skipping" << std::endl;);
                    continue;
                }

                if (aTester == valTesterInCbEq) {
                    break;
                }

                bool anEqcHasValue = false;
                get_eqc_value(aTester, anEqcHasValue);
                if (!anEqcHasValue) {
                    TRACE("str", tout << "value tester " << mk_ismt2_pp(aTester, m)
                          << " doesn't have an equivalence class value." << std::endl;);
                    refresh_theory_var(aTester);

                    expr_ref makeupAssert(gen_val_options(freeVar, len_indicator, aTester, len_valueStr, i), m);

                    TRACE("str", tout << "var: " << mk_ismt2_pp(freeVar, m) << std::endl
                          << mk_ismt2_pp(makeupAssert, m) << std::endl;);
                    assert_axiom(makeupAssert);
                } else {
                    // TRACE("str", tout << "value tester " << mk_ismt2_pp(aTester, m)
                    //      << " == " << mk_ismt2_pp(aTester_eqc_value, m) << std::endl;);
                }
            }

            if (valTesterValueStr == "more") {
                expr * valTester = nullptr;
                if (i + 1 < testerTotal) {
                    valTester = fvar_valueTester_map[freeVar][len][i + 1].second;
                    refresh_theory_var(valTester);
                } else {
                    valTester = mk_internal_valTest_var(freeVar, len, i + 1);
                    valueTester_fvar_map.insert(valTester, freeVar);
                    fvar_valueTester_map[freeVar][len].push_back(std::make_pair(sLevel, valTester));
                    print_value_tester_list(fvar_valueTester_map[freeVar][len]);
                }
                return gen_val_options(freeVar, len_indicator, valTester, len_valueStr, i + 1);
            }

            return nullptr;
        }
    }

    void theory_str::reduce_virtual_regex_in(expr * var, expr * regex, expr_ref_vector & items) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        TRACE("str", tout << "reduce regex " << mk_pp(regex, mgr) << " with respect to variable " << mk_pp(var, mgr) << std::endl;);

        app * regexFuncDecl = to_app(regex);
        if (u.re.is_to_re(regexFuncDecl)) {
            // ---------------------------------------------------------
            // var \in Str2Reg(s1)
            //   ==>
            // var = s1 /\ length(var) = length(s1)
            // ---------------------------------------------------------
            expr * strInside = to_app(regex)->get_arg(0);
            items.push_back(ctx.mk_eq_atom(var, strInside));
            items.push_back(ctx.mk_eq_atom(mk_strlen(var), mk_strlen(strInside)));
            return;
        }
        // RegexUnion
        else if (u.re.is_union(regexFuncDecl)) {
            // ---------------------------------------------------------
            // var \in RegexUnion(r1, r2)
            //   ==>
            // (var = newVar1 \/ var = newVar2)
            // (var = newVar1 --> length(var) = length(newVar1)) /\ (var = newVar2 --> length(var) = length(newVar2))
            //  /\ (newVar1 \in r1) /\  (newVar2 \in r2)
            // ---------------------------------------------------------
            expr_ref newVar1(mk_regex_rep_var(), mgr);
            expr_ref newVar2(mk_regex_rep_var(), mgr);
            items.push_back(mgr.mk_or(ctx.mk_eq_atom(var, newVar1), ctx.mk_eq_atom(var, newVar2)));
            items.push_back(mgr.mk_or(
                                mgr.mk_not(ctx.mk_eq_atom(var, newVar1)),
                                ctx.mk_eq_atom(mk_strlen(var), mk_strlen(newVar1))));
            items.push_back(mgr.mk_or(
                                mgr.mk_not(ctx.mk_eq_atom(var, newVar2)),
                                ctx.mk_eq_atom(mk_strlen(var), mk_strlen(newVar2))));

            expr * regArg1 = to_app(regex)->get_arg(0);
            reduce_virtual_regex_in(newVar1, regArg1, items);

            expr * regArg2 = to_app(regex)->get_arg(1);
            reduce_virtual_regex_in(newVar2, regArg2, items);

            return;
        }
        // RegexConcat
        else if (u.re.is_concat(regexFuncDecl)) {
            // ---------------------------------------------------------
            // var \in RegexConcat(r1, r2)
            //   ==>
            //    (var = newVar1 . newVar2) /\ (length(var) = length(vewVar1 . newVar2) )
            // /\ (newVar1 \in r1) /\  (newVar2 \in r2)
            // ---------------------------------------------------------
            expr_ref newVar1(mk_regex_rep_var(), mgr);
            expr_ref newVar2(mk_regex_rep_var(), mgr);
            expr_ref concatAst(mk_concat(newVar1, newVar2), mgr);
            items.push_back(ctx.mk_eq_atom(var, concatAst));
            items.push_back(ctx.mk_eq_atom(mk_strlen(var),
                                           m_autil.mk_add(mk_strlen(newVar1), mk_strlen(newVar2))));

            expr * regArg1 = to_app(regex)->get_arg(0);
            reduce_virtual_regex_in(newVar1, regArg1, items);
            expr * regArg2 = to_app(regex)->get_arg(1);
            reduce_virtual_regex_in(newVar2, regArg2, items);
            return;
        }
        // Unroll
        else if (u.re.is_star(regexFuncDecl)) {
            // ---------------------------------------------------------
            // var \in Star(r1)
            //   ==>
            // var = unroll(r1, t1) /\ |var| = |unroll(r1, t1)|
            // ---------------------------------------------------------
            expr * regArg = to_app(regex)->get_arg(0);
            expr_ref unrollCnt(mk_unroll_bound_var(), mgr);
            expr_ref unrollFunc(mk_unroll(regArg, unrollCnt), mgr);
            items.push_back(ctx.mk_eq_atom(var, unrollFunc));
            items.push_back(ctx.mk_eq_atom(mk_strlen(var), mk_strlen(unrollFunc)));
            return;
        }
        // re.range
        else if (u.re.is_range(regexFuncDecl)) {
            // var in range("a", "z")
            // ==>
            // (var = "a" or var = "b" or ... or var = "z")
            expr_ref lo(regexFuncDecl->get_arg(0), mgr);
            expr_ref hi(regexFuncDecl->get_arg(1), mgr);
            zstring str_lo, str_hi;
            SASSERT(u.str.is_string(lo));
            SASSERT(u.str.is_string(hi));
            u.str.is_string(lo, str_lo);
            u.str.is_string(hi, str_hi);
            SASSERT(str_lo.length() == 1);
            SASSERT(str_hi.length() == 1);
            unsigned int c1 = str_lo[0];
            unsigned int c2 = str_hi[0];
            if (c1 > c2) {
                // exchange
                unsigned int tmp = c1;
                c1 = c2;
                c2 = tmp;
            }
            expr_ref_vector range_cases(mgr);
            for (unsigned int ch = c1; ch <= c2; ++ch) {
                zstring s_ch(ch);
                expr_ref rhs(ctx.mk_eq_atom(var, u.str.mk_string(s_ch)), mgr);
                range_cases.push_back(rhs);
            }
            expr_ref rhs(mk_or(range_cases), mgr);
            SASSERT(rhs);
            assert_axiom(rhs);
            return;
        } else {
            get_manager().raise_exception("unrecognized regex operator");
            UNREACHABLE();
        }
    }

    void theory_str::gen_assign_unroll_reg(std::set<expr*> & unrolls) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        expr_ref_vector items(mgr);
        for (std::set<expr*>::iterator itor = unrolls.begin(); itor != unrolls.end(); itor++) {
            expr * unrFunc = *itor;
            TRACE("str", tout << "generating assignment for unroll " << mk_pp(unrFunc, mgr) << std::endl;);

            expr * regexInUnr = to_app(unrFunc)->get_arg(0);
            expr * cntInUnr = to_app(unrFunc)->get_arg(1);
            items.reset();

            rational low, high;
            bool low_exists = lower_bound(cntInUnr, low); (void)low_exists;
            bool high_exists = upper_bound(cntInUnr, high); (void)high_exists;

            TRACE("str",
                  tout << "unroll " << mk_pp(unrFunc, mgr) << std::endl;
                  rational unrLenValue;
                  bool unrLenValue_exists = get_len_value(unrFunc, unrLenValue);
                  tout << "unroll length: " << (unrLenValue_exists ? unrLenValue.to_string() : "?") << std::endl;
                  rational cntInUnrValue;
                  bool cntHasValue = get_arith_value(cntInUnr, cntInUnrValue);
                  tout << "unroll count: " << (cntHasValue ? cntInUnrValue.to_string() : "?")
                  << " low = "
                  << (low_exists ? low.to_string() : "?")
                  << " high = "
                  << (high_exists ? high.to_string() : "?")
                  << std::endl;
                  );

            expr_ref toAssert(mgr);
            if (low.is_neg()) {
                toAssert = m_autil.mk_ge(cntInUnr, mk_int(0));
            } else {
                if (!unroll_var_map.contains(unrFunc)) {

                    expr_ref newVar1(mk_regex_rep_var(), mgr);
                    expr_ref newVar2(mk_regex_rep_var(), mgr);
                    expr_ref concatAst(mk_concat(newVar1, newVar2), mgr);
                    expr_ref newCnt(mk_unroll_bound_var(), mgr);
                    expr_ref newUnrollFunc(mk_unroll(regexInUnr, newCnt), mgr);

                    // unroll(r1, t1) = newVar1 . newVar2
                    items.push_back(ctx.mk_eq_atom(unrFunc, concatAst));
                    items.push_back(ctx.mk_eq_atom(mk_strlen(unrFunc), m_autil.mk_add(mk_strlen(newVar1), mk_strlen(newVar2))));
                    // mk_strlen(unrFunc) >= mk_strlen(newVar{1,2})
                    items.push_back(m_autil.mk_ge(m_autil.mk_add(mk_strlen(unrFunc), m_autil.mk_mul(mk_int(-1), mk_strlen(newVar1))), mk_int(0)));
                    items.push_back(m_autil.mk_ge(m_autil.mk_add(mk_strlen(unrFunc), m_autil.mk_mul(mk_int(-1), mk_strlen(newVar2))), mk_int(0)));
                    // newVar1 \in r1
                    reduce_virtual_regex_in(newVar1, regexInUnr, items);
                    items.push_back(ctx.mk_eq_atom(cntInUnr, m_autil.mk_add(newCnt, mk_int(1))));
                    items.push_back(ctx.mk_eq_atom(newVar2, newUnrollFunc));
                    items.push_back(ctx.mk_eq_atom(mk_strlen(newVar2), mk_strlen(newUnrollFunc)));
                    toAssert = ctx.mk_eq_atom(
                        m_autil.mk_ge(cntInUnr, mk_int(1)),
                        mk_and(items));

                    // option 0
                    expr_ref op0(ctx.mk_eq_atom(cntInUnr, mk_int(0)), mgr);
                    expr_ref ast1(ctx.mk_eq_atom(unrFunc, mk_string("")), mgr);
                    expr_ref ast2(ctx.mk_eq_atom(mk_strlen(unrFunc), mk_int(0)), mgr);
                    expr_ref and1(mgr.mk_and(ast1, ast2), mgr);

                    // put together
                    toAssert = mgr.mk_and(ctx.mk_eq_atom(op0, and1), toAssert);

                    unroll_var_map.insert(unrFunc, toAssert);
                } 
                else {
                    toAssert = unroll_var_map[unrFunc];
                }
            }
            m_trail.push_back(toAssert);
            assert_axiom(toAssert);
        }
    }

    static int computeGCD(int x, int y) {
        if (x == 0) {
            return y;
        }
        while (y != 0) {
            if (x > y) {
                x = x - y;
            } else {
                y = y - x;
            }
        }
        return x;
    }

    static int computeLCM(int a, int b) {
        int temp = computeGCD(a, b);
        return temp ? (a / temp * b) : 0;
    }

    static zstring get_unrolled_string(zstring core, int count) {
        zstring res("");
        for (int i = 0; i < count; i++) {
            res = res + core;
        }
        return res;
    }

    expr * theory_str::gen_assign_unroll_Str2Reg(expr * n, std::set<expr*> & unrolls) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        int lcm = 1;
        int coreValueCount = 0;
        expr * oneUnroll = nullptr;
        zstring oneCoreStr("");
        for (std::set<expr*>::iterator itor = unrolls.begin(); itor != unrolls.end(); itor++) {
            expr * str2RegFunc = to_app(*itor)->get_arg(0);
            expr * coreVal = to_app(str2RegFunc)->get_arg(0);
            zstring coreStr;
            u.str.is_string(coreVal, coreStr);
            if (oneUnroll == nullptr) {
                oneUnroll = *itor;
                oneCoreStr = coreStr;
            }
            coreValueCount++;
            int core1Len = coreStr.length();
            lcm = computeLCM(lcm, core1Len);
        }
        //
        bool canHaveNonEmptyAssign = true;
        expr_ref_vector litems(mgr);
        zstring lcmStr = get_unrolled_string(oneCoreStr, (lcm / oneCoreStr.length()));
        for (std::set<expr*>::iterator itor = unrolls.begin(); itor != unrolls.end(); itor++) {
            expr * str2RegFunc = to_app(*itor)->get_arg(0);
            expr * coreVal = to_app(str2RegFunc)->get_arg(0);
            zstring coreStr;
            u.str.is_string(coreVal, coreStr);
            unsigned int core1Len = coreStr.length();
            zstring uStr = get_unrolled_string(coreStr, (lcm / core1Len));
            if (uStr != lcmStr) {
                canHaveNonEmptyAssign = false;
            }
            litems.push_back(ctx.mk_eq_atom(n, *itor));
        }

        if (canHaveNonEmptyAssign) {
            return gen_unroll_conditional_options(n, unrolls, lcmStr);
        } else {
            expr_ref implyL(mk_and(litems), mgr);
            expr_ref implyR(ctx.mk_eq_atom(n, mk_string("")), mgr);
            // want to return (implyL -> implyR)
            expr * final_axiom = rewrite_implication(implyL, implyR);
            return final_axiom;
        }
    }

    expr * theory_str::gen_unroll_conditional_options(expr * var, std::set<expr*> & unrolls, zstring lcmStr) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        int dist = opt_LCMUnrollStep;
        expr_ref_vector litems(mgr);
        expr_ref moreAst(mk_string("more"), mgr);
        for (expr * e : unrolls) {
            expr_ref item(ctx.mk_eq_atom(var, e), mgr);
            TRACE("str", tout << "considering unroll " << mk_pp(item, mgr) << std::endl;);
            litems.push_back(item);
        }

        // handle out-of-scope entries in unroll_tries_map

        ptr_vector<expr> outOfScopeTesters;

        for (expr * tester : unroll_tries_map[var][unrolls]) {
            bool inScope = internal_unrollTest_vars.contains(tester);
            TRACE("str", tout << "unroll test var " << mk_pp(tester, mgr)
                  << (inScope ? " in scope" : " out of scope")
                  << std::endl;);
            if (!inScope) {
                outOfScopeTesters.push_back(tester);
            }
        }

        for (expr* e : outOfScopeTesters) {
            unroll_tries_map[var][unrolls].erase(e);
        }

        if (unroll_tries_map[var][unrolls].size() == 0) {
            unroll_tries_map[var][unrolls].push_back(mk_unroll_test_var());
        }

        int tries = unroll_tries_map[var][unrolls].size();
        for (int i = 0; i < tries; i++) {
            expr * tester = unroll_tries_map[var][unrolls][i];
            // TESTING
            refresh_theory_var(tester);
            bool testerHasValue = false;
            expr * testerVal = get_eqc_value(tester, testerHasValue);
            if (!testerHasValue) {
                // generate make-up assertion
                int l = i * dist;
                int h = (i + 1) * dist;
                expr_ref lImp(mk_and(litems), mgr);
                expr_ref rImp(gen_unroll_assign(var, lcmStr, tester, l, h), mgr);

                SASSERT(lImp);
                TRACE("str", tout << "lImp = " << mk_pp(lImp, mgr) << std::endl;);
                SASSERT(rImp);
                TRACE("str", tout << "rImp = " << mk_pp(rImp, mgr) << std::endl;);

                expr_ref toAssert(mgr.mk_or(mgr.mk_not(lImp), rImp), mgr);
                SASSERT(toAssert);
                TRACE("str", tout << "Making up assignments for variable which is equal to unbounded Unroll" << std::endl;);
                m_trail.push_back(toAssert);
                return toAssert;

                // note: this is how the code looks in Z3str2's strRegex.cpp:genUnrollConditionalOptions.
                // the return is in the same place

                // insert [tester = "more"] to litems so that the implyL for next tester is correct
                litems.push_back(ctx.mk_eq_atom(tester, moreAst));
            } else {
                zstring testerStr;
                u.str.is_string(testerVal, testerStr);
                TRACE("str", tout << "Tester [" << mk_pp(tester, mgr) << "] = " << testerStr << "\n";);
                if (testerStr == "more") {
                    litems.push_back(ctx.mk_eq_atom(tester, moreAst));
                }
            }
        }
        expr * tester = mk_unroll_test_var();
        unroll_tries_map[var][unrolls].push_back(tester);
        int l = tries * dist;
        int h = (tries + 1) * dist;
        expr_ref lImp(mk_and(litems), mgr);
        expr_ref rImp(gen_unroll_assign(var, lcmStr, tester, l, h), mgr);
        SASSERT(lImp);
        SASSERT(rImp);
        expr_ref toAssert(mgr.mk_or(mgr.mk_not(lImp), rImp), mgr);
        SASSERT(toAssert);
        TRACE("str", tout << "Generating assignment for variable which is equal to unbounded Unroll" << std::endl;);
        m_trail.push_back(toAssert);
        return toAssert;
    }

    expr * theory_str::gen_unroll_assign(expr * var, zstring lcmStr, expr * testerVar, int l, int h) {
        context & ctx = get_context();
        ast_manager & mgr = get_manager();

        TRACE("str", tout << "entry: var = " << mk_pp(var, mgr) << ", lcmStr = " << lcmStr
              << ", l = " << l << ", h = " << h << "\n";);

        if (m_params.m_AggressiveUnrollTesting) {
            TRACE("str", tout << "note: aggressive unroll testing is active" << std::endl;);
        }

        expr_ref_vector orItems(mgr);
        expr_ref_vector andItems(mgr);

        for (int i = l; i < h; i++) {
            zstring iStr = int_to_string(i);
            expr_ref testerEqAst(ctx.mk_eq_atom(testerVar, mk_string(iStr)), mgr);
            TRACE("str", tout << "testerEqAst = " << mk_pp(testerEqAst, mgr) << std::endl;);
            if (m_params.m_AggressiveUnrollTesting) {
                literal l = mk_eq(testerVar, mk_string(iStr), false);
                ctx.mark_as_relevant(l);
                ctx.force_phase(l);
            }

            orItems.push_back(testerEqAst);
            zstring unrollStrInstance = get_unrolled_string(lcmStr, i);

            expr_ref x1(ctx.mk_eq_atom(testerEqAst, ctx.mk_eq_atom(var, mk_string(unrollStrInstance))), mgr);
            TRACE("str", tout << "x1 = " << mk_pp(x1, mgr) << std::endl;);
            andItems.push_back(x1);

            expr_ref x2(ctx.mk_eq_atom(testerEqAst, ctx.mk_eq_atom(mk_strlen(var), mk_int(i * lcmStr.length()))), mgr);
            TRACE("str", tout << "x2 = " << mk_pp(x2, mgr) << std::endl;);
            andItems.push_back(x2);
        }
        expr_ref testerEqMore(ctx.mk_eq_atom(testerVar, mk_string("more")), mgr);
        TRACE("str", tout << "testerEqMore = " << mk_pp(testerEqMore, mgr) << std::endl;);
        if (m_params.m_AggressiveUnrollTesting) {
            literal l = mk_eq(testerVar, mk_string("more"), false);
            ctx.mark_as_relevant(l);
            ctx.force_phase(~l);
        }

        orItems.push_back(testerEqMore);
        int nextLowerLenBound = h * lcmStr.length();
        expr_ref more2(ctx.mk_eq_atom(testerEqMore,
                                      //Z3_mk_ge(mk_length(t, var), mk_int(ctx, nextLowerLenBound))
                                      m_autil.mk_ge(m_autil.mk_add(mk_strlen(var), mk_int(-1 * nextLowerLenBound)), mk_int(0))
                                      ), mgr);
        TRACE("str", tout << "more2 = " << mk_pp(more2, mgr) << std::endl;);
        andItems.push_back(more2);

        expr_ref finalOR(mgr.mk_or(orItems.size(), orItems.c_ptr()), mgr);
        TRACE("str", tout << "finalOR = " << mk_pp(finalOR, mgr) << std::endl;);
        andItems.push_back(mk_or(orItems));

        expr_ref finalAND(mgr.mk_and(andItems.size(), andItems.c_ptr()), mgr);
        TRACE("str", tout << "finalAND = " << mk_pp(finalAND, mgr) << std::endl;);

        // doing the following avoids a segmentation fault
        m_trail.push_back(finalAND);
        return finalAND;
    }

    expr * theory_str::gen_len_test_options(expr * freeVar, expr * indicator, int tries) {
        ast_manager & m = get_manager();
        context & ctx = get_context();

        expr_ref freeVarLen(mk_strlen(freeVar), m);
        SASSERT(freeVarLen);

        {
            rational freeVar_len_value;
            if (get_len_value(freeVar, freeVar_len_value)) {
                TRACE("str", tout << "special case: length of freeVar is known to be " << freeVar_len_value << std::endl;);
                expr_ref concreteOption(ctx.mk_eq_atom(indicator, mk_string(freeVar_len_value.to_string().c_str()) ), m);
                expr_ref concreteValue(ctx.mk_eq_atom(
                        ctx.mk_eq_atom(indicator, mk_string(freeVar_len_value.to_string().c_str()) ),
                        ctx.mk_eq_atom(freeVarLen, m_autil.mk_numeral(freeVar_len_value, true))), m);
                expr_ref finalAxiom(m.mk_and(concreteOption, concreteValue), m);
                SASSERT(finalAxiom);
                m_trail.push_back(finalAxiom);
                return finalAxiom;
            }
        }

        expr_ref_vector orList(m);
        expr_ref_vector andList(m);

        int distance = 3;
        int l = (tries - 1) * distance;
        int h = tries * distance;

        TRACE("str",
              tout << "building andList and orList" << std::endl;
              if (m_params.m_AggressiveLengthTesting) {
                  tout << "note: aggressive length testing is active" << std::endl;
              }
              );

        // experimental theory-aware case split support
        literal_vector case_split_literals;

        for (int i = l; i < h; ++i) {
            expr_ref str_indicator(m);
            if (m_params.m_UseFastLengthTesterCache) {
                rational ri(i);
                expr * lookup_val;
                if(lengthTesterCache.find(ri, lookup_val)) {
                    str_indicator = expr_ref(lookup_val, m);
                } else {
                    // no match; create and insert
                    zstring i_str = int_to_string(i);
                    expr_ref new_val(mk_string(i_str), m);
                    lengthTesterCache.insert(ri, new_val);
                    m_trail.push_back(new_val);
                    str_indicator = expr_ref(new_val, m);
                }
            } else {
                zstring i_str = int_to_string(i);
                str_indicator = expr_ref(mk_string(i_str), m);
            }
            expr_ref or_expr(ctx.mk_eq_atom(indicator, str_indicator), m);
            orList.push_back(or_expr);

            double priority;
            // give high priority to small lengths if this is available
            if (i <= 5) {
                priority = 0.3;
            } else {
                // prioritize over "more"
                priority = 0.2;
            }
            add_theory_aware_branching_info(or_expr, priority, l_true);

            if (m_params.m_AggressiveLengthTesting) {
                literal l = mk_eq(indicator, str_indicator, false);
                ctx.mark_as_relevant(l);
                ctx.force_phase(l);
            }

            case_split_literals.insert(mk_eq(freeVarLen, mk_int(i), false));

            expr_ref and_expr(ctx.mk_eq_atom(orList.get(orList.size() - 1), m.mk_eq(freeVarLen, mk_int(i))), m);
            andList.push_back(and_expr);
        }

        expr_ref more_option(ctx.mk_eq_atom(indicator, mk_string("more")), m);
        orList.push_back(more_option);
        // decrease priority of this option
        add_theory_aware_branching_info(more_option, -0.1, l_true);
        if (m_params.m_AggressiveLengthTesting) {
            literal l = mk_eq(indicator, mk_string("more"), false);
            ctx.mark_as_relevant(l);
            ctx.force_phase(~l);
        }

        andList.push_back(ctx.mk_eq_atom(orList.get(orList.size() - 1), m_autil.mk_ge(freeVarLen, mk_int(h))));

        /*
          { // more experimental theory case split support
          expr_ref tmp(m_autil.mk_ge(freeVarLen, mk_int(h)), m);
          ctx.internalize(m_autil.mk_ge(freeVarLen, mk_int(h)), false);
          case_split_literals.push_back(ctx.get_literal(tmp));
          ctx.mk_th_case_split(case_split_literals.size(), case_split_literals.c_ptr());
          }
        */

        expr_ref_vector or_items(m);
        expr_ref_vector and_items(m);

        for (unsigned i = 0; i < orList.size(); ++i) {
            or_items.push_back(orList.get(i));
        }

        and_items.push_back(mk_or(or_items));
        for(unsigned i = 0; i < andList.size(); ++i) {
            and_items.push_back(andList.get(i));
        }

        TRACE("str", tout << "check: " << mk_pp(mk_and(and_items), m) << std::endl;);

        expr_ref lenTestAssert = mk_and(and_items);
        SASSERT(lenTestAssert);
        TRACE("str", tout << "crash avoidance lenTestAssert: " << mk_pp(lenTestAssert, m) << std::endl;);

        int testerCount = tries - 1;
        if (testerCount > 0) {
            expr_ref_vector and_items_LHS(m);
            expr_ref moreAst(mk_string("more"), m);
            for (int i = 0; i < testerCount; ++i) {
                expr * indicator = fvar_lenTester_map[freeVar][i];
                if (internal_variable_set.find(indicator) == internal_variable_set.end()) {
                    TRACE("str", tout << "indicator " << mk_pp(indicator, m) << " out of scope; continuing" << std::endl;);
                    continue;
                } else {
                    TRACE("str", tout << "indicator " << mk_pp(indicator, m) << " in scope" << std::endl;);
                    and_items_LHS.push_back(ctx.mk_eq_atom(indicator, moreAst));
                }
            }
            expr_ref assertL(mk_and(and_items_LHS), m);
            SASSERT(assertL);
            expr * finalAxiom = m.mk_or(m.mk_not(assertL), lenTestAssert.get());
            SASSERT(finalAxiom != NULL);
            TRACE("str", tout << "crash avoidance finalAxiom: " << mk_pp(finalAxiom, m) << std::endl;);
            return finalAxiom;
        } else {
            TRACE("str", tout << "crash avoidance lenTestAssert.get(): " << mk_pp(lenTestAssert.get(), m) << std::endl;);
            m_trail.push_back(lenTestAssert.get());
            return lenTestAssert.get();
        }
    }

    // Return an expression of the form
    // (tester = "less" | tester = "N" | tester = "more") &
    //   (tester = "less" iff len(freeVar) < N) & (tester = "more" iff len(freeVar) > N) & (tester = "N" iff len(freeVar) = N))
    expr_ref theory_str::binary_search_case_split(expr * freeVar, expr * tester, binary_search_info & bounds, literal_vector & case_split) {
        context & ctx = get_context();
        ast_manager & m = get_manager();
        rational N = bounds.midPoint;
        rational N_minus_one = N - rational::one();
        rational N_plus_one = N + rational::one();
        expr_ref lenFreeVar(mk_strlen(freeVar), m);

        TRACE("str", tout << "create case split for free var " << mk_pp(freeVar, m)
              << " over " << mk_pp(tester, m) << " with midpoint " << N << std::endl;);

        expr_ref_vector combinedCaseSplit(m);
        expr_ref_vector testerCases(m);

        expr_ref caseLess(ctx.mk_eq_atom(tester, mk_string("less")), m);
        testerCases.push_back(caseLess);
        combinedCaseSplit.push_back(ctx.mk_eq_atom(caseLess, m_autil.mk_le(lenFreeVar, m_autil.mk_numeral(N_minus_one, true) )));

        expr_ref caseMore(ctx.mk_eq_atom(tester, mk_string("more")), m);
        testerCases.push_back(caseMore);
        combinedCaseSplit.push_back(ctx.mk_eq_atom(caseMore, m_autil.mk_ge(lenFreeVar, m_autil.mk_numeral(N_plus_one, true) )));

        expr_ref caseEq(ctx.mk_eq_atom(tester, mk_string(N.to_string().c_str())), m);
        testerCases.push_back(caseEq);
        combinedCaseSplit.push_back(ctx.mk_eq_atom(caseEq, ctx.mk_eq_atom(lenFreeVar, m_autil.mk_numeral(N, true))));

        combinedCaseSplit.push_back(mk_or(testerCases));

        // force internalization on all terms in testerCases so we can extract literals
        for (unsigned i = 0; i < testerCases.size(); ++i) {
            expr * testerCase = testerCases.get(i);
            if (!ctx.b_internalized(testerCase)) {
                ctx.internalize(testerCase, false);
            }
            literal l = ctx.get_literal(testerCase);
            case_split.push_back(l);
        }

        expr_ref final_term(mk_and(combinedCaseSplit), m);
        SASSERT(final_term);
        TRACE("str", tout << "final term: " << mk_pp(final_term, m) << std::endl;);
        return final_term;
    }

    expr * theory_str::binary_search_length_test(expr * freeVar, expr * previousLenTester, zstring previousLenTesterValue) {
        ast_manager & m = get_manager();

        if (binary_search_len_tester_stack.contains(freeVar) && !binary_search_len_tester_stack[freeVar].empty()) {
            TRACE("str", tout << "checking existing length testers for " << mk_pp(freeVar, m) << std::endl;
                  for (ptr_vector<expr>::const_iterator it = binary_search_len_tester_stack[freeVar].begin();
                       it != binary_search_len_tester_stack[freeVar].end(); ++it) {
                      expr * tester = *it;
                      tout << mk_pp(tester, m) << ": ";
                      if (binary_search_len_tester_info.contains(tester)) {
                          binary_search_info & bounds = binary_search_len_tester_info[tester];
                          tout << "[" << bounds.lowerBound << " | " << bounds.midPoint << " | " << bounds.upperBound << "]!" << bounds.windowSize;
                      } else {
                          tout << "[WARNING: no bounds info available]";
                      }
                      bool hasEqcValue;
                      expr * testerEqcValue = get_eqc_value(tester, hasEqcValue);
                      if (hasEqcValue) {
                          tout << " = " << mk_pp(testerEqcValue, m);
                      } else {
                          tout << " [no eqc value]";
                      }
                      tout << std::endl;
                  }
                  );
            expr * lastTester = binary_search_len_tester_stack[freeVar].back();
            bool lastTesterHasEqcValue;
            expr * lastTesterValue = get_eqc_value(lastTester, lastTesterHasEqcValue);
            zstring lastTesterConstant;
            if (!lastTesterHasEqcValue) {
                TRACE("str", tout << "length tester " << mk_pp(lastTester, m) << " at top of stack doesn't have an EQC value yet" << std::endl;);
                // check previousLenTester
                if (previousLenTester == lastTester) {
                    lastTesterConstant = previousLenTesterValue;
                    TRACE("str", tout << "invoked with previousLenTester info matching top of stack" << std::endl;);
                } else {
                    TRACE("str", tout << "WARNING: unexpected reordering of length testers!" << std::endl;);
                    UNREACHABLE(); return nullptr;
                }
            } else {
                u.str.is_string(lastTesterValue, lastTesterConstant);
            }
            TRACE("str", tout << "last length tester is assigned \"" << lastTesterConstant << "\"" << "\n";);
            if (lastTesterConstant == "more" || lastTesterConstant == "less") {
                // use the previous bounds info to generate a new midpoint
                binary_search_info lastBounds;
                if (!binary_search_len_tester_info.find(lastTester, lastBounds)) {
                    // unexpected
                    TRACE("str", tout << "WARNING: no bounds information available for last tester!" << std::endl;);
                    UNREACHABLE();
                }
                TRACE("str", tout << "last bounds are [" << lastBounds.lowerBound << " | " << lastBounds.midPoint << " | " << lastBounds.upperBound << "]!" << lastBounds.windowSize << std::endl;);
                binary_search_info newBounds;
                expr * newTester = nullptr;
                if (lastTesterConstant == "more") {
                    // special case: if the midpoint, upper bound, and window size are all equal,
                    // we double the window size and adjust the bounds
                    if (lastBounds.midPoint == lastBounds.upperBound && lastBounds.upperBound == lastBounds.windowSize) {
                        TRACE("str", tout << "search hit window size; expanding" << std::endl;);
                        newBounds.lowerBound = lastBounds.windowSize + rational::one();
                        newBounds.windowSize = lastBounds.windowSize * rational(2);
                        newBounds.upperBound = newBounds.windowSize;
                        newBounds.calculate_midpoint();
                    } else if (false) {
                        // handle the case where the midpoint can't be increased further
                        // (e.g. a window like [50 | 50 | 50]!64 and we don't answer "50")
                    } else {
                        // general case
                        newBounds.lowerBound = lastBounds.midPoint + rational::one();
                        newBounds.windowSize = lastBounds.windowSize;
                        newBounds.upperBound = lastBounds.upperBound;
                        newBounds.calculate_midpoint();
                    }
                    if (!binary_search_next_var_high.find(lastTester, newTester)) {
                        newTester = mk_internal_lenTest_var(freeVar, newBounds.midPoint.get_int32());
                        binary_search_next_var_high.insert(lastTester, newTester);
                    }
                    refresh_theory_var(newTester);
                } else if (lastTesterConstant == "less") {
                    if (false) {
                        // handle the case where the midpoint can't be decreased further
                        // (e.g. a window like [0 | 0 | 0]!64 and we don't answer "0"
                    } else {
                        // general case
                        newBounds.upperBound = lastBounds.midPoint - rational::one();
                        newBounds.windowSize = lastBounds.windowSize;
                        newBounds.lowerBound = lastBounds.lowerBound;
                        newBounds.calculate_midpoint();
                    }
                    if (!binary_search_next_var_low.find(lastTester, newTester)) {
                        newTester = mk_internal_lenTest_var(freeVar, newBounds.midPoint.get_int32());
                        binary_search_next_var_low.insert(lastTester, newTester);
                    }
                    refresh_theory_var(newTester);
                }
                TRACE("str", tout << "new bounds are [" << newBounds.lowerBound << " | " << newBounds.midPoint << " | " << newBounds.upperBound << "]!" << newBounds.windowSize << std::endl;);
                binary_search_len_tester_stack[freeVar].push_back(newTester);
                m_trail_stack.push(binary_search_trail<theory_str>(binary_search_len_tester_stack, freeVar));
                binary_search_len_tester_info.insert(newTester, newBounds);
                m_trail_stack.push(insert_obj_map<theory_str, expr, binary_search_info>(binary_search_len_tester_info, newTester));

                literal_vector case_split_literals;
                expr_ref next_case_split(binary_search_case_split(freeVar, newTester, newBounds, case_split_literals));
                m_trail.push_back(next_case_split);
                // ctx.mk_th_case_split(case_split_literals.size(), case_split_literals.c_ptr());
                return next_case_split;
            } else { // lastTesterConstant is a concrete value
                TRACE("str", tout << "length is fixed; generating models for free var" << std::endl;);
                // defensive check that this length did not converge on a negative value.
                binary_search_info lastBounds;
                if (!binary_search_len_tester_info.find(lastTester, lastBounds)) {
                    // unexpected
                    TRACE("str", tout << "WARNING: no bounds information available for last tester!" << std::endl;);
                    UNREACHABLE();
                }
                if (lastBounds.midPoint.is_neg()) {
                    TRACE("str", tout << "WARNING: length search converged on a negative value. Negating this constraint." << std::endl;);
                    expr_ref axiom(m_autil.mk_ge(mk_strlen(freeVar), m_autil.mk_numeral(rational::zero(), true)), m);
                    return axiom;
                }
                // length is fixed
                expr * valueAssert = gen_free_var_options(freeVar, lastTester, lastTesterConstant, nullptr, zstring(""));
                return valueAssert;
            }
        } else {
            // no length testers yet
            TRACE("str", tout << "no length testers for " << mk_pp(freeVar, m) << std::endl;);
            binary_search_len_tester_stack.insert(freeVar, ptr_vector<expr>());

            expr * firstTester;
            rational lowerBound(0);
            rational upperBound(m_params.m_BinarySearchInitialUpperBound);
            rational windowSize(upperBound);
            rational midPoint(floor(upperBound / rational(2)));
            if (!binary_search_starting_len_tester.find(freeVar, firstTester)) {
                firstTester = mk_internal_lenTest_var(freeVar, midPoint.get_int32());
                binary_search_starting_len_tester.insert(freeVar, firstTester);
            }
            refresh_theory_var(firstTester);

            {
                rational freeVar_len_value;
                if (get_len_value(freeVar, freeVar_len_value)) {
                    TRACE("str", tout << "special case: length of freeVar is known to be " << freeVar_len_value << std::endl;);
                    midPoint = freeVar_len_value;
                    upperBound = midPoint * 2;
                    windowSize = upperBound;
                }
            }

            binary_search_len_tester_stack[freeVar].push_back(firstTester);
            m_trail_stack.push(binary_search_trail<theory_str>(binary_search_len_tester_stack, freeVar));
            binary_search_info new_info(lowerBound, midPoint, upperBound, windowSize);
            binary_search_len_tester_info.insert(firstTester, new_info);
            m_trail_stack.push(insert_obj_map<theory_str, expr, binary_search_info>(binary_search_len_tester_info, firstTester));

            literal_vector case_split_literals;
            expr_ref initial_case_split(binary_search_case_split(freeVar, firstTester, new_info, case_split_literals));
            m_trail.push_back(initial_case_split);
            // ctx.mk_th_case_split(case_split_literals.size(), case_split_literals.c_ptr());
            return initial_case_split;
        }
    }

    // -----------------------------------------------------------------------------------------------------
    // True branch will be taken in final_check:
    //   - When we discover a variable is "free" for the first time
    //     lenTesterInCbEq = NULL
    //     lenTesterValue = ""
    // False branch will be taken when invoked by new_eq_eh().
    //   - After we set up length tester for a "free" var in final_check,
    //     when the tester is assigned to some value (e.g. "more" or "4"),
    //     lenTesterInCbEq != NULL, and its value will be passed by lenTesterValue
    // The difference is that in new_eq_eh(), lenTesterInCbEq and its value have NOT been put into a same eqc
    // -----------------------------------------------------------------------------------------------------
    expr * theory_str::gen_len_val_options_for_free_var(expr * freeVar, expr * lenTesterInCbEq, zstring lenTesterValue) {

        ast_manager & m = get_manager();

        TRACE("str", tout << "gen for free var " << mk_ismt2_pp(freeVar, m) << std::endl;);

        if (m_params.m_UseBinarySearch) {
            TRACE("str", tout << "using binary search heuristic" << std::endl;);
            return binary_search_length_test(freeVar, lenTesterInCbEq, lenTesterValue);
        } else {
            bool map_effectively_empty = false;
            if (!fvar_len_count_map.contains(freeVar)) {
                TRACE("str", tout << "fvar_len_count_map is empty" << std::endl;);
                map_effectively_empty = true;
            }

            if (!map_effectively_empty) {
                // check whether any entries correspond to variables that went out of scope;
                // if every entry is out of scope then the map counts as being empty

                // assume empty and find a counterexample
                map_effectively_empty = true;
                if (fvar_lenTester_map.contains(freeVar)) {
                    for (expr * indicator : fvar_lenTester_map[freeVar]) {
                        if (internal_variable_set.find(indicator) != internal_variable_set.end()) {
                            TRACE("str", tout <<"found active internal variable " << mk_ismt2_pp(indicator, m)
                                    << " in fvar_lenTester_map[freeVar]" << std::endl;);
                            map_effectively_empty = false;
                            break;
                        }
                    }
                }
                CTRACE("str", map_effectively_empty, tout << "all variables in fvar_lenTester_map[freeVar] out of scope" << std::endl;);
            }

            if (map_effectively_empty) {
                // no length assertions for this free variable have ever been added.
                TRACE("str", tout << "no length assertions yet" << std::endl;);

                fvar_len_count_map.insert(freeVar, 1);
                unsigned int testNum = fvar_len_count_map[freeVar];

                expr_ref indicator(mk_internal_lenTest_var(freeVar, testNum), m);
                SASSERT(indicator);

                // since the map is "effectively empty", we can remove those variables that have left scope...
                if (!fvar_lenTester_map.contains(freeVar)) {
                    fvar_lenTester_map.insert(freeVar, ptr_vector<expr>());
                }
                fvar_lenTester_map[freeVar].shrink(0);
                fvar_lenTester_map[freeVar].push_back(indicator);
                lenTester_fvar_map.insert(indicator, freeVar);

                expr * lenTestAssert = gen_len_test_options(freeVar, indicator, testNum);
                SASSERT(lenTestAssert != NULL);
                return lenTestAssert;
            } else {
                TRACE("str", tout << "found previous in-scope length assertions" << std::endl;);

                expr * effectiveLenInd = nullptr;
                zstring effectiveLenIndiStr("");
                int lenTesterCount;
                if (fvar_lenTester_map.contains(freeVar)) {
                    lenTesterCount = fvar_lenTester_map[freeVar].size();
                } else {
                    lenTesterCount = 0;
                }

                TRACE("str",
                      tout << lenTesterCount << " length testers in fvar_lenTester_map[" << mk_pp(freeVar, m) << "]:" << std::endl;
                      for (int i = 0; i < lenTesterCount; ++i) {
                          expr * len_indicator = fvar_lenTester_map[freeVar][i];
                          tout << mk_pp(len_indicator, m) << ": ";
                          bool effectiveInScope = (internal_variable_set.find(len_indicator) != internal_variable_set.end());
                          tout << (effectiveInScope ? "in scope" : "NOT in scope");
                          tout << std::endl;
                      }
                      );

                int i = 0;
                for (; i < lenTesterCount; ++i) {
                    expr * len_indicator_pre = fvar_lenTester_map[freeVar][i];
                    // check whether this is in scope as well
                    if (internal_variable_set.find(len_indicator_pre) == internal_variable_set.end()) {
                        TRACE("str", tout << "length indicator " << mk_pp(len_indicator_pre, m) << " not in scope" << std::endl;);
                        continue;
                    }

                    bool indicatorHasEqcValue = false;
                    expr * len_indicator_value = get_eqc_value(len_indicator_pre, indicatorHasEqcValue);
                    TRACE("str", tout << "length indicator " << mk_ismt2_pp(len_indicator_pre, m) <<
                          " = " << mk_ismt2_pp(len_indicator_value, m) << std::endl;);
                    if (indicatorHasEqcValue) {
                        zstring len_pIndiStr;
                        u.str.is_string(len_indicator_value, len_pIndiStr);
                        if (len_pIndiStr != "more") {
                            effectiveLenInd = len_indicator_pre;
                            effectiveLenIndiStr = len_pIndiStr;
                            break;
                        }
                    } else {
                        if (lenTesterInCbEq != len_indicator_pre) {
                            TRACE("str", tout << "WARNING: length indicator " << mk_ismt2_pp(len_indicator_pre, m)
                                  << " does not have an equivalence class value."
                                  << " i = " << i << ", lenTesterCount = " << lenTesterCount << std::endl;);
                            if (i > 0) {
                                effectiveLenInd = fvar_lenTester_map[freeVar][i - 1];
                                bool effectiveHasEqcValue;
                                expr * effective_eqc_value = get_eqc_value(effectiveLenInd, effectiveHasEqcValue);
                                bool effectiveInScope = (internal_variable_set.find(effectiveLenInd) != internal_variable_set.end());
                                TRACE("str", tout << "checking effective length indicator " << mk_pp(effectiveLenInd, m) << ": "
                                      << (effectiveInScope ? "in scope" : "NOT in scope") << ", ";
                                      if (effectiveHasEqcValue) {
                                          tout << "~= " << mk_pp(effective_eqc_value, m);
                                      } else {
                                          tout << "no eqc string constant";
                                      }
                                      tout << std::endl;); (void)effectiveInScope;
                                if (effectiveLenInd == lenTesterInCbEq) {
                                    effectiveLenIndiStr = lenTesterValue;
                                } else {
                                    if (effectiveHasEqcValue) {
                                        u.str.is_string(effective_eqc_value, effectiveLenIndiStr);
                                    } else {
                                        NOT_IMPLEMENTED_YET();
                                    }
                                }
                            }
                            break;
                        }
                        // lenTesterInCbEq == len_indicator_pre
                        else {
                            if (lenTesterValue != "more") {
                                effectiveLenInd = len_indicator_pre;
                                effectiveLenIndiStr = lenTesterValue;
                                break;
                            }
                        }
                    } // !indicatorHasEqcValue
                } // for (i : [0..lenTesterCount-1])
                if (effectiveLenIndiStr == "more" || effectiveLenIndiStr == "") {
                    TRACE("str", tout << "length is not fixed; generating length tester options for free var" << std::endl;);
                    expr_ref indicator(m);
                    unsigned int testNum = 0;

                    TRACE("str", tout << "effectiveLenIndiStr = " << effectiveLenIndiStr
                          << ", i = " << i << ", lenTesterCount = " << lenTesterCount << "\n";);

                    if (i == lenTesterCount) {
                        fvar_len_count_map[freeVar] = fvar_len_count_map[freeVar] + 1;
                        testNum = fvar_len_count_map[freeVar];
                        indicator = mk_internal_lenTest_var(freeVar, testNum);
                        fvar_lenTester_map[freeVar].push_back(indicator);
                        lenTester_fvar_map.insert(indicator, freeVar);
                    } else {
                        indicator = fvar_lenTester_map[freeVar][i];
                        refresh_theory_var(indicator);
                        testNum = i + 1;
                    }
                    expr * lenTestAssert = gen_len_test_options(freeVar, indicator, testNum);
                    SASSERT(lenTestAssert != NULL);
                    return lenTestAssert;
                } else {
                    // if we are performing automata-based reasoning and the term associated with
                    // this length tester is in any way constrained by regex terms,
                    // do not perform value testing -- this term is not a free variable.
                    if (m_params.m_RegexAutomata) {
                        std::map<std::pair<expr*, zstring>, expr*>::iterator it = regex_in_bool_map.begin();
                        for (; it != regex_in_bool_map.end(); ++it) {
                            expr * re = it->second;
                            expr * re_str = to_app(re)->get_arg(0);
                            // recursive descent through all subterms of re_str to see if any of them are
                            // - the same as freeVar
                            // - in the same EQC as freeVar
                            if (term_appears_as_subterm(freeVar, re_str)) {
                                TRACE("str", tout << "prevent value testing on free var " << mk_pp(freeVar, m) << " as it belongs to one or more regex constraints." << std::endl;);
                                return NULL;
                            }
                        }
                    }

                    TRACE("str", tout << "length is fixed; generating models for free var" << std::endl;);
                    // length is fixed
                    expr * valueAssert = gen_free_var_options(freeVar, effectiveLenInd, effectiveLenIndiStr, nullptr, zstring(""));
                    return valueAssert;
                }
            } // fVarLenCountMap.find(...)

        } // !UseBinarySearch
    }

    void theory_str::get_concats_in_eqc(expr * n, std::set<expr*> & concats) {

        expr * eqcNode = n;
        do {
            if (u.str.is_concat(to_app(eqcNode))) {
                concats.insert(eqcNode);
            }
            eqcNode = get_eqc_next(eqcNode);
        } while (eqcNode != n);
    }

    void theory_str::get_var_in_eqc(expr * n, std::set<expr*> & varSet) {
        expr * eqcNode = n;
        do {
            if (variable_set.find(eqcNode) != variable_set.end()) {
                varSet.insert(eqcNode);
            }
            eqcNode = get_eqc_next(eqcNode);
        } while (eqcNode != n);
    }

    bool cmpvarnames(expr * lhs, expr * rhs) {
        symbol lhs_name = to_app(lhs)->get_decl()->get_name();
        symbol rhs_name = to_app(rhs)->get_decl()->get_name();
        return lhs_name.str() < rhs_name.str();
    }

    void theory_str::process_free_var(std::map<expr*, int> & freeVar_map) {
        context & ctx = get_context();

        obj_hashtable<expr> eqcRepSet;
        obj_hashtable<expr> leafVarSet;
        std::map<int, obj_hashtable<expr> > aloneVars;

        for (auto const& kv : freeVar_map) {
            expr * freeVar = kv.first;
            // skip all regular expression vars
            if (regex_variable_set.contains(freeVar)) {
                continue;
            }

            // Iterate the EQC of freeVar, its eqc variable should not be in the eqcRepSet.
            // If found, have to filter it out
            std::set<expr*> eqVarSet;
            get_var_in_eqc(freeVar, eqVarSet);
            bool duplicated = false;
            expr * dupVar = nullptr;
            for (expr* eq : eqVarSet) {
                if (eqcRepSet.contains(eq)) {
                    duplicated = true;
                    dupVar = eq;
                    break;
                }
            }
            if (duplicated && dupVar != nullptr) {
                TRACE("str", tout << "Duplicated free variable found:" << mk_pp(freeVar, get_manager())
                      << " = " << mk_ismt2_pp(dupVar, get_manager()) << " (SKIP)" << std::endl;);
                continue;
            } else {
                eqcRepSet.insert(freeVar);
            }
        }

        for (expr * freeVar : eqcRepSet) {
            // has length constraint initially
            bool standAlone = !input_var_in_len.contains(freeVar);
            // iterate parents
            if (standAlone) {
                // I hope this works!
                if (!ctx.e_internalized(freeVar)) {
                    ctx.internalize(freeVar, false);
                }
                enode * e_freeVar = ctx.get_enode(freeVar);
                enode_vector::iterator it = e_freeVar->begin_parents();
                for (; it != e_freeVar->end_parents(); ++it) {
                    expr * parentAst = (*it)->get_owner();
                    if (u.str.is_concat(to_app(parentAst))) {
                        standAlone = false;
                        break;
                    }
                }
            }

            if (standAlone) {
                rational len_value;
                bool len_value_exists = get_len_value(freeVar, len_value);
                if (len_value_exists) {
                    leafVarSet.insert(freeVar);
                } else {
                    aloneVars[-1].insert(freeVar);
                }
            } else {
                leafVarSet.insert(freeVar);
            }
        }

        for (expr* lv : leafVarSet) {
            expr * toAssert = gen_len_val_options_for_free_var(lv, nullptr, "");
            // gen_len_val_options_for_free_var() can legally return NULL,
            // as methods that it calls may assert their own axioms instead.
            if (toAssert) assert_axiom(toAssert);
            
        }

        for (auto const& kv : aloneVars) {
            for (expr* av : kv.second) {
                expr * toAssert = gen_len_val_options_for_free_var(av, nullptr, "");
                // same deal with returning a NULL axiom here
                if (toAssert) assert_axiom(toAssert);                
            }
        }
    }

    /*
     * Collect all unroll functions
     * and constant string in eqc of node n
     */
    void theory_str::get_eqc_allUnroll(expr * n, expr * &constStr, std::set<expr*> & unrollFuncSet) {
        constStr = nullptr;
        unrollFuncSet.clear();

        expr * curr = n;
        do {
            if (u.str.is_string(to_app(curr))) {
                constStr = curr;
            } else if (u.re.is_unroll(to_app(curr))) {
                if (unrollFuncSet.find(curr) == unrollFuncSet.end()) {
                    unrollFuncSet.insert(curr);
                }
            }
            curr = get_eqc_next(curr);
        } while (curr != n);
    }

    // Collect simple Unroll functions (whose core is Str2Reg) and constant strings in the EQC of n.
    void theory_str::get_eqc_simpleUnroll(expr * n, expr * &constStr, std::set<expr*> & unrollFuncSet) {
        constStr = nullptr;
        unrollFuncSet.clear();

        expr * curr = n;
        do {
            if (u.str.is_string(to_app(curr))) {
                constStr = curr;
            } else if (u.re.is_unroll(to_app(curr))) {
                expr * core = to_app(curr)->get_arg(0);
                if (u.re.is_to_re(to_app(core))) {
                    if (unrollFuncSet.find(curr) == unrollFuncSet.end()) {
                        unrollFuncSet.insert(curr);
                    }
                }
            }
            curr = get_eqc_next(curr);
        } while (curr != n);
    }

    void theory_str::init_model(model_generator & mg) {
        //TRACE("str", tout << "initializing model" << std::endl; display(tout););
        m_factory = alloc(str_value_factory, get_manager(), get_family_id());
        mg.register_factory(m_factory);
    }

    /*
     * Helper function for mk_value().
     * Attempts to resolve the expression 'n' to a string constant.
     * Stronger than get_eqc_value() in that it will perform recursive descent
     * through every subexpression and attempt to resolve those to concrete values as well.
     * Returns the concrete value obtained from this process,
     * guaranteed to satisfy m_strutil.is_string(),
     * if one could be obtained,
     * or else returns NULL if no concrete value was derived.
     */
    app * theory_str::mk_value_helper(app * n) {
        if (u.str.is_string(n)) {
            return n;
        } else if (u.str.is_concat(n)) {
            // recursively call this function on each argument
            SASSERT(n->get_num_args() == 2);
            expr * a0 = n->get_arg(0);
            expr * a1 = n->get_arg(1);

            app * a0_conststr = mk_value_helper(to_app(a0));
            app * a1_conststr = mk_value_helper(to_app(a1));

            if (a0_conststr != nullptr && a1_conststr != nullptr) {
                zstring a0_s, a1_s;
                u.str.is_string(a0_conststr, a0_s);
                u.str.is_string(a1_conststr, a1_s);
                zstring result = a0_s + a1_s;
                return to_app(mk_string(result));
            }
        }
        // fallback path
        // try to find some constant string, anything, in the equivalence class of n
        bool hasEqc = false;
        expr * n_eqc = get_eqc_value(n, hasEqc);
        if (hasEqc) {
            return to_app(n_eqc);
        } else {
            return nullptr;
        }
    }

    model_value_proc * theory_str::mk_value(enode * n, model_generator & mg) {
        TRACE("str", tout << "mk_value for: " << mk_ismt2_pp(n->get_owner(), get_manager()) <<
              " (sort " << mk_ismt2_pp(get_manager().get_sort(n->get_owner()), get_manager()) << ")" << std::endl;);
        ast_manager & m = get_manager();
        app_ref owner(m);
        owner = n->get_owner();

        // If the owner is not internalized, it doesn't have an enode associated.
        SASSERT(get_context().e_internalized(owner));

        app * val = mk_value_helper(owner);
        if (val != nullptr) {
            return alloc(expr_wrapper_proc, val);
        } else {
            TRACE("str", tout << "WARNING: failed to find a concrete value, falling back" << std::endl;);
            std::ostringstream unused;
            unused << "**UNUSED**" << (m_unused_id++);
            return alloc(expr_wrapper_proc, to_app(mk_string(unused.str().c_str())));
        }
    }

    void theory_str::finalize_model(model_generator & mg) {}

    void theory_str::display(std::ostream & out) const {
        out << "TODO: theory_str display" << std::endl;
    }

}; /* namespace smt */
