// -*- coding: utf-8 -*-
// Copyright (C) by the Spot authors, see the AUTHORS file for details.
//
// This file is part of Spot, a model checking library.
//
// Spot is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// Spot is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
// License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include "config.h"
#include <iostream>
//#define TRACE
#ifdef TRACE
#define trace std::cerr
#else
#define trace while (0) std::cerr
#endif

#include <spot/tl/simplify.hh>
#include <spot/priv/robin_hood.hh>
#include <spot/tl/contain.hh>
#include <spot/tl/print.hh>
#include <spot/tl/snf.hh>
#include <spot/tl/length.hh>
#include <spot/tl/apcollect.hh>
#include <spot/tl/relabel.hh>
#include <spot/twa/formula2bdd.hh>
#include <spot/misc/minato.hh>
#include <cassert>
#include <memory>
#include <unordered_set>
#include <map>



namespace spot
{
  typedef std::vector<formula> vec;

  // The name of this class is public, but not its contents.
  class tl_simplifier_cache final
  {
    typedef robin_hood::unordered_map<formula, formula> f2f_map;
    typedef robin_hood::unordered_map<formula, bdd> f2b_map;
    typedef robin_hood::unordered_map<int, formula> b2f_map;
    typedef std::pair<formula, formula> pairf;
    typedef std::map<pairf, bool> syntimpl_cache_t;
  public:
    bdd_dict_ptr dict;
    tl_simplifier_options options;
    language_containment_checker lcc;

    ~tl_simplifier_cache()
    {
      dict->unregister_all_my_variables(this);
    }

    tl_simplifier_cache(const bdd_dict_ptr& d)
      : dict(d), lcc(d, false, true, false, false)
    {
    }

    tl_simplifier_cache(const bdd_dict_ptr& d,
                         const tl_simplifier_options& opt)
      : dict(d), options(opt),
        lcc(d, false, true, false, false, opt.containment_max_states)
    {
      options.containment_checks |= options.containment_checks_stronger;
      options.event_univ |= options.favor_event_univ;
    }

    void
    print_stats(std::ostream& os) const
    {
      os << "simplified formulae:    " << simplified_.size() << " entries\n"
         << "negative normal form:   " << nenoform_.size() << " entries\n"
         << "syntactic implications: " << syntimpl_.size() << " entries\n"
         << "boolean to bdd:         " << as_bdd_.size() << " entries\n"
         << "star normal form:       " << snf_cache_.size() << " entries\n"
         << "boolean isop:           " << bool_isop_.size() << " entries\n"
         << "as dnf:                 " << as_dnf_.size() << " entries\n"
         << "as cnf:                 " << as_cnf_.size() << " entries\n";
    }

    void
    clear_as_bdd_cache()
    {
      as_bdd_.clear();
      for (auto p: bdd_to_f_)
        dict->unregister_variable(p.first, this);
      bdd_to_f_.clear();
    }

    // Convert a Boolean formula into a BDD for easier comparison.
    bdd
    as_bdd(formula f)
    {
      // Lookup the result in case it has already been computed.
      f2b_map::const_iterator it = as_bdd_.find(f);
      if (it != as_bdd_.end())
        return it->second;

      bdd result = bddfalse;

      switch (f.kind())
        {
        case op::tt:
          result = bddtrue;
          break;
        case op::ff:
          result = bddfalse;
          break;
        case op::ap:
          result = bdd_ithvar(dict->register_proposition(f, this));
          break;
        case op::Not:
          result = !as_bdd(f[0]);
          break;
        case op::Xor:
          {
            bdd l = as_bdd(f[0]);
            result = bdd_apply(l, as_bdd(f[1]), bddop_xor);
            break;
          }
        case op::Implies:
          {
            bdd l = as_bdd(f[0]);
            result = bdd_apply(l, as_bdd(f[1]), bddop_imp);
            break;
          }
        case op::Equiv:
          {
            bdd l = as_bdd(f[0]);
            result = bdd_apply(l, as_bdd(f[1]), bddop_biimp);
            break;
          }
        case op::And:
          {
            result = bddtrue;
            for (auto c: f)
              result &= as_bdd(c);
            break;
          }
        case op::Or:
          {
            result = bddfalse;
            for (auto c: f)
              result |= as_bdd(c);
            break;
          }
        default:
          {
            unsigned var = dict->register_anonymous_variables(1, this);
            bdd_to_f_[var] = f;
            result = bdd_ithvar(var);
            break;
          }
        }

      // Cache the result before returning.
      as_bdd_[f] = result;
      return result;
    }

    formula as_xnf(formula f, bool cnf)
    {
      bdd in = as_bdd(f);
      if (cnf)
        in = !in;
      minato_isop isop(in);
      bdd cube;
      vec clauses;
      while ((cube = isop.next()) != bddfalse)
        {
          vec literals;
          while (cube != bddtrue)
            {
              int var = bdd_var(cube);
              const bdd_dict::bdd_info& i = dict->bdd_map[var];
              formula res;
              if (i.type == bdd_dict::var)
                {
                  res = i.f;
                }
              else
                {
                  res = bdd_to_f_[var];
                  assert(res);
                }
              bdd high = bdd_high(cube);
              if (high == bddfalse)
                {
                  if (!cnf)
                    res = formula::Not(res);
                  cube = bdd_low(cube);
                }
              else
                {
                  if (cnf)
                    res = formula::Not(res);
                  // If bdd_low is not false, then cube was not a
                  // conjunction.
                  assert(bdd_low(cube) == bddfalse);
                  cube = high;
                }
              assert(cube != bddfalse);
              literals.emplace_back(res);
            }
          if (cnf)
            clauses.emplace_back(formula::Or(literals));
          else
            clauses.emplace_back(formula::And(literals));
        }
      if (cnf)
        return formula::And(clauses);
      else
        return formula::Or(clauses);
    }

    formula as_dnf(formula f)
    {
      auto i = as_dnf_.find(f);
      if (i != as_dnf_.end())
        return i->second;
      formula r = as_xnf(f, false);
      as_dnf_[f] = r;
      return r;
    }

    formula as_cnf(formula f)
    {
      auto i = as_cnf_.find(f);
      if (i != as_cnf_.end())
        return i->second;
      formula r = as_xnf(f, true);
      as_cnf_[f] = r;
      return r;
    }



    formula
    lookup_nenoform(formula f)
    {
      f2f_map::const_iterator i = nenoform_.find(f);
      if (i == nenoform_.end())
        return nullptr;
      return i->second;
    }

    void
    cache_nenoform(formula orig, formula nenoform)
    {
      nenoform_[orig] = nenoform;
    }

    // Return true iff the option set (syntactic implication
    // or containment checks) allow to prove that f1 => f2.
    bool
    implication(formula f1, formula f2)
    {
      trace << "[->] does " << str_psl(f1) << " implies "
            << str_psl(f2) << " ?" << std::endl;
      if ((options.synt_impl && syntactic_implication(f1, f2))
          || (options.containment_checks && contained(f1, f2)))
        {
          trace << "[->] Yes" << std::endl;
          return true;
        }
      trace << "[->] No" << std::endl;
      return false;
    }

    // Return true if f1 => f2 syntactically
    bool
    syntactic_implication(formula f1, formula f2);
    bool
    syntactic_implication_aux(formula f1, formula f2);

    // Return true if f1 => f2
    bool
    contained(formula f1, formula f2)
    {
      if (!f1.is_psl_formula() || !f2.is_psl_formula())
        return false;
      return lcc.contained(f1, f2);
    }

    // If right==false, true if !f1 => f2, false otherwise.
    // If right==true, true if f1 => !f2, false otherwise.
    bool
    syntactic_implication_neg(formula f1, formula f2,
                              bool right);

    // Return true if f1 => !f2
    bool contained_neg(formula f1, formula f2)
    {
      if (!f1.is_psl_formula() || !f2.is_psl_formula())
        return false;
      trace << "[CN] Does (" << str_psl(f1) << ") imply !("
            << str_psl(f2) << ") ?" << std::endl;
      if (lcc.contained_neg(f1, f2))
        {
          trace << "[CN] Yes" << std::endl;
          return true;
        }
      else
        {
          trace << "[CN] No" << std::endl;
          return false;
        }
    }

    // Return true if !f1 => f2
    bool neg_contained(formula f1, formula f2)
    {
      if (!f1.is_psl_formula() || !f2.is_psl_formula())
        return false;
      trace << "[NC] Does !(" << str_psl(f1) << ") imply ("
            << str_psl(f2) << ") ?" << std::endl;
      if (lcc.neg_contained(f1, f2))
        {
          trace << "[NC] Yes" << std::endl;
          return true;
        }
      else
        {
          trace << "[NC] No" << std::endl;
          return false;
        }
    }

    // Return true iff the option set (syntactic implication
    // or containment checks) allow to prove that
    //   - !f1 => f2   (case where right=false)
    //   - f1 => !f2   (case where right=true)
    bool
    implication_neg(formula f1, formula f2, bool right)
    {
      trace << "[IN] Does " << (right ? "(" : "!(")
            << str_psl(f1) << ") imply "
            << (right ? "!(" : "(") << str_psl(f2) << ") ?"
            << std::endl;
      if ((options.synt_impl && syntactic_implication_neg(f1, f2, right))
          || (options.containment_checks && right && contained_neg(f1, f2))
          || (options.containment_checks && !right && neg_contained(f1, f2)))
        {
          trace << "[IN] Yes" << std::endl;
          return true;
        }
      else
        {
          trace << "[IN] No" << std::endl;
          return false;
        }
    }

    formula
    lookup_simplified(formula f)
    {
      f2f_map::const_iterator i = simplified_.find(f);
      if (i == simplified_.end())
        return nullptr;
      return i->second;
    }

    void
    cache_simplified(formula orig, formula simplified)
    {
      simplified_[orig] = simplified;
    }

    formula
    star_normal_form(formula f)
    {
      return spot::star_normal_form(f, &snf_cache_);
    }

    formula
    star_normal_form_bounded(formula f)
    {
      return spot::star_normal_form_bounded(f, &snfb_cache_);
    }


    formula
    boolean_to_isop(formula f)
    {
      f2f_map::const_iterator it = bool_isop_.find(f);
      if (it != bool_isop_.end())
        return it->second;

      assert(f.is_boolean());
      formula res = bdd_to_formula(as_bdd(f), dict);
      bool_isop_[f] = res;
      return res;
    }

  private:
    f2b_map as_bdd_;
    b2f_map bdd_to_f_;
    f2f_map simplified_;
    f2f_map as_dnf_;
    f2f_map as_cnf_;
    f2f_map nenoform_;
    syntimpl_cache_t syntimpl_;
    snf_cache snf_cache_;
    snf_cache snfb_cache_;
    f2f_map bool_isop_;
  };


  namespace
  {
    //////////////////////////////////////////////////////////////////////
    //
    //  NEGATIVE_NORMAL_FORM
    //
    //////////////////////////////////////////////////////////////////////

    formula
    nenoform_rec(formula f, bool negated, tl_simplifier_cache* c,
                 bool deep);

    formula equiv_or_xor(bool equiv, formula f1, formula f2,
                         tl_simplifier_cache* c, bool deep)
    {
      auto rec = [c, deep](formula f, bool negated)
        {
          return nenoform_rec(f, negated, c, deep);
        };

      if (equiv)
        {
          // Rewrite a<=>b as (a&b)|(!a&!b)
          auto recurse_f1_false = rec(f1, false);
          auto recurse_f2_false = rec(f2, false);
          if (!deep && c->options.keep_top_xor)
            return formula::Equiv(recurse_f1_false, recurse_f2_false);
          auto recurse_f1_true = rec(f1, true);
          auto recurse_f2_true = rec(f2, true);
          auto left = formula::And(recurse_f1_false, recurse_f2_false);
          auto right = formula::And(recurse_f1_true, recurse_f2_true);
          return formula::Or(left, right);
        }
      else
        {
          // Rewrite a^b as (a&!b)|(!a&b)
          auto recurse_f1_false = rec(f1, false);
          auto recurse_f2_false = rec(f2, false);
          if (!deep && c->options.keep_top_xor)
            return formula::Xor(recurse_f1_false, recurse_f2_false);
          auto recurse_f1_true = rec(f1, true);
          auto recurse_f2_true = rec(f2, true);
          auto left = formula::And(recurse_f1_false, recurse_f2_true);
          auto right = formula::And(recurse_f1_true, recurse_f2_false);
          return formula::Or(left, right);
        }
    }

    // The deep argument indicate whether we are under a temporal
    // operator (except X).
    formula
    nenoform_rec(formula f, bool negated, tl_simplifier_cache* c,
                 bool deep)
    {
      if (f.is(op::Not))
        {
          negated = !negated;
          f = f[0];
        }

      formula key = f;
      if (negated)
        key = formula::Not(f);
      formula result = c->lookup_nenoform(key);
      if (result)
        return result;

      if (key.is_in_nenoform()
          || (c->options.nenoform_stop_on_boolean && key.is_boolean()))
        {
          result = key;
        }
      else
        {
          auto rec = [c, &deep](formula f, bool neg)
            {
              return nenoform_rec(f, neg, c, deep);
            };

          switch (op o = f.kind())
            {
            case op::ff:
            case op::tt:
              // Negation of constants is taken care of in the
              // constructor of unop::Not, so these cases should be
              // caught by nenoform_recursively().
              assert(!negated);
              result = f;
              break;
            case op::ap:
              result = negated ? formula::Not(f) : f;
              break;
            case op::X:
            case op::strong_X:
              // Currently we don't distinguish between weak and
              // strong semantics, so we treat the two operators
              // identically.
              //
              //   !Xa == X!a
              //   !X[!]a = X!a
              result = formula::X(rec(f[0], negated));
              break;
            case op::F:
              // !Fa == G!a
              deep = true;
              result = formula::unop(negated ? op::G : op::F,
                                     rec(f[0], negated));
              break;
            case op::G:
              // !Ga == F!a
              deep = true;
              result = formula::unop(negated ? op::F : op::G,
                                     rec(f[0], negated));
              break;
            case op::Closure:
              deep = true;
              result = formula::unop(negated ?
                                     op::NegClosure : op::Closure,
                                     rec(f[0], false));
              break;
            case op::NegClosure:
            case op::NegClosureMarked:
              deep = true;
              result = formula::unop(negated ? op::Closure : o,
                                     rec(f[0], false));
              break;

            case op::Implies:
              if (negated)
                // !(a => b) == a & !b
                {
                  auto f2 = rec(f[1], true);
                  result = formula::And(rec(f[0], false), f2);
                }
              else // a => b == !a | b
                {
                  auto f2 = rec(f[1], false);
                  result = formula::Or(rec(f[0], true), f2);
                }
              break;
            case op::Xor:
              {
                // !(a ^ b) == a <=> b
                result = equiv_or_xor(negated, f[0], f[1], c, deep);
                break;
              }
            case op::Equiv:
              {
                // !(a <=> b) == a ^ b
                result = equiv_or_xor(!negated, f[0], f[1], c, deep);
                break;
              }
            case op::U:
              {
                deep = true;
                // !(a U b) == !a R !b
                auto f1 = rec(f[0], negated);
                auto f2 = rec(f[1], negated);
                result = formula::binop(negated ? op::R : op::U, f1, f2);
                break;
              }
            case op::R:
              {
                deep = true;
                // !(a R b) == !a U !b
                auto f1 = rec(f[0], negated);
                auto f2 = rec(f[1], negated);
                result = formula::binop(negated ? op::U : op::R, f1, f2);
                break;
              }
            case op::W:
              {
                deep = true;
                // !(a W b) == !a M !b
                auto f1 = rec(f[0], negated);
                auto f2 = rec(f[1], negated);
                result = formula::binop(negated ? op::M : op::W, f1, f2);
                break;
              }
            case op::M:
              {
                deep = true;
                // !(a M b) == !a W !b
                auto f1 = rec(f[0], negated);
                auto f2 = rec(f[1], negated);
                result = formula::binop(negated ? op::W : op::M, f1, f2);
                break;
              }
            case op::Or:
            case op::And:
              {
                unsigned mos = f.size();
                vec v;
                for (unsigned i = 0; i < mos; ++i)
                  v.emplace_back(rec(f[i], negated));
                op on = o;
                if (negated)
                  on = o == op::Or ? op::And : op::Or;
                result = formula::multop(on, v);
                break;
              }
            case op::OrRat:
            case op::AndRat:
            case op::AndNLM:
            case op::Concat:
            case op::Fusion:
            case op::Star:
            case op::FStar:
            case op::first_match:
              // !(a*) etc. should never occur.
              {
                assert(!negated);
                result = f.map([c, deep](formula f)
                               {
                                 return nenoform_rec(f, false, c, deep);
                               });
                break;
              }
            case op::EConcat:
            case op::EConcatMarked:
              {
                deep = true;
                // !(a <>-> b) == a[]-> !b
                auto f1 = f[0];
                auto f2 = f[1];
                result = formula::binop(negated ? op::UConcat : o,
                                        rec(f1, false), rec(f2, negated));
                break;
              }
            case op::UConcat:
              {
                deep = true;
                // !(a []-> b) == a<>-> !b
                auto f1 = f[0];
                auto f2 = f[1];
                result = formula::binop(negated ? op::EConcat : op::UConcat,
                                        rec(f1, false), rec(f2, negated));
                break;
              }
            case op::exists:
            case op::forall:
              {
                unsigned sz = f.size();
                std::vector<formula> v;
                for (unsigned i = 0; i < sz - 1; ++i)
                  v.emplace_back(f[i]);
                formula body = rec(f[sz - 1], negated);
                op q = o;
                if (negated)
                  q = o == op::exists ? op::forall : op::exists;
                result = formula::quantify(q, v, body);
                break;
              }
            case op::eword:
            case op::Not:
              SPOT_UNREACHABLE();
            }
        }

      c->cache_nenoform(key, result);
      return result;
    }

