// -*- 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 <spot/misc/common.hh>
#include <spot/tl/formula.hh>
#include <iostream>
#include <sstream>
#include <map>
#include <set>
#include <cstring>
#include <string_view>
#include <algorithm>
#include <spot/misc/bareword.hh>
#include <spot/tl/print.hh>
#include <spot/priv/robin_hood.hh>
#include <spot/misc/hashfunc.hh>

#ifndef HAVE_STRVERSCMP
// If the libc does not have this, a version is compiled in lib/.
extern "C" int strverscmp(const char *s1, const char *s2);
#endif

namespace spot
{
  namespace
  {
    typedef std::vector<const fnode*> vec;

    [[noreturn]] static void
    report_repetition_overflow(unsigned val)
    {
      std::ostringstream s;
      s << val << " exceeds maximum supported repetition ("
        << (fnode::unbounded()-1) << ')';
      throw std::overflow_error(s.str());
    }

    [[noreturn]] static void
    report_swapped_minmax(unsigned min, unsigned max)
    {
      std::ostringstream s;
      s << "range " << min << ".." << max << " looks reversed (min>max)";
      throw std::overflow_error(s.str());
    }

    // Compare two formulas, by looking at their operators and
    // children.  This does not use id for the top-level operator,
    // because it is used to decide whether to reuse an equal existing
    // formula.
    struct formula_equal
    {
      bool operator()(const fnode* l, const fnode* r) const
      {
        op opl = l->kind();
        op opr = r->kind();
        if (opl != opr)
          return false;

        if (SPOT_UNLIKELY(opl == op::Star || opl == op::FStar))
          {
            {
              auto minl = l->min();
              auto minr = r->min();
              if (minl != minr)
                return false;
            }
            {
              auto maxl = l->max();
              auto maxr = r->max();
              if (maxl != maxr)
                return false;
            }
          }
        else
          {
            auto szl = l->size();
            auto szr = r->size();
            if (szl != szr)
              return false;
          }

        auto el = l->end();
        auto ir = r->begin();
        for (auto il = l->begin(); il != el; ++il, ++ir)
          if (*il != *ir)
            return false;

        return true;
      }
    };

    struct formula_hash
    {
      size_t operator()(const fnode* f) const
      {
        op o = f->kind();
        size_t h = fnv<size_t>::init;
        h ^= static_cast<size_t>(o);
        h *= fnv<size_t>::prime;
        if (SPOT_UNLIKELY(o == op::Star || o == op::FStar))
          {
            h ^= f->min();
            h *= fnv<size_t>::prime;
          }
        for (auto child: *f)
          {
            h ^= static_cast<size_t>(child->id());
            h *= fnv<size_t>::prime;
          }
        return h;
      }
    };

    struct maps_t final
    {
      // Each formula is uniquely represented by the pointer given in this
      // hash table.
      robin_hood::unordered_set<const fnode*, formula_hash, formula_equal> uniq;

      // Atomic propositions are stored differently from the rest of
      // other nodes.  The field fnode::ap_id_ is used as an index
      // into apid2name to retrieve a pair (U, S) where S points to
      // the name2ap entry, and U should be 0 as long as this atomic
      // proposition is used.
      //
      // Additionally name2ap can be used to associate a name to the
      // corresponding fnode*.
      //
      // free_apid, and the first field of the apid2name element (U)
      // are used to maintain a linked list of unused apid2name
      // entries, So that they can be recycled to declare new APs.  If
      // free_apid or U is 0, the list is empty.  Otherwise, they
      // designate entry at position free_apid-1 or U-1 as the next free
      // entry.
      robin_hood::unordered_node_map<std::string, const fnode*> name2ap;
      std::vector<std::pair<unsigned,
                            decltype(name2ap)::value_type*>> apid2name;
      unsigned free_apid = 0;
      unsigned free_apid_count = 0;
    };
    static maps_t m;

    static void
    gather_bool(vec& v, op o)
    {
      // Gather all boolean terms.
      vec b;
      vec::iterator i = v.begin();
      while (i != v.end())
        {
          if ((*i)->is_boolean())
            {
              b.emplace_back(*i);
              i = v.erase(i);
            }
          else
            {
              ++i;
            }
        }
      // - AndNLM(Exps1...,Bool1,Exps2...,Bool2,Exps3...) =
      //    AndNLM(And(Bool1,Bool2),Exps1...,Exps2...,Exps3...)
      // - AndRat(Exps1...,Bool1,Exps2...,Bool2,Exps3...) =
      //    AndRat(And(Bool1,Bool2),Exps1...,Exps2...,Exps3...)
      // - OrRat(Exps1...,Bool1,Exps2...,Bool2,Exps3...) =
      //    OrRat(Or(Bool1,Bool2),Exps1...,Exps2...,Exps3...)
      if (!b.empty())
        v.insert(v.begin(), fnode::multop(o, std::move(b)));
    }
  }

  const fnode* fnode::unique(fnode* f)
  {
    auto ires = m.uniq.emplace(f);
    if (!ires.second)
      {
        //(*ires.first)->dump(std::cerr << "UNI: ") << '\n';
        for (auto c: *f)
          c->destroy();
        f->~fnode();
        ::operator delete(f);
        return (*ires.first)->clone();
      }
    //f->dump(std::cerr << "INS: ") << '\n';
    return f;
  }

  void
  fnode::destroy_aux() const
  {
    if (SPOT_UNLIKELY(is(op::ap)))
      {
        unsigned apid = ap_id_;
        auto& entry = m.apid2name[apid];
        auto n = m.name2ap.erase(entry.second->first);
        assert(n == 1);
        (void)n;
        ++m.free_apid_count;
        // If we cleared all variables, let's just reset the vector.
        if (SPOT_UNLIKELY(m.apid2name.size() == m.free_apid_count))
          {
            m.apid2name.clear();
            m.free_apid_count = m.free_apid = 0;
          }
        else if (SPOT_UNLIKELY(apid == m.apid2name.size() - 1))
          {
            m.apid2name.pop_back();
            --m.free_apid_count; // undo
          }
        else
          {
            entry.first = m.free_apid;
            entry.second = nullptr;
            m.free_apid = apid + 1;
          }
      }
    else
      {
        auto n = m.uniq.erase(this);
        assert(n == 1);
        (void)n;
        for (auto c: *this)
          c->destroy();
      }
    this->~fnode();
    ::operator delete(const_cast<fnode*>(this));
  }

  void
  fnode::report_non_existing_child()
  {
    throw std::runtime_error("access to non-existing child");
  }

  void
  fnode::report_too_many_children()
  {
    throw std::runtime_error("too many children for formula");
  }

  void
  fnode::report_get_child_of_expecting_single_child_node()
  {
    throw std::invalid_argument
      ("get_child_of() expecting single-child node");
  }

  void
  fnode::report_min_invalid_arg()
  {
    throw std::invalid_argument
      ("min() only works on Star and FStar nodes");
  }

  void
  fnode::report_max_invalid_arg()
  {
    throw std::invalid_argument
      ("min() only works on Star and FStar nodes");
  }

  void
  fnode::report_apid_on_nonap()
  {
    throw std::runtime_error("apid() called on non-AP formula");
  }

  void
  formula::report_ap_invalid_arg()
  {
    throw std::invalid_argument("atomic propositions cannot be "
                                "constructed from arbitrary formulas");
  }

  void
  formula::report_message(const char* message)
  {
    throw std::runtime_error(message);
  }



  std::string fnode::kindstr() const
  {
    switch (op_)
      {
#define C(x)                                        \
        case op::x:                                \
          return #x;                                \
          break
        C(ff);
        C(tt);
        C(eword);
        C(ap);
        C(Not);
        C(X);
        C(F);
        C(G);
        C(Closure);
        C(NegClosure);
        C(NegClosureMarked);
        C(Xor);
        C(Implies);
        C(Equiv);
        C(U);
        C(R);
        C(W);
        C(M);
        C(EConcat);
        C(EConcatMarked);
        C(UConcat);
        C(Or);
        C(OrRat);
        C(And);
        C(AndRat);
        C(AndNLM);
        C(Concat);
        C(Fusion);
        C(Star);
        C(FStar);
        C(first_match);
        C(strong_X);
        C(exists);
        C(forall);
#undef C
      }
    SPOT_UNREACHABLE();
  }


  const fnode*
  fnode::multop_sorted(op o, vec&& v)
  {
    unsigned orig_size = v.size();

    const fnode* neutral;       // neutral element
    const fnode* neutral2;      // second neutral element (if any)
    const fnode* abs;           // absorbent element
    const fnode* abs2;          // second absorbent element (if any)
    const fnode* weak_abs;      // almost absorbent element (if any)
    // The notion of "almost absorbent" captures situation where the
    // present of the element can be simplified in itself or another
    // element depending on a condition on the rest of the formula.
    switch (o)
      {
      case op::And:
        neutral = tt();
        neutral2 = nullptr;
        abs = ff();
        abs2 = nullptr;
        weak_abs = nullptr;
        break;
      case op::AndRat:
        neutral = one_star();
        {
          // If this AndRat contains an operand that does not accept
          // the empty word, and that is not [+], then any [+] can be
          // removed.
          bool one_non_eword_non_plus =
          std::find_if(v.begin(), v.end(),
                       [o = one_plus()](const fnode* f) {
                         return !f->accepts_eword() && f != o;
                       }) != v.end();
          neutral2 = one_non_eword_non_plus ? one_plus() : nullptr;
        }
        abs = ff();
        abs2 = nullptr;
        weak_abs = eword();
        gather_bool(v, op::And);
        break;
      case op::AndNLM:
        neutral = eword();
        neutral2 = nullptr;
        abs = ff();
        abs2 = nullptr;
        weak_abs = tt();
        gather_bool(v, op::And);
        break;
      case op::Or:
        neutral = ff();
        neutral2 = nullptr;
        abs = tt();
        abs2 = nullptr;
        weak_abs = nullptr;
        break;
      case op::OrRat:
        neutral = ff();
        {
          // If this OrRat contains an operand that accept [*0] but
          // isn't [*0], then any [+0] can be removed.
          bool eword_accepted =
            std::find_if(v.begin(), v.end(),
                         [](const fnode* f) {
                           return f->accepts_eword() && !f->is_eword();
                         }) != v.end();
          neutral2 = eword_accepted ? eword() : nullptr;
        }
        abs = one_star();
        abs2 = nullptr;
        weak_abs = one_plus();
        gather_bool(v, op::Or);
        break;
      case op::Concat:
        neutral = eword();
        neutral2 = nullptr;
        abs = ff();
        abs2 = nullptr;
        weak_abs = nullptr;

        // - Concat(Exps1...,FExps2...,1[*],FExps3...,Exps4) =
        //     Concat(Exps1...,1[*],Exps4)
        // If FExps2... and FExps3 all accept [*0].
        {
          vec::iterator i = v.begin();
          const fnode* os = one_star();
          while (i != v.end())
            {
              while (i != v.end() && !(*i)->accepts_eword())
                ++i;
              if (i == v.end())
                break;
              vec::iterator b = i;
              // b is the first expressions that accepts [*0].
              // let's find more, and locate the position of
              // 1[*] at the same time.
              bool os_seen = false;
              do
                {
                  os_seen |= (*i == os);
                  ++i;
                }
              while (i != v.end() && (*i)->accepts_eword());

              if (os_seen) // [b..i) is a range that contains [*].
                {
                  // Place [*] at the start of the range, and erase
                  // all other formulae.
                  (*b)->destroy();
                  *b++ = os->clone();
                  for (vec::iterator c = b; c < i; ++c)
                    (*c)->destroy();
                  i = v.erase(b, i);
                }
            }
        }
        break;
      case op::Fusion:
        neutral = tt();
        neutral2 = nullptr;
        abs = ff();
        abs2 = eword();
        weak_abs = nullptr;

        // Make a first pass to group adjacent Boolean formulae.
        // - Fusion(Exps1...,BoolExp1...BoolExpN,Exps2,Exps3...) =
        //   Fusion(Exps1...,And(BoolExp1...BoolExpN),Exps2,Exps3...)
        {
          vec::iterator i = v.begin();
          while (i != v.end())
            {
              if ((*i)->is_boolean())
                {
                  vec::iterator first = i;
                  ++i;
                  if (i == v.end())
                    break;
                  if (!(*i)->is_boolean())
                    {
                      ++i;
                      continue;
                    }
                  do
                    ++i;
                  while (i != v.end() && (*i)->is_boolean());
                  // We have at least two adjacent Boolean formulae.
                  // Replace the first one by the conjunction of all.
                  // FIXME: Investigate the removal of the temporary
                  // vector, by allowing range to be passed to
                  // multop() instead of entire containers.  We
                  // should then able to do
                  //   *first = multop(op::And, first, i);
                  //   i = v.erase(first + 1, i);
                  vec b;
                  b.insert(b.begin(), first, i);
                  i = v.erase(first + 1, i);
                  *first = multop(op::And, b);
                }
              else
                {
                  ++i;
                }
            }
        }

        break;

      default:
        neutral = nullptr;
        neutral2 = nullptr;
        abs = nullptr;
        abs2 = nullptr;
        weak_abs = nullptr;
        break;
      }

    // Remove duplicates (except for Concat and Fusion).  We can't use
    // std::unique(), because we must destroy() any formula we drop.
    // Also ignore neutral elements and handle absorbent elements.
    {
      const fnode* last = nullptr;
      vec::iterator i = v.begin();
      bool weak_abs_seen = false;
      while (i != v.end())
        {
          if ((*i == neutral) || (*i == neutral2) || (*i == last))
            {
              (*i)->destroy();
              i = v.erase(i);
            }
          else if (*i == abs || *i == abs2)
            {
              for (i = v.begin(); i != v.end(); ++i)
                (*i)->destroy();
              assert(abs);
              return abs->clone();
            }
          else
            {
              weak_abs_seen |= (*i == weak_abs);
              if (o != op::Concat && o != op::Fusion)
                // Don't remove duplicates
                last = *i;
              ++i;
            }
        }

      if (weak_abs_seen)
        {
          if (o == op::AndRat)
            {
              // We have    a* && [*0] && c  = 0
              //     and    a* && [*0] && c* = [*0]
              // So if [*0] has been seen, check if alls term
              // recognize the empty word.
              bool acc_eword = true;
              for (i = v.begin(); i != v.end(); ++i)
                {
                  acc_eword &= (*i)->accepts_eword();
                  (*i)->destroy();
                }
              if (acc_eword)
                return weak_abs;
              else
                return abs;
            }
          else if (o == op::AndNLM)
            {
              // Similarly,  a* & 1 & (c;d) = c;d
              //             a* & 1 & c* = 1
              vec tmp;
              for (auto i: v)
                {
                  if (i == weak_abs)
                    continue;
                  if (i->accepts_eword())
                    {
                      i->destroy();
                      continue;
                    }
                  tmp.emplace_back(i);
                }
              if (tmp.empty())
                tmp.emplace_back(weak_abs);
              v.swap(tmp);
            }
          else if (o == op::OrRat)
            {
              // We have    a[*] | [+] | c = [*]
              //     and    a | [+] | c = [+]
              // So if [+] has been seen, check if some term
              // recognize the empty word.
              bool acc_eword = false;
              for (i = v.begin(); i != v.end(); ++i)
                {
                  acc_eword |= (*i)->accepts_eword();
                  (*i)->destroy();
                }
              if (acc_eword)
                return abs;
              else
                return weak_abs;
            }
          else
            {
              SPOT_UNREACHABLE();
            }
        }
      else if (o == op::Concat || o == op::Fusion)
        {
          // Perform an extra loop to merge starable items.
          //   f;f -> f[*2]
          //   f;f[*i..j] -> f[*i+1..j+1]
          //   f[*i..j];f -> f[*i+1..j+1]
          //   f[*i..j];f[*k..l] -> f[*i+k..j+l]
          // same for FStar:
          //   f:f -> f[:*2]
          //   f:f[*i..j] -> f[:*i+1..j+1]
          //   f[:*i..j];f -> f[:*i+1..j+1]
          //   f[:*i..j];f[:*k..l] -> f[:*i+k..j+l]
          op bop = o == op::Concat ? op::Star : op::FStar;
          i = v.begin();
          while (i != v.end())
            {
              vec::iterator fpos = i;
              const fnode* f;
              unsigned min;
              unsigned max;
              bool changed = false;
              if ((*i)->is(bop))
                {
                  f = (*i)->nth(0);
                  min = (*i)->min();
                  max = (*i)->max();
                }
              else
                {
                  f = *i;
                  min = max = 1;
                }

              ++i;
              while (i != v.end())
                {
                  const fnode* f2;
                  unsigned min2;
                  unsigned max2;
                  if ((*i)->is(bop))
                    {
                      f2 = (*i)->nth(0);
                      if (f2 != f)
                        break;
                      min2 = (*i)->min();
                      max2 = (*i)->max();
                    }
                  else
                    {
                      f2 = *i;
                      if (f2 != f)
                        break;
                      min2 = max2 = 1;
                    }
                  if (min2 == unbounded())
                    {
                      min = unbounded();
                    }
                  else if (min != unbounded())
                    {
                      if (SPOT_UNLIKELY(min + min2 >= unbounded()))
                        break;
                      min += min2;
                    }
                  if (max2 == unbounded())
                    {
                      max = unbounded();
                    }
                  else if (max != unbounded())
                    {
                      if (SPOT_UNLIKELY(max + max2 >= unbounded()))
                        break;
                      max += max2;
                    }
                  (*i)->destroy();
                  i = v.erase(i);
                  changed = true;
                }
              if (changed)
                {
                  const fnode* newfs = bunop(bop, f->clone(), min, max);
                  (*fpos)->destroy();
                  *fpos = newfs;
                }
            }
          // also
          //   b[*i..j]:b -> b[*max(1,i),j]
          //   b:b[*i..j] -> b[*max(1,i),j]
          //   b[*i..j]:b[*k..l] -> b[*max(i,1)+max(j,1)-1,j+l-1]
          if (o == op::Fusion && v.size() > 1)
            {
              i = v.begin();
              while (i != v.end())
                {
                  if (!(((*i)->is(op::Star) && (*i)->nth(0)->is_boolean())
                        || (*i)->is_boolean()))
                    {
                      ++i;
                      continue;
                    }
                  const fnode *b;
                  unsigned min;
                  unsigned max;
                  if ((*i)->is_boolean())
                    {
                      min = max = 1;
                      b = *i;
                    }
                  else
                    {
                      b = (*i)->nth(0);
                      min = (*i)->min();
                      max = (*i)->max();
                    }
                  vec::iterator prev = i;
                  ++i;
                  bool changed = false;
                  while (i != v.end())
                    {
                      unsigned min2;
                      unsigned max2;
                      if ((*i)->is_boolean())
                        {
                          if (*i != b)
                            break;
                          min2 = max2 = 1;
                        }
                      else if ((*i)->is(op::Star) && (*i)->nth(0)->is_boolean())
                        {
                          if ((*i)->nth(0) != b)
                            break;
                          min2 = (*i)->min();
                          max2 = (*i)->max();
                        }
                      else
                        {
                          break;
                        }
                      // Now we can merge prev and i.
                      min = min + (min == 0) + min2 + (min2 == 0) - 1;
                      assert(max != 0 && max2 != 0);
                      if (max2 == unbounded() || max == unbounded())
                        max = unbounded();
                      else if (max + max2 < unbounded())
                        max = max + max2 - 1;
                      else
                        break;
                      changed = true;
                      (*i)->destroy();
                      i = v.erase(i);
                    }
                  if (changed)
                    {
                      const fnode* newf =
                        fnode::bunop(op::Star, b->clone(), min, max);
                      (*prev)->destroy();
                      *prev = newf;
                    }
                }
            }
        }
    }

    vec::size_type s = v.size();
    if (s == 0)
      {
        assert(neutral != nullptr);
        return neutral->clone();
      }
    else if (s == 1)
      {
        // Simply replace Multop(Op,X) by X.
        // Except we should never reduce the
        // arguments of a Fusion operator to
        // a list with a single formula that
        // accepts [*0].
        const fnode* res = v[0];
        if (o != op::Fusion || orig_size == 1
            || !res->accepts_eword())
          return res;
        // If Fusion(f, ...) reduces to Fusion(f), emit Fusion(1,f).
        // to ensure that [*0] is not accepted.
        v.insert(v.begin(), tt());
      }

    auto mem = ::operator new(sizeof(fnode)
                              + (v.size() - 1)*sizeof(*children));
    return unique(new(mem) fnode(o, v.begin(), v.end()));
  }