    //////////////////////////////////////////////////////////////////////
    //
    //  SIMPLIFY_VISITOR
    //
    //////////////////////////////////////////////////////////////////////

    // Forward declaration.
    formula
    simplify_recursively(formula f, tl_simplifier_cache* c);

    // X(a) R b   or   X(a) M b
    // This returns a.
    formula
    is_XRM(formula f)
    {
      if (!f.is(op::R, op::M))
        return nullptr;
      auto left = f[0];
      if (!left.is(op::X))
        return nullptr;
      return left[0];
    }

    // X(a) W b   or   X(a) U b
    // This returns a.
    formula
    is_XWU(formula f)
    {
      if (!f.is(op::W, op::U))
        return nullptr;
      auto left = f[0];
      if (!left.is(op::X))
        return nullptr;
      return left[0];
    }

    // b & X(b W a)  or   b & X(b U a)
    // This returns (b W a) or (b U a).
    formula
    is_bXbWU(formula f)
    {
      if (!f.is(op::And))
        return nullptr;
      unsigned s = f.size();
      for (unsigned pos = 0; pos < s; ++pos)
        {
          auto p = f[pos];
          if (!(p.is(op::X)))
            continue;
          auto c = p[0];
          if (!c.is(op::U, op::W))
            continue;
          formula b = f.all_but(pos);
          if (b == c[0])
            return c;
        }
      return nullptr;
    }

    // b | X(b R a)  or   b | X(b M a)
    // This returns (b R a) or (b M a).
    formula
    is_bXbRM(formula f)
    {
      if (!f.is(op::Or))
        return nullptr;
      unsigned s = f.size();
      for (unsigned pos = 0; pos < s; ++pos)
        {
          auto p = f[pos];
          if (!(p.is(op::X)))
            continue;
          auto c = p[0];
          if (!c.is(op::R, op::M))
            continue;
          formula b = f.all_but(pos);
          if (b == c[0])
            return c;
        }
      return nullptr;
    }

    formula
    unop_multop(op uop, op mop, vec v)
    {
      formula f = formula::unop(uop, formula::multop(mop, v));
      if (f.is(op::X) && f[0].is_ff())
        return formula::ff();
      return f;
    }

    formula
    unop_unop_multop(op uop1, op uop2, op mop, vec v)
    {
      return formula::unop(uop1, unop_multop(uop2, mop, v));
    }

    formula
    unop_unop(op uop1, op uop2, formula f)
    {
      return formula::unop(uop1, formula::unop(uop2, f));
    }

    struct mospliter
    {
      enum what { Split_GF = (1 << 0),
                  Strip_GF = (1 << 1) | (1 << 0),
                  Split_FG = (1 << 2),
                  Strip_FG = (1 << 3) | (1 << 2),
                  Split_F = (1 << 4),
                  Strip_F = (1 << 5) | (1 << 4),
                  Split_G = (1 << 6),
                  Strip_G = (1 << 7) | (1 << 6),
                  Strip_X = (1 << 8),
                  Split_U_or_W = (1 << 9),
                  Split_R_or_M = (1 << 10),
                  Split_EventUniv = (1 << 11),
                  Split_Event = (1 << 12),
                  Split_Univ = (1 << 13),
                  Split_Bool = (1 << 14)
      };

    private:
      mospliter(unsigned split, tl_simplifier_cache* cache)
        : split_(split), c_(cache),
          res_GF{(split_ & Split_GF) ? new vec : nullptr},
          res_FG{(split_ & Split_FG) ? new vec : nullptr},
          res_F{(split_ & Split_F) ? new vec : nullptr},
          res_G{(split_ & Split_G) ? new vec : nullptr},
          res_X{(split_ & Strip_X) ? new vec : nullptr},
          res_U_or_W{(split_ & Split_U_or_W) ? new vec : nullptr},
          res_R_or_M{(split_ & Split_R_or_M) ? new vec : nullptr},
          res_Event{(split_ & Split_Event) ? new vec : nullptr},
          res_Univ{(split_ & Split_Univ) ? new vec : nullptr},
          res_EventUniv{(split_ & Split_EventUniv) ? new vec : nullptr},
          res_Bool{(split_ & Split_Bool) ? new vec : nullptr},
          res_other{new vec}
      {
      }

    public:
      mospliter(unsigned split, vec v, tl_simplifier_cache* cache)
        : mospliter(split, cache)
      {
        for (auto f: v)
          {
            if (f) // skip null pointers left by previous simplifications
              process(f);
          }
      }

      mospliter(unsigned split, formula mo,
                tl_simplifier_cache* cache)
        : mospliter(split, cache)
      {
        unsigned mos = mo.size();
        for (unsigned i = 0; i < mos; ++i)
          {
            formula f = simplify_recursively(mo[i], cache);
            process(f);
          }
      }

      void process(formula f)
      {
        bool e = f.is_eventual();
        bool u = f.is_universal();
        bool eu = res_EventUniv && e & u && c_->options.event_univ;
        switch (f.kind())
          {
          case op::X:
          case op::strong_X:
            if (res_X && !eu)
              {
                res_X->emplace_back(f[0]);
                return;
              }
            break;
          case op::F:
            {
              formula c = f[0];
              if (res_FG && u && c.is(op::G))
                {
                  res_FG->emplace_back(((split_ & Strip_FG) == Strip_FG
                                     ? c[0] : f));
                  return;
                }
              if (res_F && !eu)
                {
                  res_F->emplace_back(((split_ & Strip_F) == Strip_F
                                    ? c : f));
                  return;
                }
              break;
            }
          case op::G:
            {
              formula c = f[0];
              if (res_GF && e && c.is(op::F))
                {
                  res_GF->emplace_back(((split_ & Strip_GF) == Strip_GF
                                     ? c[0] : f));
                  return;
                }
              if (res_G && !eu)
                {
                  res_G->emplace_back(((split_ & Strip_G) == Strip_G
                                    ? c : f));
                  return;
                }
              break;
            }
          case op::U:
          case op::W:
            if (res_U_or_W)
              {
                res_U_or_W->emplace_back(f);
                return;
              }
            break;
          case op::R:
          case op::M:
            if (res_R_or_M)
              {
                res_R_or_M->emplace_back(f);
                return;
              }
            break;
          default:
            if (res_Bool && f.is_boolean())
              {
                res_Bool->emplace_back(f);
                return;
              }
            break;
          }
        if (c_->options.event_univ)
          {
            if (res_EventUniv && e && u)
              {
                res_EventUniv->emplace_back(f);
                return;
              }
            if (res_Event && e)
              {
                res_Event->emplace_back(f);
                return;
              }
            if (res_Univ && u)
              {
                res_Univ->emplace_back(f);
                return;
              }
          }
        res_other->emplace_back(f);
      }

      unsigned split_;
      tl_simplifier_cache* c_;
      std::unique_ptr<vec> res_GF;
      std::unique_ptr<vec> res_FG;
      std::unique_ptr<vec> res_F;
      std::unique_ptr<vec> res_G;
      std::unique_ptr<vec> res_X;
      std::unique_ptr<vec> res_U_or_W;
      std::unique_ptr<vec> res_R_or_M;
      std::unique_ptr<vec> res_Event;
      std::unique_ptr<vec> res_Univ;
      std::unique_ptr<vec> res_EventUniv;
      std::unique_ptr<vec> res_Bool;
      std::unique_ptr<vec> res_other;
    };

    class simplify_visitor final
    {
    public:

      simplify_visitor(tl_simplifier_cache* cache)
        : c_(cache), opt_(cache->options)
        {
        }

      // if !neg build c&X(c&X(...&X(tail))) with n occurrences of c
      // if neg build !c|X(!c|X(...|X(tail))).
      formula
        dup_b_x_tail(bool neg, formula c, formula tail, unsigned n)
      {
        op mop;
        if (neg)
          {
            c = formula::Not(c);
            mop = op::Or;
          }
        else
          {
            mop = op::And;
          }
        while (n--)
          tail = // b&X(tail) or !b|X(tail)
            formula::multop(mop, c, formula::X(tail));
        return tail;
      }