  template<op o>
  const fnode*
  fnode::multop_build_and_or(const fnode* left, const fnode* right)
  {
    bool liso = left->is(o);
    bool riso = right->is(o);
    formula_ptr_less_than_bool_first cmp;
    if (liso || riso)
      {
        if (!liso)              // insert left into the sorted right
          {
            if constexpr (o == op::And)
              {
                if (left->is_tt())
                  {
                    left->destroy();
                    return right;
                  }
                else if (left->is_ff())
                  {
                    right->destroy();
                    return left;
                  }
              }
            if constexpr (o == op::Or)
              {
                if (left->is_ff())
                  {
                    left->destroy();
                    return right;
                  }
                else if (left->is_tt())
                  {
                    right->destroy();
                    return left;
                  }
              }
            auto pos =
              std::lower_bound(right->begin(), right->end(), left, cmp);
            if (pos != right->end() && *pos == left) // already there
              {
                left->destroy();
                return right;
              }
            vec v;
            v.reserve(right->size() + 1);
            for (auto it = right->begin(); it != pos; ++it)
              v.emplace_back((*it)->clone());
            v.emplace_back(left);
            for (auto end = right->end(); pos != end; ++pos)
              v.emplace_back((*pos)->clone());
            right->destroy();

            auto mem = ::operator new(sizeof(fnode)
                                      + (v.size() - 1)*sizeof(*children));
            return unique(new(mem) fnode(o, v.begin(), v.end()));
          }
        if (!riso)              // insert right into the sorted left
          {
            if constexpr (o == op::And)
              {
                if (right->is_tt())
                  {
                    right->destroy();
                    return left;
                  }
                else if (right->is_ff())
                  {
                    left->destroy();
                    return right;
                  }
              }
            if constexpr (o == op::Or)
              {
                if (right->is_ff())
                  {
                    right->destroy();
                    return left;
                  }
                else if (right->is_tt())
                  {
                    left->destroy();
                    return right;
                  }
              }
            auto pos =
              std::lower_bound(left->begin(), left->end(), right, cmp);
            if (pos != left->end() && *pos == right) // already there
              {
                right->destroy();
                return left;
              }
            vec v;
            v.reserve(left->size() + 1);
            for (auto it = left->begin(); it != pos; ++it)
              v.emplace_back((*it)->clone());
            v.emplace_back(right);
            for (auto end = left->end(); pos != end; ++pos)
              v.emplace_back((*pos)->clone());
            left->destroy();

            auto mem = ::operator new(sizeof(fnode)
                                      + (v.size() - 1)*sizeof(*children));
            return unique(new(mem) fnode(o, v.begin(), v.end()));
          }

        // merge two sorted vectors, while cloning elements
        vec v;
        v.reserve(left->size() + right->size());
        auto lit = left->begin();
        auto rit = right->begin();
        auto lend = left->end();
        auto rend = right->end();
        while (lit != lend && rit != rend)
          {
            // duplicate elements can be removed
            if (SPOT_UNLIKELY(*lit == *rit))
              {
                v.emplace_back((*lit)->clone());
                ++lit;
                ++rit;
              }
            else if (cmp(*lit, *rit))
              {
                v.emplace_back((*lit++)->clone());
              }
            else
              {
                v.emplace_back((*rit++)->clone());
              }
          }
        while (lit != lend)
          v.emplace_back((*lit++)->clone());
        while (rit != rend)
          v.emplace_back((*rit++)->clone());
        left->destroy();
        right->destroy();

        auto mem = ::operator new(sizeof(fnode)
                                  + (v.size() - 1)*sizeof(*children));
        return unique(new(mem) fnode(o, v.begin(), v.end()));
      }
    if (left == right)
      {
        right->destroy();
        return left;
      }
    // neither left nor right is a Multop(o,...)
    if (cmp(right, left))
      std::swap(right, left);
    // if left or right was 0/1, then after the swap left is 0 or 1,
    // so we don't have to check both sides.
    if constexpr (o == op::And)
      {
        if (left->is_tt())
          {
            left->destroy();
            return right;
          }
        else if (left->is_ff())
          {
            right->destroy();
            return left;
          }
      }
    if constexpr (o == op::Or)
      {
        if (left->is_ff())
          {
            left->destroy();
            return right;
          }
        else if (left->is_tt())
          {
            right->destroy();
            return left;
          }
      }
    vec v{left, right};
    auto mem = ::operator new(sizeof(fnode)
                              + (v.size() - 1)*sizeof(*children));
    return unique(new(mem) fnode(o, v.begin(), v.end()));
  }

  // Work around the interdiction to use SPOT _ API in *.cc file.
  // We those explicit template instantiations to have public visibility,
  // but older versions of gcc will not pickup the visibility from their
  // template declaration.
  #define SPOT__API SPOT##_API

  template SPOT__API const fnode*
  fnode::multop_build_and_or<op::And>(const fnode* left, const fnode* right);

  template SPOT__API const fnode*
  fnode::multop_build_and_or<op::Or>(const fnode* left, const fnode* right);

  const fnode*
  fnode::multop(op o, const fnode* left, const fnode* right)
  {
    bool liso = left->is(o);
    bool riso = right->is(o);

    if (o == op::Concat || o == op::Fusion)
      {
        // Concat and Fusion are not commutative, so we must keep
        // the order of left and right.
        vec v;
        v.reserve((liso ? left->size() : 1) + (riso ? right->size() : 1));
        if (liso)
          {
            for (auto* c: *left)
              v.emplace_back(c->clone());
            left->destroy();
          }
        else
          {
            v.emplace_back(left);
          }
        if (riso)
          {
            for (auto* c: *right)
              v.emplace_back(c->clone());
            right->destroy();
          }
        else
          {
            v.emplace_back(right);
          }
        return multop_sorted(o, std::move(v));
      }

    formula_ptr_less_than_bool_first cmp;
    if (liso || riso)
      {
        if (!liso)              // insert left into the sorted right
          {
            auto pos =
              std::lower_bound(right->begin(), right->end(), left, cmp);
            if (pos != right->end() && *pos == left) // already there
              {
                left->destroy();
                return right;
              }
            vec v;
            v.reserve(right->size() + 1);
            for (auto it = right->begin(); it != pos; ++it)
              v.emplace_back((*it)->clone());
            v.emplace_back(left);
            for (auto end = right->end(); pos != end; ++pos)
              v.emplace_back((*pos)->clone());
            right->destroy();
            return multop_sorted(o, std::move(v));
          }
        if (!riso)              // insert right into the sorted left
          {
            auto pos =
              std::lower_bound(left->begin(), left->end(), right, cmp);
            if (pos != left->end() && *pos == right) // already there
              {
                right->destroy();
                return left;
              }
            vec v;
            v.reserve(left->size() + 1);
            for (auto it = left->begin(); it != pos; ++it)
              v.emplace_back((*it)->clone());
            v.emplace_back(right);
            for (auto end = left->end(); pos != end; ++pos)
              v.emplace_back((*pos)->clone());
            left->destroy();
            return multop_sorted(o, std::move(v));
          }

        // merge two sorted vectors, while cloning elements
        vec v;
        v.reserve(left->size() + right->size());
        auto lit = left->begin();
        auto rit = right->begin();
        auto lend = left->end();
        auto rend = right->end();
        while (lit != lend && rit != rend)
          {
            // duplicate elements can be removed
            if (SPOT_UNLIKELY(*lit == *rit))
              {
                v.emplace_back((*lit)->clone());
                ++lit;
                ++rit;
              }
            else if (cmp(*lit, *rit))
              {
                v.emplace_back((*lit++)->clone());
              }
            else
              {
                v.emplace_back((*rit++)->clone());
              }
          }
        while (lit != lend)
          v.emplace_back((*lit++)->clone());
        while (rit != rend)
          v.emplace_back((*rit++)->clone());
        left->destroy();
        right->destroy();
        return multop_sorted(o, std::move(v));
      }
    // neither left nor right is a Multop(o,...)
    if (left == right)
      {
        right->destroy();
        return left;
      }
    if (cmp(right, left))
      std::swap(right, left);
    vec v{left, right};
    return multop_sorted(o, std::move(v));
  }

  const fnode*
  fnode::multop(op o, vec v)
  {
    // Inline children of same kind.
    //
    // When we construct a formula such as Multop(Op,X,Multop(Op,Y,Z))
    // we will want to inline it as Multop(Op,X,Y,Z).
    //
    // At the same time, it's possible that vec contains some null
    // pointers we should remove.  We can do it in the same loop.
    //
    // It is simpler to construct a separate vector to do that, but that's
    // only needed if we have nested multops or null pointers.
    if (std::find_if(v.begin(), v.end(),
                     [o](const fnode* f) { return f == nullptr || f->is(o); })
        != v.end())
      {
        vec inlined;
        for (const fnode* f: v)
          {
            if (f == nullptr)
              continue;
            if (f->is(o))
              {
                for (auto* g: *f)
                  inlined.emplace_back(g->clone());
                f->destroy();
              }
            else
              {
                inlined.emplace_back(f);
              }
          }
        v.swap(inlined);
      }

    if (o != op::Concat && o != op::Fusion)
      {
        unsigned s = v.size();
        if (s > 2)
          {
            std::sort(v.begin(), v.end(), formula_ptr_less_than_bool_first());
          }
        else if (s == 2)
          {
            formula_ptr_less_than_bool_first cmp;
            if (cmp(v[1], v[0]))
              std::swap(v[1], v[0]);
          }
      }

    return multop_sorted(o, std::move(v));
  }

  const fnode*
  fnode::bunop(op o, const fnode* child, unsigned min, unsigned max)
  {
    if (SPOT_UNLIKELY(min >= unbounded()))
      report_repetition_overflow(min);
    // We are testing strict ">", because unbounded() is a legitimate
    // input for max.  We just cannot tell if it was really indented
    // as "unbounded".
    if (SPOT_UNLIKELY(max > unbounded()))
      report_repetition_overflow(max);
    if (SPOT_UNLIKELY(min > max))
      report_swapped_minmax(min, max);

    const fnode* neutral = nullptr;
    switch (o)
      {
      case op::Star:
        if (max == unbounded() && child == tt_)
          {
            // bypass normal construction: 1[*] and 1[+] are
            // frequently used, so they are not reference counted.
            if (min == 0)
              return one_star();
            if (min == 1)
              return one_plus();
          }

        neutral = eword();
        break;
      case op::FStar:
        neutral = tt();
        break;
      default:
        SPOT_UNREACHABLE();
      }


    // common trivial simplifications

    //   - [*0][*min..max] = [*0]
    //   - [*0][:*0..max] = 1
    //   - [*0][:*min..max] = 0 if min > 0
    if (child->is_eword())
      switch (o)
        {
        case op::Star:
          return neutral;
        case op::FStar:
          if (min == 0)
            return neutral;
          else
            return ff();
        default:
          SPOT_UNREACHABLE();
        }

    //   - 0[*0..max] = [*0]
    //   - 0[*min..max] = 0 if min > 0
    //   - b[:*0..max] = 1
    //   - b[:*min..max] = 0 if min > 0
    if (child->is_ff()
        || (o == op::FStar && child->is_boolean()))
      {
        if (min == 0)
          {
            child->destroy();
            return neutral;
          }
        return child;
      }

    //   - Exp[*0] = [*0]
    //   - Exp[:*0] = 1
    if (max == 0)
      {
        child->destroy();
        return neutral;
      }

    //   - Exp[*1] = Exp
    //   - Exp[:*1] = Exp if Exp does not accept [*0]
    if (min == 1 && max == 1)
      if (o == op::Star || !child->accepts_eword())
        return child;

    //   - Exp[*i..j][*k..l] = Exp[*ik..jl] if i*(k+1)<=jk+1.
    //   - Exp[:*i..j][:*k..l] = Exp[:*ik..jl] if i*(k+1)<=jk+1.
    if (child->is(o))
      {
        unsigned i = child->min();
        unsigned j = child->max();

        // Exp has to be true between i*min and j*min
        //               then between i*(min+1) and j*(min+1)
        //               ...
        //            finally between i*max and j*max
        //
        // We can merge these intervals into [i*min..j*max] iff the
        // first are adjacent or overlap, i.e. iff
        //   i*(min+1) <= j*min+1.
        // (Because i<=j, this entails that the other intervals also
        // overlap).

        const fnode* exp = child->nth(0);
        if (j == unbounded())
          {
            // cannot simplify if the bound overflows
            if ((min * i) < unbounded())
              {
                min *= i;
                max = unbounded();

                // Exp[*min..max]
                exp->clone();
                child->destroy();
                child = exp;
              }
          }
        else
          {
            if ((i * (min + 1) <= (j * min) + 1)
                // cannot simplify if the bound overflows
                && (min < unbounded()
                    && (max == unbounded()
                        || j == unbounded()
                        || max * j < unbounded())))
              {
                min *= i;
                if (max != unbounded())
                  {
                    if (j == unbounded())
                      max = unbounded();
                    else
                      max *= j;
                  }
                exp->clone();
                child->destroy();
                child = exp;
              }
          }
      }

    return unique(new fnode(o, child, min, max));
  }

  const fnode*
  fnode::unop(op o, const fnode* f)
  {
    // Some trivial simplifications.
    switch (o)
      {
      case op::F:
      case op::G:
        {
          // F and G are idempotent.
          if (f->is(o))
            return f;

          // F(0) = G(0) = 0
          // F(1) = G(1) = 1
          if (f->is_ff() || f->is_tt())
            return f;

          assert(!f->is_eword());
        }
        break;

      case op::Not:
        {
          // !1 = 0
          if (f->is_tt())
            return ff();
          // !0 = 1
          if (f->is_ff())
            return tt();
          // ![*0] = 1[+]
          if (f->is_eword())
            return one_plus();

          auto fop = f->kind();
          // "Not" is an involution.
          if (fop == o)
            {
              auto c = f->nth(0)->clone();
              f->destroy();
              return c;
            }
          // !Closure(Exp) = NegClosure(Exp)
          if (fop == op::Closure)
            {
              const fnode* c = unop(op::NegClosure, f->nth(0)->clone());
              f->destroy();
              return c;
            }
          // !NegClosure(Exp) = Closure(Exp)
          if (fop == op::NegClosure || fop == op::NegClosureMarked)
            {
              const fnode* c = unop(op::Closure, f->nth(0)->clone());
              f->destroy();
              return c;
            }
          break;
        }

      case op::X:
        // X(1) = 1
        if (f->is_tt())
          return f;
        // We do not have X(0)=0 because that
        // is not true with finite semantics.
        assert(!f->is_eword());
        break;
      case op::strong_X:
        // X[!](0) = 0
        if (f->is_ff())
          return f;
        // Note: with finite semantics X[!](1)≠1.
        assert(!f->is_eword());
        break;

      case op::Closure:
        // {0} = 0, {1} = 1,  {b} = b
        if (f->is_boolean())
          return f;
        // {[*0]} = 0
        if (f->is_eword())
          return ff();
        break;

      case op::NegClosure:
      case op::NegClosureMarked:
        // {1} = 0
        if (f->is_tt())
          return ff();
        // {0} = 1,  {[*0]} = 1
        if (f->is_ff() || f->is_eword())
          return tt();
        // {b} = !b
        if (f->is_boolean())
          return unop(op::Not, f);
        break;
      case op::first_match:
        // first_match(first_match(sere)) = first_match(sere);
        // first_match(b) = b
        if (f->is(o) || f->is_boolean())
          return f;
        // first_match(r*) = [*0]
        if (f->accepts_eword())
          {
            f->destroy();
            return eword();
          }
        break;
      default:
        SPOT_UNREACHABLE();
      }

    return unique(new fnode(o, {f}));
  }

  const fnode*
  fnode::binop(op o, const fnode* first, const fnode* second)
  {
    // Sort the operands of commutative operators, so that for
    // example the formula instance for 'a xor b' is the same as
    // that for 'b xor a'.

    // Trivial identities:
    switch (o)
      {
      case op::Xor:
        {
          // Xor is commutative: sort operands.
          formula_ptr_less_than_bool_first cmp;
          if (cmp(second, first))
            std::swap(second, first);
        }
        //   - (1 ^ Exp) = !Exp
        //   - (0 ^ Exp) = Exp
        if (first->is_tt())
          return unop(op::Not, second);
        if (first->is_ff())
          return second;
        if (first == second)
          {
            first->destroy();
            second->destroy();
            return ff();
          }
        // We expect constants to appear first, because they are
        // instantiated first.
        assert(!second->is_constant());
        break;
      case op::Equiv:
        {
          // Equiv is commutative: sort operands.
          formula_ptr_less_than_bool_first cmp;
          if (cmp(second, first))
            std::swap(second, first);
        }
        //   - (0 <=> Exp) = !Exp
        //   - (1 <=> Exp) = Exp
        //   - (Exp <=> Exp) = 1
        if (first->is_ff())
          return unop(op::Not, second);
        if (first->is_tt())
          return second;
        if (first == second)
          {
            first->destroy();
            second->destroy();
            return tt();
          }
        // We expect constants to appear first, because they are
        // instantiated first.
        assert(!second->is_constant());
        break;
      case op::Implies:
        //   - (1 => Exp) = Exp
        //   - (0 => Exp) = 1
        //   - (Exp => 1) = 1
        //   - (Exp => 0) = !Exp
        //   - (Exp => Exp) = 1
        if (first->is_tt())
          return second;
        if (first->is_ff())
          {
            second->destroy();
            return tt();
          }
        if (second->is_tt())
          {
            first->destroy();
            return second;
          }
        if (second->is_ff())
          return unop(op::Not, first);
        if (first == second)
          {
            first->destroy();
            second->destroy();
            return tt();
          }
        break;
      case op::U:
        //   - (Exp U 1) = 1
        //   - (Exp U 0) = 0
        //   - (0 U Exp) = Exp
        //   - (Exp U Exp) = Exp
        if (second->is_tt()
            || second->is_ff()
            || first->is_ff()
            || first == second)
          {
            first->destroy();
            return second;
          }
        break;
      case op::W:
        //   - (Exp W 1) = 1
        //   - (0 W Exp) = Exp
        //   - (1 W Exp) = 1
        //   - (Exp W Exp) = Exp
        if (second->is_tt()
            || first->is_ff()
            || first == second)
          {
            first->destroy();
            return second;
          }
        if (first->is_tt())
          {
            second->destroy();
            return first;
          }
        break;
      case op::R:
        //   - (Exp R 1) = 1
        //   - (Exp R 0) = 0
        //   - (1 R Exp) = Exp
        //   - (Exp R Exp) = Exp
        if (second->is_tt()
            || second->is_ff()
            || first->is_tt()
            || first == second)
          {
            first->destroy();
            return second;
          }
        break;
      case op::M:
        //   - (Exp M 0) = 0
        //   - (1 M Exp) = Exp
        //   - (0 M Exp) = 0
        //   - (Exp M Exp) = Exp
        if (second->is_ff()
            || first->is_tt()
            || first == second)
          {
            first->destroy();
            return second;
          }
        if (first->is_ff())
          {
            second->destroy();
            return first;
          }
        break;
      case op::EConcat:
      case op::EConcatMarked:
        //   - 0 <>-> Exp = 0
        //   - 1 <>-> Exp = Exp
        //   - [*0] <>-> Exp = 0
        //   - Exp <>-> 0 = 0
        //   - [*] <>-> 1 = 1
        //   - boolExp <>-> Exp = boolExp & Exp
        if (first->is_tt())
          return second;
        if (first->is_ff()
            || first->is_eword())
          {
            second->destroy();
            return ff();
          }
        if (second->is_ff() || (second->is_tt() && first == one_star()))
          {
            first->destroy();
            return second;
          }
        if (first->is_boolean())
          return multop(op::And, first, second);
        break;
      case op::UConcat:
        //   - 0 []-> Exp = 1
        //   - 1 []-> Exp = Exp
        //   - [*0] []-> Exp = 1
        //   - Exp []-> 1 = 1
        //   - [*] []-> 0 = 0
        //   - boolExp []-> Exp = !boolExp | Exp
        if (first->is_tt())
          return second;
        if (first->is_ff()
            || first->is_eword())
          {
            second->destroy();
            return tt();
          }
        if (second->is_tt() || (second->is_ff() && first == one_star()))
          {
            first->destroy();
            return second;
          }
        if (first->is_boolean())
          return multop(op::Or, unop(op::Not, first), second);
        break;
      default:
        SPOT_UNREACHABLE();
      }

    auto mem = ::operator new(sizeof(fnode) + sizeof(*children));
    return unique(new(mem) fnode(o, {first, second}));
  }