      formula
        visit(formula f)
      {
        formula result = f;

        auto recurse = [this](formula f)
          {
            return simplify_recursively(f, c_);
          };


        // Deal with quantification first.
        if (f.is_quantified() && f.is(op::exists, op::forall))
          {
            std::vector<unsigned char> polarities;
            std::vector<formula> assignments;
            assignments.resize(formula::apid_count(), nullptr);

            auto rec = [&](formula g, auto self) -> formula
            {
              if (!g.is_quantified())
                {
                  g = recurse(g);
                  polarities = collect_apids_with_polarities(g);
                  return g;
                }
              // A variable can be removed from the quantification
              // - if it is not present in the formula
              // - if it has constant polarity below the quantifier
              //   (in that case it should be replaced by a constant
              //   in the formula).
              unsigned sz = g.size();
              formula body = self(g[sz - 1], self);
              std::vector<formula> v;
              v.reserve(sz - 1);
              for (unsigned i = 0; i < sz - 1; ++i)
                {
                  formula h = g[i];
                  unsigned id = h.apid();
                  unsigned pol = polarities[id];
                  if (pol == 0b00)
                    {
                      continue;
                    }
                  else if (pol == 0b11)
                    {
                      v.emplace_back(h);
                    }
                  else
                    {
                      bool is_positive = pol == 0b10;
                      bool is_exists = (g.kind() == op::exists);
                      assignments[id] = (is_positive == is_exists)
                        ? formula::tt() : formula::ff();
                    }
                  // if the atomic proposition is kept (in v)
                  // or will be replaced by a constant (in assignments)
                  // then we can remove it from outer quantifiers.
                  polarities[id] = 0;
                }
              return formula::quantify(g.kind(), v, body);
            };

            formula f2 = rec(f, rec);

            if (!assignments.empty())
              f2 = relabel_apply(f2, assignments);
            if (f2 != f)
              return recurse(f2);
            return f2;
          }

        f = f.map(recurse);

        switch (op o = f.kind())
          {
          case op::ff:
          case op::tt:
          case op::eword:
          case op::ap:
          case op::Not:
          case op::FStar:
            return f;
          case op::X:
          case op::strong_X:
            {
              formula c = f[0];
              // The following rules are not trivial simplifications,
              // because they are not true in LTLf.
              //   X(0)=0
              //   X[!](1)=1
              if (c.is_constant())
                return c;
              // Xf = f if f is both eventual and universal.
              if (c.is_universal() && c.is_eventual())
                {
                  if (opt_.event_univ)
                    return c;
                  // If EventUniv simplification is disabled, use
                  // only the following basic rewriting rules:
                  //   XGF(f) = GF(f) and XFG(f) = FG(f)
                  // The former comes from Somenzi&Bloem (CAV'00).
                  // It's not clear why they do not list the second.
                  if (opt_.reduce_basics &&
                      (c.is({op::G, op::F}) || c.is({op::F, op::G})))
                    return c;
                }

              // If Xa = a, keep only a.
              if (opt_.containment_checks_stronger
                  && c_->lcc.equal(f, c))
                return c;

              // X(f1 & GF(f2)) = X(f1) & GF(f2)
              // X(f1 | GF(f2)) = X(f1) | GF(f2)
              // X(f1 & FG(f2)) = X(f1) & FG(f2)
              // X(f1 | FG(f2)) = X(f1) | FG(f2)
              //
              // The above usually make more sense when reversed (see
              // them in the And and Or rewritings), except when we
              // try to maximize the size of subformula that do not
              // have EventUniv formulae.
              if (opt_.favor_event_univ)
                if (c.is(op::Or, op::And))
                  {
                    mospliter s(mospliter::Split_EventUniv, c, c_);
                    op oc = c.kind();
                    s.res_EventUniv->
                      emplace_back(unop_multop(op::X, oc,
                                            std::move(*s.res_other)));
                    formula result =
                      formula::multop(oc,
                                      std::move(*s.res_EventUniv));
                    if (result != f)
                      result = recurse(result);
                    return result;
                  }
              return f;
            }
          case op::F:
            {
              formula c = f[0];
              // If f is a pure eventuality formula then F(f)=f.
              if (opt_.event_univ && c.is_eventual())
                return c;

              auto g_in_f = [this](formula g, std::vector<formula>* to,
                                   std::vector<formula>* eventual = nullptr)
                {
                  if (g[0].is(op::Or))
                    {
                      mospliter s2(mospliter::Split_Univ |
                                   (eventual ? mospliter::Split_Event : 0),
                                   g[0], c_);
                      for (formula e: *s2.res_Univ)
                        to->push_back(e.is(op::X) ? e[0] : e);
                      to->push_back
                      (unop_multop(op::G, op::Or,
                                   std::move(*s2.res_other)));
                      if (eventual)
                        std::swap(*s2.res_Event, *eventual);
                    }
                  else
                    {
                      to->push_back(g);
                    }
                };

              if (opt_.reduce_basics)
                {
                  // F(a U b) = F(b)
                  if (c.is(op::U))
                    return recurse(formula::F(c[1]));

                  // F(a M b) = F(a & b)
                  if (c.is(op::M))
                    return recurse(unop_multop(op::F, op::And,
                                               {c[0], c[1]}));

                  // FX(a) = XF(a)
                  // FXX(a) = XXF(a) ...
                  // FXG(a) = XFG(a) = FG(a) ...
                  if (c.is(op::X))
                    return recurse(unop_unop(op::X, op::F, c[0]));

                  // F(G(a | Gb)) = F(Ga | Gb)
                  // F(G(a | Fb)) = FGa | GFb // opt_.favor_event_univ
                  if (c.is({op::G, op::Or}))
                    {
                      std::vector<formula> toadd, eventual;
                      g_in_f(c, &toadd,
                             opt_.favor_event_univ ? &eventual : nullptr);
                      formula res = unop_multop(op::F, op::Or,
                                                std::move(toadd));
                      if (!eventual.empty())
                        {
                          formula ev = unop_multop(op::G, op::Or,
                                                   std::move(eventual));
                          res = formula::Or(res, ev);
                        }
                      if (res != f)
                        return recurse(res);
                    }

                  // F(G(a & Fb) = FGa & GFb // !opt_.reduce_size_strictly
                  if (c.is({op::G, op::And}) && !opt_.reduce_size_strictly)
                    {
                      mospliter s2(mospliter::Split_Event, c[0], c_);
                      for (formula& e: *s2.res_Event)
                        while (e.is(op::X))
                          e = e[0];
                      formula fg = unop_unop_multop(op::F, op::G, op::And,
                                                    std::move(*s2.res_other));
                      formula gf = unop_multop(op::G, op::And,
                                               std::move(*s2.res_Event));
                      formula res = formula::And(fg, gf);
                      if (res != f)
                        return recurse(res);
                    }

                  // FG(a) = FG(dnf(a)) if a is not Boolean
                  // and contains some | above non-Boolean subformulas.
                  if (c.is(op::G) && !c[0].is_boolean())
                    {
                      formula m = c[0];
                      bool want_cnf = m.is(op::And);
                      if (!want_cnf && m.is(op::Or))
                        for (auto cc : m)
                          if (cc.is(op::And))
                            {
                              want_cnf = true;
                              break;
                            }
                      if (want_cnf && !opt_.reduce_size_strictly)
                        m = c_->as_cnf(m);
                      // FG(a & Xb) = FG(a & b)
                      // FG(a & Gb) = FG(a & b)
                      if (m.is(op::And))
                        m = m.map([](formula f)
                                  {
                                    if (f.is(op::X, op::G))
                                      return f[0];
                                    return f;
                                  });
                      if (c[0] != m)
                        return recurse(unop_unop(op::F, op::G, m));
                    }
                }
              // if Fa => a, keep a.
              if (opt_.containment_checks_stronger
                  && c_->lcc.contained(f, c))
                return c;

              // Disabled by default:
              //     F(f1 & GF(f2)) = F(f1) & GF(f2)
              //
              // As is, these two formulae are translated into
              // equivalent Büchi automata so the rewriting is
              // useless.
              //
              // However when taken in a larger formula such
              // as F(f1 & GF(f2)) | F(a & GF(b)), this
              // rewriting used to produce (F(f1) & GF(f2)) |
              // (F(a) & GF(b)), missing the opportunity to
              // apply the F(E1)|F(E2) = F(E1|E2) rule which
              // really helps the translation. F((f1 & GF(f2))
              // | (a & GF(b))) is indeed easier to translate.
              //
              // So we do not consider this rewriting rule by
              // default.  However if favor_event_univ is set,
              // we want to move the GF out of the F.
              //
              // Also if this appears inside a G, we want to
              // reduce it:
              //     GF(f1 & GF(f2)) = G(F(f1) & GF(f2))
              //                     = G(F(f1) & F(f2))
              // But this is handled by the G case.
              if (opt_.favor_event_univ)
                // F(f1&f2&FG(f3)&FG(f4)&f5&f6) =
                //                     F(f1&f2) & FG(f3&f4) & f5 & f6
                // if f5 and f6 are both eventual and universal.
                if (c.is(op::And))
                  {
                    mospliter s(mospliter::Strip_FG |
                                mospliter::Split_EventUniv,
                                c, c_);
                    s.res_EventUniv->
                      emplace_back(unop_multop(op::F, op::And,
                                            std::move(*s.res_other)));
                    s.res_EventUniv->
                      emplace_back(unop_unop_multop(op::F, op::G, op::And,
                                                 std::move(*s.res_FG)));
                    formula res =
                      formula::And(std::move(*s.res_EventUniv));
                    if (res != f)
                      return recurse(res);
                  }
              // If u3 and u4 are universal formulae and h is not:
              // F(f1 | f2 | Fu3 | u4 | FGg | Fh | Xu5 | G(f6 | Xu7 | u8))
              //    = F(f1 | f2 | u3 | u4 | Gg | h | u5 | Gf6 | u7 | u8)
              // or
              // F(f1 | f2 | Fu3 | u4 | FGg | Fh | Xu5)
              //    = F(f1 | f2 | h) | F(u3 | u4 | Gg | u5 | Gf6 | u7 | u8)
              // depending on whether favor_event_univ is set.
              if (c.is(op::Or))
                {
                  int w = mospliter::Strip_F
                    | mospliter::Strip_X | mospliter::Split_G;
                  if (opt_.favor_event_univ)
                    w |= mospliter::Split_Univ;
                  mospliter s(w, c, c_);
                  s.res_other->insert(s.res_other->end(),
                                      s.res_F->begin(), s.res_F->end());
                  for (formula f: *s.res_X)
                    if (f.is_universal())
                      s.res_other->push_back(f);
                    else
                      s.res_other->push_back(formula::X(f));
                  std::vector<formula>* to = opt_.favor_event_univ ?
                    s.res_Univ.get() : s.res_other.get();
                  for (formula g: *s.res_G)
                    g_in_f(g, to);
                  formula res = unop_multop(op::F, op::Or,
                                            std::move(*s.res_other));
                  if (s.res_Univ)
                    {
                      std::vector<formula> toadd;
                      for (auto& g: *s.res_Univ)
                        // Strip any F or X
                        if (g.is(op::F, op::X))
                          g = g[0];
                      s.res_Univ->insert(s.res_Univ->end(),
                                         toadd.begin(), toadd.end());
                      formula fu = unop_multop(op::F, op::Or,
                                               std::move(*s.res_Univ));
                      res = formula::Or(res, fu);
                    }
                  if (res != f)
                    return recurse(res);
                }
            }
            return f;
          case op::G:
            {
              formula c = f[0];
              // If f is a pure universality formula then G(f)=f.
              if (opt_.event_univ && c.is_universal())
                return c;

              auto f_in_g = [this](formula f, std::vector<formula>* to,
                                   std::vector<formula>* univ = nullptr)
                {
                  if (f[0].is(op::And))
                    {
                      mospliter s2(mospliter::Split_Event |
                                   (univ ? mospliter::Split_Univ : 0),
                                   f[0], c_);
                      for (formula e: *s2.res_Event)
                        to->push_back(e.is(op::X) ? e[0] : e);
                      to->push_back
                      (unop_multop(op::F, op::And,
                                   std::move(*s2.res_other)));
                      if (univ)
                        std::swap(*s2.res_Univ, *univ);
                    }
                  else
                    {
                      to->push_back(f);
                    }
                };

              if (opt_.reduce_basics)
                {
                  // G(a R b) = G(b)
                  if (c.is(op::R))
                    return recurse(formula::G(c[1]));

                  // G(a W b) = G(a | b)
                  if (c.is(op::W))
                    return recurse(unop_multop(op::G, op::Or,
                                               {c[0], c[1]}));

                  // GX(a) = XG(a)
                  // GXX(a) = XXG(a) ...
                  // GXF(a) = XGF(a) = GF(a) ...
                  if (c.is(op::X))
                    return recurse(unop_unop(op::X, op::G, c[0]));

                  // G(F(a & Fb)) = G(Fa & Fb)
                  // G(F(a & Gb)) = GFa & FGb // !opt_.reduce_size_strictly
                  if (c.is({op::F, op::And}))
                    {
                      std::vector<formula> toadd, univ;
                      f_in_g(c, &toadd,
                             opt_.reduce_size_strictly ? nullptr : &univ);
                      formula res = unop_multop(op::G, op::And,
                                                std::move(toadd));
                      if (!univ.empty())
                        {
                          formula un = unop_multop(op::F, op::And,
                                                   std::move(univ));
                          res = formula::And(res, un);
                        }
                      if (res != f)
                        return recurse(res);
                    }

                  // G(F(a | Gb)) = GFa | FGb   // opt_.favor_event_univ
                  if (c.is({op::F, op::Or}) && opt_.favor_event_univ)
                    {
                      mospliter s2(mospliter::Split_Univ, c[0], c_);
                      for (formula& u: *s2.res_Univ)
                        while (u.is(op::X))
                          u = u[0];
                      formula gf = unop_unop_multop(op::G, op::F, op::Or,
                                                    std::move(*s2.res_other));
                      formula fg = unop_multop(op::F, op::Or,
                                               std::move(*s2.res_Univ));
                      formula res = formula::Or(gf, fg);
                      if (res != f)
                        return recurse(res);
                    }

                  // G(f1|f2|GF(f3)|GF(f4)|f5|f6) =
                  //                        G(f1|f2) | GF(f3|f4) | f5 | f6
                  // if f5 and f6 are both eventual and universal.
                  if (c.is(op::Or))
                    {
                      mospliter s(mospliter::Strip_GF |
                                  mospliter::Split_EventUniv,
                                  c, c_);
                      s.res_EventUniv->
                        emplace_back(unop_multop(op::G, op::Or,
                                              std::move(*s.res_other)));
                      s.res_EventUniv->
                        emplace_back(unop_unop_multop(op::G, op::F, op::Or,
                                                   std::move(*s.res_GF)));
                      formula res =
                        formula::Or(std::move(*s.res_EventUniv));

                      if (res != f)
                        return recurse(res);
                    }
                  // If e3 and e4 are eventual formulae and h is not:
                  // G(f1 & f2 & Ge3 & e4 & GFg & Gh & Xe5 & F(f6 & Xe7 & e8))
                  //    = G(f1 & f2 & e3 & e4 & Fg & h & e5 & Ff6 & e7 & e8)
                  // or
                  // G(f1 & f2 & Ge3 & e4 & GFg & Gh & Xe5 & F(f6 & Xe7 & e8))
                  //    = G(f1 & f2 & h) & G(e3 & e4 & Fg & e5 & Ff6 & e7 & e8)
                  // depending on whether favor_event_univ is set.
                  else if (c.is(op::And))
                    {
                      int w = mospliter::Strip_G |
                        mospliter::Strip_X | mospliter::Split_F;
                      if (opt_.favor_event_univ)
                        w |= mospliter::Split_Event;
                      mospliter s(w, c, c_);
                      s.res_other->insert(s.res_other->end(),
                                          s.res_G->begin(), s.res_G->end());
                      for (formula f: *s.res_X)
                        if (f.is_eventual())
                          s.res_other->push_back(f);
                        else
                          s.res_other->push_back(formula::X(f));
                      std::vector<formula>* to = opt_.favor_event_univ ?
                        s.res_Event.get() : s.res_other.get();
                      for (formula f: *s.res_F)
                        f_in_g(f, to);

                      formula res = unop_multop(op::G, op::And,
                                                std::move(*s.res_other));
                      if (s.res_Event)
                        {
                          std::vector<formula> toadd;
                          // Strip any G or X
                          for (auto& g: *s.res_Event)
                            if (g.is(op::G, op::X))
                              {
                                g = g[0];
                              }
                          s.res_Event->insert(s.res_Event->end(),
                                              toadd.begin(), toadd.end());
                          formula ge =
                            unop_multop(op::G, op::And,
                                        std::move(*s.res_Event));
                          res = formula::And(res, ge);
                        }
                      if (res != f)
                        return recurse(res);
                    }


                  // GF(a) = GF(dnf(a)) if a is not Boolean
                  // and contains some | above non-Boolean subformulas.
                  if (c.is(op::F) && !c[0].is_boolean())
                    {
                      formula m = c[0];
                      bool want_dnf = m.is(op::Or);
                      if (!want_dnf && m.is(op::And))
                        for (auto cc : m)
                          if (cc.is(op::Or))
                            {
                              want_dnf = true;
                              break;
                            }
                      if (want_dnf && !opt_.reduce_size_strictly)
                        m = c_->as_dnf(m);
                      // GF(a | Xb) = GF(a | b)
                      // GF(a | Fb) = GF(a | b)
                      if (m.is(op::Or))
                        m = m.map([](formula f)
                                  {
                                    if (f.is(op::X, op::F))
                                      return f[0];
                                    return f;
                                  });
                      if (c[0] != m)
                        return recurse(unop_unop(op::G, op::F, m));
                    }
                  // GF(f1 & f2 & eu1 & eu2) = G(F(f1 & f2) & eu1 & eu2
                  if (opt_.event_univ && c.is({op::F, op::And}))
                    {
                      mospliter s(mospliter::Split_EventUniv,
                                  c[0], c_);
                      s.res_EventUniv->
                        emplace_back(unop_multop(op::F, op::And,
                                              std::move(*s.res_other)));
                      formula res =
                        formula::G(formula::And(std::move(*s.res_EventUniv)));
                      if (res != f)
                        return recurse(res);
                    }
                }
              // if a => Ga, keep a.
              if (opt_.containment_checks_stronger
                  && c_->lcc.contained(c, f))
                return c;
            }
            return f;
          case op::Closure:
          case op::NegClosure:
          case op::NegClosureMarked:
            {
              formula c = f[0];
              // {e[*]} = {e}
              // !{e[*]} = !{e}
              if (c.accepts_eword() && c.is(op::Star))
                return recurse(formula::unop(o, c[0]));

              if (!opt_.reduce_size_strictly)
                if (c.is(op::OrRat))
                  {
                    //  {a₁|a₂} =  {a₁}| {a₂}
                    // !{a₁|a₂} = !{a₁}&!{a₂}
                    unsigned s = c.size();
                    vec v;
                    for (unsigned n = 0; n < s; ++n)
                      v.emplace_back(formula::unop(o, c[n]));
                    return recurse(formula::multop(o == op::Closure
                                                   ? op::Or : op::And, v));
                  }
              if (c.is(op::Concat))
                {
                  if (c.accepts_eword())
                    {
                      if (opt_.reduce_size_strictly)
                        return f;
                      // If all terms accept the empty word, we have
                      // {e₁;e₂;e₃} =  {e₁}|{e₂}|{e₃}
                      // !{e₁;e₂;e₃} = !{e₁}&!{e₂}&!{e₃}
                      vec v;
                      unsigned end = c.size();
                      v.reserve(end);
                      for (unsigned i = 0; i < end; ++i)
                        v.emplace_back(formula::unop(o, c[i]));
                      return recurse(formula::multop(o == op::Closure ?
                                                     op::Or : op::And, v));
                    }

                  // Some term does not accept the empty word.
                  unsigned end = c.size() - 1;

                  // {r;1} = 1 if r accepts [*0], else {r}
                  // !{r;1} = 0 if r accepts [*0], else !{r}
                  if (c[end].is_tt())
                    {
                      formula rest = c.all_but(end);
                      if (rest.accepts_eword())
                        return o == op::Closure ? formula::tt() : formula::ff();
                      return recurse(formula::unop(o, rest));
                    }

                  // {b₁;b₂;e₁;f₁;e₂;f₂;e₂;e₃;e₄}
                  //    = b₁&X(b₂&X({e₁;f₁;e₂;f₂}))
                  // !{b₁;b₂;e₁;f₁;e₂;f₂;e₂;e₃;e₄}
                  //    = !b₁|X(!b₂|X(!{e₁;f₁;e₂;f₂}))
                  // if e denotes a term that accepts [*0]
                  // and b denotes a Boolean formula.
                  //
                  // if reduce_size_strictly is set, we simply remove
                  // the trailing e2;e3;e4.
                  while (c[end].accepts_eword())
                    --end;
                  unsigned start = 0;
                  while (start <= end)
                    {
                      formula r = c[start];
                      if (r.is_boolean() && !opt_.reduce_size_strictly)
                        ++start;
                      else
                        break;
                    }
                  unsigned s = end + 1 - start;
                  if (s != c.size())
                    {
                      bool doneg = o != op::Closure;
                      formula tail;
                      if (s > 0)
                        {
                          vec v;
                          v.reserve(s);
                          for (unsigned n = start; n <= end; ++n)
                            v.emplace_back(c[n]);
                          tail = formula::Concat(v);
                          tail = formula::unop(o, tail);
                        }
                      else
                        {
                          tail = doneg ? formula::ff() : formula::tt();
                        }

                      for (unsigned n = start; n > 0;)
                        {
                          --n;
                          formula e = c[n];
                          // {b;f} = b & X{f}
                          // !{b;f} = !b | X!{f}
                          if (e.is_boolean())
                            {
                              tail = formula::X(tail);
                              if (doneg)
                                tail = formula::Or(formula::Not(e), tail);
                              else
                                tail = formula::And(e, tail);
                            }
                        }
                      return recurse(tail);
                    }

                  // {b[*i..j];c} = b&X(b&X(... b&X{b[*0..j-i];c}))
                  // !{b[*i..j];c} = !b&X(!b&X(... !b&X!{b[*0..j-i];c}))
                  if (!opt_.reduce_size_strictly)
                    if (c[0].is(op::Star))
                      {
                        formula s = c[0];
                        formula sc = s[0];
                        unsigned min = s.min();
                        if (sc.is_boolean() && min > 0)
                          {
                            unsigned max = s.max();
                            if (max != formula::unbounded())
                              max -= min;
                            unsigned ss = c.size();
                            vec v;
                            v.reserve(ss);
                            v.emplace_back(formula::Star(sc, 0, max));
                            for (unsigned n = 1; n < ss; ++n)
                              v.emplace_back(c[n]);
                            formula tail = formula::Concat(v);
                            tail = // {b[*0..j-i]} or !{b[*0..j-i]}
                              formula::unop(o, tail);
                            tail =
                              dup_b_x_tail(o != op::Closure,
                                           sc, tail, min);
                            return recurse(tail);
                          }
                      }
                }
              // {b[*i..j]} = b&X(b&X(... b))  with i occurrences of b
              // !{b[*i..j]} = !b&X(!b&X(... !b))
              if (!opt_.reduce_size_strictly)
                if (c.is(op::Star))
                  {
                    formula cs = c[0];
                    if (cs.is_boolean())
                      {
                        unsigned min = c.min();
                        assert(min > 0);
                        formula tail;
                        if (o == op::Closure)
                          tail = dup_b_x_tail(false, cs,
                                              formula::tt(), min);
                        else
                          tail = dup_b_x_tail(true, cs,
                                              formula::ff(), min);
                        return recurse(tail);
                      }
                  }
              return f;
            }
          case op::Xor:
          case op::Implies:
          case op::Equiv:
          case op::U:
          case op::R:
          case op::W:
          case op::M:
          case op::EConcat:
          case op::EConcatMarked:
          case op::UConcat:
            return visit_binop(f);
          case op::Or:
          case op::OrRat:
          case op::And:
          case op::AndRat:
          case op::AndNLM:
          case op::Concat:
            return visit_multop(f);
          case op::Fusion:
            return f;
          case op::Star:
            {
              if (!f.accepts_eword())
                return f;
              formula h = f[0];
              if (f.max() == 1 && h.accepts_eword())
                return h;
              auto min = 0;
              if (f.max() == formula::unbounded())
                {
                  h = c_->star_normal_form(h);
                }
              else
                {
                  h = c_->star_normal_form_bounded(h);
                  if (h.accepts_eword())
                    min = 1;
                }
              return formula::Star(h, min, f.max());
            }
          case op::first_match:
            {
              formula h = f[0];
              if (h.is(op::Star))
                {
                  // first_match(b[*i..j]) = b[*i]
                  // first_match(f[*i..j]) = first_match(f[*i])
                  unsigned m = h.min();
                  if (m < h.max())
                    {
                      formula s = formula::Star(h[0], m, m);
                      s = h[0].is_boolean() ? s : formula::first_match(s);
                      return recurse(s);
                    }
                }
              else if (h.is(op::FStar))
                {
                  // first_match(f[:*i..j]) = first_match(f[:*i])
                  unsigned m = h.min();
                  if (m < h.max())
                    {
                      formula s = formula::FStar(h[0], m, m);
                      return recurse(formula::first_match(s));
                    }
                }
              else if (h.is(op::Concat))
                {
                  // If b is Boolean and e accepts [*0], we have
                  // 1. first_match(b;f) ≡ b;first_match(f)
                  // 2. first_match(f;e) ≡ first_match(f)
                  // 3. first_match(first_match(f);g) =
                  //      first_match(f);first_match(g)
                  // 4. first_match(f;g[*i:j]) ≡ first_match(f;g[*i])
                  // 5. first_match(f;g[:*i..j]) = first_match(f;g[:*i])
                  // 6. first_match(b[*i:j];f) ≡ b[*i];first_match(b[*0:j-i];f)
                  // Rules 1-3 can be repeated, so we will loop
                  // to save the recursion and intermediate caching.

                  // Extract Boolean formulas at the beginning.
                  int i = 0;
                  int n = h.size();
                  while (i < n
                         && (h[i].is_boolean() || h[i].is(op::first_match)))
                    ++i;
                  vec prefix;
                  // +1 to append first_match(suffix), +1 to for rule 6.
                  prefix.reserve(i + 2);
                  for (int ii = 0; ii < i; ++ii)
                    prefix.push_back(h[ii]);
                  // Extract suffix, minus trailing formulas that accept [*0].
                  // "i" is the start of the suffix.
                  do
                    --n;
                  while (i <= n && h[n].accepts_eword());
                  vec suffix;
                  suffix.reserve(n + 1 - i);
                  for (int ii = i; ii <= n; ++ii)
                    suffix.push_back(h[ii]);
                  // Rules 4-5
                  if (!suffix.empty() && suffix.back().is(op::Star, op::FStar))
                    {
                      formula s = suffix.back();
                      unsigned smin = s.min();
                      suffix.back() = formula::bunop(s.kind(),
                                                     s[0], smin, smin);
                    }
                  // Rule 6
                  if (!suffix.empty() && suffix.front().is(op::Star))
                    {
                      formula s = suffix.front();
                      unsigned smin = s.min();
                      if (smin > 0 && s[0].is_boolean())
                        {
                          prefix.push_back(formula::Star(s[0], smin, smin));
                          suffix.front() =
                            formula::Star(s[0], 0, s.max() - smin);
                        }
                    }
                  prefix.push_back(formula::first_match
                                   (formula::Concat(suffix)));
                  formula res = formula::Concat(prefix);
                  if (res != f)
                    return recurse(res);
                }
              else if (h.is(op::Fusion))
                {
                  // 1. first_match(b:f) = b:first_match(f) if f rejects [*0]
                  // (not implemented, because first_match will build a DFA
                  // and limiting its choices is good)
                  // 2. first_match(b[*i..j]:f) =
                  //       b[*i-1];first_match(b[*1..j-i+1]:f) if i>1
                  if (h[0].is(op::Star))
                    {
                      formula s = h[0];
                      unsigned smin = s.min();
                      if (smin > 1 && s[0].is_boolean())
                        {
                          --smin;
                          unsigned smax = s.max();
                          if (smax != formula::unbounded())
                            smax -= smin;
                          formula s2 = formula::Star(s[0], 1, smax);
                          formula in = formula::Fusion(s2, h.all_but(0));
                          in = formula::first_match(in);
                          formula s3 = formula::Star(s[0], smin, smin);
                          return recurse(formula::Concat(s3, in));
                        }
                    }
                  // 3. first_match(first_match(f):g) =
                  //       first_match(f):first_match(1:g)
                  if (h[0].is(op::first_match))
                    {
                      formula rest = h.all_but(0);
                      if (rest.accepts_eword())
                        rest = formula::Fusion(formula::tt(), rest);
                      rest = formula::first_match(rest);
                      return recurse(formula::Fusion(h[0], rest));
                    }
                  // 4. first_match(f:g[*i..j]) = first_match(f:g[*max(1,i)])
                  // 5. first_match(f:g[:*i..j]) = first_match(f:g[:*i])
                  unsigned last = h.size() - 1;
                  formula tail = h[last];
                  if (tail.is(op::Star, op::FStar))
                    {
                      unsigned smin = tail.min();
                      if (smin < tail.max())
                        {
                          if (smin == 0 && tail.is(op::Star))
                            ++smin;
                          formula s2 =
                            formula::bunop(tail.kind(), tail[0], smin, smin);
                          formula in = formula::Fusion(h.all_but(last), s2);
                          return recurse(formula::first_match(in));
                        }
                    }
                }
              return f;
            }
          case op::exists:
          case op::forall:
            SPOT_UNREACHABLE();
          }
        SPOT_UNREACHABLE();
      }