  const fnode*
  fnode::quantify(op quantifier, const fnode* ap, const fnode* f)
  {
    if (f->kind() != quantifier)
      {
        if (f->is_ff() || f->is_tt())
          {
            ap->destroy();
            return f;
          }
        auto mem = ::operator new(sizeof(fnode) + sizeof(*children));
        return unique(new(mem) fnode(quantifier, {ap, f}));
      }
    // f is already a quantification of the same kind, and alrady
    // contains a list of APs to quantify followed by the formula.  we
    // want to insert ap into the list of APs and keep the AP sorted
    // by ap_id.
    unsigned other_sz = f->size();
    // find the position where ap should be inserted, and check if it
    // is already there.
    for (unsigned i = 0; i < other_sz - 1; ++i)
      if (const fnode* c = f->nth(i); c->apid() >= ap->apid())
        {
          if (c->apid() == ap->apid())
            {
              // ap is already quantified, so we can simply reuse f.
              ap->destroy();
              return f->clone();
            }
          break;
        }
    vec v;
    v.reserve(1 + other_sz);
    unsigned i = 0;
    for (; i < other_sz - 1; ++i)
      if (const fnode* c = f->nth(i); c->apid() < ap->apid())
        v.emplace_back(c->clone());
      else
        break;
    for (; i < other_sz - 1; ++i)
      if (const fnode* c = f->nth(i); c->apid() != ap->apid())
        v.emplace_back(c->clone());
    v.emplace_back(f->nth(other_sz - 1)->clone());
    f->destroy();
    auto mem = ::operator new(sizeof(fnode) + (v.size() - 1)*sizeof(*children));
    return unique(new(mem) fnode(quantifier, v.begin(), v.end()));
  }

  const fnode*
  fnode::quantify(op quantifier,
                  vec aps,
                  const fnode* f)
  {
    if (aps.empty())
      return f;
    if (f->is_ff() || f->is_tt())
      {
        for (const fnode* ap: aps)
          ap->destroy();
        return f;
      }
    // first, sort aps
    std::sort(aps.begin(), aps.end(),
              [](const fnode* a, const fnode* b)
              { return a->apid() < b->apid(); });
    // if f is already a quantification of the same kind, we should merge
    // the two lists of aps, and check if there are duplicates.
    if (f->kind() == quantifier)
      {
        unsigned f_sz = f->size();
        unsigned ap_sz = aps.size();
        vec merged;
        merged.reserve(ap_sz + f_sz);
        unsigned i = 0;
        unsigned j = 0;
        while (i < ap_sz && j < f_sz - 1)
          {
            unsigned i_id = aps[i]->apid();
            unsigned j_id = f->nth(j)->apid();
            if (i_id < j_id)
              {
                merged.emplace_back(aps[i++]);
              }
            else if (i_id > j_id)
              {
                merged.emplace_back(f->nth(j++)->clone());
              }
            else                // duplicate AP, ignore
              {
                ++i;
                ++j;
              }
          }
        while (i < ap_sz)
          merged.emplace_back(aps[i++]);
        while (j < f_sz)        // includes the body of the quantification
          merged.emplace_back(f->nth(j++)->clone());

        f->destroy();
        auto mem = ::operator new(sizeof(fnode) + (merged.size() - 1)
                                  * sizeof(*children));
        return unique(new(mem) fnode(quantifier, merged.begin(), merged.end()));
      }
    else
      {
        // f is not quantified, so we can simply create a new quantification
        // with the list of aps and f as the body.
        aps.emplace_back(f);
        auto mem = ::operator new(sizeof(fnode) + (aps.size() - 1)
                                  * sizeof(*children));
        return unique(new(mem) fnode(quantifier, aps.begin(), aps.end()));
      }
  }


  const fnode*
  fnode::ap(const std::string& name)
  {
    auto ires = m.name2ap.emplace(name, nullptr);
    if (!ires.second)
      return ires.first->second->clone();

    // Name the formula before creating it, because the constructor
    // will call ap_name() during fnode::setup_props().
    unsigned apid;
    if (unsigned freeapid = m.free_apid; freeapid)
      {
        // if discared apid entry exist, recycle them
        apid = freeapid - 1;
        auto& entry = m.apid2name[apid];
        m.free_apid = entry.first;
        --m.free_apid_count;
        entry.first = 0U;
        entry.second = &*ires.first;
      }
    else
      {
        // otherwise, simply extend the vector
        apid = m.apid2name.size();
        m.apid2name.emplace_back(0U, &*ires.first);
      }
    return ires.first->second = new fnode(op::ap, apid);
  }

  const std::string&
  fnode::ap_name() const
  {
    if (SPOT_UNLIKELY(op_ != op::ap))
      throw std::runtime_error("ap_name() called on non-AP formula");
    return m.apid2name[apid()].second->first;
  }

  size_t fnode::next_id_ = 0U;

  size_t fnode::bump_next_id()
  {
    size_t id = next_id_++;
    // If the counter of formulae ever loops, we want to skip the
    // first three values, because they are permanently associated
    // to constants, and it is convenient to have constants
    // smaller than all other formulas.
    if (SPOT_UNLIKELY(next_id_ == 0))
      next_id_ = 3;
    return id;
  }

  const fnode* fnode::ff_ = new fnode(op::ff, {}, true);
  const fnode* fnode::tt_ = new fnode(op::tt, {}, true);
  const fnode* fnode::ew_ = new fnode(op::eword, {}, true);
  const fnode* fnode::one_star_ = nullptr; // Only built when necessary.
  const fnode* fnode::one_plus_ = nullptr; // Only built when necessary.

  void fnode::setup_props(op o)
  {
    id_ = bump_next_id();

    switch (o)
      {
      case op::ff:
      case op::tt:
        is_.boolean = true;
        is_.sugar_free_boolean = true;
        is_.in_nenoform = true;
        is_.syntactic_si = true; // for LTL (not PSL)
        is_.sugar_free_ltl = true;
        is_.ltl_formula = true;
        is_.psl_formula = true;
        is_.sere_formula = true;
        is_.finite = true;
        is_.eventual = true;
        is_.universal = true;
        is_.syntactic_safety = true;
        is_.syntactic_guarantee = true;
        is_.syntactic_obligation = true;
        is_.syntactic_recurrence = true;
        is_.syntactic_persistence = true;
        is_.not_marked = true;
        is_.accepting_eword = false;
        is_.lbt_atomic_props = true;
        is_.spin_atomic_props = true;
        is_.delta1 = true;
        is_.sigma2 = true;
        is_.pi2 = true;
        is_.delta2 = true;
        is_.quantified = false;
        break;
      case op::eword:
        is_.boolean = false;
        is_.sugar_free_boolean = false;
        is_.in_nenoform = true;
        is_.syntactic_si = true;
        is_.sugar_free_ltl = true;
        is_.ltl_formula = false;
        is_.psl_formula = false;
        is_.sere_formula = true;
        is_.finite = true;
        is_.eventual = false;
        is_.syntactic_safety = false;
        is_.syntactic_guarantee = false;
        is_.syntactic_obligation = false;
        is_.syntactic_recurrence = false;
        is_.syntactic_persistence = false;
        is_.universal = false;
        is_.not_marked = true;
        is_.accepting_eword = true;
        is_.lbt_atomic_props = true;
        is_.spin_atomic_props = true;
        is_.delta1 = true;
        is_.sigma2 = true;
        is_.pi2 = true;
        is_.delta2 = true;
        is_.quantified = false;
        break;
      case op::ap:
        is_.boolean = true;
        is_.sugar_free_boolean = true;
        is_.in_nenoform = true;
        is_.syntactic_si = true;        // Assuming LTL (for PSL, a Boolean
        // term that is not stared will be regarded as non-SI where
        // this matters.)
        is_.sugar_free_ltl = true;
        is_.ltl_formula = true;
        is_.psl_formula = true;
        is_.sere_formula = true;
        is_.finite = true;
        is_.eventual = false;
        is_.universal = false;
        is_.syntactic_safety = true;
        is_.syntactic_guarantee = true;
        is_.syntactic_obligation = true;
        is_.syntactic_recurrence = true;
        is_.syntactic_persistence = true;
        is_.not_marked = true;
        is_.accepting_eword = false;
        {
          // is_.lbt_atomic_props should be true if the name has the
          // form pNN where NN is any number of digit.
          const std::string& n = ap_name();
          std::string::const_iterator pos = n.begin();
          bool lbtap = (pos != n.end() && *pos++ == 'p');
          while (lbtap && pos != n.end())
            {
              char l = *pos++;
              lbtap = (l >= '0' && l <= '9');
            }
          is_.lbt_atomic_props = lbtap;
          is_.spin_atomic_props = lbtap || is_spin_ap(n.c_str());
        }
        is_.delta1 = true;
        is_.sigma2 = true;
        is_.pi2 = true;
        is_.delta2 = true;
        is_.quantified = false;
        break;
      case op::Not:
        props = children[0]->props;
        is_.not_marked = true;
        is_.eventual = children[0]->is_universal();
        is_.universal = children[0]->is_eventual();
        is_.in_nenoform = (children[0]->is(op::ap));
        is_.sere_formula = is_.boolean;

        is_.syntactic_safety = children[0]->is_syntactic_guarantee();
        is_.syntactic_guarantee = children[0]->is_syntactic_safety();
        // is_.syntactic_obligation inherited from child
        is_.syntactic_recurrence = children[0]->is_syntactic_persistence();
        is_.syntactic_persistence = children[0]->is_syntactic_recurrence();

        is_.accepting_eword = false;
        // is_.delta1 inherited
        is_.sigma2 = children[0]->is_pi2();
        is_.pi2 = children[0]->is_sigma2();
        // is_.delta2 inherited
        break;
      case op::X:
      case op::strong_X:
        props = children[0]->props;
        is_.not_marked = true;
        is_.boolean = false;
        is_.syntactic_si = false;
        is_.sere_formula = false;
        // is_.syntactic_safety inherited
        // is_.syntactic_guarantee inherited
        // is_.syntactic_obligation inherited
        // is_.syntactic_recurrence inherited
        // is_.syntactic_persistence inherited

        // is_.accepting_eword is currently unused outside SEREs, but
        // we could make sense of it if we start supporting LTL over
        // finite traces.
        is_.accepting_eword = false;
        // is_.delta1 inherited
        // is_.sigma2 inherited
        // is_.pi2 inherited
        // is_.delta2 inherited
        break;
      case op::F:
        props = children[0]->props;
        is_.not_marked = true;
        is_.boolean = false;
        is_.sere_formula = false;
        is_.finite = false;
        is_.sugar_free_ltl = false;
        is_.eventual = true;
        is_.syntactic_safety = false;
        // is_.syntactic_guarantee inherited
        is_.syntactic_obligation = is_.syntactic_guarantee;
        is_.syntactic_recurrence = is_.syntactic_guarantee;
        // is_.syntactic_persistence inherited
        is_.accepting_eword = false;
        is_.delta1 = is_.syntactic_guarantee;
        is_.pi2 = is_.syntactic_guarantee;
        // is_.sigma2 inherited
        is_.delta2 = is_.pi2 | is_.sigma2;
        break;
      case op::G:
        props = children[0]->props;
        is_.not_marked = true;
        is_.boolean = false;
        is_.sere_formula = false;
        is_.finite = false;
        is_.sugar_free_ltl = false;
        is_.universal = true;
        // is_.syntactic_safety inherited
        is_.syntactic_guarantee = false;
        is_.syntactic_obligation = is_.syntactic_safety;
        // is_.syntactic_recurrence inherited
        is_.syntactic_persistence = is_.syntactic_safety;
        is_.accepting_eword = false;
        is_.delta1 = is_.syntactic_safety;
        is_.sigma2 = is_.syntactic_safety;
        // is_.pi2 inherited
        is_.delta2 = is_.pi2 | is_.sigma2;
        break;
      case op::NegClosure:
      case op::NegClosureMarked:
        props = children[0]->props;
        is_.not_marked = (op_ == op::NegClosure);
        is_.boolean = false;
        is_.ltl_formula = false;
        is_.psl_formula = true;
        is_.sere_formula = false;
        is_.syntactic_safety = is_.finite;
        is_.syntactic_guarantee = true;
        is_.syntactic_obligation = true;
        is_.syntactic_recurrence = true;
        is_.syntactic_persistence = true;
        is_.accepting_eword = false;
        is_.delta1 = true;
        is_.sigma2 = true;
        is_.pi2 = true;
        is_.delta2 = true;
        assert(children[0]->is_sere_formula());
        assert(!children[0]->is_boolean());
        break;
      case op::Closure:
        props = children[0]->props;
        is_.not_marked = true;
        is_.boolean = false;
        is_.ltl_formula = false;
        is_.psl_formula = true;
        is_.sere_formula = false;
        is_.syntactic_safety = true;
        is_.syntactic_guarantee = is_.finite;
        is_.syntactic_obligation = true;
        is_.syntactic_recurrence = true;
        is_.syntactic_persistence = true;
        is_.accepting_eword = false;
        is_.delta1 = true;
        is_.sigma2 = true;
        is_.pi2 = true;
        is_.delta2 = true;
        assert(children[0]->is_sere_formula());
        assert(!children[0]->is_boolean());
        break;
      case op::Xor:
      case op::Equiv:
        props = children[0]->props & children[1]->props;
        // Preserve suspendable property
        is_.eventual = is_.universal = is_.eventual && is_.universal;
        is_.sere_formula = is_.boolean;
        is_.sugar_free_boolean = false;
        is_.in_nenoform = false;
        // is_.syntactic_obligation inherited;
        is_.accepting_eword = false;
        if (is_.syntactic_obligation)
          {
            // Only formula that are in the intersection of
            // guarantee and safety are closed by Xor and <=>.
            bool sg = is_.syntactic_safety && is_.syntactic_guarantee;
            is_.syntactic_safety = sg;
            is_.syntactic_guarantee = sg;
            assert(is_.syntactic_recurrence == true);
            assert(is_.syntactic_persistence == true);
          }
        else
          {
            is_.syntactic_safety = false;
            is_.syntactic_guarantee = false;
            is_.syntactic_recurrence = false;
            is_.syntactic_persistence = false;
          }
        if (is_.delta1)
          {
            assert(is_.pi2 == true);
            assert(is_.sigma2 == true);
            assert(is_.delta2 == true);
          }
        else
          {
            is_.pi2 = false;
            is_.sigma2 = false;
          }
        break;
      case op::Implies:
        props = children[0]->props & children[1]->props;
        is_.eventual =
          children[0]->is_universal() && children[1]->is_eventual();
        is_.universal =
          children[0]->is_eventual() && children[1]->is_universal();
        is_.sere_formula = (children[0]->is_boolean()
                            && children[1]->is_sere_formula());
        is_.sugar_free_boolean = false;
        is_.in_nenoform = false;
        is_.syntactic_safety = (children[0]->is_syntactic_guarantee()
                                && children[1]->is_syntactic_safety());
        is_.syntactic_guarantee = (children[0]->is_syntactic_safety()
                                   && children[1]->is_syntactic_guarantee());
        // is_.syntactic_obligation inherited
        is_.syntactic_persistence = children[0]->is_syntactic_recurrence()
          && children[1]->is_syntactic_persistence();
        is_.syntactic_recurrence = children[0]->is_syntactic_persistence()
          && children[1]->is_syntactic_recurrence();
        is_.accepting_eword = false;
        // is_.delta1 inherited
        is_.sigma2 = children[0]->is_pi2() && children[1]->is_sigma2();
        is_.pi2 = children[0]->is_sigma2() && children[1]->is_pi2();
        // is_.delta2 inherited
        break;
      case op::EConcatMarked:
      case op::EConcat:
        props = children[0]->props & children[1]->props;
        is_.not_marked = (op_ != op::EConcatMarked);
        is_.ltl_formula = false;
        is_.boolean = false;
        is_.sere_formula = false;
        is_.accepting_eword = false;
        is_.psl_formula = true;

        is_.syntactic_guarantee = children[1]->is_syntactic_guarantee();
        is_.syntactic_persistence = children[1]->is_syntactic_persistence();
        is_.sigma2 = children[1]->is_sigma2();
        if (children[0]->is_finite()) // behaves like X
          {
            is_.syntactic_safety = children[1]->is_syntactic_safety();
            is_.syntactic_obligation = children[1]->is_syntactic_obligation();
            is_.syntactic_recurrence = children[1]->is_syntactic_recurrence();
            is_.delta1 = children[1]->is_delta1();
            is_.pi2 = children[1]->is_pi2();
            is_.delta2 = children[1]->is_delta2();
          }
        else                    // behaves like F
          {
            is_.syntactic_safety = false;
            bool g = children[1]->is_syntactic_guarantee();
            is_.syntactic_obligation = g;
            is_.syntactic_recurrence = g;
            is_.delta1 = g;
            is_.pi2 = g;
            is_.delta2 = g | is_.sigma2;
          }
        assert(children[0]->is_sere_formula());
        assert(children[1]->is_psl_formula());
        if (children[0]->is_boolean())
          is_.syntactic_si = false;
        break;
      case op::UConcat:
        props = children[0]->props & children[1]->props;
        is_.not_marked = true;
        is_.ltl_formula = false;
        is_.boolean = false;
        is_.sere_formula = false;
        is_.accepting_eword = false;
        is_.psl_formula = true;

        is_.syntactic_safety = children[1]->is_syntactic_safety();
        is_.syntactic_recurrence = children[1]->is_syntactic_recurrence();
        is_.pi2 = children[1]->is_pi2();
        if (children[0]->is_finite()) // behaves like X
          {
            is_.syntactic_guarantee = children[1]->is_syntactic_guarantee();
            is_.syntactic_obligation = children[1]->is_syntactic_obligation();
            is_.syntactic_persistence = children[1]->is_syntactic_persistence();
            is_.delta1 = children[1]->is_delta1();
            is_.sigma2 = children[1]->is_sigma2();
            is_.delta2 = children[1]->is_delta2();
          }
        else                    // behaves like G
          {
            is_.syntactic_guarantee = false;
            bool s = children[1]->is_syntactic_safety();
            is_.syntactic_obligation = s;
            is_.syntactic_persistence = s;
            is_.delta1 = s;
            is_.sigma2 = s;
            is_.delta2 = is_.pi2 | s;
          }
        assert(children[0]->is_sere_formula());
        assert(children[1]->is_psl_formula());
        if (children[0]->is_boolean())
          is_.syntactic_si = false;
        break;
      case op::U:
        // Beware: (f U g) is a pure eventuality if both operands
        // are pure eventualities, unlike in the proceedings of
        // Concur'00.  (The revision of the paper available at
        // http://www.bell-labs.com/project/TMP/ is fixed.)  See
        // also http://arxiv.org/abs/1011.4214v2 for a discussion
        // about this problem.  (Which we fixed in 2005 thanks
        // to LBTT.)
        // This means that we can use the following line to handle
        // all cases of (f U g), (f R g), (f W g), (f M g) for
        // universality and eventuality.
        props = children[0]->props & children[1]->props;
        // The matter can be further refined because:
        //  (f U g) is a pure eventuality if
        //                g is a pure eventuality (regardless of f),
        //             or f == 1
        //  (g M f) is a pure eventuality if f and g are,
        //                                or f == 1
        //  (g R f) is purely universal if
        //                f is purely universal (regardless of g)
        //               or g == 0
        //  (f W g) is purely universal if f and g are
        //                              or g == 0
        is_.not_marked = true;
        // f U g is universal if g is eventual, or if f == 1.
        is_.eventual = children[1]->is_eventual();
        is_.eventual |= children[0]->is_tt();
        is_.boolean = false;
        is_.sere_formula = false;
        is_.finite = false;
        is_.accepting_eword = false;

        is_.syntactic_safety = false;
        // is_.syntactic_guarantee = Guarantee U Guarantee
        is_.syntactic_obligation = // Obligation U Guarantee
          children[0]->is_syntactic_obligation()
          && children[1]->is_syntactic_guarantee();
        is_.syntactic_recurrence = // Recurrence U Guarantee
          children[0]->is_syntactic_recurrence()
          && children[1]->is_syntactic_guarantee();
        // is_.syntactic_persistence = Persistence U Persistance
        is_.delta1 = is_.syntactic_guarantee;
        // is_.sigma2 = Σ₂ U Σ₂
        is_.pi2 = is_.syntactic_guarantee;
        is_.delta2 = is_.sigma2 | is_.pi2;
        break;
      case op::W:
        // See comment for op::U.
        props = children[0]->props & children[1]->props;
        is_.not_marked = true;
        // f W g is universal if f and g are, or if g == 0.
        is_.universal |= children[1]->is_ff();
        is_.boolean = false;
        is_.sere_formula = false;
        is_.finite = false;
        is_.accepting_eword = false;

        // is_.syntactic_safety = Safety W Safety;
        is_.syntactic_guarantee = false;
        is_.syntactic_obligation = // Safety W Obligation
          children[0]->is_syntactic_safety()
          && children[1]->is_syntactic_obligation();
        // is_.syntactic_recurrence = Recurrence W Recurrence
        is_.syntactic_persistence = // Safety W Persistance
          children[0]->is_syntactic_safety()
          && children[1]->is_syntactic_persistence();
        is_.delta1 = is_.syntactic_safety;
        is_.sigma2 = is_.syntactic_safety;
        // is_.pi2 = Π₂ U Π₂
        is_.delta2 = is_.sigma2 | is_.pi2;
        break;
      case op::R:
        // See comment for op::U.
        props = children[0]->props & children[1]->props;
        is_.not_marked = true;
        // g R f is universal if f is universal, or if g == 0.
        is_.universal = children[1]->is_universal();
        is_.universal |= children[0]->is_ff();
        is_.boolean = false;
        is_.sere_formula = false;
        is_.finite = false;
        is_.accepting_eword = false;

        // is_.syntactic_safety = Safety R Safety;
        is_.syntactic_guarantee = false;
        is_.syntactic_obligation = // Obligation R Safety
          children[0]->is_syntactic_obligation()
          && children[1]->is_syntactic_safety();
        //is_.syntactic_recurrence = Recurrence R Recurrence
        is_.syntactic_persistence = // Persistence R Safety
          children[0]->is_syntactic_persistence()
          && children[1]->is_syntactic_safety();
        is_.delta1 = is_.syntactic_safety;
        is_.sigma2 = is_.syntactic_safety;
        // is_.pi2 = Π₂ U Π₂
        is_.delta2 = is_.sigma2 | is_.pi2;
        break;
      case op::M:
        // See comment for op::U.
        props = children[0]->props & children[1]->props;
        is_.not_marked = true;
        // g M f is eventual if both g and f are eventual, or if f == 1.
        is_.eventual |= children[1]->is_tt();
        is_.boolean = false;
        is_.sere_formula = false;
        is_.finite = false;
        is_.accepting_eword = false;

        is_.syntactic_safety = false;
        // is_.syntactic_guarantee = Guarantee M Guarantee
        is_.syntactic_obligation = // Guarantee M Obligation
          children[0]->is_syntactic_guarantee()
          && children[1]->is_syntactic_obligation();
        is_.syntactic_recurrence = // Guarantee M Recurrence
          children[0]->is_syntactic_guarantee()
          && children[1]->is_syntactic_recurrence();
        // is_.syntactic_persistence = Persistence M Persistance
        is_.delta1 = is_.syntactic_guarantee;
        // is_.sigma2 = Σ₂ M Σ₂
        is_.pi2 = is_.syntactic_guarantee;
        is_.delta2 = is_.sigma2 | is_.pi2;
        break;
      case op::Or:
        {
          props = children[0]->props;
          unsigned s = size_;
          bool ew = children[0]->accepts_eword();
          for (unsigned i = 1; i < s; ++i)
            {
              ew |= children[i]->accepts_eword();
              props &= children[i]->props;
            }
          is_.accepting_eword = ew;
          break;
        }
      case op::OrRat:
        {
          props = children[0]->props;
          unsigned s = size_;
          bool syntactic_si = is_.syntactic_si && !is_.boolean;
          // Note: OrRat(p1,p2) is a Boolean formula, but its is
          // actually rewritten as Or(p1,p2) by trivial identities
          // before this constructor is called.  So at this point,
          // AndNLM is always used with at most one Boolean argument,
          // and the result is therefore NOT Boolean.
          is_.boolean = false;
          is_.ltl_formula = false;
          is_.psl_formula = false;
          is_.eventual = false;
          is_.universal = false;

          bool ew = children[0]->accepts_eword();
          for (unsigned i = 1; i < s; ++i)
            {
              ew |= children[i]->accepts_eword();
              syntactic_si &= children[i]->is_syntactic_stutter_invariant()
                && !children[i]->is_boolean();
              props &= children[i]->props;
            }
          is_.accepting_eword = ew;
          is_.syntactic_si = syntactic_si;
          break;
        }
      case op::And:
        {
          props = children[0]->props;
          unsigned s = size_;
          for (unsigned i = 1; i < s; ++i)
            props &= children[i]->props;
          break;
        }
      case op::Fusion:
      case op::Concat:
      case op::AndNLM:
      case op::AndRat:
        {
          props = children[0]->props;
          unsigned s = size_;
          bool syntactic_si = is_.syntactic_si && !is_.boolean;
          // Note: AndNLM(p1,p2) and AndRat(p1,p2) are Boolean
          // formulae, but they are actually rewritten as And(p1,p2)
          // by trivial identities before this constructor is called.
          // So at this point, AndNLM/AndRat are always used with at
          // most one Boolean argument, and the result is therefore
          // NOT Boolean.
          is_.boolean = false;
          is_.ltl_formula = false;
          is_.psl_formula = false;
          is_.eventual = false;
          is_.universal = false;

          for (unsigned i = 1; i < s; ++i)
            {
              syntactic_si &= children[i]->is_syntactic_stutter_invariant()
                && !children[i]->is_boolean();
              props &= children[i]->props;
            }
          is_.syntactic_si = syntactic_si;
          if (op_ == op::Fusion)
            is_.accepting_eword = false;
          // A concatenation is an siSERE if it looks like
          //    r;b*  or  b*;r
          // where b is Boolean and r is siSERE.  generalized to n-ary
          // concatenation, it means all arguments should be of the
          // form b*, except one that is siSERE (i.e., a sub-formula
          // that verify is_syntactic_stutter_invariant() and
          // !is_boolean()).   Since b* is siSERE, that means we
          // want at least s-1 operands of the form b*.
          if (op_ == op::Concat)
            {
              unsigned sb = 0; // stared Boolean formulas seen
              for (unsigned i = 0; i < s; ++i)
                {
                  auto ci = children[i];
                  if (ci->is_Kleene_star())
                    {
                      sb += ci->nth(0)->is_boolean();
                    }
                  else if (!ci->is_syntactic_stutter_invariant()
                           || ci->is_boolean())
                    {
                      sb = 0;
                      break;
                    }
                }
              is_.syntactic_si = sb >= s - 1;
            }
          break;
        }

      case op::Star:
      case op::FStar:
        {
          props = children[0]->props;
          assert(is_.sere_formula);
          is_.boolean = false;
          is_.ltl_formula = false;
          is_.psl_formula = false;
          is_.eventual = false;
          is_.universal = false;
          is_.syntactic_safety = false;
          is_.syntactic_guarantee = false;
          is_.syntactic_obligation = false;
          is_.syntactic_recurrence = false;
          is_.syntactic_persistence = false;
          is_.delta1 = false;
          is_.pi2 = false;
          is_.sigma2 = false;
          is_.delta2 = false;

          switch (op_)
            {
            case op::Star:
              if (range_.max == unbounded())
                {
                  is_.finite = false;
                  is_.syntactic_si = (range_.min <= 1)
                    && children[0]->is_boolean();
                }
              else
                {
                  is_.syntactic_si = false;
                }
              if (range_.min == 0)
                is_.accepting_eword = true;
              break;
            case op::FStar:
              is_.accepting_eword = false;
              is_.syntactic_si &= !children[0]->is_boolean();
              if (range_.max == unbounded())
                is_.finite = false;
              if (range_.min == 0)
                is_.syntactic_si = false;
              break;
            default:
              SPOT_UNREACHABLE();
            }
        }
        break;
      case op::first_match:
        props = children[0]->props;
        assert(is_.sere_formula);
        // {(a[+];b*);c*}<>->d is stutter invariant
        // {first_match(a[+];b*);c*}<>->d is NOT.
        is_.syntactic_si = false;
        is_.boolean = false;
        is_.ltl_formula = false;
        is_.psl_formula = false;
        is_.eventual = false;
        is_.universal = false;
        is_.syntactic_safety = false;
        is_.syntactic_guarantee = false;
        is_.syntactic_obligation = false;
        is_.syntactic_recurrence = false;
        is_.syntactic_persistence = false;
        is_.delta1 = false;
        is_.pi2 = false;
        is_.sigma2 = false;
        is_.delta2 = false;
        break;
      case op::exists:
      case op::forall:
        props = children[size_ - 1]->props;
        is_.quantified = true;
        break;
      }
  }