      formula reduce_sere_ltl(formula orig)
      {
        op bindop = orig.kind();
        formula a = orig[0];
        formula b = orig[1];

        auto recurse = [this](formula f)
          {
            return simplify_recursively(f, c_);
          };

        // All this function is documented assuming bindop ==
        // UConcat, but by changing the following variables it can
        // perform the rules for EConcat as well.
        op op_g;
        op op_w;
        op op_r;
        op op_and;
        bool doneg;
        if (bindop == op::UConcat)
          {
            op_g = op::G;
            op_w = op::W;
            op_r = op::R;
            op_and = op::And;
            doneg = true;
          }
        else // EConcat & EConcatMarked
          {
            op_g = op::F;
            op_w = op::M;
            op_r = op::U;
            op_and = op::Or;
            doneg = false;
          }

        if (!opt_.reduce_basics)
          return orig;
        if (a.is(op::Star))
          {
            // {[*]}[]->b = Gb
            if (a == formula::one_star())
              return recurse(formula::unop(op_g, b));

            formula s = a[0];
            unsigned min = a.min();
            unsigned max = a.max();
            // {s[*]}[]->b = b W !s   if s is Boolean.
            // {s[+]}[]->b = b W !s   if s is Boolean.
            if (s.is_boolean() && max == formula::unbounded() && min <= 1)
              {
                formula ns = doneg ? formula::Not(s) : s;
                // b W !s
                return recurse(formula::binop(op_w, b, ns));
              }
            // {s[*0..j]}[]->b = {s[*1..j]}[]->b
            // {s[*0..j]}<>->b = {s[*1..j]}<>->b
            if (min == 0)
              return recurse(formula::binop(bindop,
                                            formula::Star(s, 1, max), b));

            if (opt_.reduce_size_strictly)
              return orig;
            // {s[*i..j]}[]->b = {s;s;...;s[*1..j-i+1]}[]->b
            // = {s}[]->X({s}[]->X(...[]->X({s[*1..j-i+1]}[]->b)))
            // if i>0 and s does not accept the empty word
            assert(min > 0);
            if (s.accepts_eword())
              return orig;
            --min;
            if (max != formula::unbounded())
              max -= min; // j-i+1
            // Don't rewrite s[1..].
            if (min == 0)
              return orig;
            formula tail = // {s[*1..j-i]}[]->b
              formula::binop(bindop, formula::Star(s, 1, max), b);
            for (unsigned n = 0; n < min; ++n)
              tail = // {s}[]->X(tail)
                formula::binop(bindop, s, formula::X(tail));
            return recurse(tail);
          }
        else if (a.is(op::Concat))
          {
            unsigned s = a.size() - 1;
            formula last = a[s];
            // {r;[*]}[]->b = {r}[]->Gb
            if (last == formula::one_star())
              return recurse(formula::binop(bindop, a.all_but(s),
                                            formula::unop(op_g, b)));

            formula first = a[0];
            // {[*];r}[]->b = G({r}[]->b)
            if (first == formula::one_star())
              return recurse(formula::unop(op_g,
                                           formula::binop(bindop,
                                                          a.all_but(0), b)));

            if (opt_.reduce_size_strictly)
              return orig;

            // {r;s[*]}[]->b = {r}[]->(b & X(b W !s))
            // if s is Boolean and r does not accept [*0];
            if (last.is_Kleene_star()) // l = s[*]
              if (last[0].is_boolean())
                {
                  formula r = a.all_but(s);
                  if (!r.accepts_eword())
                    {
                      formula ns = // !s
                        doneg ? formula::Not(last[0]) : last[0];
                      formula w = // b W !s
                        formula::binop(op_w, b, ns);
                      formula x = // X(b W !s)
                        formula::X(w);
                      formula d = // b & X(b W !s)
                        formula::multop(op_and, b, x);
                      // {r}[]->(b & X(b W !s))
                      return recurse(formula::binop(bindop, r, d));
                    }
                }
            // {s[*];r}[]->b = !s R ({r}[]->b)
            // if s is Boolean and r does not accept [*0];
            if (first.is_Kleene_star())
              if (first[0].is_boolean())
                {
                  formula r = a.all_but(0);
                  if (!r.accepts_eword())
                    {
                      formula ns = // !s
                        doneg
                        ? formula::Not(first[0])
                        : first[0];
                      formula u = // {r}[]->b
                        formula::binop(bindop, r, b);
                      // !s R ({r}[]->b)
                      return recurse(formula::binop(op_r, ns, u));
                    }
                }

            // {r₁;r₂;r₃}[]->b = {r₁}[]->X({r₂}[]->X({r₃}[]->b))
            // if r₁, r₂, r₃ do not accept [*0].
            if (!a.accepts_eword())
              {
                unsigned count = 0;
                for (unsigned n = 0; n <= s; ++n)
                  count += !a[n].accepts_eword();
                assert(count > 0);
                if (count == 1)
                  return orig;
                // Let e denote a term that accepts [*0]
                // and let f denote a term that do not.
                // A formula such as {e₁;f₁;e₂;e₃;f₂;e₄}[]->b
                // in which count==2 will be grouped
                // as follows:  r₁ = e₁;f₁;e₂;e₃
                //              r₂ = f₂;e₄
                // this way we have
                // {e₁;f₁;e₂;e₃;f₂;e₄}[]->b = {r₁;r₂;r₃}[]->b
                // where r₁ and r₂ do not accept [*0].
                unsigned pos = s + 1;

                // We compute the r formulas from the right
                // (i.e., r₂ before r₁.)
                vec r;
                do
                  r.insert(r.begin(), a[--pos]);
                while (r.front().accepts_eword());
                formula tail = // {r₂}[]->b
                  formula::binop(bindop, formula::Concat(r), b);
                while (--count)
                  {
                    vec r;
                    do
                      r.insert(r.begin(), a[--pos]);
                    while (r.front().accepts_eword());
                    // If it's the last block, take all leading
                    // formulae as well.
                    if (count == 1)
                      while (pos > 0)
                        {
                          r.insert(r.begin(), a[--pos]);
                          assert(r.front().accepts_eword());
                        }

                    tail = // X({r₂}[]->b)
                      formula::X(tail);
                    tail = // {r₁}[]->X({r₂}[]->b)
                      formula::binop(bindop, formula::Concat(r), tail);
                  }
                return recurse(tail);
              }
          }
        else if (opt_.reduce_size_strictly)
          {
            return orig;
          }
        else if (a.is(op::Fusion))
          {
            // {r₁:r₂:r₃}[]->b = {r₁}[]->({r₂}[]->({r₃}[]->b))
            unsigned s = a.size();
            formula tail = b;
            do
              {
                --s;
                tail = formula::binop(bindop, a[s], tail);
              }
            while (s != 0);
            return recurse(tail);
          }
        else if (a.is(op::OrRat))
          {
            // {r₁|r₂|r₃}[]->b = ({r₁}[]->b)&({r₂}[]->b)&({r₃}[]->b)
            unsigned s = a.size();
            vec v;
            for (unsigned n = 0; n < s; ++n)
              // {r₁}[]->b
              v.emplace_back(formula::binop(bindop, a[n], b));
            return recurse(formula::multop(op_and, v));
          }
        return orig;
      }

      formula
        visit_binop(formula bo)
      {
        auto recurse = [this](formula f)
          {
            return simplify_recursively(f, c_);
          };
        op o = bo.kind();
        formula b = bo[1];
        if (opt_.event_univ)
          {
            trace << "bo: trying eventuniv rules" << std::endl;
            /* If b is a pure eventuality formula then a U b = b.
               If b is a pure universality formula a R b = b. */
            if ((b.is_eventual() && bo.is(op::U))
                || (b.is_universal() && bo.is(op::R)))
              return b;
          }

        formula a = bo[0];
        if (opt_.event_univ)
          {
            /* If a is a pure eventuality formula then a M b = a & b.
               If a is a pure universality formula a W b = a | b. */
            if (a.is_eventual() && bo.is(op::M))
              return recurse(formula::And(a, b));
            if (a.is_universal() && bo.is(op::W))
              return recurse(formula::Or(a, b));

            // (q R Xf) = X(q R f)
            // (q U Xf) = X(q U f)
            if (a.is_eventual() && a.is_universal()
                && bo.is(op::R, op::U) && b.is(op::X))
              return recurse(formula::X(formula::binop(o, a, b[0])));

            // e₁ W e₂ = Ge₁ | e₂
            // u₁ M u₂ = Fu₁ & u₂
            //
            // The above formulas are actually true if e₁ and u₁ are
            // unconstrained, however there are many cases were such a
            // more generic reduction rule will actually produce more
            // states once the resulting formula is translated.
            if (!opt_.reduce_size_strictly)
              {
                if (bo.is(op::W) && a.is_eventual() && b.is_eventual())
                  return recurse(formula::Or(formula::G(a), b));
                if (bo.is(op::M) && a.is_universal() && b.is_universal())
                  return recurse(formula::And(formula::F(a), b));
              }

            // In the following rewritings we assume that
            // - e is a pure eventuality
            // - u is purely universal
            // - q is purely universal pure eventuality
            // (a U (b|e)) = (a U b)|e
            // (a W (b|e)) = (a W b)|e
            // (a U (b&q)) = (a U b)&q
            // ((a&q) M b) = (a M b)&q
            // (a R (b&u)) = (a R b)&u
            // (a M (b&u)) = (a M b)&u
            if (opt_.favor_event_univ)
              {
                if (bo.is(op::U, op::W))
                  if (b.is(op::Or))
                    {
                      mospliter s(mospliter::Split_Event, b, c_);
                      formula b2 = formula::Or(std::move(*s.res_other));
                      if (b2 != b)
                        {
                          s.res_Event->emplace_back(formula::binop(o, a, b2));
                          return recurse
                            (formula::Or(std::move(*s.res_Event)));
                        }
                    }
                if (bo.is(op::U))
                  if (b.is(op::And))
                    {
                      mospliter s(mospliter::Split_EventUniv, b, c_);
                      formula b2 = formula::And(std::move(*s.res_other));
                      if (b2 != b)
                        {
                          s.res_EventUniv->emplace_back(formula::binop(o,
                                                                    a, b2));
                          return recurse
                            (formula::And(std::move(*s.res_EventUniv)));
                        }
                    }
                if (bo.is(op::M))
                  if (a.is(op::And))
                    {
                      mospliter s(mospliter::Split_EventUniv, a, c_);
                      formula a2 = formula::And(std::move(*s.res_other));
                      if (a2 != a)
                        {
                          s.res_EventUniv->emplace_back(formula::binop(o,
                                                                    a2, b));
                          return recurse
                            (formula::And(std::move(*s.res_EventUniv)));
                        }
                    }
                if (bo.is(op::R, op::M))
                  if (b.is(op::And))
                    {
                      mospliter s(mospliter::Split_Univ, b, c_);
                      formula b2 = formula::And(std::move(*s.res_other));
                      if (b2 != b)
                        {
                          s.res_Univ->emplace_back(formula::binop(o, a, b2));
                          return recurse
                            (formula::And(std::move(*s.res_Univ)));
                        }
                    }
              }
            trace << "bo: no eventuniv rule matched" << std::endl;
          }

        // Inclusion-based rules
        if (opt_.synt_impl | opt_.containment_checks)
          {
            trace << "bo: trying inclusion-based rules" << std::endl;
            switch (o)
              {
              case op::Equiv:
                {
                  if (c_->implication(a, b))
                    return recurse(formula::Implies(b, a));
                  if (c_->implication(b, a))
                    return recurse(formula::Implies(a, b));
                  break;
                }
              case op::Implies:
                {
                  if (c_->implication(a, b))
                    return formula::tt();
                  break;
                }
              case op::Xor:
                {
                  // if (!a)->b then a xor b = b->!a = a->!b
                  if (c_->implication_neg(a, b, false))
                    {
                      if (b.is(op::Not))
                        return recurse(formula::Implies(a, b[0]));
                      return recurse(formula::Implies(b, formula::Not(a)));
                    }
                  // if a->!b then a xor b = (!b)->a = (!a)->b
                  if (c_->implication_neg(a, b, true))
                    {
                      if (b.is(op::Not))
                        return recurse(formula::Implies(b[0], a));
                      return recurse(formula::Implies(formula::Not(a), b));
                    }
                  break;
                }
              case op::U:
                // if a => b, then a U b = b
                if (c_->implication(a, b))
                  {
                    // but if also b => a, pick the smallest one.
                    if ((length_boolone(a) < length_boolone(b))
                        && c_->implication(b, a))
                      return a;
                    return b;
                  }
                // if (a U b) => b, then a U b = b (for stronger containment)
                if (opt_.containment_checks_stronger
                    && c_->contained(bo, b))
                  return b;
                // if !a => b, then a U b = Fb
                // if a eventual && b => a, then a U b = Fb
                if (c_->implication_neg(a, b, false)
                    || (a.is_eventual() && c_->implication(b, a)))
                  return recurse(formula::F(b));
                // if a => b, then a U (b U c) = (b U c)
                // if a => b, then a U (b W c) = (b W c)
                if (b.is(op::U, op::W) && c_->implication(a, b[0]))
                  return b;
                // if b => a, then a U (b U c) = (a U c)
                if (b.is(op::U) && c_->implication(b[0], a))
                  return recurse(formula::U(a, b[1]));
                // if a => c, then a U (b R (c U d)) = (b R (c U d))
                // if a => c, then a U (b R (c W d)) = (b R (c W d))
                // if a => c, then a U (b M (c U d)) = (b M (c U d))
                // if a => c, then a U (b M (c W d)) = (b M (c W d))
                if (b.is(op::R, op::M))
                  {
                    auto c1 = b[1];
                    if (c1.is(op::U, op::W) && c_->implication(a, c1[0]))
                      return b;
                  }
                // if a => b, then (a U c) U b = c U b
                // if a => b, then (a W c) U b = c U b
                if (a.is(op::U, op::W) && c_->implication(a[0], b))
                  return recurse(formula::U(a[1], b));
                // if c => b, then (a U c) U b = (a U c) | b
                if (a.is(op::U) && c_->implication(a[1], b))
                  return recurse(formula::Or(a, b));
                // if g => h, then (f|g) U h = f U h
                if (a.is(op::Or))
                  {
                    unsigned n = a.size();
                    for (unsigned child = 0; child < n; ++child)
                      if (c_->implication(a[child], b))
                        return recurse(formula::U(a.all_but(child), b));
                  }
                // a U (b & e) = F(b & e) if !b => a
                if (b.is(op::And))
                  for (formula c: b)
                    if (c.is_eventual())
                      {
                        // We know there is one pure eventuality
                        // formula but we might have more.  So lets
                        // extract everything else.
                        vec rest;
                        rest.reserve(c.size());
                        for (formula cc: b)
                          if (!cc.is_eventual())
                            rest.emplace_back(cc);
                        if (c_->implication_neg(formula::And(rest), a, false))
                          return recurse(formula::F(b));
                        break;
                      }
                break;

              case op::R:
                // if b => a, then a R b = b
                if (c_->implication(b, a))
                  {
                    // but if also a => b, pick the smallest one.
                    if ((length_boolone(a) < length_boolone(b))
                        && c_->implication(a, b))
                      return a;
                    return b;
                  }
                // if b => !a, then a R b = Gb
                // if a universal && a => b, then a R b = Gb
                if (c_->implication_neg(b, a, true)
                    || (a.is_universal() && c_->implication(a, b)))
                  return recurse(formula::G(b));
                // if b => a, then a R (b R c) = b R c
                // if b => a, then a R (b M c) = b M c
                if (b.is(op::R, op::M) && c_->implication(b[0], a))
                  return b;
                // if a => b, then a R (b R c) = a R c
                if (b.is(op::R) && c_->implication(a, b[0]))
                  return recurse(formula::R(a, b[1]));
                // if b => a, then (a R c) R b = c R b
                // if b => a, then (a M c) R b = c R b
                // if c => b, then (a R c) R b = (a & c) R b
                // if c => b, then (a M c) R b = (a & c) R b
                if (a.is(op::R, op::M))
                  {
                    if (c_->implication(b, a[0]))
                      return recurse(formula::R(a[1], b));
                    if (c_->implication(a[1], b))
                      {
                        formula ac = formula::And(a[0], a[1]);
                        return recurse(formula::R(ac, b));
                      }
                  }
                // if h => g, then (f&g) R h = f R h
                if (a.is(op::And))
                  {
                    unsigned n = a.size();
                    for (unsigned child = 0; child < n; ++child)
                      if (c_->implication(b, a[child]))
                        return recurse(formula::R(a.all_but(child), b));
                  }
                // a R (b | u) = G(b | u) if b => !a
                if (b.is(op::Or))
                  for (formula c: b)
                    if (c.is_universal())
                      {
                        // We know there is one purely universal
                        // formula but we might have more.  So lets
                        // extract everything else.
                        vec rest;
                        rest.reserve(c.size());
                        for (formula cc: b)
                          if (!cc.is_universal())
                            rest.emplace_back(cc);
                        if (c_->implication_neg(formula::Or(rest), a, true))
                          return recurse(formula::G(b));
                        break;
                      }
                break;

              case op::W:
                // if !a => b then a W b = 1
                if (c_->implication_neg(a, b, false))
                  return formula::tt();
                // if a => b, then a W b = b
                if (c_->implication(a, b))
                  {
                    // but if also b => a, pick the smallest one.
                    if ((length_boolone(a) < length_boolone(b))
                        && c_->implication(b, a))
                      return a;
                    return b;
                  }

                // if a W b => b, then a W b = b (for stronger containment)
                if (opt_.containment_checks_stronger
                    && c_->contained(bo, b))
                  return b;
                // if a => b, then a W (b W c) = b W c
                // (Beware: even if a => b we do not have a W (b U c) = b U c)
                if (b.is(op::W) && c_->implication(a, b[0]))
                  return b;
                // if b => a, then a W (b U c) = a W c
                // if b => a, then a W (b W c) = a W c
                if (b.is(op::U, op::W) && c_->implication(b[0], a))
                  return recurse(formula::W(a, b[1]));
                // if a => b, then (a U c) W b = c W b
                // if a => b, then (a W c) W b = c W b
                if (a.is(op::U, op::W) && c_->implication(a[0], b))
                  return recurse(formula::W(a[1], b));
                // if c => b, then (a W c) W b = (a W c) | b
                // if c => b, then (a U c) W b = (a U c) | b
                if (a.is(op::U, op::W) && c_->implication(a[1], b))
                  return recurse(formula::Or(a, b));
                // if g => h, then (f|g) W h = f M h
                if (a.is(op::Or))
                  {
                    unsigned n = a.size();
                    for (unsigned child = 0; child < n; ++child)
                      if (c_->implication(a[child], b))
                        return recurse(formula::W(a.all_but(child), b));
                  }
                break;

              case op::M:
                // if b => !a, then a M b = 0
                if (c_->implication_neg(b, a, true))
                  return formula::ff();
                // if b => a, then a M b = b
                if (c_->implication(b, a))
                  {
                    // but if also a => b, pick the smallest one.
                    if ((length_boolone(a) < length_boolone(b))
                        && c_->implication(a, b))
                      return a;
                    return b;
                  }
                // if b => a, then a M (b M c) = b M c
                if (b.is(op::M) && c_->implication(b[0], a))
                  return b;
                // if a => b, then a M (b M c) = a M c
                // if a => b, then a M (b R c) = a M c
                if (b.is(op::M, op::R) && c_->implication(a, b[0]))
                  return recurse(formula::M(a, b[1]));
                // if b => a, then (a R c) M b = c M b
                // if b => a, then (a M c) M b = c M b
                if (a.is(op::R, op::M) && c_->implication(b, a[0]))
                  return recurse(formula::M(a[1], b));
                // if c => b, then (a M c) M b = (a & c) M b
                if (a.is(op::M) && c_->implication(a[1], b))
                  return
                    recurse(formula::M(formula::And(a[0], a[1]),
                                       b));
                // if h => g, then (f&g) M h = f M h
                if (a.is(op::And))
                  {
                    unsigned n = a.size();
                    for (unsigned child = 0; child < n; ++child)
                      if (c_->implication(b, a[child]))
                        return recurse(formula::M(a.all_but(child), b));
                  }
                break;

              default:
                break;
              }
            trace << "bo: no inclusion-based rules matched" << std::endl;
          }

        if (!opt_.reduce_basics)
          {
            trace << "bo: basic reductions disabled" << std::endl;
            return bo;
          }

        trace << "bo: trying basic reductions" << std::endl;
        // Rewrite U,R,W,M as F or G when possible.
        // true U b == F(b)
        if (bo.is(op::U) && a.is_tt())
          return recurse(formula::F(b));
        // false R b == G(b)
        if (bo.is(op::R) && a.is_ff())
          return recurse(formula::G(b));
        // a W false == G(a)
        if (bo.is(op::W) && b.is_ff())
          return recurse(formula::G(a));
        // a M true == F(a)
        if (bo.is(op::M) && b.is_tt())
          return recurse(formula::F(a));

        if (bo.is(op::W, op::M) || bo.is(op::U, op::R))
          {
            // X(a) U X(b) = X(a U b)
            // X(a) R X(b) = X(a R b)
            // X(a) W X(b) = X(a W b)
            // X(a) M X(b) = X(a M b)
            if (a.is(op::X) && b.is(op::X))
              return recurse(formula::X(formula::binop(o,
                                                       a[0], b[0])));

            if (bo.is(op::U, op::W))
              {
                // a U Ga = Ga
                // a W Ga = Ga
                if (b.is(op::G) && a == b[0])
                  return b;
                // a U (b | c | G(a)) = a W (b | c)
                // a W (b | c | G(a)) = a W (b | c)
                if (b.is(op::Or))
                  for (int i = 0, s = b.size(); i < s; ++i)
                    {
                      formula c = b[i];
                      if (c.is(op::G) && c[0] == a)
                        return recurse(formula::W(a, b.all_but(i)));
                    }
                // a U (b & a & c) == (b & c) M a
                // a W (b & a & c) == (b & c) R a
                if (b.is(op::And))
                  for (int i = 0, s = b.size(); i < s; ++i)
                    if (b[i] == a)
                      return recurse(formula::binop(o == op::U ?
                                                    op::M : op::R,
                                                    b.all_but(i), a));
                // If b is Boolean:
                // (Xc) U b = b | X(b M c)
                // (Xc) W b = b | X(b R c)
                if (!opt_.reduce_size_strictly
                    && a.is(op::X) && b.is_boolean())
                  {
                    formula x = formula::X(formula::binop(o == op::U ?
                                                          op::M : op::R,
                                                          b, a[0]));
                    return recurse(formula::Or(b, x));
                  }
              }
            else if (bo.is(op::M, op::R))
              {
                // a R Fa = Fa
                // a M Fa = Fa
                if (b.is(op::F) && b[0] == a)
                  return b;

                // a R (b & c & F(a)) = a M (b & c)
                // a M (b & c & F(a)) = a M (b & c)
                if (b.is(op::And))
                  for (int i = 0, s = b.size(); i < s; ++i)
                    {
                      formula c = b[i];
                      if (c.is(op::F) && c[0] == a)
                        return recurse(formula::M(a, b.all_but(i)));
                    }
                // a M (b | a | c) == (b | c) U a
                // a R (b | a | c) == (b | c) W a
                if (b.is(op::Or))
                  for (int i = 0, s = b.size(); i < s; ++i)
                    if (b[i] == a)
                      return recurse(formula::binop(o == op::M ?
                                                    op::U : op::W,
                                                    b.all_but(i), a));
                // If b is Boolean:
                // (Xc) R b = b & X(b W c)
                // (Xc) M b = b & X(b U c)
                if (!opt_.reduce_size_strictly
                    && a.is(op::X) && b.is_boolean())
                  {
                    formula x =
                      formula::X(formula::binop(o == op::M ? op::U : op::W,
                                                b, a[0]));
                    return recurse(formula::And(b, x));
                  }
              }
          }
        if (bo.is(op::UConcat) || bo.is(op::EConcat, op::EConcatMarked))
          return reduce_sere_ltl(bo);
        return bo;
      }