  const fnode*
  fnode::nested_unop_range(op uo, op bo, unsigned min, unsigned max,
                           const fnode* f)
  {
    if (max < min)
      std::swap(min, max);
    if (SPOT_UNLIKELY(min >= unbounded()))
      report_repetition_overflow(min);
    // We are testing strict ">", because unbounded() is a legitimate
    // input for max.  We just cannot tell if it was really indented
    // as "unbounded".
    if (SPOT_UNLIKELY(max > unbounded()))
      report_repetition_overflow(max);
    const fnode* res = f;
    if (max != unbounded())
      for (unsigned i = min; i < max; ++i)
        {
          const fnode* a = f->clone();
          res = fnode::multop(bo, a, fnode::unop(uo, res));
        }
    else
      res = fnode::unop(bo == op::Or ? op::F : op::G, res);
    for (unsigned i = 0; i < min; ++i)
      res = fnode::unop(uo, res);
    return res;
  }

  std::ostream& fnode::dump(std::ostream& os) const
  {
    os << kindstr() << "(@" << id_ << " #" << refs_;
    if (op_ == op::Star || op_ == op::FStar)
      {
        os << ' ' << +min() << "..";
        auto m = max();
        if (m != unbounded())
          os << +m;
      }
    if (op_ == op::ap)
      os << " \"" << ap_name() << '"';
    if (auto s = size())
      {
        os << " [";
        for (auto c: *this)
          {
            c->dump(os);
            if (--s)
              os << ", ";
          }
        os << ']';
      }
    return os << ')';
  }