      formula
        visit_multop(formula mo)
      {
        auto recurse = [this](formula f)
          {
            return simplify_recursively(f, c_);
          };

        unsigned mos = mo.size();

        if ((opt_.synt_impl | opt_.containment_checks)
            && mo.is(op::Or, op::And)
            && (opt_.containment_max_ops == 0
                || opt_.containment_max_ops >= mos))
          {
            bool is_and = mo.is(op::And);
            // Do not merge these two loops, as rewritings from the
            // second loop could prevent rewritings from the first one
            // to trigger.
            for (unsigned i = 0; i < mos; ++i)
              {
                formula fi = mo[i];
                formula fo = mo.all_but(i);
                // if fi => !fo, then fi & fo = false
                // if fo => !fi, then fi & fo = false
                // if !fi => fo, then fi | fo = true
                // if !fo => fi, then fi | fo = true
                if (c_->implication_neg(fi, fo, is_and)
                    || c_->implication_neg(fo, fi, is_and))
                  return recurse(is_and ? formula::ff() : formula::tt());
              }
            for (unsigned i = 0; i < mos; ++i)
              {
                formula fi = mo[i];
                formula fo = mo.all_but(i);
                // if fi => fo, then fi | fo = fo
                // if fo => fi, then fi & fo = fo
                if (((!is_and) && c_->implication(fi, fo))
                    || (is_and && c_->implication(fo, fi)))
                  {
                    // We are about to pick fo, but hold on!
                    // Maybe we actually have fi <=> fo, in
                    // which case we could decide to work on fi or fo.
                    //
                    // As a heuristic, let's return the smallest
                    // subformula.  So we only need to check this
                    // other implication if fi is smaller than fo,
                    // otherwise we don't care.
                    if ((length_boolone(fi) < length_boolone(fo))
                        && ((mo.is(op::Or) && c_->implication(fo, fi))
                            || (mo.is(op::And) && c_->implication(fi, fo))))
                      return recurse(fi);
                    else
                      return recurse(fo);
                  }
              }
          }

        vec res;
        res.reserve(mos);
        for (auto f: mo)
          res.emplace_back(f);
        op o = mo.kind();

        // basics reduction do not concern Boolean formulas,
        // so don't waste time trying to apply them.
        if (opt_.reduce_basics && !mo.is_boolean())
          {
            switch (o)
              {
              case op::And:
                assert(!mo.is_sere_formula());
                {
                  // a & X(G(a&b...) & c...) = Ga & X(G(b...) & c...)
                  // a & (Xa W b) = b R a
                  // a & (Xa U b) = b M a
                  // a & (b | X(b R a)) = b R a
                  // a & (b | X(b M a)) = b M a
                  if (!mo.is_syntactic_stutter_invariant()) // Skip if no X.
                    {
                      typedef std::unordered_set<formula> fset_t;
                      typedef robin_hood::unordered_node_map
                        <formula, std::set<unsigned>> fmap_t;
                      fset_t xgset; // XG(...)
                      fset_t xset;  // X(...)
                      fmap_t wuset; // (X...)W(...) or (X...)U(...)

                      std::vector<bool> tokill(mos);

                      // Make a pass to search for subterms
                      // of the form XGa or  X(... & G(...&a&...) & ...)
                      for (unsigned n = 0; n < mos; ++n)
                        {
                          if (!res[n])
                            continue;
                          if (res[n].is_syntactic_stutter_invariant())
                            continue;

                          if (formula xarg = is_XWU(res[n]))
                            {
                              wuset[xarg].insert(n);
                              continue;
                            }

                          // Now we are looking for
                          // - X(...)
                          // - b | X(b R ...)
                          // - b | X(b M ...)
                          if (formula barg = is_bXbRM(res[n]))
                            {
                              wuset[barg[1]].insert(n);
                              continue;
                            }

                          if (!res[n].is(op::X))
                            continue;

                          formula c = res[n][0];
                          auto handle_G = [&xgset](formula c)
                            {
                              formula a2 = c[0];
                              if (a2.is(op::And))
                                for (auto c: a2)
                                  xgset.insert(c);
                              else
                                xgset.insert(a2);
                            };

                          if (c.is(op::G))
                            {
                              handle_G(c);
                            }
                          else if (c.is(op::And))
                            {
                              for (auto cc: c)
                                if (cc.is(op::G))
                                  handle_G(cc);
                                else
                                  xset.insert(cc);
                            }
                          else
                            {
                              xset.insert(c);
                            }
                          res[n] = nullptr;
                        }
                      // Make a second pass to check if the "a"
                      // terms can be used to simplify "Xa W b",
                      // "Xa U b", "b | X(b R a)", or "b | X(b M a)".
                      vec resorig(res);
                      for (unsigned n = 0; n < mos; ++n)
                        {
                          formula x = resorig[n];
                          if (!x)
                            continue;
                          fmap_t::const_iterator gs = wuset.find(x);
                          if (gs == wuset.end())
                            continue;

                          for (unsigned pos: gs->second)
                            {
                              formula wu = resorig[pos];
                              if (wu.is(op::W, op::U))
                                {
                                  // a & (Xa W b) = b R a
                                  // a & (Xa U b) = b M a
                                  op t = wu.is(op::U) ? op::M : op::R;
                                  assert(wu[0].is(op::X));
                                  formula a = wu[0][0];
                                  formula b = wu[1];
                                  res[pos] = formula::binop(t, b, a);
                                }
                              else
                                {
                                  // a & (b | X(b R a)) = b R a
                                  // a & (b | X(b M a)) = b M a
                                  wu = is_bXbRM(resorig[pos]);
                                  assert(wu);
                                  res[pos] = wu;
                                }
                              // Remember to kill "a".
                              tokill[n] = true;
                            }
                        }

                      // Make third pass to search for terms 'a'
                      // that also appears as 'XGa'.  Replace them
                      // by 'Ga' and delete XGa.
                      for (unsigned n = 0; n < mos; ++n)
                        {
                          formula x = res[n];
                          if (!x)
                            continue;
                          fset_t::const_iterator g = xgset.find(x);
                          if (g != xgset.end())
                            {
                              // x can appear only once.
                              formula gf = *g;
                              xgset.erase(g);
                              res[n] = formula::G(x);
                            }
                          else if (tokill[n])
                            {
                              res[n] = nullptr;
                            }
                        }

                      vec xv;
                      unsigned xgs = xgset.size();
                      xv.reserve(xset.size() + 1);
                      if (xgs > 0)
                        {
                          vec xgv;
                          xgv.reserve(xgs);
                          for (auto f: xgset)
                            xgv.emplace_back(f);
                          xv.emplace_back(unop_multop(op::G, op::And, xgv));
                        }
                      for (auto f: xset)
                        xv.emplace_back(f);
                      res.emplace_back(unop_multop(op::X, op::And, xv));
                    }

                  // Gather all operands by type.
                  mospliter s(mospliter::Strip_X |
                              mospliter::Strip_FG |
                              mospliter::Strip_G |
                              mospliter::Split_F |
                              mospliter::Split_U_or_W |
                              mospliter::Split_R_or_M |
                              mospliter::Split_EventUniv,
                              res, c_);

                  // FG(a) & FG(b) = FG(a & b)
                  formula allFG = unop_unop_multop(op::F, op::G, op::And,
                                                   std::move(*s.res_FG));
                  // Xa & Xb = X(a & b)
                  // Xa & Xb & FG(c) = X(a & b & FG(c))
                  // For Universal&Eventual formulae f1...fn we also have:
                  // Xa & Xb & f1...fn = X(a & b & f1...fn)
                  if (!s.res_X->empty() && !opt_.favor_event_univ)
                    {
                      s.res_X->emplace_back(allFG);
                      allFG = nullptr;
                      s.res_X->insert(s.res_X->begin(),
                                      s.res_EventUniv->begin(),
                                      s.res_EventUniv->end());
                    }
                  else
                    // If f1...fn are event&univ formulae, with at least
                    // one formula of the form G(...),
                    // Rewrite  g & f1...fn  as  g & G(f1..fn) while
                    // stripping any leading G from f1...fn.
                    // This gathers eventual&universal formulae
                    // under the same term.
                    {
                      vec eu;
                      bool seen_g = false;
                      for (auto f: *s.res_EventUniv)
                        if (f.is(op::G))
                          {
                            seen_g = true;
                            eu.emplace_back(f[0]);
                          }
                        else
                          {
                            eu.emplace_back(f);
                          }
                      if (seen_g)
                        {
                          eu.emplace_back(allFG);
                          allFG = nullptr;
                          formula andeu = formula::multop(op::And, eu);
                          if (!opt_.favor_event_univ)
                            s.res_G->emplace_back(andeu);
                          else
                            s.res_other->emplace_back(formula::G(andeu));
                        }
                      else
                        {
                          s.res_other->insert(s.res_other->end(),
                                              eu.begin(), eu.end());
                        }
                    }

                  // Xa & Xb & f1...fn = X(a & b & f1...fn)
                  // is built at the end of this op::And case.
                  // G(a) & G(b) = G(a & b)
                  // is built at the end of this op::And case.

                  // The following three loops perform these rewritings:
                  // (a U b) & (c U b) = (a & c) U b
                  // (a U b) & (c W b) = (a & c) U b
                  // (a W b) & (c W b) = (a & c) W b
                  // (a R b) & (a R c) = a R (b & c)
                  // (a R b) & (a M c) = a M (b & c)
                  // (a M b) & (a M c) = a M (b & c)
                  // F(a) & (a R b) = a M b
                  // F(a) & (a M b) = a M b
                  // F(b) & (a W b) = a U b
                  // F(b) & (a U b) = a U b
                  // F(c) & G(phi | e) = c M (phi | e)  if c => !phi.
                  typedef robin_hood::unordered_map<formula,
                                                    vec::iterator> fmap_t;
                  fmap_t uwmap; // associates "b" to "a U b" or "a W b"
                  fmap_t rmmap; // associates "a" to "a R b" or "a M b"
                  // (a U b) & (c U b) = (a & c) U b
                  // (a U b) & (c W b) = (a & c) U b
                  // (a W b) & (c W b) = (a & c) W b
                  for (auto i = s.res_U_or_W->begin();
                       i != s.res_U_or_W->end(); ++i)
                    {
                      formula b = (*i)[1];
                      auto j = uwmap.find(b);
                      if (j == uwmap.end())
                        {
                          // First occurrence.
                          uwmap[b] = i;
                          continue;
                        }
                      // We already have one occurrence.  Merge them.
                      formula old = *j->second;
                      op o = op::W;
                      if (i->is(op::U) || old.is(op::U))
                        o = op::U;
                      formula fst_arg = formula::And(old[0], (*i)[0]);
                      *j->second = formula::binop(o, fst_arg, b);
                      assert(j->second->is(o));
                      *i = nullptr;
                    }
                  // (a R b) & (a R c) = a R (b & c)
                  // (a R b) & (a M c) = a M (b & c)
                  // (a M b) & (a M c) = a M (b & c)
                  for (auto i = s.res_R_or_M->begin();
                       i != s.res_R_or_M->end(); ++i)
                    {
                      formula a = (*i)[0];
                      auto j = rmmap.find(a);
                      if (j == rmmap.end())
                        {
                          // First occurrence.
                          rmmap[a] = i;
                          continue;
                        }
                      // We already have one occurrence.  Merge them.
                      formula old = *j->second;
                      op o = op::R;
                      if (i->is(op::M) || old.is(op::M))
                        o = op::M;
                      formula snd_arg = formula::And(old[1], (*i)[1]);
                      *j->second = formula::binop(o, a, snd_arg);
                      assert(j->second->is(o));
                      *i = nullptr;
                    }
                  // F(b) & (a W b) = a U b
                  // F(b) & (a U b) = a U b
                  // F(a) & (a R b) = a M b
                  // F(a) & (a M b) = a M b
                  for (auto& f: *s.res_F)
                    {
                      bool superfluous = false;
                      formula c = f[0];

                      fmap_t::iterator j = uwmap.find(c);
                      if (j != uwmap.end())
                        {
                          superfluous = true;
                          formula bo = *j->second;
                          if (bo.is(op::W))
                            {
                              *j->second = formula::U(bo[0], bo[1]);
                              assert(j->second->is(op::U));
                            }
                        }
                      j = rmmap.find(c);
                      if (j != rmmap.end())
                        {
                          superfluous = true;
                          formula bo = *j->second;
                          if (bo.is(op::R))
                            {
                              *j->second = formula::M(bo[0], bo[1]);
                              assert(j->second->is(op::M));
                            }
                        }
                      if (opt_.synt_impl | opt_.containment_checks)
                        {
                          // if the input looks like o1|u1|u2|o2,
                          // return o1 | o2.  The input must have at
                          // least on universal formula.
                          auto extract_not_un =
                            [&](formula f) {
                              if (f.is(op::Or))
                                for (auto u: f)
                                  if (u.is_universal())
                                    {
                                      vec phi;
                                      phi.reserve(f.size());
                                      for (auto uu: f)
                                        if (!uu.is_universal())
                                          phi.push_back(uu);
                                      return formula::Or(phi);
                                    }
                              return formula(nullptr);
                            };

                          // F(c) & G(phi | e) = c M (phi | e)  if  c => !phi.
                          for (auto in_g = s.res_G->begin();
                               in_g != s.res_G->end();)
                            {
                              if (formula phi = extract_not_un(*in_g))
                                if (c_->implication_neg(phi, c, true))
                                  {
                                    s.res_other->push_back(formula::M(c,
                                                                      *in_g));
                                    in_g = s.res_G->erase(in_g);
                                    superfluous = true;
                                    continue;
                                  }
                              ++in_g;
                            }
                        }
                      if (superfluous)
                        f = nullptr;
                    }

                  s.res_other->reserve(s.res_other->size()
                                       + s.res_F->size()
                                       + s.res_U_or_W->size()
                                       + s.res_R_or_M->size()
                                       + 3);
                  s.res_other->insert(s.res_other->end(),
                                      s.res_F->begin(),
                                      s.res_F->end());
                  s.res_other->insert(s.res_other->end(),
                                      s.res_U_or_W->begin(),
                                      s.res_U_or_W->end());
                  s.res_other->insert(s.res_other->end(),
                                      s.res_R_or_M->begin(),
                                      s.res_R_or_M->end());

                  // Those "G" formulae that are eventual can be
                  // postponed inside the X term if there is one.
                  //
                  // In effect we rewrite
                  //   Xa&Xb&GFc&GFd&Ge as X(a&b&G(Fc&Fd))&Ge
                  if (!s.res_X->empty() && !opt_.favor_event_univ)
                    {
                      vec event;
                      for (auto& f: *s.res_G)
                        if (f.is_eventual())
                          {
                            event.emplace_back(f);
                            f = nullptr; // Remove it from res_G.
                          }
                      s.res_X->emplace_back(unop_multop(op::G, op::And,
                                                     std::move(event)));
                    }

                  // G(a) & G(b) & ... = G(a & b & ...)
                  formula allG = unop_multop(op::G, op::And,
                                             std::move(*s.res_G));
                  // Xa & Xb & ... = X(a & b & ...)
                  formula allX = unop_multop(op::X, op::And,
                                             std::move(*s.res_X));
                  s.res_other->emplace_back(allX);
                  s.res_other->emplace_back(allG);
                  s.res_other->emplace_back(allFG);
                  formula r = formula::And(std::move(*s.res_other));
                  // If we altered the formula in some way, process
                  // it another time.
                  if (r != mo)
                    return recurse(r);
                  return r;
                }
              case op::AndRat:
                {
                  mospliter s(mospliter::Split_Bool, res, c_);
                  if (!s.res_Bool->empty())
                    {
                      // b1 & b2 & b3 = b1 ∧ b2 ∧ b3
                      formula b = formula::And(std::move(*s.res_Bool));

                      vec ares;
                      for (auto& f: *s.res_other)
                        switch (f.kind())
                          {
                          case op::Star:
                            // b && r[*i..j] = b & r  if i<=1<=j
                            //               = 0      otherwise
                            if (f.min() > 1 || f.max() < 1)
                              return formula::ff();
                            ares.emplace_back(f[0]);
                            f = nullptr;
                            break;
                          case op::Fusion:
                            // b && {r1:..:rn} = b && r1 && .. && rn
                            for (auto ri: f)
                              ares.emplace_back(ri);
                            f = nullptr;
                            break;
                          case op::Concat:
                            // b && {r1;...;rn} =
                            // - b && ri if there is only one ri
                            //           that does not accept [*0]
                            // - b && (r1|...|rn) if all ri
                            //           do not accept [*0]
                            // - 0 if more than one ri accept [*0]
                            {
                              formula ri = nullptr;
                              unsigned nonempty = 0;
                              unsigned rs = f.size();
                              for (unsigned j = 0; j < rs; ++j)
                                {
                                  formula jf = f[j];
                                  if (!jf.accepts_eword())
                                    {
                                      ri = jf;
                                      ++nonempty;
                                    }
                                }
                              if (nonempty == 1)
                                {
                                  ares.emplace_back(ri);
                                }
                              else if (nonempty == 0)
                                {
                                  vec sum;
                                  for (auto j: f)
                                    sum.emplace_back(j);
                                  ares.emplace_back(formula::OrRat(sum));
                                }
                              else
                                {
                                  return formula::ff();
                                }
                              f = nullptr;
                              break;
                            }
                          default:
                            ares.emplace_back(f);
                            f = nullptr;
                            break;
                          }
                      ares.emplace_back(b);
                      auto r = formula::AndRat(std::move(ares));
                      // If we altered the formula in some way, process
                      // it another time.
                      if (r != mo)
                        return recurse(r);
                      return r;
                    }
                  // No Boolean as argument of &&.

                  // Look for occurrences of {b;r} or {b:r}.  We have
                  // {b1;r1}&&{b2;r2} = {b1&&b2};{r1&&r2}
                  //                     head1    tail1
                  // {b1:r1}&&{b2:r2} = {b1&&b2}:{r1&&r2}
                  //                     head2    tail2
                  vec head1;
                  vec tail1;
                  vec head2;
                  vec tail2;
                  for (auto& i: *s.res_other)
                    {
                      if (!i)
                        continue;
                      if (!i.is(op::Concat, op::Fusion))
                        continue;
                      formula h = i[0];
                      if (!h.is_boolean())
                        continue;
                      if (i.is(op::Concat))
                        {
                          head1.emplace_back(h);
                          tail1.emplace_back(i.all_but(0));
                        }
                      else // op::Fusion
                        {
                          head2.emplace_back(h);
                          tail2.emplace_back(i.all_but(0));
                        }
                      i = nullptr;
                    }
                  if (!head1.empty())
                    {
                      formula h = formula::And(std::move(head1));
                      formula t = formula::AndRat(std::move(tail1));
                      s.res_other->emplace_back(formula::Concat(h, t));
                    }
                  if (!head2.empty())
                    {
                      formula h = formula::And(std::move(head2));
                      formula t = formula::AndRat(std::move(tail2));
                      s.res_other->emplace_back(formula::Fusion(h, t));
                    }

                  // {r1;b1}&&{r2;b2} = {r1&&r2};{b1∧b2}
                  //                     head3    tail3
                  // {r1:b1}&&{r2:b2} = {r1&&r2}:{b1∧b2}
                  //                     head4    tail4
                  vec head3;
                  vec tail3;
                  vec head4;
                  vec tail4;
                  for (auto& i: *s.res_other)
                    {
                      if (!i)
                        continue;
                      if (!i.is(op::Concat, op::Fusion))
                        continue;
                      unsigned s = i.size() - 1;
                      formula t = i[s];
                      if (!t.is_boolean())
                        continue;
                      if (i.is(op::Concat))
                        {
                          tail3.emplace_back(t);
                          head3.emplace_back(i.all_but(s));
                        }
                      else // op::Fusion
                        {
                          tail4.emplace_back(t);
                          head4.emplace_back(i.all_but(s));
                        }
                      i = nullptr;
                    }
                  if (!head3.empty())
                    {
                      formula h = formula::AndRat(std::move(head3));
                      formula t = formula::And(std::move(tail3));
                      s.res_other->emplace_back(formula::Concat(h, t));
                    }
                  if (!head4.empty())
                    {
                      formula h = formula::AndRat(std::move(head4));
                      formula t = formula::And(std::move(tail4));
                      s.res_other->emplace_back(formula::Fusion(h, t));
                    }

                  auto r = formula::AndRat(std::move(*s.res_other));
                  // If we altered the formula in some way, process
                  // it another time.
                  if (r != mo)
                    return recurse(r);
                  return r;
                }
              case op::Or:
                {
                  // a | X(F(a) | c...) = Fa | X(c...)
                  // a | (Xa R b) = b W a
                  // a | (Xa M b) = b U a
                  // a | (b & X(b W a)) = b W a
                  // a | (b & X(b U a)) = b U a
                  if (!mo.is_syntactic_stutter_invariant()) // Skip if no X
                    {
                      typedef std::unordered_set<formula> fset_t;
                      typedef robin_hood::unordered_node_map
                        <formula, std::set<unsigned>> fmap_t;
                      fset_t xfset; // XF(...)
                      fset_t xset;  // X(...)
                      fmap_t rmset; // (X...)R(...) or (X...)M(...) or
                      // b & X(b W ...) or b & X(b U ...)

                      std::vector<bool> tokill(mos);

                      // Make a pass to search for subterms
                      // of the form XFa or  X(... | F(...|a|...) | ...)
                      for (unsigned n = 0; n < mos; ++n)
                        {
                          if (!res[n])
                            continue;
                          if (res[n].is_syntactic_stutter_invariant())
                            continue;

                          if (formula xarg = is_XRM(res[n]))
                            {
                              rmset[xarg].insert(n);
                              continue;
                            }

                          // Now we are looking for
                          // - X(...)
                          // - b & X(b W ...)
                          // - b & X(b U ...)
                          if (formula barg = is_bXbWU(res[n]))
                            {
                              rmset[barg[1]].insert(n);
                              continue;
                            }

                          if (!res[n].is(op::X))
                            continue;

                          formula c = res[n][0];

                          auto handle_F = [&xfset](formula c)
                            {
                              formula a2 = c[0];
                              if (a2.is(op::Or))
                                for (auto c: a2)
                                  xfset.insert(c);
                              else
                                xfset.insert(a2);
                            };

                          if (c.is(op::F))
                            {
                              handle_F(c);
                            }
                          else if (c.is(op::Or))
                            {
                              for (auto cc: c)
                                if (cc.is(op::F))
                                  handle_F(cc);
                                else
                                  xset.insert(cc);
                            }
                          else
                            {
                              xset.insert(c);
                            }
                          res[n] = nullptr;
                        }
                      // Make a second pass to check if we can
                      // remove all instance of XF(a).
                      unsigned allofthem = xfset.size();
                      vec resorig(res);
                      for (unsigned n = 0; n < mos; ++n)
                        {
                          formula x = resorig[n];
                          if (!x)
                            continue;
                          fset_t::const_iterator f = xfset.find(x);
                          if (f != xfset.end())
                            --allofthem;
                          assert(allofthem != -1U);
                          // At the same time, check if "a" can also
                          // be used to simplify "Xa R b", "Xa M b".
                          // "b & X(b W a)", or "b & X(b U a)".
                          fmap_t::const_iterator gs = rmset.find(x);
                          if (gs == rmset.end())
                            continue;
                          for (unsigned pos: gs->second)
                            {
                              formula rm = resorig[pos];
                              if (rm.is(op::M, op::R))
                                {
                                  // a | (Xa R b) = b W a
                                  // a | (Xa M b) = b U a
                                  op t = rm.is(op::M) ? op::U : op::W;
                                  assert(rm[0].is(op::X));
                                  formula a = rm[0][0];
                                  formula b = rm[1];
                                  res[pos] = formula::binop(t, b, a);
                                }
                              else
                                {
                                  // a | (b & X(b W a)) = b W a
                                  // a | (b & X(b U a)) = b U a
                                  rm = is_bXbWU(resorig[pos]);
                                  assert(rm);
                                  res[pos] = rm;
                                }
                              // Remember to kill "a".
                              tokill[n] = true;
                            }
                        }

                      // If we can remove all of them...
                      if (allofthem == 0)
                        // Make third pass to search for terms 'a'
                        // that also appears as 'XFa'.  Replace them
                        // by 'Fa' and delete XFa.
                        for (unsigned n = 0; n < mos; ++n)
                          {
                            formula x = res[n];
                            if (!x)
                              continue;
                            fset_t::const_iterator f = xfset.find(x);
                            if (f != xfset.end())
                              {
                                // x can appear only once.
                                formula ff = *f;
                                xfset.erase(f);
                                res[n] = formula::F(x);
                                // We don't need to kill "a" anymore.
                                tokill[n] = false;
                              }
                          }
                      // Kill any remaining "a", used to simplify Xa R b
                      // or Xa M b.
                      for (unsigned n = 0; n < mos; ++n)
                        if (tokill[n] && res[n])
                          res[n] = nullptr;

                      // Now rebuild the formula that remains.
                      vec xv;
                      size_t xfs = xfset.size();
                      xv.reserve(xset.size() + 1);
                      if (xfs > 0)
                        {
                          // Group all XF(a)|XF(b|c|...)|... as XF(a|b|c|...)
                          vec xfv;
                          xfv.reserve(xfs);
                          for (auto f: xfset)
                            xfv.emplace_back(f);
                          xv.emplace_back(unop_multop(op::F, op::Or, xfv));
                        }
                      // Also gather the remaining Xa | X(b|c) as X(b|c).
                      for (auto f: xset)
                        xv.emplace_back(f);
                      res.emplace_back(unop_multop(op::X, op::Or, xv));
                    }

                  // Gather all operand by type.
                  mospliter s(mospliter::Strip_X |
                              mospliter::Strip_GF |
                              mospliter::Strip_F |
                              mospliter::Split_G |
                              mospliter::Split_U_or_W |
                              mospliter::Split_R_or_M |
                              mospliter::Split_EventUniv,
                              res, c_);
                  // GF(a) | GF(b) = GF(a | b)
                  formula allGF = unop_unop_multop(op::G, op::F, op::Or,
                                                   std::move(*s.res_GF));

                  bool eu_has_F = false;
                  for (auto f: *s.res_EventUniv)
                    if (f.is(op::F))
                      eu_has_F = true;

                  // Xa | Xb = X(a | b)
                  // Xa | Xb | GF(c) = X(a | b | GF(c))
                  // For Universal&Eventual formula f1...fn we also have:
                  // Xa | Xb | f1...fn = X(a | b | f1...fn)
                  if (!s.res_X->empty() && !opt_.favor_event_univ)
                    {
                      s.res_X->emplace_back(allGF);
                      allGF = nullptr;
                      s.res_X->insert(s.res_X->end(),
                                      s.res_EventUniv->begin(),
                                      s.res_EventUniv->end());
                    }
                  else if (!opt_.favor_event_univ
                           && (!s.res_F->empty() || eu_has_F)
                           && s.res_G->empty()
                           && s.res_U_or_W->empty()
                           && s.res_R_or_M->empty()
                           && s.res_other->empty())
                    {
                      // If there is no X but some F and only
                      // eventual&universal formulae f1...fn|GF(c), do:
                      // Fa|Fb|f1...fn|GF(c) = F(a|b|f1...fn|GF(c))
                      //
                      // The reasoning here is that if we should
                      // move f1...fn|GF(c) inside the "F" only
                      // if it allows us to move all terms under F,
                      // allowing a nice initial self-loop.
                      //
                      // For instance:
                      //   F(a|GFb)  3st.6tr. with initial self-loop
                      //   Fa|GFb    4st.8tr. without initial self-loop
                      //
                      // However, if other terms are presents they will
                      // prevent the formation of a self-loop, and the
                      // rewriting is unwelcome:
                      //   F(a|GFb)|Gc  5st.11tr.  without initial self-loop
                      //   Fa|GFb|Gc    5st.10tr.  without initial self-loop
                      // (counting the number of "subtransitions"
                      // or, degeneralizing the automaton amplifies
                      // these differences)
                      s.res_F->emplace_back(allGF);
                      allGF = nullptr;
                      for (auto f: *s.res_EventUniv)
                        s.res_F->emplace_back(f.is(op::F) ? f[0] : f);
                    }
                  else if (opt_.favor_event_univ)
                    {
                      s.res_EventUniv->emplace_back(allGF);
                      allGF = nullptr;
                      bool seen_f = false;
                      if (s.res_EventUniv->size() > 1)
                        {
                          // If some of the EventUniv formulas start
                          // with an F, Gather them all under the
                          // same F.  Striping any leading F.
                          for (auto& f: *s.res_EventUniv)
                            if (f.is(op::F))
                              {
                                seen_f = true;
                                f = f[0];
                              }
                          if (seen_f)
                            {
                              formula eu =
                                unop_multop(op::F, op::Or,
                                            std::move(*s.res_EventUniv));
                              s.res_other->emplace_back(eu);
                            }
                        }
                      if (!seen_f)
                        s.res_other->insert(s.res_other->end(),
                                            s.res_EventUniv->begin(),
                                            s.res_EventUniv->end());
                    }
                  else
                    {
                      for (auto f: *s.res_EventUniv)
                        {
                          if (f.is(op::F))
                            s.res_F->emplace_back(f[0]);
                          else
                            s.res_other->emplace_back(f);
                        }
                    }
                  // Xa | Xb | f1...fn = X(a | b | f1...fn)
                  // is built at the end of this multop::Or case.
                  // F(a) | F(b) = F(a | b)
                  // is built at the end of this multop::Or case.

                  // The following three loops perform these rewritings:
                  // (a U b) | (a U c) = a U (b | c)
                  // (a W b) | (a U c) = a W (b | c)
                  // (a W b) | (a W c) = a W (b | c)
                  // (a R b) | (c R b) = (a | c) R b
                  // (a R b) | (c M b) = (a | c) R b
                  // (a M b) | (c M b) = (a | c) M b
                  // G(a) | (a U b) = a W b
                  // G(a) | (a W b) = a W b
                  // G(b) | (a R b) = a R b.
                  // G(b) | (a M b) = a R b.
                  // G(c) | F(phi & e) = c W (phi & e)  if  !c => phi.
                  typedef robin_hood::unordered_map<formula,
                                                    vec::iterator> fmap_t;
                  fmap_t uwmap; // associates "a" to "a U b" or "a W b"
                  fmap_t rmmap; // associates "b" to "a R b" or "a M b"
                  // (a U b) | (a U c) = a U (b | c)
                  // (a W b) | (a U c) = a W (b | c)
                  // (a W b) | (a W c) = a W (b | c)
                  for (auto i = s.res_U_or_W->begin();
                       i != s.res_U_or_W->end(); ++i)
                    {
                      formula a = (*i)[0];
                      auto j = uwmap.find(a);
                      if (j == uwmap.end())
                        {
                          // First occurrence.
                          uwmap[a] = i;
                          continue;
                        }
                      // We already have one occurrence.  Merge them.
                      formula old = *j->second;
                      op o = op::U;
                      if (i->is(op::W) || old.is(op::W))
                        o = op::W;
                      formula snd_arg = formula::Or(old[1], (*i)[1]);
                      *j->second = formula::binop(o, a, snd_arg);
                      assert(j->second->is(o));
                      *i = nullptr;
                    }
                  // (a R b) | (c R b) = (a | c) R b
                  // (a R b) | (c M b) = (a | c) R b
                  // (a M b) | (c M b) = (a | c) M b
                  for (auto i = s.res_R_or_M->begin();
                       i != s.res_R_or_M->end(); ++i)
                    {
                      formula b = (*i)[1];
                      auto j = rmmap.find(b);
                      if (j == rmmap.end())
                        {
                          // First occurrence.
                          rmmap[b] = i;
                          continue;
                        }
                      // We already have one occurrence.  Merge them.
                      formula old = *j->second;
                      op o = op::M;
                      if (i->is(op::R) || old.is(op::R))
                        o = op::R;
                      formula fst_arg = formula::Or(old[0], (*i)[0]);
                      *j->second = formula::binop(o, fst_arg, b);
                      assert(j->second->is(o));
                      *i = nullptr;
                    }
                  // G(a) | (a U b) = a W b
                  // G(a) | (a W b) = a W b
                  // G(b) | (a R b) = a R b.
                  // G(b) | (a M b) = a R b.
                  for (auto& f: *s.res_G)
                    {
                      bool superfluous = false;
                      formula c = f[0];

                      fmap_t::iterator j = uwmap.find(c);
                      if (j != uwmap.end())
                        {
                          superfluous = true;
                          formula bo = *j->second;
                          if (bo.is(op::U))
                            {
                              *j->second = formula::W(bo[0], bo[1]);
                              assert(j->second->is(op::W));
                            }
                        }
                      j = rmmap.find(c);
                      if (j != rmmap.end())
                        {
                          superfluous = true;
                          formula bo = *j->second;
                          if (bo.is(op::M))
                            {
                              *j->second = formula::R(bo[0], bo[1]);
                              assert(j->second->is(op::R));
                            }
                        }
                      if (opt_.synt_impl | opt_.containment_checks)
                        {
                          // if the input looks like o1&e1&e2&o2,
                          // return o1 & o2.  The input must have at
                          // least on eventual formula.
                          auto extract_not_ev =
                            [&](formula f) {
                              if (f.is(op::And))
                                for (auto e: f)
                                  if (e.is_eventual())
                                    {
                                      vec phi;
                                      phi.reserve(f.size());
                                      for (auto ee: f)
                                        if (!ee.is_eventual())
                                          phi.push_back(ee);
                                      return formula::And(phi);
                                    }
                              return formula(nullptr);
                            };

                          // G(c) | F(phi & e) = c W (phi & e)  if  !c => phi.
                          for (auto in_f = s.res_F->begin();
                               in_f != s.res_F->end();)
                            {
                              if (formula phi = extract_not_ev(*in_f))
                                if (c_->implication_neg(c, phi, false))
                                  {
                                    s.res_other->push_back(formula::W(c,
                                                                      *in_f));
                                    in_f = s.res_F->erase(in_f);
                                    superfluous = true;
                                    continue;
                                  }
                              ++in_f;
                            }
                        }
                      if (superfluous)
                        f = nullptr;
                    }

                  s.res_other->reserve(s.res_other->size()
                                       + s.res_G->size()
                                       + s.res_U_or_W->size()
                                       + s.res_R_or_M->size()
                                       + 3);
                  s.res_other->insert(s.res_other->end(),
                                      s.res_G->begin(),
                                      s.res_G->end());
                  s.res_other->insert(s.res_other->end(),
                                      s.res_U_or_W->begin(),
                                      s.res_U_or_W->end());
                  s.res_other->insert(s.res_other->end(),
                                      s.res_R_or_M->begin(),
                                      s.res_R_or_M->end());

                  // Those "F" formulae that are universal can be
                  // postponed inside the X term if there is one.
                  //
                  // In effect we rewrite
                  //   Xa|Xb|FGc|FGd|Fe as X(a|b|F(Gc|Gd))|Fe
                  if (!s.res_X->empty())
                    {
                      vec univ;
                      for (auto& f: *s.res_F)
                        if (f.is_universal())
                          {
                            univ.emplace_back(f);
                            f = nullptr; // Remove it from res_F.
                          }
                      s.res_X->emplace_back(unop_multop(op::F, op::Or,
                                                     std::move(univ)));
                    }

                  // F(a) | F(b) | ... = F(a | b | ...)
                  formula allF = unop_multop(op::F, op::Or,
                                             std::move(*s.res_F));
                  // Xa | Xb | ... = X(a | b | ...)
                  formula allX = unop_multop(op::X, op::Or,
                                             std::move(*s.res_X));
                  s.res_other->emplace_back(allX);
                  s.res_other->emplace_back(allF);
                  s.res_other->emplace_back(allGF);
                  formula r = formula::Or(std::move(*s.res_other));
                  // If we altered the formula in some way, process
                  // it another time.
                  if (r != mo)
                    return recurse(r);
                  return r;
                }
              case op::AndNLM:
                {
                  mospliter s(mospliter::Split_Bool, res, c_);
                  if (!s.res_Bool->empty())
                    {
                      // b1 & b2 & b3 = b1 ∧ b2 ∧ b3
                      formula b = formula::And(std::move(*s.res_Bool));

                      // now we just consider  b & rest
                      formula rest = formula::AndNLM(std::move(*s.res_other));

                      // We have  b & rest = b : rest  if rest does not
                      // accept [*0]. Otherwise  b & rest = b | (b : rest)
                      // FIXME: It would be nice to remove [*0] from rest.
                      formula r = nullptr;
                      if (rest.accepts_eword())
                        {
                          // The b & rest = b | (b : rest) rewriting
                          // augment the size, so do that only when
                          // explicitly requested.
                          if (!opt_.reduce_size_strictly)
                            return recurse(formula::OrRat
                                           (b, formula::Fusion(b, rest)));
                          else
                            return mo;
                        }
                      else
                        {
                          return recurse(formula::Fusion(b, rest));
                        }
                    }
                  // No Boolean as argument of &&.

                  // Look for occurrences of {b;r} or {b:r}.  We have
                  // {b1;r1}&{b2;r2} = {b1∧b2};{r1&r2}
                  //                    head1   tail1
                  // {b1:r1}&{b2:r2} = {b1∧b2}:{r1&r2}
                  //                    head2   tail2
                  // BEWARE: The second rule is correct only when
                  // both r1 and r2 do not accept [*0].

                  vec head1;
                  vec tail1;
                  vec head2;
                  vec tail2;
                  for (auto& i: *s.res_other)
                    {
                      if (!i)
                        continue;
                      if (!i.is(op::Concat, op::Fusion))
                        continue;
                      formula h = i[0];
                      if (!h.is_boolean())
                        continue;
                      if (i.is(op::Concat))
                        {
                          head1.emplace_back(h);
                          tail1.emplace_back(i.all_but(0));
                        }
                      else // op::Fusion
                        {
                          formula t = i.all_but(0);
                          if (t.accepts_eword())
                            continue;
                          head2.emplace_back(h);
                          tail2.emplace_back(t);
                        }
                      i = nullptr;
                    }
                  if (!head1.empty())
                    {
                      formula h = formula::And(std::move(head1));
                      formula t = formula::AndNLM(std::move(tail1));
                      s.res_other->emplace_back(formula::Concat(h, t));
                    }
                  if (!head2.empty())
                    {
                      formula h = formula::And(std::move(head2));
                      formula t = formula::AndNLM(std::move(tail2));
                      s.res_other->emplace_back(formula::Fusion(h, t));
                    }

                  formula r = formula::AndNLM(std::move(*s.res_other));
                  // If we altered the formula in some way, process
                  // it another time.
                  if (r != mo)
                    return recurse(r);
                  return r;
                }
              case op::OrRat:
              case op::Concat:
              case op::Fusion:
                // FIXME: No simplifications yet.
                return mo;
              default:
                SPOT_UNIMPLEMENTED();
                return nullptr;
              }
            SPOT_UNREACHABLE();
          }
        return mo;
      }