  const fnode* fnode::all_but(unsigned i) const
  {
    switch (op o = kind())
      {
      case op::Or:
      case op::OrRat:
      case op::And:
      case op::AndRat:
      case op::AndNLM:
      case op::Concat:
      case op::Fusion:
        {
          unsigned s = size();
          assert(s > 1);
          vec v;
          v.reserve(s - 1);
          for (unsigned j = 0; j < s; ++j)
            if (i != j)
              v.emplace_back(nth(j)->clone());
          return multop(o, v);
        }
      default:
        throw
          std::runtime_error("all_but() is incompatible with this operator");
      }
    SPOT_UNREACHABLE();
  }

  const fnode* fnode::boolean_operands(unsigned* width) const
  {
    unsigned s = boolean_count();
    if (width)
      *width = s;
    if (s == 0)
      return nullptr;
    if (s == 1)
      return nth(0)->clone();
    vec v(children, children + s);
    for (auto c: v)
      c->clone();
    return multop(op_, v);
  }

  bool fnode::instances_check()
  {
    unsigned cnt = 0;
    for (auto i: m.uniq)
      if (!i->saturated_)
        {                       // GCOVR_EXCL_START
          if (!cnt++)
            std::cerr << "*** m.uniq is not empty ***\n";
          i->dump(std::cerr) << '\n';
        }                       // GCOVR_EXCL_STOP
    if (m.apid2name.size() != m.free_apid_count)
      {                         // GCOVR_EXCL_START
        std::cerr << "*** m.apid2name is no empty ***\n";
        unsigned e = m.apid2name.size();
        for (unsigned i = 0; i < e; ++i)
          {
            auto& ap = m.apid2name[i];
            std::cerr << " [" << i;
            if (!ap.second)
              std::cerr << "] = -> " << ap.first - 1 << '\n';
            else
              std::cerr << "] = (" << ap.second->first << ", "
                        << ap.second->second << ")\n";
          }
        std::cerr << "free_apid = " << m.free_apid << '\n';
        std::cerr << "free_apid_count = " << m.free_apid_count << '\n';
        ++cnt;
      }                         // GCOVR_EXCL_STOP
    return cnt == 0;
  }

  unsigned formula::apid_count() noexcept
  {
    return m.apid2name.size();
  }

  std::vector<formula> formula::apid_map()
  {
    std::vector<formula> r;
    unsigned sz = apid_count();
    r.reserve(sz);
    for (unsigned i = 0; i < sz; ++i)
      {
        auto* ptr = m.apid2name[i].second;
        r.emplace_back(ptr ? ptr->second->clone() : nullptr);
      }
    return r;
  }

  namespace
  {
    [[noreturn]]
    void incorrect_apid(const std::string_view fn)
    {
      throw std::runtime_error(std::string(fn) + "(): incorrect id");
    }
  }

  bool formula::is_valid_apid(unsigned id) noexcept
  {
    return (m.apid2name.size() > id) && m.apid2name[id].second;
  }

  formula formula::ap_from_apid(unsigned id)
  {
    if (SPOT_UNLIKELY(m.apid2name.size() <= id))
      incorrect_apid("formula::ap_from_apid");
    auto& entry = m.apid2name[id];
    if (SPOT_UNLIKELY(!entry.second))
      incorrect_apid("formula::ap_from_apid");
    return formula(entry.second->second->clone());
  }

  const std::string& formula::apname_from_apid(unsigned id)
  {
    if (SPOT_UNLIKELY(m.apid2name.size() <= id))
      incorrect_apid("formula::apname_from_apid");
    auto& entry = m.apid2name[id];
    if (SPOT_UNLIKELY(!entry.second))
      incorrect_apid("formula::apname_from_apid");
    return entry.second->first;
  }

  formula formula::sugar_goto(const formula& b, unsigned min, unsigned max)
  {
    if (!b.is_boolean())
      throw
        std::runtime_error("sugar_goto() called with non-Boolean argument");
    // b[->min..max] is implemented as ((!b)[*];b)[*min..max]
    return Star(Concat({Star(Not(b)), b}), min, max);
  }

  formula formula::sugar_equal(const formula& b, unsigned min, unsigned max)
  {
    if (!b.is_boolean())
      throw
        std::runtime_error("sugar_equal() called with non-Boolean argument");

    // b[=0..] = 1[*]
    if (min == 0 && max == unbounded())
      return one_star();

    // b[=min..max] is implemented as ((!b)[*];b)[*min..max];(!b)[*]
    formula s = Star(Not(b));
    return Concat({Star(Concat({s, b}), min, max), s});
  }

  formula formula::sugar_delay(const formula& b,
                               unsigned min, unsigned max)
  {
    // ##[min:max] b = 1[*min:max];b
    return Concat({Star(tt(), min, max), b});
  }
  formula formula::sugar_delay(const formula& a, const formula& b,
                               unsigned min, unsigned max)
  {
    // If min>=1
    //   a ##[min:max] b  = a;1[*min-1:max-1];b
    // If min==0 we can use
    //   a ##[0:0] b = a:b
    //   a ##[0:max] b = a:(1[*0:max];b)  if a rejects [*0]
    //   a ##[0:max] b = (a;1[*0:max]):b  if b rejects [*0]
    //   a ##[0:max] b = (a:b)|(a;[*0:max-1];b)  else
    if (min > 0)
      {
        --min;
        if (max != unbounded())
          --max;
        return Concat({a, Star(tt(), min, max), b});
      }
    if (max == 0)
      return Fusion({a, b});
    if (!a.accepts_eword())
      return Fusion({a, Concat({Star(tt(), 0, max), b})});
    if (!b.accepts_eword())
      return Fusion({Concat({a, Star(tt(), 0, max)}), b});

    if (max != unbounded())
      --max;
    formula left = Fusion({a, b});
    formula right = Concat({a, Star(tt(), 0, max), b});
    return OrRat({left, right});
  }

  int atomic_prop_cmp(const fnode* f, const fnode* g)
  {
    return strverscmp(f->ap_name().c_str(), g->ap_name().c_str());
  }

#define printprops                                                      \
  proprint(is_boolean, "B", "Boolean formula");                         \
  proprint(is_sugar_free_boolean, "&", "without Boolean sugar");        \
  proprint(is_in_nenoform, "!", "in negative normal form");             \
  proprint(is_syntactic_stutter_invariant, "x",                         \
           "syntactic stutter invariant");                              \
  proprint(is_sugar_free_ltl, "f", "without LTL sugar");                \
  proprint(is_ltl_formula, "L", "LTL formula");                         \
  proprint(is_psl_formula, "P", "PSL formula");                         \
  proprint(is_sere_formula, "S", "SERE formula");                       \
  proprint(is_finite, "F", "finite");                                   \
  proprint(is_eventual, "e", "pure eventuality");                       \
  proprint(is_universal, "u", "purely universal");                      \
  proprint(is_syntactic_safety, "s", "syntactic safety");               \
  proprint(is_syntactic_guarantee, "g", "syntactic guarantee");         \
  proprint(is_syntactic_obligation, "o", "syntactic obligation");       \
  proprint(is_syntactic_persistence, "p", "syntactic persistence");     \
  proprint(is_syntactic_recurrence, "r", "syntactic recurrence");       \
  proprint(is_marked, "+", "marked");                                   \
  proprint(accepts_eword, "0", "accepts the empty word");               \
  proprint(has_lbt_atomic_props, "l",                                   \
           "has LBT-style atomic props");                               \
  proprint(has_spin_atomic_props, "a",                                  \
           "has Spin-style atomic props");                              \
  proprint(is_delta1, "O", "delta1");                                   \
  proprint(is_sigma2, "P", "sigma2");                                   \
  proprint(is_pi2, "R", "pi2");                                         \
  proprint(is_delta2, "D", "delta2");

  // O (for Δ₁), P (for Σ₂), R (for Π₂) are the uppercase versions of
  // o (obligation), p (persistence), r (recurrence) because they are
  // stricter subsets of those.

  std::list<std::string>
  list_formula_props(const formula& f)
  {
    std::list<std::string> res;
#define proprint(m, a, l)                        \
    if (f.m())                                        \
      res.emplace_back(l);
    printprops;
#undef proprint
    return res;
  }

  std::ostream&
  print_formula_props(std::ostream& out, const formula& f, bool abbr)
  {
    const char* comma = abbr ? "" : ", ";
    const char* sep = "";

#define proprint(m, a, l)                        \
    if (f.m())                                        \
      {                                                \
        out << sep; out << (abbr ? a : l);        \
        sep = comma;                                \
      }
    printprops;
#undef proprint

    return out;
  }

  std::ostream& operator<<(std::ostream& os, const formula& f)
  {
    return print_psl(os, f);
  }

  bool
  formula_ptr_less_than_bool_first::operator()(const formula& left,
                                               const formula& right) const
  {
    return operator()(left.ptr_, right.ptr_);
  }
}