    protected:
      tl_simplifier_cache* c_;
      const tl_simplifier_options& opt_;
    };


    formula
    simplify_recursively(formula f,
                         tl_simplifier_cache* c)
    {
#ifdef TRACE
      static int srec = 0;
      for (int i = srec; i; --i)
        trace << ' ';
      trace << "** simplify_recursively(" << str_psl(f) << ')';
#endif

      formula result = c->lookup_simplified(f);
      if (result)
        {
          trace << " cached: " << str_psl(result) << std::endl;
          return result;
        }
      else
        {
          trace << " miss" << std::endl;
        }

#ifdef TRACE
      ++srec;
#endif

      if (f.is_boolean() && c->options.boolean_to_isop)
        {
          result = c->boolean_to_isop(f);
        }
      else
        {
          simplify_visitor v(c);
          result = v.visit(f);
        }

#ifdef TRACE
      --srec;
      for (int i = srec; i; --i)
        trace << ' ';
      trace << "** simplify_recursively(" << str_psl(f) << ") result: "
            << str_psl(result) << std::endl;
#endif

      c->cache_simplified(f, result);
      return result;
    }

  } // anonymous namespace

    //////////////////////////////////////////////////////////////////////
    // tl_simplifier_cache


    // This implements the recursive rules for syntactic implication.
    // (To follow this code please look at the table given as an
    // appendix in the documentation for temporal logic operators.)
  inline
  bool
  tl_simplifier_cache::syntactic_implication_aux(formula f, formula g)
  {
    // We first process all lines from the table except the
    // first two, and then we process the first two as a fallback.
    //
    // However for Boolean formulas we skip the bottom lines
    // (keeping only the first one) to prevent them from being
    // further split.
    if (!f.is_boolean())
      // Deal with all lines of the table except the first two.
      switch (f.kind())
        {
        case op::X:
        case op::strong_X:
          if (g.is_eventual() && syntactic_implication(f[0], g))
            return true;
          if (g.is(op::X, op::strong_X) && syntactic_implication(f[0], g[0]))
            return true;
          break;

        case op::F:
          if (g.is_eventual() && syntactic_implication(f[0], g))
            return true;
          break;

        case op::G:
          if (g.is(op::U, op::R) && syntactic_implication(f[0], g[1]))
            return true;
          if (g.is(op::W) && (syntactic_implication(f[0], g[0])
                              || syntactic_implication(f[0], g[1])))
            return true;
          if (g.is(op::M) && (syntactic_implication(f[0], g[0])
                              && syntactic_implication(f[0], g[1])))
            return true;
          // First column.
          if (syntactic_implication(f[0], g))
            return true;
          break;

        case op::U:
          {
            formula f1 = f[0];
            formula f2 = f[1];
            if (g.is(op::U, op::W)
                && syntactic_implication(f1, g[0])
                && syntactic_implication(f2, g[1]))
              return true;
            if (g.is(op::M, op::R)
                && syntactic_implication(f1, g[1])
                && syntactic_implication(f2, g[0])
                && syntactic_implication(f2, g[1]))
              return true;
            if (g.is(op::F) && syntactic_implication(f2, g[0]))
              return true;
            // First column.
            if (syntactic_implication(f1, g) && syntactic_implication(f2, g))
              return true;
            break;
          }
        case op::W:
          {
            formula f1 = f[0];
            formula f2 = f[1];
            if (g.is(op::U) && (syntactic_implication(f1, g[1])
                                && syntactic_implication(f2, g[1])))
              return true;
            if (g.is(op::W) && (syntactic_implication(f1, g[0])
                                && syntactic_implication(f2, g[1])))
              return true;
            if (g.is(op::R) && (syntactic_implication(f1, g[1])
                                && syntactic_implication(f2, g[0])
                                && syntactic_implication(f2, g[1])))
              return true;
            if (g.is(op::F) && (syntactic_implication(f1, g[0])
                                && syntactic_implication(f2, g[0])))
              return true;
            // First column.
            if (syntactic_implication(f1, g) && syntactic_implication(f2, g))
              return true;
            break;
          }
        case op::R:
          {
            formula f1 = f[0];
            formula f2 = f[1];
            if (g.is(op::W) && (syntactic_implication(f1, g[1])
                                && syntactic_implication(f2, g[0])))
              return true;
            if (g.is(op::R) && (syntactic_implication(f1, g[0])
                                && syntactic_implication(f2, g[1])))
              return true;
            if (g.is(op::M) && (syntactic_implication(f2, g[0])
                                && syntactic_implication(f2, g[1])))
              return true;
            if (g.is(op::F) && syntactic_implication(f2, g[0]))
              return true;
            // First column.
            if (syntactic_implication(f2, g))
              return true;
            break;
          }
        case op::M:
          {
            formula f1 = f[0];
            formula f2 = f[1];
            if (g.is(op::U, op::W) && (syntactic_implication(f1, g[1])
                                       && syntactic_implication(f2,
                                                                g[0])))
              return true;
            if (g.is(op::R, op::M) && (syntactic_implication(f1, g[0])
                                       && syntactic_implication(f2,
                                                                g[1])))
              return true;
            if (g.is(op::F) && (syntactic_implication(f1, g[0])
                                || syntactic_implication(f2, g[0])))
              return true;
            // First column.
            if (syntactic_implication(f2, g))
              return true;
            break;
          }
        case op::Or:
          {
            // If we are checking something like
            //   (a | b | Xc) => g,
            // split it into
            //   (a | b) => g
            //   Xc      => g
            unsigned i = 0;
            if (formula bops = f.boolean_operands(&i))
              if (!syntactic_implication(bops, g))
                break;
            bool b = true;
            unsigned fs = f.size();
            for (; i < fs; ++i)
              if (!syntactic_implication(f[i], g))
                {
                  b = false;
                  break;
                }
            if (b)
              return true;
            break;
          }
        case op::And:
          {
            // If we are checking something like
            //   (a & b & Xc) => g,
            // split it into
            //   (a & b) => g
            //   Xc      => g
            unsigned i = 0;
            if (formula bops = f.boolean_operands(&i))
              if (syntactic_implication(bops, g))
                return true;
            unsigned fs = f.size();
            for (; i < fs; ++i)
              if (syntactic_implication(f[i], g))
                return true;
            break;
          }
        default:
          break;
        }
    // First two lines of the table.
    // (Don't check equality, it has already be done.)
    if (!g.is_boolean())
      switch (g.kind())
        {
        case op::F:
          if (syntactic_implication(f, g[0]))
            return true;
          break;

        case op::G:
        case op::X:
        case op::strong_X:
          if (f.is_universal() && syntactic_implication(f, g[0]))
            return true;
          break;

        case op::U:
        case op::W:
          if (syntactic_implication(f, g[1]))
            return true;
          break;

        case op::M:
        case op::R:
          if (syntactic_implication(f, g[0])
              && syntactic_implication(f, g[1]))
            return true;
          break;

        case op::And:
          {
            // If we are checking something like
            //   f => (a & b & Xc),
            // split it into
            //   f => (a & b)
            //   f => Xc
            unsigned i = 0;
            if (formula bops = g.boolean_operands(&i))
              if (!syntactic_implication(f, bops))
                break;
            bool b = true;
            unsigned gs = g.size();
            for (; i < gs; ++i)
              if (!syntactic_implication(f, g[i]))
                {
                  b = false;
                  break;
                }
            if (b)
              return true;
            break;
          }

        case op::Or:
          {
            // If we are checking something like
            //   f => (a | b | Xc),
            // split it into
            //   f => (a | b)
            //   f => Xc
            unsigned i = 0;
            if (formula bops = g.boolean_operands(&i))
              if (syntactic_implication(f, bops))
                return true;
            unsigned gs = g.size();
            for (; i < gs; ++i)
              if (syntactic_implication(f, g[i]))
                return true;
            break;
          }
        default:
          break;
        }
    return false;
  }

  // Return true if f => g syntactically
  bool
  tl_simplifier_cache::syntactic_implication(formula f,
                                              formula g)
  {
    // We cannot run syntactic_implication on SERE formulae,
    // except on Boolean formulae.
    if (f.is_sere_formula() && !f.is_boolean())
      return false;
    if (g.is_sere_formula() && !g.is_boolean())
      return false;

    if (f == g)
      return true;
    if (g.is_tt() || f.is_ff())
      return true;
    if (g.is_ff() || f.is_tt())
      return false;

    // Often we compare a literal (an atomic_prop or its negation)
    // to another literal.  The result is necessarily false. To be
    // true, the two literals would have to be equal, but we have
    // already checked that.
    if (f.is_literal() && g.is_literal())
      return false;

    // Cache lookup
    {
      pairf p(f, g);
      syntimpl_cache_t::const_iterator i = syntimpl_.find(p);
      if (i != syntimpl_.end())
        return i->second;
    }

    bool result;

    if (f.is_boolean() && g.is_boolean())
      {
        bdd l = as_bdd(f);
        result = bdd_implies(l, as_bdd(g));
      }
    else
      {
        result = syntactic_implication_aux(f, g);
      }

    // Cache result
    {
      pairf p(f, g);
      syntimpl_[p] = result;
      // std::cerr << str_psl(f) << (result ? " ==> " : " =/=> ")
      //           << str_psl(g) << std::endl;
    }

    return result;
  }

  // If right==false, true if !f1 => f2, false otherwise.
  // If right==true, true if f1 => !f2, false otherwise.
  bool
  tl_simplifier_cache::syntactic_implication_neg(formula f1,
                                                  formula f2,
                                                  bool right)
  {
    // We cannot run syntactic_implication_neg on SERE formulae,
    // except on Boolean formulae.
    if (f1.is_sere_formula() && !f1.is_boolean())
      return false;
    if (f2.is_sere_formula() && !f2.is_boolean())
      return false;
    if (right)
      f2 = nenoform_rec(f2, true, this, false);
    else
      f1 = nenoform_rec(f1, true, this, false);
    return syntactic_implication(f1, f2);
  }


  /////////////////////////////////////////////////////////////////////
  // tl_simplifier

  tl_simplifier::tl_simplifier(const bdd_dict_ptr& d)
  {
    cache_ = new tl_simplifier_cache(d);
  }

  tl_simplifier::tl_simplifier(const tl_simplifier_options& opt,
                                 bdd_dict_ptr d)
  {
    cache_ = new tl_simplifier_cache(d, opt);
  }

  tl_simplifier::~tl_simplifier()
  {
    delete cache_;
  }

  formula
  tl_simplifier::simplify(formula f)
  {
    if (!f.is_in_nenoform())
      f = negative_normal_form(f, false);
    return simplify_recursively(f, cache_);
  }

  tl_simplifier_options&
  tl_simplifier::options()
  {
    return cache_->options;
  }

  formula
  tl_simplifier::negative_normal_form(formula f, bool negated)
  {
    return nenoform_rec(f, negated, cache_, false);
  }

  bool
  tl_simplifier::syntactic_implication(formula f1, formula f2)
  {
    return cache_->syntactic_implication(f1, f2);
  }

  bool
  tl_simplifier::syntactic_implication_neg(formula f1,
                                            formula f2, bool right)
  {
    return cache_->syntactic_implication_neg(f1, f2, right);
  }

  bool
  tl_simplifier::are_equivalent(formula f, formula g)
  {
    return cache_->lcc.equal(f, g);
  }

  bool
  tl_simplifier::implication(formula f, formula g)
  {
    return cache_->lcc.contained(f, g);
  }

  bdd
  tl_simplifier::as_bdd(formula f)
  {
    return cache_->as_bdd(f);
  }

  formula
  tl_simplifier::star_normal_form(formula f)
  {
    return cache_->star_normal_form(f);
  }

  formula
  tl_simplifier::boolean_to_isop(formula f)
  {
    return cache_->boolean_to_isop(f);
  }

  bdd_dict_ptr
  tl_simplifier::get_dict() const
  {
    return cache_->dict;
  }

  void
  tl_simplifier::print_stats(std::ostream& os) const
  {
    cache_->print_stats(os);
  }

  void
  tl_simplifier::clear_as_bdd_cache()
  {
    cache_->clear_as_bdd_cache();
    cache_->lcc.clear();
  }

  void
  tl_simplifier::clear_caches()
  {
    tl_simplifier_cache* c =
      new tl_simplifier_cache(get_dict(), cache_->options);
    std::swap(c, cache_);
    delete c;
  }


  formula normalize_quantifiers(formula f)
  {
    if (!f.is(op::exists, op::forall))
      return f;

    std::vector<unsigned char> polarities;
    std::vector<formula> assignments;
    assignments.resize(formula::apid_count(), nullptr);

    auto rec = [&](formula g, auto self) -> formula
    {
      if (!g.is_quantified())
        {
          polarities = collect_apids_with_polarities(g);
          return g;
        }
      // A variable can be removed from the quantification
      // - if it is not present in the formula
      // - if it has constant polarity below the quantifier
      //   (in that case it should be replaced by a constant
      //   in the formula).
      unsigned sz = g.size();
      formula body = self(g[sz - 1], self);
      std::vector<formula> v;
      v.reserve(sz - 1);
      for (unsigned i = 0; i < sz - 1; ++i)
        {
          formula h = g[i];
          unsigned id = h.apid();
          unsigned pol = polarities[id];
          if (pol == 0b00)
            {
              continue;
            }
          else if (pol == 0b11)
            {
              v.emplace_back(h);
            }
          else
            {
              bool is_positive = pol == 0b10;
              bool is_exists = (g.kind() == op::exists);
              assignments[id] = (is_positive == is_exists)
                ? formula::tt() : formula::ff();
            }
          // if the atomic proposition is kept (in v)
          // or will be replaced by a constant (in assignments)
          // then we can remove it from outer quantifiers.
          polarities[id] = 0;
        }
      return formula::quantify(g.kind(), v, body);
    };

    formula f2 = rec(f, rec);
    if (!assignments.empty())
      f2 = relabel_apply(f2, assignments);
    if (f2 != f)
      return normalize_quantifiers(f2);
    else
      return f;
  }
}
