Repository: VeriFIT/mata (language: python)

Issue:
Fix transducer operations
This PR fixes various bugs in transducer operations, discovered during benchmark generation from runs of Noodler on transducer-plus and webapp benchmarks.

The PR fixes:
- setting levels in concatenation, and
- leftmost reluctant replacement insertion of the replacement literal.

The PR adds:
- assignment operators in alphabets,
- parameters for number of levels for creation of predefined NFTs, and
- method to append to the vector of levels.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index b8040cca2..0896d236a 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 [![Python-Binding (build-&-test)](https://github.com/VeriFIT/mata/actions/workflows/python-binding.yml/badge.svg?branch=devel)](https://github.com/VeriFIT/mata/actions/workflows/python-binding.yml)
 [![codecov](https://codecov.io/gh/VeriFIT/mata/branch/devel/graph/badge.svg?token=9VAVD19N4D)](https://codecov.io/gh/VeriFIT/mata)
 
-Mata is an open source automata library that offers interface for different kinds of automata (NFA, etc.). Currently, Mata offers two interfaces:
+Mata is an open source automata library that offers interface for different kinds of automata, e.g. (non-)deterministic finite automata (NFAs), (non-)deterministic finite transducers, etc. Currently, Mata offers two interfaces:
 
   1. An efficient library implemented in C/C++
   2. A flexible wrapper implemented in Python that uses the efficient library
@@ -215,6 +215,29 @@ The usage of the binding copies (to certain levels) the usage of the C++ library
 
 You can either run your scripts directly using `python` or compile it using the `cython` project.
 
+# Supported Automata Models
+
+While we try to keep the provided libray interface stable, Mata is still a project in its infancy.
+Hence, the interface is subject to change.
+For further information, see section [Versioning](README.md#Versioning).
+
+Mata currently supports (non-)deterministic finite automata (NFAs), defined in `include/mata/nfa/`.
+
+> [!WARNING]
+> Mata provides an experimental (unstable) support for (non-)deterministic finite transducers (NFTs), defined in `include/mata/nft/`.
+> The provided interface may change.
+
+# Versioning
+
+Mata follows the versioning scheme `GENERATION.MAJOR.MINOR`, e.g. `1.2.3` where `1` is the generation number, and `2` the major version, and `3` the minor version.
+The `MAJOR` and `MINOR` versions follow loosely the [Semantic Versioning](https://semver.org/) scheme.
+
+- The `GENERATION` number represents our subjective generation of the Mata library.
+  Typically, we increment the `GENERATION` number when a substantial change to the library is introduced, such as adding support for new automata models, extensive rewrite of (a part of) the Mata library, or to denote a new scientific paper documenting the latest developments in the Mata library is published, etc.
+- Generally, the changes in the `MINOR` version should not break the existing interface, but exceptions might occur.
+- The changes in the `MAJOR` version might introduce breaking changes that modify the existing interface and might require changing the code in your projects that use the Mata library.
+
+
 # Publications
 - Chocholatý, D., Fiedor, T., Havlena, V., Holík, L., Hruška, M., Lengál, O., & Síč, J. (2023). [Mata, a Fast and Simple Finite Automata Library](https://doi.org/10.1007/978-3-031-57249-4_7). In *Proc. of TACAS'24*, volume 14571 of LNCS, pages 130-151, 2024. Springer.
     - Chocholatý, D., Fiedor, T., Havlena, V., Holík, L., Hruška, M., Lengál, O., Síč, J.: [A replication package for reproducing the results of paper “Mata: A fast and simple finite automata library”](https://doi.org/10.5281/zenodo.10044515) (Oct 2023).
diff --git a/bindings/python/libmata/nfa/nfa.pxd b/bindings/python/libmata/nfa/nfa.pxd
index 876350eb2..384669895 100644
--- a/bindings/python/libmata/nfa/nfa.pxd
+++ b/bindings/python/libmata/nfa/nfa.pxd
@@ -138,7 +138,7 @@ cdef extern from "mata/nfa/nfa.hh" namespace "mata::nfa":
         # Constructor
         CNfa() except +
         CNfa(unsigned long) except +
-        CNfa(unsigned long, StateSet, StateSet, CAlphabet*)
+        CNfa(unsigned long, CSparseSet[State], CSparseSet[State], CAlphabet*)
         CNfa(const CNfa&)
 
         # Public Functions
diff --git a/bindings/python/libmata/nfa/nfa.pyx b/bindings/python/libmata/nfa/nfa.pyx
index f117f0713..6f75fcb97 100644
--- a/bindings/python/libmata/nfa/nfa.pyx
+++ b/bindings/python/libmata/nfa/nfa.pyx
@@ -197,11 +197,11 @@ cdef class Nfa:
         :param alph.Alphabet alphabet: alphabet corresponding to the automaton
         """
         cdef CAlphabet* c_alphabet = NULL
-        cdef StateSet empty_default_state_set
+        cdef CSparseSet[State] empty_default_sparse_set
         if alphabet:
             c_alphabet = alphabet.as_base()
         self.thisptr = make_shared[CNfa](
-            mata_nfa.CNfa(state_number, empty_default_state_set, empty_default_state_set, c_alphabet)
+            mata_nfa.CNfa(state_number, empty_default_sparse_set, empty_default_sparse_set, c_alphabet)
         )
         self.label = label
 
diff --git a/include/mata/alphabet.hh b/include/mata/alphabet.hh
index dc2bfdb77..8c254f6dd 100644
--- a/include/mata/alphabet.hh
+++ b/include/mata/alphabet.hh
@@ -82,10 +82,15 @@ public:
 
     bool operator==(const Alphabet &) const = delete;
 
+    /**
+     * Checks whether the alphabet has any symbols.
+     */
+    virtual bool empty() const = 0;
+
     virtual void clear() { throw std::runtime_error("Unimplemented"); }
 
 protected:
-    virtual const void *address() const { return this; }
+    virtual const void* address() const { return this; }
 }; // class Alphabet.
 
 /**
@@ -117,6 +122,8 @@ public:
 
     IntAlphabet& operator=(const IntAlphabet& int_alphabet) = delete;
 
+    bool empty() const override { return false; }
+
     void clear() override { throw std::runtime_error("Nonsensical use of clear() on IntAlphabet."); }
 
 protected:
@@ -175,9 +182,6 @@ public:
     explicit EnumAlphabet(const EnumAlphabet* const alphabet): EnumAlphabet(*alphabet) {}
     EnumAlphabet(EnumAlphabet&& rhs) = default;
 
-    EnumAlphabet& operator=(const EnumAlphabet& rhs) = default;
-    EnumAlphabet& operator=(EnumAlphabet&& rhs) = default;
-
     utils::OrdVector<Symbol> get_alphabet_symbols() const override { return symbols_; }
     utils::OrdVector<Symbol> get_complement(const utils::OrdVector<Symbol>& symbols) const override {
         return symbols_.difference(symbols);
@@ -185,7 +189,9 @@ public:
 
     std::string reverse_translate_symbol(Symbol symbol) const override;
 
-public:
+    EnumAlphabet& operator=(const EnumAlphabet& rhs) = default;
+    EnumAlphabet& operator=(EnumAlphabet&& rhs) = default;
+
     /**
      * @brief Expand alphabet by symbols from the passed @p symbols.
      *
@@ -239,6 +245,8 @@ public:
      */
     size_t get_number_of_symbols() const { return symbols_.size(); }
 
+    bool empty() const override { return symbols_.empty(); }
+
 private:
     mata::utils::OrdVector<Symbol> symbols_{}; ///< Map of string transition symbols to symbol values.
     Symbol next_symbol_value_{ 0 }; ///< Next value to be used for a newly added symbol.
@@ -293,9 +301,6 @@ public:
     explicit OnTheFlyAlphabet(const OnTheFlyAlphabet* const alphabet): OnTheFlyAlphabet(*alphabet) {}
     explicit OnTheFlyAlphabet(StringToSymbolMap str_sym_map) : symbol_map_(std::move(str_sym_map)) {}
 
-    OnTheFlyAlphabet& operator=(const OnTheFlyAlphabet& rhs) = default;
-    OnTheFlyAlphabet& operator=(OnTheFlyAlphabet&& rhs) = default;
-
     /**
      * Create alphabet from a list of symbol names.
      * @param symbol_names Names for symbols on transitions.
@@ -321,6 +326,9 @@ public:
     std::string reverse_translate_symbol(Symbol symbol) const override;
 
 public:
+    OnTheFlyAlphabet& operator=(const OnTheFlyAlphabet& rhs) = default;
+    OnTheFlyAlphabet& operator=(OnTheFlyAlphabet&& rhs) = default;
+
     /**
      * @brief Expand alphabet by symbols from the passed @p symbol_names.
      *
@@ -391,6 +399,8 @@ public:
      */
     const StringToSymbolMap& get_symbol_map() const { return symbol_map_; }
 
+    bool empty() const override { return symbol_map_.empty(); }
+
 private:
     StringToSymbolMap symbol_map_{}; ///< Map of string transition symbols to symbol values.
     Symbol next_symbol_value_{}; ///< Next value to be used for a newly added symbol.
diff --git a/include/mata/nfa/algorithms.hh b/include/mata/nfa/algorithms.hh
index 13e8c229f..1eb7e0258 100644
--- a/include/mata/nfa/algorithms.hh
+++ b/include/mata/nfa/algorithms.hh
@@ -129,7 +129,7 @@ Nfa product(const Nfa& lhs, const Nfa& rhs, const std::function<bool(State,State
  * Supports epsilon symbols when @p use_epsilon is set to true.
  * @param[in] lhs First automaton to concatenate.
  * @param[in] rhs Second automaton to concatenate.
- * @param[in] epsilon Epsilon to be used co concatenation (provided @p use_epsilon is true)
+ * @param[in] epsilon Epsilon to be used for concatenation (provided @p use_epsilon is true)
  * @param[in] use_epsilon Whether to concatenate over epsilon symbol.
  * @param[out] lhs_state_renaming Map mapping lhs states to result states.
  * @param[out] rhs_state_renaming Map mapping rhs states to result states.
diff --git a/include/mata/nfa/delta.hh b/include/mata/nfa/delta.hh
index 6d9878a66..1dc32c70d 100644
--- a/include/mata/nfa/delta.hh
+++ b/include/mata/nfa/delta.hh
@@ -118,13 +118,13 @@ public:
     using super::empty, super::size;
     using super::to_vector;
     // dangerous, breaks the sortedness invariant
-    using super::emplace_back;
-    // dangerous, breaks the sortedness invariant
-    using super::push_back;
+    using super::push_back, super::emplace_back;
     // is adding non-const version as well ok?
+    using super::front;
     using super::back;
     using super::pop_back;
     using super::filter;
+    using super::clear;
 
     using super::erase;
 
@@ -301,50 +301,50 @@ public:
     };
 
     /**
-     * @brief Get constant reference to the state post of @p src_state.
+     * @brief Get constant reference to the state post of @p source.
      *
-     * If we try to access a state post of a @p src_state which is present in the automaton as an initial/final state,
+     * If we try to access a state post of a @p source which is present in the automaton as an initial/final state,
      *  yet does not have allocated space in @c Delta, an @c empty_post is returned. Hence, the function has no side
      *  effects (no allocation is performed; iterators remain valid).
-     * @param state_from[in] Source state of a state post to access.
-     * @return State post of @p src_state.
+     * @param source[in] Source state of a state post to access.
+     * @return State post of @p source.
      */
-    const StatePost& state_post(const State src_state) const {
-        if (src_state >= num_of_states()) {
+    const StatePost& state_post(const State source) const {
+        if (source >= num_of_states()) {
             return empty_state_post;
         }
-        return state_posts_[src_state];
+        return state_posts_[source];
     }
 
     /**
-     * @brief Get constant reference to the state post of @p src_state.
+     * @brief Get constant reference to the state post of @p source.
      *
-     * If we try to access a state post of a @p src_state which is present in the automaton as an initial/final state,
+     * If we try to access a state post of a @p source which is present in the automaton as an initial/final state,
      *  yet does not have allocated space in @c Delta, an @c empty_post is returned. Hence, the function has no side
      *  effects (no allocation is performed; iterators remain valid).
-     * @param state_from[in] Source state of a state post to access.
-     * @return State post of @p src_state.
+     * @param source[in] Source state of a state post to access.
+     * @return State post of @p source.
      */
-    const StatePost& operator[](State src_state) const { return state_post(src_state); }
+    const StatePost& operator[](const State source) const { return state_post(source); }
 
     /**
-     * @brief Get mutable (non-constant) reference to the state post of @p src_state.
+     * @brief Get mutable (non-constant) reference to the state post of @p source.
      *
      * The function allows modifying the state post.
      *
      * BEWARE, IT HAS A SIDE EFFECT.
      *
-     * If we try to access a state post of a @p src_state which is present in the automaton as an initial/final state,
-     *  yet does not have allocated space in @c Delta, a new state post for @p src_state will be allocated along with
+     * If we try to access a state post of a @p source which is present in the automaton as an initial/final state,
+     *  yet does not have allocated space in @c Delta, a new state post for @p source will be allocated along with
      *  all state posts for all previous states. This in turn may cause that the entire post data structure is
      *  re-allocated. Iterators to @c Delta will get invalidated.
      * Use the constant 'state_post()' is possible. Or, to prevent the side effect from causing issues, one might want
      *  to make sure that posts of all states in the automaton are allocated, e.g., write an NFA method that allocate
      *  @c Delta for all states of the NFA.
-     * @param state_from[in] Source state of a state post to access.
-     * @return State post of @p src_state.
+     * @param source[in] Source state of a state post to access.
+     * @return State post of @p source.
      */
-    StatePost& mutable_state_post(State src_state);
+    StatePost& mutable_state_post(State source);
 
     void defragment(const BoolVector& is_staying, const std::vector<State>& renaming);
 
@@ -383,15 +383,15 @@ public:
      */
     size_t num_of_transitions() const;
 
-    void add(State state_from, Symbol symbol, State state_to);
+    void add(State source, Symbol symbol, State target);
     void add(const Transition& trans) { add(trans.source, trans.symbol, trans.target); }
-    void remove(State src, Symbol symb, State tgt);
-    void remove(const Transition& trans) { remove(trans.source, trans.symbol, trans.target); }
+    void remove(State source, Symbol symbol, State target);
+    void remove(const Transition& transition) { remove(transition.source, transition.symbol, transition.target); }
 
     /**
      * Check whether @c Delta contains a passed transition.
      */
-    bool contains(State src, Symbol symb, State tgt) const;
+    bool contains(State source, Symbol symbol, State target) const;
     /**
      * Check whether @c Delta contains a transition passed as a triple.
      */
@@ -429,11 +429,11 @@ public:
     /**
      * @brief Add transitions to multiple destinations
      *
-     * @param state_from From
+     * @param source From
      * @param symbol Symbol
-     * @param states Set of states to
+     * @param targets Set of states to
      */
-    void add(const State state_from, const Symbol symbol, const StateSet& states);
+    void add(const State source, const Symbol symbol, const StateSet& targets);
 
     using const_iterator = std::vector<StatePost>::const_iterator;
     const_iterator cbegin() const { return state_posts_.cbegin(); }
@@ -498,7 +498,7 @@ public:
      * @brief Get the maximum non-epsilon used symbol.
      */
     Symbol get_max_symbol() const;
-private:
+protected:
     std::vector<StatePost> state_posts_;
 }; // class Delta.
 
diff --git a/include/mata/nfa/nfa.hh b/include/mata/nfa/nfa.hh
index 4ce4505a7..c771052a5 100644
--- a/include/mata/nfa/nfa.hh
+++ b/include/mata/nfa/nfa.hh
@@ -19,6 +19,7 @@
 #include <utility>
 #include <vector>
 #include <queue>
+#include <optional>
 
 #include "mata/alphabet.hh"
 #include "mata/parser/parser.hh"
@@ -44,9 +45,9 @@
 namespace mata::nfa {
 
 /**
- * A struct representing an NFA.
+ * A class representing an NFA.
  */
-struct Nfa {
+class Nfa {
 public:
     /**
      * @brief For state q, delta[q] keeps the list of transitions ordered by symbols.
@@ -77,9 +78,9 @@ public:
      *
      * @param[in] num_of_states Number of states for which to preallocate Delta.
      */
-    explicit Nfa(const unsigned long num_of_states, StateSet initial_states = {},
-                 StateSet final_states = {}, Alphabet* alphabet = nullptr)
-        : delta(num_of_states), initial(initial_states), final(final_states), alphabet(alphabet) {}
+    explicit Nfa(const size_t num_of_states, utils::SparseSet<State> initial_states = {},
+                 utils::SparseSet<State> final_states = {}, Alphabet* alphabet = nullptr)
+        : delta(num_of_states), initial(std::move(initial_states)), final(std::move(final_states)), alphabet(alphabet) {}
 
     /**
      * @brief Construct a new explicit NFA from other NFA.
@@ -105,6 +106,27 @@ public:
      */
     State add_state(State state);
 
+    /**
+     * Inserts a @p word into the NFA from a source state @p source to a target state @p target.
+     * Creates new states along the path of the @p word.
+     *
+     * @param source The source state where the word begins. It must already be a part of the automaton.
+     * @param word The nonempty word to be inserted into the NFA.
+     * @param target The target state where the word ends.
+     * @return The state @p target where the inserted @p word ends.
+     */
+    State insert_word(State source, const Word& word, State target);
+
+    /**
+     * Inserts a @p word into the NFA from a source state @p source to a new target state.
+     * Creates new states along the path of the @p word.
+     *
+     * @param source The source state where the word begins. It must already be a part of the automaton.
+     * @param word The nonempty word to be inserted into the NFA.
+     * @return The newly created target state where the inserted @p word ends.
+     */
+    State insert_word(State source, const Word& word);
+
     /**
      * @brief Get the current number of states in the whole automaton.
      *
@@ -280,14 +302,16 @@ public:
     /**
      * @brief Prints the automaton in DOT format
      *
+     * @param[in] ascii Whether to use ASCII characters for the output.
      * @return automaton in DOT format
      */
-    std::string print_to_dot() const;
+    std::string print_to_dot(const bool ascii = false) const;
     /**
      * @brief Prints the automaton to the output stream in DOT format
+     *
+     * @param[in] ascii Whether to use ASCII characters for the output.
      */
-    void print_to_dot(std::ostream &output) const;
-
+    void print_to_dot(std::ostream &output, const bool ascii = false) const;
     /**
      * @brief Prints the automaton to the file in DOT format
      * @param filename Name of the file to print the automaton to
@@ -404,7 +428,7 @@ public:
      * you can get all words by calling
      *      get_words(aut.num_of_states())
      */
-    std::set<Word> get_words(unsigned max_length) const;
+    std::set<Word> get_words(size_t max_length) const;
 
     /**
      * @brief Get any arbitrary accepted word in the language of the automaton.
@@ -471,7 +495,7 @@ public:
      * @pre @c this is a deterministic automaton.
      */
     Nfa& complement_deterministic(const mata::utils::OrdVector<Symbol>& symbols, std::optional<State> sink_state = std::nullopt);
-}; // struct Nfa.
+}; // class Nfa.
 
 // Allow variadic number of arguments of the same type.
 //
diff --git a/include/mata/nfa/strings.hh b/include/mata/nfa/strings.hh
index 22cf477fa..fc6fd60ac 100644
--- a/include/mata/nfa/strings.hh
+++ b/include/mata/nfa/strings.hh
@@ -116,6 +116,15 @@ std::set<Symbol> get_accepted_symbols(const Nfa& nfa);
  */
 std::set<std::pair<int, int>> get_word_lengths(const Nfa& aut);
 
+/**
+ * @brief Modify @p nfa in-place to remove outgoing transitions from final states.
+ *
+ * If @p nfa accepts empty string, returned NFA will accept only the empty string.
+ * @param nfa NFA to modify.
+ * @return The reluctant version of @p nfa.
+ */
+Nfa reluctant_nfa(Nfa nfa);
+
 /**
  * @brief Checks if the automaton @p nfa accepts only a single word \eps.
  *
diff --git a/include/mata/nfa/types.hh b/include/mata/nfa/types.hh
index 40a016d7f..9d19143f2 100644
--- a/include/mata/nfa/types.hh
+++ b/include/mata/nfa/types.hh
@@ -43,7 +43,7 @@ public:
     static const Symbol max_symbol = std::numeric_limits<Symbol>::max();
 };
 
-struct Nfa; ///< A non-deterministic finite automaton.
+class Nfa; ///< A non-deterministic finite automaton.
 
 /// An epsilon symbol which is now defined as the maximal value of data type used for symbols.
 constexpr Symbol EPSILON = Limits::max_symbol;
diff --git a/include/mata/nft/algorithms.hh b/include/mata/nft/algorithms.hh
new file mode 100644
index 000000000..6b5aefe81
--- /dev/null
+++ b/include/mata/nft/algorithms.hh
@@ -0,0 +1,129 @@
+/* algorithms.hh -- Wrapping up algorithms for Nft manipulation which would be otherwise in anonymous namespaces.
+ */
+
+#ifndef MATA_NFT_INTERNALS_HH_
+#define MATA_NFT_INTERNALS_HH_
+
+#include "nft.hh"
+#include "mata/simlib/util/binary_relation.hh"
+
+/**
+ * Concrete NFT implementations of algorithms, such as complement, inclusion, or universality checking.
+ *
+ * This is a separation of the implementation from the interface defined in mata::nft.
+ * Note, that in mata::nft interface, there are particular dispatch functions calling
+ * these function according to parameters provided by a user.
+ * E.g. we can call the following function: `is_universal(aut, alph, {{'algorithm', 'antichains'}})`
+ * to check for universality based on antichain-based algorithm.
+ *
+ * In particular, this includes algorithms for:
+ *   1. Complementation,
+ *   2. Inclusion,
+ *   3. Universality checking,
+ *   4. Intersection/concatenation with epsilon transitions, or,
+ *   5. Computing relation.
+ */
+namespace mata::nft::algorithms {
+
+/**
+ * Brzozowski minimization of automata (revert -> determinize -> revert -> determinize).
+ * @param[in] aut Automaton to be minimized.
+ * @return Minimized automaton.
+ */
+Nft minimize_brzozowski(const Nft& aut);
+
+/**
+ * Complement implemented by determization, adding sink state and making automaton complete. Then it adds final states
+ *  which were non final in the original automaton.
+ * @param[in] aut Automaton to be complemented.
+ * @param[in] symbols Symbols needed to make the automaton complete.
+ * @param[in] minimize_during_determinization Whether the determinized automaton is computed by (brzozowski)
+ *  minimization.
+ * @return Complemented automaton.
+ */
+Nft complement_classical(const Nft& aut, const mata::utils::OrdVector<Symbol>& symbols,
+                         bool minimize_during_determinization = false);
+
+/**
+ * Inclusion implemented by complementation of bigger automaton, intersecting it with smaller and then it checks
+ *  emptiness of intersection.
+ * @param[in] smaller Automaton which language should be included in the bigger one.
+ * @param[in] bigger Automaton which language should include the smaller one.
+ * @param[in] alphabet Alphabet of both automata (it is computed automatically, but it is more efficient to set it if
+ *  you have it).
+ * @param[out] cex A potential counterexample word which breaks inclusion
+ * @return True if smaller language is included,
+ * i.e., if the final intersection of smaller complement of bigger is empty.
+ */
+bool is_included_naive(const Nft& smaller, const Nft& bigger, const Alphabet* alphabet = nullptr, Run* cex = nullptr, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * Inclusion implemented by antichain algorithms.
+ * @param[in] smaller Automaton which language should be included in the bigger one
+ * @param[in] bigger Automaton which language should include the smaller one
+ * @param[in] alphabet Alphabet of both automata (not needed for antichain algorithm)
+ * @param[out] cex A potential counterexample word which breaks inclusion
+ * @return True if smaller language is included,
+ * i.e., if the final intersection of smaller complement of bigger is empty.
+ */
+bool is_included_antichains(const Nft& smaller, const Nft& bigger, const Alphabet*  alphabet = nullptr, Run* cex = nullptr, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * Universality check implemented by checking emptiness of complemented automaton
+ * @param[in] aut Automaton which universality is checked
+ * @param[in] alphabet Alphabet of the automaton
+ * @param[out] cex Counterexample word which eventually breaks the universality
+ * @return True if the complemented automaton has non empty language, i.e., the original one is not universal
+ */
+bool is_universal_naive(const Nft& aut, const Alphabet& alphabet, Run* cex);
+
+/**
+ * Universality checking based on subset construction with antichain.
+ * @param[in] aut Automaton which universality is checked
+ * @param[in] alphabet Alphabet of the automaton
+ * @param[out] cex Counterexample word which eventually breaks the universality
+ * @return True if the automaton is universal, otherwise false.
+ */
+bool is_universal_antichains(const Nft& aut, const Alphabet& alphabet, Run* cex);
+
+Simlib::Util::BinaryRelation compute_relation(
+        const Nft& aut,
+        const ParameterMap&  params = {{ "relation", "simulation"}, { "direction", "forward"}});
+
+/**
+ * @brief Compute product of two NFTs, final condition is to be specified.
+ *
+ * @param[in] lhs First NFT to compute intersection for.
+ * @param[in] rhs Second NFT to compute intersection for.
+ * @param[in] final_condition The predicate that tells whether a pair of states is final (conjunction for intersection).
+ * @param[out] prod_map Can be used to get the mapping of the pairs of the original states to product states.
+ *   Mostly useless, it is only filled in and returned if !=nullptr, but the algorithm internally uses another data structures,
+ *   because this one is too slow.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence of @c DONT_CARE.
+ * @param[in] lhs_first_aux_state The first auxiliary state in @p lhs. Two auxiliary states can not form a product state.
+ * @param[in] rhs_first_aux_state The first auxiliary state in @p rhs. Two auxiliary states con not form a product state.
+ * @return NFT as a product of NFTs @p lhs and @p rhs with ε handled as regular symbols.
+ */
+Nft product(const Nft& lhs, const Nft& rhs, const std::function<bool(State,State)> && final_condition,
+            std::unordered_map<std::pair<State,State>, State> *prod_map = nullptr, JumpMode jump_mode = JumpMode::RepeatSymbol,
+            const State lhs_first_aux_state = Limits::max_state, const State rhs_first_aux_state = Limits::max_state);
+
+/**
+ * @brief Concatenate two NFTs.
+ *
+ * Supports epsilon symbols when @p use_epsilon is set to true.
+ * @param[in] lhs First automaton to concatenate.
+ * @param[in] rhs Second automaton to concatenate.
+ * @param[in] epsilon Epsilon to be used for concatenation (provided @p use_epsilon is true)
+ * @param[in] use_epsilon Whether to concatenate over epsilon symbol.
+ * @param[out] lhs_state_renaming Map mapping lhs states to result states.
+ * @param[out] rhs_state_renaming Map mapping rhs states to result states.
+ * @return Concatenated automaton.
+ */
+Nft concatenate_eps(const Nft& lhs, const Nft& rhs, const Symbol& epsilon, bool use_epsilon = false,
+                    StateRenaming* lhs_state_renaming = nullptr, StateRenaming* rhs_state_renaming = nullptr);
+
+} // Namespace mata::nft::algorithms.
+
+#endif // MATA_NFT_INTERNALS_HH_
diff --git a/include/mata/nft/builder.hh b/include/mata/nft/builder.hh
new file mode 100644
index 000000000..046f9eef9
--- /dev/null
+++ b/include/mata/nft/builder.hh
@@ -0,0 +1,115 @@
+// TODO: Insert file header.
+
+#ifndef LIBMATA_NFT_BUILDER_HH
+#define LIBMATA_NFT_BUILDER_HH
+
+#include "mata/nfa/builder.hh"
+#include "nft.hh"
+
+#include <optional>
+#include <filesystem>
+
+/**
+ * Namespace providing options to build NFAs.
+ */
+namespace mata::nft::builder {
+
+using namespace mata::nft;
+
+using NameStateMap = std::unordered_map<std::string, State>;
+
+/**
+ * Create an automaton accepting only a single @p word.
+ */
+Nft create_single_word_nft(const Word& word);
+
+/**
+ * Create an automaton accepting only a single @p word.
+ *
+ * @param word Word to accept.
+ * @param alphabet Alphabet to use in NFA for translating word into symbols. If specified, the alphabet has to contain
+ *  translations for all the word symbols. If left empty, a new alphabet with only the symbols of the word will be
+ *  created.
+ */
+Nft create_single_word_nft(const WordName& word, Alphabet* alphabet = nullptr);
+
+/**
+ * Create automaton accepting only epsilon string.
+ */
+Nft create_empty_string_nft(size_t num_of_levels = DEFAULT_NUM_OF_LEVELS);
+
+/**
+ * Create automaton accepting sigma star over the passed alphabet using DONT_CARE symbol.
+ *
+ * @param[in] num_of_levels Number of levels in the created NFT.
+ */
+Nft create_sigma_star_nft(size_t num_of_levels = DEFAULT_NUM_OF_LEVELS);
+
+/**
+ * Create automaton accepting sigma star over the passed alphabet.
+ *
+ * @param[in] alphabet Alphabet to construct sigma star automaton with. When alphabet is left empty, the default empty
+ *  alphabet is used, creating an automaton accepting only the empty string.
+ * @param[in] num_of_levels Number of levels in the created NFT.
+ */
+Nft create_sigma_star_nft(const Alphabet* alphabet = new OnTheFlyAlphabet{}, size_t num_of_levels = DEFAULT_NUM_OF_LEVELS);
+
+/** Loads an automaton from Parsed object */
+// TODO this function should the same thing as the one taking IntermediateAut or be deleted
+Nft construct(const parser::ParsedSection& parsec, Alphabet* alphabet, NameStateMap* state_map = nullptr);
+
+/** Loads an automaton from Parsed object */
+Nft construct(const IntermediateAut& inter_aut, Alphabet* alphabet, NameStateMap* state_map = nullptr);
+/** Loads an automaton from Parsed object; version for python binding */
+void construct(
+    Nft* result, const IntermediateAut& inter_aut, Alphabet* alphabet, NameStateMap* state_map = nullptr
+);
+
+template<class ParsedObject>
+Nft construct(const ParsedObject& parsed, Alphabet* alphabet = nullptr,
+              NameStateMap* state_map = nullptr) {
+    OnTheFlyAlphabet tmp_alphabet{};
+    if (!alphabet) {
+        alphabet = &tmp_alphabet;
+    }
+    return construct(parsed, alphabet, state_map);
+} // construct().
+
+/**
+ * Parse NFA from the mata format in an input stream.
+ *
+ * @param nft_stream Input stream containing NFA in mata format.
+ * @throws std::runtime_error Parsing of NFA fails.
+ */
+Nft parse_from_mata(std::istream& nft_stream);
+
+/**
+ * Parse NFA from the mata format in a string.
+ *
+ * @param nft_in_mata String containing NFA in mata format.
+ * @throws std::runtime_error Parsing of NFA fails.
+ */
+Nft parse_from_mata(const std::string& nft_in_mata);
+
+/**
+ * Parse NFA from the mata format in a file.
+ *
+ * @param nft_file Path to the file containing NFA in mata format.
+ * @throws std::runtime_error @p nft_file does not exist.
+ * @throws std::runtime_error Parsing of NFA fails.
+ */
+Nft parse_from_mata(const std::filesystem::path& nft_file);
+
+/**
+ * Create NFT from NFA.
+ * @param nfa_state NFA to create NFT from.
+ * @param num_of_levels Number of levels of NFT.
+ * @param next_levels_symbol Symbol to use on non-NFA levels. std::nullopt means using the same symbol on all levels.
+ * @param epsilons Which symbols handle as epsilons.
+ * @return NFT representing @p nfa_state with @p num_of_levels number of levels.
+ */
+Nft create_from_nfa(const nfa::Nfa& nfa_state, size_t num_of_levels = DEFAULT_NUM_OF_LEVELS, std::optional<Symbol> next_levels_symbol = {}, const std::set<Symbol>& epsilons = { EPSILON });
+
+} // namespace mata::nft::builder.
+
+#endif //LIBMATA_NFT_BUILDER_HH
diff --git a/include/mata/nft/delta.hh b/include/mata/nft/delta.hh
new file mode 100644
index 000000000..9130fc8f6
--- /dev/null
+++ b/include/mata/nft/delta.hh
@@ -0,0 +1,65 @@
+// TODO: Insert file header.
+
+#ifndef MATA_NFT_DELTA_HH
+#define MATA_NFT_DELTA_HH
+
+#include "mata/utils/sparse-set.hh"
+#include "mata/utils/synchronized-iterator.hh"
+#include "mata/alphabet.hh"
+#include "mata/nft/types.hh"
+
+#include "mata/nfa/delta.hh"
+
+#include <iterator>
+
+namespace mata::nft {
+
+/// A single transition in Delta represented as a triple(source, symbol, target).
+using Transition = mata::nfa::Transition;
+
+/**
+ * Move from a @c StatePost for a single source state, represented as a pair of @c symbol and target state @c target.
+ */
+using Move = mata::nfa::Move;
+
+/**
+ * Structure represents a post of a single @c symbol: a set of target states in transitions.
+ *
+ * A set of @c SymbolPost, called @c StatePost, is describing the automata transitions from a single source state.
+ */
+using SymbolPost = mata::nfa::SymbolPost;
+
+/**
+ * @brief A data structure representing possible transitions over different symbols from a source state.
+ *
+ * It is an ordered vector containing possible @c SymbolPost (i.e., pair of symbol and target states).
+ * @c SymbolPosts in the vector are ordered by symbols in @c SymbolPosts.
+ */
+using StatePost = mata::nfa::StatePost;
+
+
+/**
+ * @brief Specialization of utils::SynchronizedExistentialIterator for iterating over SymbolPosts.
+ */
+using SynchronizedExistentialSymbolPostIterator = mata::nfa::SynchronizedExistentialSymbolPostIterator;
+
+/**
+ * @brief Delta is a data structure for representing transition relation.
+ *
+ * Transition is represented as a triple Trans(source state, symbol, target state). Move is the part (symbol, target
+ *  state), specified for a single source state.
+ * Its underlying data structure is vector of StatePost classes. Each index to the vector corresponds to one source
+ *  state, that is, a number for a certain state is an index to the vector of state posts.
+ * Transition relation (delta) in Mata stores a set of transitions in a four-level hierarchical structure:
+ *  Delta, StatePost, SymbolPost, and a set of target states.
+ * A vector of 'StatePost's indexed by a source states on top, where the StatePost for a state 'q' (whose number is
+ *  'q' and it is the index to the vector of 'StatePost's) stores a set of 'Move's from the source state 'q'.
+ * Namely, 'StatePost' has a vector of 'SymbolPost's, where each 'SymbolPost' stores a symbol 'a' and a vector of
+ *  target states of 'a'-moves from state 'q'. 'SymbolPost's are ordered by the symbol, target states are ordered by
+ *  the state number.
+ */
+using Delta = mata::nfa::Delta;
+
+} // namespace mata::nft.
+
+#endif //MATA_DELTA_HH
diff --git a/include/mata/nft/nft.hh b/include/mata/nft/nft.hh
new file mode 100644
index 000000000..7c2609379
--- /dev/null
+++ b/include/mata/nft/nft.hh
@@ -0,0 +1,835 @@
+/* nft.hh -- Nondeterministic finite automaton (over finite words).
+ */
+
+#ifndef MATA_NFT_HH_
+#define MATA_NFT_HH_
+
+// Static data structures, such as search stack, in algorithms. Might have some effect on some algorithms (like
+//  fragile_revert).
+//#define _STATIC_STRUCTURES_
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <memory>
+#include <limits>
+#include <set>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "mata/alphabet.hh"
+#include "mata/parser/parser.hh"
+#include "mata/utils/utils.hh"
+#include "mata/utils/ord-vector.hh"
+#include "mata/parser/inter-aut.hh"
+#include "mata/utils/synchronized-iterator.hh"
+#include "mata/utils/sparse-set.hh"
+#include "types.hh"
+#include "delta.hh"
+
+#include "mata/nfa/nfa.hh"
+
+/**
+ * Nondeterministic Finite Automata including structures, transitions and algorithms.
+ *
+ * In particular, this includes:
+ *   1. Structures (Automaton, Transitions, Results, Delta),
+ *   2. Algorithms (operations, checks, tests),
+ *   3. Constructions.
+ *
+ * Other algorithms are included in mata::nft::Plumbing (simplified API for, e.g., binding)
+ * and mata::nft::algorithms (concrete implementations of algorithms, such as for complement).
+ */
+namespace mata::nft {
+
+class Levels: public std::vector<Level> {
+    using super = std::vector<Level>;
+public:
+    Levels& set(State state, Level level = DEFAULT_LEVEL);
+    using super::vector;
+    using super::operator=;
+    Levels(const std::vector<Level>& levels): super{ levels } {}
+    Levels(std::vector<Level>&& levels): super{ std::move(levels) } {}
+    Levels& operator=(const std::vector<Level>& levels) {
+        if (this != &levels) { super::operator=(levels); }
+        return *this;
+    }
+    Levels& operator=(std::vector<Level>&& levels) {
+        if (this != &levels) { super::operator=(std::move(levels)); }
+        return *this;
+    }
+
+    /**
+     * @brief Append @p levels_vector to the end of @c this.
+     *
+     * @param[in] levels_vector Vector of levels to be appended.
+     */
+    void append(const Levels& levels_vector) { for (const Level& level: levels_vector) { push_back(level); } }
+};
+
+/**
+ * A class representing an NFT.
+ */
+class Nft: public nfa::Nfa {
+public:
+    /**
+     * @brief Vector of levels giving each state a level in range from 0 to @c num_of_levels - 1.
+     *
+     * For state `q`, `levels[q]` gives the state `q` a level.
+     */
+    Levels levels{};
+    /**
+     * @brief Number of levels (tracks) the transducer recognizes. Each transducer transition will comprise
+     *  @c num_of_levels of NFA transitions.
+     */
+    size_t num_of_levels{ DEFAULT_NUM_OF_LEVELS };
+
+    /// Key value store for additional attributes for the NFT. Keys are attribute names as strings and the value types
+    ///  are up to the user.
+    /// For example, we can set up attributes such as "state_dict" for state dictionary attribute mapping states to their
+    ///  respective names, or "transition_dict" for transition dictionary adding a human-readable meaning to each
+    ///  transition.
+    // TODO: When there is a need for state dictionary, consider creating default library implementation of state
+    //  dictionary in the attributes.
+
+public:
+    explicit Nft(Delta delta = {}, utils::SparseSet<State> initial_states = {},
+                 utils::SparseSet<State> final_states = {}, Levels levels = {}, const size_t num_of_levels = DEFAULT_NUM_OF_LEVELS,
+                 Alphabet* alphabet = nullptr)
+        : Nfa(std::move(delta), std::move(initial_states), std::move(final_states), alphabet), num_of_levels(num_of_levels) {
+        this->levels = levels.empty() ? Levels(num_of_states(), DEFAULT_LEVEL) : std::move(levels);
+    }
+    /**
+     * @brief Construct a new explicit NFT with num_of_states states and optionally set initial and final states.
+     *
+     * @param[in] num_of_states Number of states for which to preallocate Delta.
+     */
+    explicit Nft(const size_t num_of_states, utils::SparseSet<State> initial_states = {},
+                 utils::SparseSet<State> final_states = {}, Levels levels = {}, const size_t num_of_levels = DEFAULT_NUM_OF_LEVELS,
+                 Alphabet* alphabet = nullptr)
+        : Nfa(num_of_states, std::move(initial_states), std::move(final_states), alphabet),
+          num_of_levels(num_of_levels) {
+        this->levels = levels.empty() ? Levels(num_of_states, DEFAULT_LEVEL) : std::move(levels);
+    }
+
+    static Nft with_levels(const size_t num_of_levels, const size_t num_of_states = 0, Levels levels = {}, utils::SparseSet<State> initial_states = {},
+                 utils::SparseSet<State> final_states = {}, Alphabet* alphabet = nullptr) {
+        return Nft{ num_of_states, std::move(initial_states), std::move(final_states), std::move(levels), num_of_levels, alphabet };
+    }
+    static Nft with_levels(const size_t num_of_levels, Delta delta, Levels levels = {}, utils::SparseSet<State> initial_states = {},
+                 utils::SparseSet<State> final_states = {}, Alphabet* alphabet = nullptr) {
+        return Nft{ std::move(delta), std::move(initial_states), std::move(final_states), std::move(levels), num_of_levels, alphabet };
+    }
+
+    explicit Nft(const mata::nfa::Nfa& other): mata::nfa::Nfa(other), levels(other.num_of_states(), DEFAULT_LEVEL) {}
+
+    /**
+     * @brief Construct a new explicit NFT from other NFT.
+     */
+    Nft(const Nft& other) = default;
+
+    Nft(Nft&& other) noexcept
+        : levels{ std::move(other.levels) }, num_of_levels{ other.num_of_levels } {
+            delta = std::move(other.delta);
+            initial = std::move(other.initial);
+            final = std::move(other.final);
+            attributes = std::move(other.attributes);
+            alphabet = other.alphabet;
+            other.alphabet = nullptr;
+    }
+
+    Nft& operator=(const Nft& other) = default;
+    Nft& operator=(Nft&& other) noexcept;
+
+    /**
+     * Add a new (fresh) state to the automaton.
+     * @return The newly created state.
+     */
+    State add_state();
+
+    /**
+     * Add state @p state to @c this if @p state is not in @c this yet.
+     * @return The requested @p state.
+     */
+    State add_state(State state);
+
+    /**
+     * Add a new (fresh) state to the automaton with level @p level.
+     * @return The newly created state.
+     */
+    State add_state_with_level(Level level);
+
+    /**
+     * Add state @p state to @c this with level @p level if @p state is not in @c this yet.
+     * @return The requested @p state.
+     */
+    State add_state_with_level(State state, Level level);
+
+    /**
+     * Inserts a @p word into the NFT from a source state @p source to a target state @p target.
+     * Creates new states along the path of the @p word.
+     *
+     * If the length of @p word is less than @c num_of_levels, then the last symbol of @p word will form a transition
+     *  going directly from the last inner state to @p target.
+     *
+     * @param source The source state where the word begins. @p source must already exist.
+     * @param word The nonempty word to be inserted into the NFA.
+     * @param target The target state where the word ends. @p target must already exist.
+     * @return The state @p target where the inserted @p word ends.
+     */
+    State insert_word(State source, const Word &word, State target);
+
+    /**
+     * @brief Inserts a @p word into the NFT from a source state @p source to a newly created target state, creating
+     *  new states along the path of the @p word.
+     *
+     * If the length of @p word is less than @c num_of_levels, then the last symbol of @p word
+     *  will form a transition going directly from the last inner state to the newly created target.
+     *
+     * @param source The source state where the word begins. @p source must already exist.
+     * @param word The nonempty word to be inserted into the NFA.
+     * @return The newly created target where the inserted word ends.
+     */
+    State insert_word(State source, const Word &word);
+
+    /**
+     * @brief Add a single NFT transition.
+     *
+     * The transition leads from a source state @p source to a target state @p target, creating new inner states for all
+     *  tapes.
+     *
+     * If the length of @p symbols is less than @c num_of_levels, then the last symbol of @p symbols will form a jump
+     *  transition going directly from the last inner state to @p target.
+     *
+     * @param source The source state where the NFT transition begins. @p source must already exist.
+     * @param symbols The nonempty set of symbols, one for each tape to be inserted into the NFT.
+     * @param target The target state where the NFT transition ends. @p target must already exist.
+     * @return The target state @p target.
+     */
+    State add_transition(State source, const std::vector<Symbol>& symbols, State target);
+
+    /**
+     * @brief Add a single NFT transition to the NFT from a source state @p source to a newly created target state,
+     *  creating new inner states for all tapes.
+     *
+     * If the length of @p symbols is less than @c num_of_levels, the last symbol of @p symbols
+     *  will form a transition going directly from the last inner state to the newly created target.
+     *
+     * @param source The source state where the transition begins. @p source must already exist.
+     * @param symbols The nonempty set of symbols, one for each tape to be inserted into the NFT.
+     * @return The target state @p target.
+     */
+    State add_transition(State source, const std::vector<Symbol>& symbols);
+
+    /**
+     * @brief Inserts a word, which is created by interleaving parts from @p word_parts_on_levels, into the NFT
+     *  from a source state @p source to a target state @p target, creating new states along the path of @p word.
+     *
+     * The length of the inserted word equals @c num_of_levels * the maximum word length in the vector @p word_parts_on_levels.
+     * At least one Word in @p word_parts_on_levels must be nonempty.
+     * The vector @p word_parts_on_levels must have a size equal to @c num_of_levels.
+     * Words shorter than the maximum word length are interpreted as words followed by a sequence of epsilons to match the maximum length.
+     *
+     * @param source The source state where the word begins. @p source must already exist and be of a level 0.
+     * @param word_parts_on_levels The vector of word parts, with each part corresponding to a different level.
+     * @param target The target state where the word ends. @p target must already exist and be of a level 0.
+     * @return The state @p target where the inserted @p word_parts_on_levels ends.
+     */
+    State insert_word_by_parts(State source, const std::vector<Word>& word_parts_on_levels, State target);
+
+    /**
+     * @brief Inserts a word, which is created by interleaving parts from @p word_parts_on_levels, into the NFT
+     *  from a source state @p source to a target state @p target, creating new states along the path of @p word.
+     *
+     * The length of the inserted word equals @c num_of_levels * the maximum word length in the vector @p word_parts_on_levels.
+     * At least one Word in @p word_parts_on_levels must be nonempty.
+     * The vector @p word_parts_on_levels must have a size equal to @c num_of_levels.
+     * Words shorter than the maximum word length are interpreted as words followed by a sequence of epsilons to match the maximum length.
+     *
+     * @param source The source state where the word begins. @p source must already exist be of a level 0.
+     * @param word_parts_on_levels The vector of word parts, with each part corresponding to a different level.
+     * @return The newly created target where the inserted @p word_parts_on_levels ends.
+     */
+    State insert_word_by_parts(State source, const std::vector<Word>& word_parts_on_levels);
+
+    /**
+    * Inserts identity transitions into the NFT.
+    *
+    * @param state The state where the identity transition will be inserted. @p state server as both the source and
+    *  target state.
+    * @param symbols The vector of symbols used for the identity transition. Identity will be created for each symbol in
+    *  the vector.
+    * @param jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+    * is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+    * of @c DONT_CARE symbols.
+    */
+    void insert_identity(State state, const std::vector<Symbol>& symbols, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+    /**
+    * Inserts an identity transition into the NFT.
+    *
+    * @param state The state where the identity transition will be inserted. @p state server as both the source and
+    *  target state.
+    * @param symbol The symbol used for the identity transition.
+    * @param jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+    * is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+    * of @c DONT_CARE symbols.
+    */
+    void insert_identity(State state, Symbol symbol, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+    /**
+     * @brief Clear the underlying NFT to a blank NFT.
+     *
+     * The whole NFT is cleared, each member is set to its zero value.
+     */
+    void clear();
+
+    /**
+     * @brief Check if @c this is exactly identical to @p aut.
+     *
+     * This is exact equality of automata, including state numbering (so even stronger than isomorphism),
+     *  essentially only useful for testing purposes.
+     * @return True if automata are exactly identical, false otherwise.
+     */
+    bool is_identical(const Nft& aut) const;
+
+    /**
+     * @brief Remove inaccessible (unreachable) and not co-accessible (non-terminating) states in-place.
+     *
+     * Remove states which are not accessible (unreachable; state is accessible when the state is the endpoint of a path
+     * starting from an initial state) or not co-accessible (non-terminating; state is co-accessible when the state is
+     * the starting point of a path ending in a final state).
+     *
+     * @param[out] state_renaming Mapping of trimmed states to new states.
+     * @return @c this after trimming.
+     */
+    Nft& trim(StateRenaming* state_renaming = nullptr);
+
+    /**
+     * @brief In-place concatenation.
+     */
+    Nft& concatenate(const Nft& aut);
+
+    /**
+     * @brief In-place union
+     */
+    Nft& uni(const Nft &aut);
+
+    /**
+     * Unify transitions to create a directed graph with at most a single transition between two states.
+     * @param[in] abstract_symbol Abstract symbol to use for transitions in digraph.
+     * @return An automaton representing a directed graph.
+     */
+    Nft get_one_letter_aut(Symbol abstract_symbol = 'x') const;
+
+    /**
+     * Unify transitions to create a directed graph with at most a single transition between two states.
+     *
+     * @param[out] result An automaton representing a directed graph.
+     */
+    void get_one_letter_aut(Nft& result) const;
+
+    /**
+     * @brief Modifies transducer to have only one level.
+     *
+     * @param[in] dont_care_symbol_replacements Vector of symbols to replace @c DONT_CARE symbols with.
+     * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+     * is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+     * of @c DONT_CARE symbols.
+     */
+    void make_one_level_aut(const utils::OrdVector<Symbol> &dont_care_symbol_replacements = { DONT_CARE }, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+    /**
+     * @brief Creates transducer from the current one with only one level.
+     *
+     * @param[in] dont_care_symbol_replacements Vector of symbols to replace @c DONT_CARE symbols with.
+     * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+     * is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+     * of @c DONT_CARE symbols.
+     */
+    Nft get_one_level_aut(const utils::OrdVector<Symbol> &dont_care_symbol_replacements = { DONT_CARE }, JumpMode jump_mode = JumpMode::RepeatSymbol) const;
+
+    /**
+     * @brief Modifies transducer to have only one level.
+     *
+     * @param[out] result A transducer with only one level.
+     * @param[in] dont_care_symbol_replacements Vector of symbols to replace @c DONT_CARE symbols with.
+     * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+     * is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+     * of @c DONT_CARE symbols.
+     */
+    void get_one_level_aut(Nft& result, const utils::OrdVector<Symbol> &dont_care_symbol_replacements = { DONT_CARE }, JumpMode jump_mode = JumpMode::RepeatSymbol) const;
+
+    /**
+     * @brief Prints the automaton in DOT format
+     *
+     * @param[in] ascii Whether to use ASCII characters for the output.
+     * @return automaton in DOT format
+     */
+    std::string print_to_DOT(bool ascii = false) const;
+    /**
+     * @brief Prints the automaton to the output stream in DOT format
+     *
+     * @param[in] ascii Whether to use ASCII characters for the output.
+     */
+    void print_to_DOT(std::ostream &output, bool ascii = false) const;
+    /**
+     * @brief Prints the automaton in mata format
+     *
+     * If you need to parse the automaton again, use IntAlphabet in construct()
+     *
+     * @return automaton in mata format
+     * TODO handle alphabet of the automaton, currently we print the exact value of the symbols
+     */
+    std::string print_to_mata() const;
+    /**
+     * @brief Prints the automaton to the output stream in mata format
+     *
+     * If you need to parse the automaton again, use IntAlphabet in construct()
+     *
+     * TODO handle alphabet of the automaton, currently we print the exact value of the symbols
+     */
+    void print_to_mata(std::ostream &output) const;
+
+    /// Is the language of the automaton universal?
+    bool is_universal(const Alphabet& alphabet, Run* cex = nullptr,
+                      const ParameterMap& params = {{ "algorithm", "antichains" }}) const;
+    /// Is the language of the automaton universal?
+    bool is_universal(const Alphabet& alphabet, const ParameterMap& params) const;
+
+    /// Checks whether a word is in the language of an automaton.
+    bool is_in_lang(const Run& word) const;
+    /// Checks whether a word is in the language of an automaton.
+    bool is_in_lang(const Word& word) { return is_in_lang(Run{ word, {} }); }
+
+    /// Checks whether the prefix of a string is in the language of an automaton
+    bool is_prfx_in_lang(const Run& word) const;
+
+    /**
+     * @brief Checks whether track words are in the language of the transducer.
+     *
+     * That is, the function checks whether a tuple @p track_words (word1, word2, word3, ..., wordn) is in the regular
+     *  relation accepted by the transducer with 'n' levels (tracks).
+     */
+    bool is_tuple_in_lang(const std::vector<Word>& track_words);
+
+    std::pair<Run, bool> get_word_for_path(const Run& run) const;
+
+    /**
+     * @brief Get the set of all words in the language of the automaton whose length is <= @p max_length
+     *
+     * If you have an automaton with finite language (can be checked using @ref is_acyclic),
+     * you can get all words by calling
+     *      get_words(aut.num_of_states())
+     */
+    std::set<Word> get_words(size_t max_length) const;
+
+    /**
+     * @brief Apply @p nfa to @c this.
+     *
+     * Identical to `Id(nfa) || this`.
+     * @param nfa NFA to apply.
+     * @param level_to_apply_on Which level to apply the @p nfa on.
+     * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+     *  is interpreted as a sequence repeating the same symbol, or as a single instance of the symbol followed by a
+     *  sequence of @c DONT_CARE symbols.
+     * @return
+     */
+    Nft apply(
+        const nfa::Nfa& nfa, Level level_to_apply_on = 0,
+        JumpMode jump_mode = JumpMode::RepeatSymbol) const;
+
+    /**
+     * @brief Copy NFT as NFA.
+     *
+     * Transitions are not updated to only have one level.
+     * @return A newly created NFA with copied members from NFT.
+     */
+    Nfa to_nfa_copy() const { return Nfa{ delta, initial, final, alphabet }; }
+
+    /**
+     * @brief Move NFT as NFA.
+     *
+     * The NFT can no longer be used.
+     * Transitions are not updated to only have one level.
+     * @return A newly created NFA with moved members from NFT.
+     */
+    Nfa to_nfa_move() { return Nfa{ std::move(delta), std::move(initial), std::move(final), alphabet }; }
+
+    /**
+     * @brief Copy NFT as NFA updating the transitions to have one level only.
+     *
+     * @param[in] dont_care_symbol_replacements Vector of symbols to replace @c DONT_CARE symbols with.
+     * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+     * is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+     * of @c DONT_CARE symbols.
+     * @return A newly created NFA with copied members from NFT with updated transitions.
+     */
+    Nfa to_nfa_update_copy(
+        const utils::OrdVector<Symbol>& dont_care_symbol_replacements = { DONT_CARE },
+        JumpMode jump_mode = JumpMode::RepeatSymbol
+    ) const;
+
+    /**
+     * @brief Move NFT as NFA updating the transitions to have one level only.
+     *
+     * The NFT can no longer be used.
+     * @param[in] dont_care_symbol_replacements Vector of symbols to replace @c DONT_CARE symbols with.
+     * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+     * is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+     * of @c DONT_CARE symbols.
+     * @return A newly created NFA with moved members from NFT with updated transitions.
+     */
+    Nfa to_nfa_update_move(
+        const utils::OrdVector<Symbol>& dont_care_symbol_replacements = { DONT_CARE },
+        JumpMode jump_mode = JumpMode::RepeatSymbol
+    );
+
+}; // class Nft.
+
+// Allow variadic number of arguments of the same type.
+//
+// Using parameter pack and variadic arguments.
+// Adapted from: https://www.fluentcpp.com/2019/01/25/variadic-number-function-parameters-type/.
+/// Pack of bools for reasoning about a sequence of parameters.
+template<bool...> struct bool_pack{};
+/// Check that for all values in a pack @p Ts are 'true'.
+template<typename... Ts> using conjunction = std::is_same<bool_pack<true,Ts::value...>, bool_pack<Ts::value..., true>>;
+/// Check that all types in a sequence of parameters @p Ts are of type @p T.
+template<typename T, typename... Ts> using AreAllOfType = typename conjunction<std::is_same<Ts, T>...>::type;
+
+Nft uni(const Nft &lhs, const Nft &rhs);
+
+/**
+ * @brief Compute intersection of two NFTs.
+ *
+ * Both automata can contain ε-transitions. Epsilons will be handled as alphabet symbols.
+ *
+ * Automata must share alphabets. //TODO: this is not implemented yet.
+ * Transducers must have equal values of @c num_of_levels.
+ *
+ * @param[in] lhs First NFT to compute intersection for.
+ * @param[in] rhs Second NFT to compute intersection for.
+ * @param[out] prod_map Mapping of pairs of the original states (lhs_state, rhs_state) to new product states (not used internally, allocated only when !=nullptr, expensive).
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence of @c DONT_CARE.
+ * @param[in] lhs_first_aux_state The first auxiliary state in @p lhs. Two auxiliary states does not form a product state.
+ * @param[in] rhs_first_aux_state The first auxiliary state in @p rhs. Two auxiliary states does not form a product state.
+ * @return NFT as a product of NFTs @p lhs and @p rhs.
+ */
+Nft intersection(const Nft& lhs, const Nft& rhs,
+                 std::unordered_map<std::pair<State, State>, State> *prod_map = nullptr, JumpMode jump_mode = JumpMode::RepeatSymbol,
+                 State lhs_first_aux_state = Limits::max_state, State rhs_first_aux_state = Limits::max_state);
+
+/**
+ * @brief Composes two NFTs.
+ *
+ * Takes two NFTs and their corresponding synchronization levels as input, and returns a new NFT that represents their
+ *  composition as `lhs || rhs` where `a || b` (read as "a pipe b", or "b after a") means apply `a` on input and then apply `b` on
+ *  output of `a`.
+ *
+ * Vectors of synchronization levels have to be non-empty and of the the same size.
+ *
+ * @param[in] lhs First transducer to compose.
+ * @param[in] rhs Second transducer to compose.
+ * @param[in] lhs_sync_levels Ordered vector of synchronization levels of the @p lhs.
+ * @param[in] rhs_sync_levels Ordered vector of synchronization levels of the @p rhs.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence of @c DONT_CARE.
+ * @return A new NFT after the composition.
+ */
+Nft compose(const Nft& lhs, const Nft& rhs,
+            const utils::OrdVector<Level>& lhs_sync_levels, const utils::OrdVector<Level>& rhs_sync_levels,
+            JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * @brief Composes two NFTs.
+ *
+ * Takes two NFTs and their corresponding synchronization levels as input, and returns a new NFT that represents their
+ *  composition as `lhs || rhs` where `a || b` (read as "a pipe b", or "b after a") means apply `a` on input and then apply `b` on
+ *  output of `a`.
+ *
+ * @param[in] lhs First transducer to compose.
+ * @param[in] rhs Second transducer to compose.
+ * @param[in] lhs_sync_level The synchronization level of the @p lhs.
+ * @param[in] rhs_sync_level The synchronization level of the @p rhs.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence of @c DONT_CARE.
+ * @return A new NFT after the composition.
+ */
+Nft compose(const Nft& lhs, const Nft& rhs, Level lhs_sync_level = 1, Level rhs_sync_level = 0, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * @brief Concatenate two NFTs.
+ *
+ * Supports epsilon symbols when @p use_epsilon is set to true.
+ * @param[in] lhs First automaton to concatenate.
+ * @param[in] rhs Second automaton to concatenate.
+ * @param[in] use_epsilon Whether to concatenate over epsilon symbol.
+ * @param[out] lhs_state_renaming Map mapping lhs states to result states.
+ * @param[out] rhs_state_renaming Map mapping rhs states to result states.
+ * @return Concatenated automaton.
+ */
+// TODO: check how fast is using just concatenate over epsilon and then call remove_epsilon().
+Nft concatenate(const Nft& lhs, const Nft& rhs, bool use_epsilon = false,
+                StateRenaming* lhs_state_renaming = nullptr, StateRenaming* rhs_state_renaming = nullptr);
+
+/**
+ * @brief Compute automaton accepting complement of @p aut.
+ *
+ * @param[in] aut Automaton whose complement to compute.
+ * @param[in] alphabet Alphabet used for complementation.
+ * @param[in] params Optional parameters to control the complementation algorithm:
+ * - "algorithm": "classical" (classical algorithm determinizes the automaton, makes it complete and swaps final and non-final states);
+ * - "minimize": "true"/"false" (whether to compute minimal deterministic automaton for classical algorithm);
+ * @return Complemented automaton.
+ */
+Nft complement(const Nft& aut, const Alphabet& alphabet,
+               const ParameterMap& params = {{ "algorithm", "classical" }, { "minimize", "false" }});
+
+/**
+ * @brief Compute automaton accepting complement of @p aut.
+ *
+ * This overloaded version complements over an already created ordered set of @p symbols instead of an alphabet.
+ * This is a more efficient solution in case you already have @p symbols precomputed or want to complement multiple
+ *  automata over the same set of @c symbols: the function does not need to compute the ordered set of symbols from
+ *  the alphabet again (and for each automaton).
+ *
+ * @param[in] aut Automaton whose complement to compute.
+ * @param[in] symbols Symbols to complement over.
+ * @param[in] params Optional parameters to control the complementation algorithm:
+ * - "algorithm": "classical" (classical algorithm determinizes the automaton, makes it complete and swaps final and non-final states);
+ * - "minimize": "true"/"false" (whether to compute minimal deterministic automaton for classical algorithm);
+ * @return Complemented automaton.
+ */
+Nft complement(const Nft& aut, const utils::OrdVector<Symbol>& symbols,
+               const ParameterMap& params = {{ "algorithm", "classical" }, { "minimize", "false" }});
+
+/**
+ * @brief Compute minimal deterministic automaton.
+ *
+ * @param[in] aut Automaton whose minimal version to compute.
+ * @param[in] params Optional parameters to control the minimization algorithm:
+ * - "algorithm": "brzozowski"
+ * @return Minimal deterministic automaton.
+ */
+Nft minimize(const Nft &aut, const ParameterMap& params = {{ "algorithm", "brzozowski" }});
+
+/**
+ * @brief Determinize automaton.
+ *
+ * @param[in] aut Automaton to determinize.
+ * @param[out] subset_map Map that maps sets of states of input automaton to states of determinized automaton.
+ * @return Determinized automaton.
+ */
+Nft determinize(const Nft& aut, std::unordered_map<StateSet, State> *subset_map = nullptr);
+
+/**
+ * @brief Reduce the size of the automaton.
+ *
+ * @param[in] aut Automaton to reduce.
+ * @param[out] state_renaming Mapping of original states to reduced states.
+ * @param[in] params Optional parameters to control the reduction algorithm:
+ * - "algorithm": "simulation".
+ * @return Reduced automaton.
+ */
+Nft reduce(const Nft &aut, StateRenaming *state_renaming = nullptr,
+           const ParameterMap& params = {{ "algorithm", "simulation" } });
+
+/**
+ * @brief Checks inclusion of languages of two NFTs: @p smaller and @p bigger (smaller <= bigger).
+ *
+ * @param[in] smaller First automaton to concatenate.
+ * @param[in] bigger Second automaton to concatenate.
+ * @param[out] cex Counterexample for the inclusion.
+ * @param[in] alphabet Alphabet of both NFTs to compute with.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @p DONT_CARE symbols.
+ * @param[in] params Optional parameters to control the equivalence check algorithm:
+ * - "algorithm": "naive", "antichains" (Default: "antichains")
+ * @return True if @p smaller is included in @p bigger, false otherwise.
+ */
+bool is_included(const Nft& smaller, const Nft& bigger, Run* cex, const Alphabet* alphabet = nullptr, JumpMode jump_mode = JumpMode::RepeatSymbol,
+                 const ParameterMap& params = {{ "algorithm", "antichains" }});
+
+/**
+ * @brief Checks inclusion of languages of two NFTs: @p smaller and @p bigger (smaller <= bigger).
+ *
+ * @param[in] smaller First automaton to concatenate.
+ * @param[in] bigger Second automaton to concatenate.
+ * @param[in] alphabet Alphabet of both NFTs to compute with.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @p DONT_CARE symbols.
+ * @param[in] params Optional parameters to control the equivalence check algorithm:
+ * - "algorithm": "naive", "antichains" (Default: "antichains")
+ * @return True if @p smaller is included in @p bigger, false otherwise.
+ */
+inline bool is_included(const Nft& smaller, const Nft& bigger, const Alphabet* const alphabet = nullptr, JumpMode jump_mode = JumpMode::RepeatSymbol,
+                        const ParameterMap& params = {{ "algorithm", "antichains" }}) {
+    return is_included(smaller, bigger, nullptr, alphabet, jump_mode, params);
+}
+
+/**
+ * @brief Perform equivalence check of two NFTs: @p lhs and @p rhs.
+ *
+ * @param[in] lhs First automaton to concatenate.
+ * @param[in] rhs Second automaton to concatenate.
+ * @param[in] alphabet Alphabet of both NFTs to compute with.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @p DONT_CARE symbols.
+ * @param[in] params[ Optional parameters to control the equivalence check algorithm:
+ * - "algorithm": "naive", "antichains" (Default: "antichains")
+ * @return True if @p lhs and @p rhs are equivalent, false otherwise.
+ */
+bool are_equivalent(const Nft& lhs, const Nft& rhs, const Alphabet* alphabet, JumpMode jump_mode = JumpMode::RepeatSymbol,
+                    const ParameterMap& params = {{ "algorithm", "antichains"}});
+
+/**
+ * @brief Perform equivalence check of two NFTs: @p lhs and @p rhs.
+ *
+ * The current implementation of @c Nft does not accept input alphabet. For this reason, an alphabet
+ * has to be created from all transitions each time an operation on alphabet is called. When calling this function,
+ * the alphabet has to be computed first.
+ *
+ * Hence, this function is less efficient than its alternative taking already defined alphabet as its parameter.
+ * That way, alphabet has to be computed only once, as opposed to the current ad-hoc construction of the alphabet.
+ * The use of the alternative with defined alphabet should be preferred.
+ *
+ * @param[in] lhs First automaton to concatenate.
+ * @param[in] rhs Second automaton to concatenate.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @p DONT_CARE symbols.
+ * @param[in] params Optional parameters to control the equivalence check algorithm:
+ * - "algorithm": "naive", "antichains" (Default: "antichains")
+ * @return True if @p lhs and @p rhs are equivalent, false otherwise.
+ */
+bool are_equivalent(const Nft& lhs, const Nft& rhs, JumpMode JumpMode = JumpMode::RepeatSymbol, const ParameterMap& params = {{ "algorithm", "antichains"}});
+
+// Reverting the automaton by one of the three functions below,
+// currently simple_revert seems best (however, not tested enough).
+Nft revert(const Nft& aut);
+
+// This revert algorithm is fragile, uses low level accesses to Nft and static data structures,
+// and it is potentially dangerous when there are used symbols with large numbers (allocates an array indexed by symbols)
+// It is faster asymptotically and for somewhat dense automata,
+// the same or a little bit slower than simple_revert otherwise.
+// Not affected by pre-reserving vectors.
+Nft fragile_revert(const Nft& aut);
+
+// Reverting the automaton by a simple algorithm, which does a lot of random access addition to Post and Move.
+//  Much affected by pre-reserving vectors.
+Nft simple_revert(const Nft& aut);
+
+// Reverting the automaton by a modification of the simple algorithm.
+// It replaces random access addition to SymbolPost by push_back and sorting later, so far seems the slowest of all, except on
+//  dense automata, where it is almost as slow as simple_revert. Candidate for removal.
+Nft somewhat_simple_revert(const Nft& aut);
+
+// Removing epsilon transitions
+Nft remove_epsilon(const Nft& aut, Symbol epsilon = EPSILON);
+
+/**
+ * @brief Projects out specified levels @p levels_to_project in the given transducer @p nft.
+ *
+ * @param[in] nft The transducer for projection.
+ * @param[in] levels_to_project A non-empty ordered vector of levels to be projected out from the transducer. It must
+ *  contain only values that are greater than or equal to 0 and smaller than @c num_of_levels.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @p DONT_CARE symbols.
+ * @return A new projected transducer.
+ */
+Nft project_out(const Nft& nft, const utils::OrdVector<Level>& levels_to_project, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * @brief Projects out specified level @p level_to_project in the given transducer @p nft.
+ *
+ * @param[in] nft The transducer for projection.
+ * @param[in] level_to_project A level that is going to be projected out from the transducer. It has to be greater than or
+ *  equal to 0 and smaller than @c num_of_levels.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @c DONT_CARE symbols.
+ * @return A new projected transducer.
+ */
+Nft project_out(const Nft& nft, Level level_to_project, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * @brief Projects to specified levels @p levels_to_project in the given transducer @p nft.
+ *
+ * @param[in] nft The transducer for projection.
+ * @param[in] levels_to_project A non-empty ordered vector of levels the transducer is going to be projected to.
+ *  It must contain only values greater than or equal to 0 and smaller than @c num_of_levels.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @c DONT_CARE symbols.
+ * @return A new projected transducer.
+ */
+Nft project_to(const Nft& nft, const utils::OrdVector<Level>& levels_to_project, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * @brief Projects to a specified level @p level_to_project in the given transducer @p nft.
+ *
+ * @param[in] nft The transducer for projection.
+ * @param[in] level_to_project A level the transducer is going to be projected to. It has to be greater than or equal to 0
+ *  and smaller than @c num_of_levels.
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @c DONT_CARE symbols.
+ * @return A new projected transducer.
+ */
+Nft project_to(const Nft& nft, Level level_to_project, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * @brief Inserts new levels, as specified by the mask @p new_levels_mask, into the given transducer @p nft.
+ *
+ * @c num_of_levels must be greater than 0.
+ * The vector @c new_levels_mask must be nonempty, its length must be greater than @c num_of_levels,
+ * and it must contain exactly @c num_of_levels occurrences of false.
+ *
+ * @param[in] nft The original transducer.
+ * @param[in] new_levels_mask A mask representing the old and new levels. The vector {1, 0, 1, 1, 0} indicates
+ *  that one level is inserted before level 0 and two levels are inserted before level 1.
+ * @param[in] jump_mode Specifies whether the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @c DONT_CARE symbols.
+ */
+Nft insert_levels(const Nft& nft, const BoolVector& new_levels_mask, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/**
+ * @brief Inserts a new level @p new_level into the given transducer @p nft.
+ *
+ * @c num_of_levels must be greater than 0.
+ *
+ * @param[in] nft The original transducer.
+ * @param[in] new_level Specifies the new level to be inserted into the transducer.
+ *  If @p new_level is 0, then it is inserted before the 0-th level.
+ *  If @p new_level is less than @c num_of_levels, then it is inserted before the level @c new_level-1.
+ *  If @p new_level is greater than or equal to @c num_of_levels, then all levels from @c num_of_levels through @p new_level are appended after the last level.
+ * @param[in] jump_mode Specifies whether the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence
+ *  of @c DONT_CARE symbols.
+ */
+Nft insert_level(const Nft& nft, Level new_level, JumpMode jump_mode = JumpMode::RepeatSymbol);
+
+/** Encodes a vector of strings (each corresponding to one symbol) into a
+ *  @c Word instance
+ */
+ // TODO: rename to something, but no idea to what.
+ // Maybe we need some terminology - Symbols and Words are made of numbers.
+ // What are the symbol names and their sequences?
+Run encode_word(const Alphabet* alphabet, const std::vector<std::string>& input);
+
+} // namespace mata::nft.
+
+namespace std {
+std::ostream& operator<<(std::ostream& os, const mata::nft::Nft& nft);
+} // namespace std.
+
+#endif /* MATA_NFT_HH_ */
diff --git a/include/mata/nft/plumbing.hh b/include/mata/nft/plumbing.hh
new file mode 100644
index 000000000..58afe64c7
--- /dev/null
+++ b/include/mata/nft/plumbing.hh
@@ -0,0 +1,102 @@
+/* nfa-plumbings.hh -- Wrapping up different supporting functions.
+ */
+
+#ifndef MATA_NFT_PLUMBING_HH_
+#define MATA_NFT_PLUMBING_HH_
+
+
+#include "nft.hh"
+#include "builder.hh"
+
+
+using namespace mata::nft::builder;
+
+/**
+ * Simplified NFA API, used in binding to call NFA algorithms.
+ *
+ * In particular, this mostly includes operations and checks, that do not return Automaton,
+ * but instead take resulting automaton as pointer (e.g. `void f(Nft* result, const Nft& lhs, const Nft& rhs)`).
+ */
+namespace mata::nft::plumbing {
+
+
+inline void get_elements(StateSet* element_set, const BoolVector& bool_vec) {
+    element_set->clear();
+    element_set->reserve(bool_vec.count());
+    for (size_t i{ 0 }; i < bool_vec.size(); ++i) {
+        if (bool_vec[i] == 1) {
+            element_set->push_back(i);
+        }
+    }
+}
+
+inline void complement(
+        Nft*               result,
+        const Nft&         aut,
+        const Alphabet&    alphabet,
+        const ParameterMap&  params = {{ "algorithm", "classical"},
+                                       { "minimize",  "false"}}) { *result = complement(aut, alphabet, params);
+}
+
+inline void minimize(Nft* res, const Nft &aut) { *res = minimize(aut); }
+
+inline void determinize(Nft* result, const Nft& aut, std::unordered_map<StateSet, State> *subset_map = nullptr) {
+    *result = determinize(aut, subset_map);
+}
+
+inline void reduce(Nft* result, const Nft &aut, StateRenaming *state_renaming = nullptr,
+                   const ParameterMap& params = {{ "algorithm", "simulation"}}) {
+    *result = reduce(aut, state_renaming, params);
+}
+
+inline void revert(Nft* result, const Nft& aut) { *result = revert(aut); }
+
+inline void remove_epsilon(Nft* result, const Nft& aut, Symbol epsilon = EPSILON) { *result = remove_epsilon(aut, epsilon); }
+
+/** Loads an automaton from Parsed object */
+template <class ParsedObject>
+void construct(Nft* result, const ParsedObject& parsed, Alphabet* alphabet = nullptr,
+               NameStateMap* state_map = nullptr) {
+    OnTheFlyAlphabet tmp_alphabet{};
+    if (!alphabet) { alphabet = &tmp_alphabet; }
+    *result = builder::construct(parsed, alphabet, state_map);
+}
+
+inline void uni(Nft *unionAutomaton, const Nft &lhs, const Nft &rhs) { *unionAutomaton = uni(lhs, rhs); }
+
+/**
+ * @brief Compute intersection of two NFTs.
+ *
+ * Both automata can contain ε-transitions. Epsilons will be handled as alphabet symbols.
+ *
+ * Transducers must share alphabets. //TODO: this is not implemented yet.
+ * Transducers must have equal values of @c num_of_levels.
+ *
+ * @param[in] lhs First NFT to compute intersection for.
+ * @param[in] rhs Second NFT to compute intersection for.
+ * @param[out] prod_map Mapping of pairs of the original states (lhs_state, rhs_state) to new product states (not used internally, allocated only when !=nullptr, expensive).
+ * @param[in] jump_mode Specifies if the symbol on a jump transition (a transition with a length greater than 1)
+ *  is interpreted as a sequence repeating the same symbol or as a single instance of the symbol followed by a sequence of @c DONT_CARE
+ * @param[in] lhs_first_aux_state The first auxiliary state in @p lhs. Two auxiliary states does not form a product state.
+ * @param[in] rhs_first_aux_state The first auxiliary state in @p rhs. Two auxiliary states does not form a product state.
+ * @return NFT as a product of NFTs @p lhs and @p rhs.
+ */
+inline void intersection(Nft* res, const Nft& lhs, const Nft& rhs,
+                  std::unordered_map<std::pair<State, State>, State> *prod_map = nullptr, const JumpMode jump_mode = JumpMode::RepeatSymbol,
+                  const State lhs_first_aux_state = Limits::max_state, const State rhs_first_aux_state = Limits::max_state) {
+    *res = intersection(lhs, rhs, prod_map, jump_mode, lhs_first_aux_state, rhs_first_aux_state);
+}
+
+/**
+ * @brief Concatenate two NFAs.
+ * @param[out] lhs_result_state_renaming Map mapping lhs states to result states.
+ * @param[out] rhs_result_state_renaming Map mapping rhs states to result states.
+ */
+inline void concatenate(Nft* res, const Nft& lhs, const Nft& rhs, bool use_epsilon = false,
+                 StateRenaming* lhs_result_state_renaming = nullptr, StateRenaming* rhs_result_state_renaming = nullptr) {
+    *res = concatenate(lhs, rhs, use_epsilon, lhs_result_state_renaming, rhs_result_state_renaming);
+}
+
+} // namespace mata::nfa::Plumbing.
+
+#endif // MATA_NFA_PLUMBING_HH_
diff --git a/include/mata/nft/strings.hh b/include/mata/nft/strings.hh
new file mode 100644
index 000000000..719f3ce01
--- /dev/null
+++ b/include/mata/nft/strings.hh
@@ -0,0 +1,176 @@
+/* nfa-strings.hh -- Operations on NFAs for string solving.
+ */
+
+#ifndef MATA_NFT_STRING_SOLVING_HH_
+#define MATA_NFT_STRING_SOLVING_HH_
+
+#include "mata/nfa/strings.hh"
+#include "nft.hh"
+
+namespace mata::nft::strings {
+
+constexpr Symbol BEGIN_MARKER{ EPSILON - 100 }; ///< Marker marking the beginning of the regex to be replaced.
+constexpr Symbol END_MARKER{ EPSILON - 99 }; ///< Marker marking the end of the regex to be replaced.
+
+/**
+ * How many occurrences of the regex to replace, in order from left to right?
+ */
+enum class ReplaceMode {
+    Single, ///< Replace only the first occurrence of the regex.
+    All, ///< Replace all occurrences of the regex.
+};
+
+/**
+ * Create identity transducer over the @p alphabet with @p num_of_levels levels.
+ */
+Nft create_identity(mata::Alphabet* alphabet, size_t num_of_levels = 2);
+
+/**
+ * Create identity input/output transducer with 2 levels over the @p alphabet with @p level_cnt levels with single
+ *  symbol @p from_symbol replaced with @to_symbol.
+ */
+Nft create_identity_with_single_symbol_replace(mata::Alphabet* alphabet, Symbol from_symbol, Symbol replacement,
+                                               ReplaceMode replace_mode = ReplaceMode::All);
+
+/**
+ * Create identity input/output transducer with 2 levels over the @p alphabet with @p level_cnt levels with single
+ *  symbol @p from_symbol replaced with word @p replacement.
+ */
+Nft create_identity_with_single_symbol_replace(mata::Alphabet* alphabet, Symbol from_symbol, const Word& replacement,
+                                               ReplaceMode replace_mode = ReplaceMode::All);
+
+/**
+ * @brief Create NFT modelling a reluctant leftmost replace of regex @p regex to @p replacement.
+ *
+ * The most general replace operation, handling any regex as the part to be replaced.
+ * @param regex A string containing regex to be replaced.
+ * @param replacement Literal to be replaced with.
+ * @param alphabet Alphabet over which to create the NFT.
+ * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p regex.
+ * @param begin_marker Symbol to be used internally as a begin marker of replaced @p regex.
+ * @return The reluctant leftmost replace NFT.
+ */
+Nft replace_reluctant_regex(const std::string& regex, const Word& replacement, Alphabet* alphabet,
+                            ReplaceMode replace_mode = ReplaceMode::All, Symbol begin_marker = BEGIN_MARKER);
+
+/**
+ * @brief Create NFT modelling a reluctant leftmost replace of regex @p regex to @p replacement.
+ *
+ * The most general replace operation, handling any regex as the part to be replaced.
+ * @param regex NFA representing regex to be replaced.
+ * @param replacement Literal to replace with.
+ * @param alphabet Alphabet over which to create the NFT.
+ * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p regex.
+ * @param begin_marker Symbol to be used internally as a begin marker of replaced @p regex.
+ * @return The reluctant leftmost replace NFT.
+ */
+Nft replace_reluctant_regex(nfa::Nfa regex, const Word& replacement, Alphabet* alphabet,
+                            ReplaceMode replace_mode = ReplaceMode::All, Symbol begin_marker = BEGIN_MARKER);
+
+/**
+ * Create NFT modelling a reluctant leftmost replace of literal @p literal to @p replacement.
+ * @param literal Literal to replace.
+ * @param replacement Literal to replace with.
+ * @param alphabet Alphabet over which to create the NFT.
+ * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p literal.
+ * @param end_marker Symbol to be used internally as an end marker marking the end of the replaced literal.
+ * @return The reluctant leftmost replace NFT.
+ */
+Nft replace_reluctant_literal(const Word& literal, const Word& replacement, Alphabet* alphabet,
+                              ReplaceMode replace_mode = ReplaceMode::All, Symbol end_marker = END_MARKER);
+
+/**
+ * Create NFT modelling a reluctant leftmost replace of symbol @p from_symbol to @p replacement.
+ * @param from_symbol Symbol to replace.
+ * @param replacement Symbol to replace with.
+ * @param alphabet Alphabet over which to create the NFT.
+ * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p from_symbol.
+ * @return The reluctant leftmost replace NFT.
+ */
+Nft replace_reluctant_single_symbol(Symbol from_symbol, Symbol replacement, mata::Alphabet* alphabet,
+                                    ReplaceMode replace_mode = ReplaceMode::All);
+
+/**
+ * Create NFT modelling a reluctant leftmost replace of symbol @p from_symbol to @p replacement.
+ * @param from_symbol Symbol to replace.
+ * @param replacement Literal to replace with.
+ * @param alphabet Alphabet over which to create the NFT.
+ * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p from_symbol.
+ * @return The reluctant leftmost replace NFT.
+ */
+Nft replace_reluctant_single_symbol(Symbol from_symbol, const Word& replacement, mata::Alphabet* alphabet,
+                                    ReplaceMode replace_mode = ReplaceMode::All);
+
+/**
+ * @brief Implementation of all reluctant replace versions.
+ */
+class ReluctantReplace {
+public:
+    /**
+     * @brief Create NFT modelling a reluctant leftmost replace of regex @p regex to @p replacement.
+     *
+     * The most general replace operation, handling any regex as the part to be replaced.
+     * @param regex NFA representing regex to be replaced.
+     * @param replacement Literal to replace with.
+     * @param alphabet Alphabet over which to create the NFT.
+     * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p regex.
+     * @param begin_marker Symbol to be used internally as a begin marker of replaced @p regex.
+     * @return The reluctant leftmost replace NFT.
+     */
+    static Nft replace_regex(nfa::Nfa regex, const Word& replacement, Alphabet* alphabet,
+                             ReplaceMode replace_mode = ReplaceMode::All, Symbol begin_marker = BEGIN_MARKER);
+    /**
+     * Create NFT modelling a reluctant leftmost replace of literal @p literal to @p replacement.
+     * @param literal Literal to replace.
+     * @param replacement Literal to replace with.
+     * @param alphabet Alphabet over which to create the NFT.
+     * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p literal.
+     * @param end_marker Symbol to be used internally as an end marker marking the end of the replaced literal.
+     * @return The reluctant leftmost replace NFT.
+     */
+    static Nft replace_literal(const Word& literal, const Word& replacement, Alphabet* alphabet,
+                                            ReplaceMode replace_mode = ReplaceMode::All, Symbol end_marker = END_MARKER);
+    /**
+     * Create NFT modelling a reluctant leftmost replace of symbol @p from_symbol to @p replacement.
+     * @param from_symbol Symbol to replace.
+     * @param replacement Symbol to replace with.
+     * @param alphabet Alphabet over which to create the NFT.
+     * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p from_symbol.
+     * @return The reluctant leftmost replace NFT.
+     */
+    static Nft replace_symbol(Symbol from_symbol, Symbol replacement, mata::Alphabet* alphabet,
+                                           ReplaceMode replace_mode = ReplaceMode::All);
+    /**
+     * Create NFT modelling a reluctant leftmost replace of symbol @p from_symbol to @p replacement.
+     * @param from_symbol Symbol to replace.
+     * @param replacement Literal to replace with.
+     * @param alphabet Alphabet over which to create the NFT.
+     * @param replace_mode Whether to replace all or just the single (the leftmost) occurrence of @p from_symbol.
+     * @return The reluctant leftmost replace NFT.
+     */
+    static Nft replace_symbol(Symbol from_symbol, const Word& replacement, mata::Alphabet* alphabet,
+                                           ReplaceMode replace_mode = ReplaceMode::All);
+protected:
+    nfa::Nfa end_marker_dfa(nfa::Nfa regex);
+    Nft marker_nft(const nfa::Nfa& marker_dfa, Symbol marker);
+
+    nfa::Nfa generic_marker_dfa(const std::string& regex, Alphabet* alphabet);
+    nfa::Nfa generic_marker_dfa(nfa::Nfa regex, Alphabet* alphabet);
+
+    nfa::Nfa begin_marker_nfa(const std::string& regex, Alphabet* alphabet);
+    nfa::Nfa begin_marker_nfa(nfa::Nfa regex, Alphabet* alphabet);
+
+    Nft begin_marker_nft(const nfa::Nfa& marker_nfa, Symbol begin_marker);
+    Nft end_marker_dft(const nfa::Nfa& end_marker_dfa, Symbol end_marker);
+    nfa::Nfa reluctant_nfa_with_marker(nfa::Nfa nfa, Symbol marker, Alphabet* alphabet);
+
+    Nft reluctant_leftmost_nft(const std::string& regex, Alphabet* alphabet, Symbol begin_marker, const Word& replacement, ReplaceMode replace_mode);
+    Nft reluctant_leftmost_nft(nfa::Nfa nfa, Alphabet* alphabet, Symbol begin_marker, const Word& replacement, ReplaceMode replace_mode);
+
+    Nft replace_literal_nft(const Word& literal, const Word& replacement, const Alphabet* alphabet, Symbol end_marker,
+                            ReplaceMode replace_mode = ReplaceMode::All);
+};
+
+} // Namespace mata::nft::strings.
+
+#endif // MATA_NFT_STRING_SOLVING_HH_.
diff --git a/include/mata/nft/types.hh b/include/mata/nft/types.hh
new file mode 100644
index 000000000..f90470cc0
--- /dev/null
+++ b/include/mata/nft/types.hh
@@ -0,0 +1,57 @@
+// TODO: Insert file header.
+
+#ifndef MATA_NFT_TYPES_HH
+#define MATA_NFT_TYPES_HH
+
+#include "mata/alphabet.hh"
+#include "mata/parser/parser.hh"
+
+#include "mata/nfa/types.hh"
+
+#include <limits>
+
+
+namespace mata::nft {
+
+extern const std::string TYPE_NFT;
+
+using Level = unsigned;
+using State = mata::nfa::State;
+using StateSet = mata::nfa::StateSet;
+
+using Run = mata::nfa::Run;
+
+using StateRenaming = mata::nfa::StateRenaming;
+
+/**
+ * @brief Map of additional parameter name and value pairs.
+ *
+ * Used by certain functions for specifying some additional parameters in the following format:
+ * ```cpp
+ * ParameterMap {
+ *     { "algorithm", "classical" },
+ *     { "minimize", "true" }
+ * }
+ * ```
+ */
+using ParameterMap = mata::nfa::ParameterMap;
+
+using Limits = mata::nfa::Limits;
+
+class Nft; ///< A non-deterministic finite transducer.
+
+enum class JumpMode {
+    RepeatSymbol, ///< Repeat the symbol on the jump.
+    AppendDontCares ///< Append a sequence of DONT_CAREs to the symbol on the jump.
+};
+
+/// An epsilon symbol which is now defined as the maximal value of data type used for symbols.
+constexpr Symbol EPSILON = mata::nfa::EPSILON;
+constexpr Symbol DONT_CARE = EPSILON - 1;
+
+constexpr Level DEFAULT_LEVEL{ 0 };
+constexpr Level DEFAULT_NUM_OF_LEVELS{ 2 };
+
+} // namespace mata::nft.
+
+#endif //MATA_TYPES_HH
diff --git a/include/mata/parser/inter-aut.hh b/include/mata/parser/inter-aut.hh
index f5b49dd7a..f6729d319 100644
--- a/include/mata/parser/inter-aut.hh
+++ b/include/mata/parser/inter-aut.hh
@@ -147,7 +147,8 @@ public:
      */
     enum class AutomatonType {
         NFA,
-        AFA
+        AFA,
+        NFT
     };
 
     /**
@@ -229,6 +230,7 @@ public:
     bool are_nodes_enum_type() const {return node_naming == Naming::ENUM;}
 
     bool is_bitvector() const {return alphabet_type == AlphabetType::BITVECTOR;}
+    bool is_nft() const {return automaton_type == AutomatonType::NFT;}
     bool is_nfa() const {return automaton_type == AutomatonType::NFA;}
     bool is_afa() const {return automaton_type == AutomatonType::AFA;}
 
diff --git a/include/mata/utils/utils.hh b/include/mata/utils/utils.hh
index 3dbf874c2..356ca0a9f 100644
--- a/include/mata/utils/utils.hh
+++ b/include/mata/utils/utils.hh
@@ -15,7 +15,6 @@
 #include <cassert>
 #include <unordered_map>
 #include <vector>
-#include <ranges>
 #include <cstdint>
 
 /// macro for debug outputs
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 698a7239a..0bfc68a4b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -23,6 +23,17 @@ add_library(libmata STATIC
 	nfa/delta.cc
 	nfa/operations.cc
 	nfa/builder.cc
+
+	nft/nft.cc
+	nft/inclusion.cc
+	nft/universal.cc
+	nft/complement.cc
+	nft/composition.cc
+	nft/intersection.cc
+	nft/concatenation.cc
+	nft/operations.cc
+	nft/builder.cc
+	nft/strings.cc
 )
 
 # libmata needs at least c++20
diff --git a/src/inter-aut.cc b/src/inter-aut.cc
index b60d478a9..8afe806e8 100644
--- a/src/inter-aut.cc
+++ b/src/inter-aut.cc
@@ -349,6 +349,8 @@ bool has_atmost_one_auto_naming(const mata::IntermediateAut& aut) {
             aut.automaton_type = mata::IntermediateAut::AutomatonType::NFA;
         } else if (section.type.find("AFA") != std::string::npos) {
             aut.automaton_type = mata::IntermediateAut::AutomatonType::AFA;
+        } else if (section.type.find("NFT") != std::string::npos) {
+            aut.automaton_type = mata::IntermediateAut::AutomatonType::NFT;
         }
         aut.alphabet_type = get_alphabet_type(section.type);
 
@@ -459,6 +461,23 @@ void mata::IntermediateAut::parse_transition(mata::IntermediateAut &aut, const s
         } else
             assert(false && "Unknown NFA type");
 
+        postfix.emplace_back(mata::FormulaNode::Type::OPERATOR, "&", "&", mata::FormulaNode::OperatorType::AND);
+    } else if (aut.automaton_type == mata::IntermediateAut::AutomatonType::NFT && tokens[tokens.size() - 2] != "&") {
+        // we need to take care about this case manually since user does not need to determine
+        // symbol and state naming and put conjunction to transition
+        if (aut.alphabet_type != mata::IntermediateAut::AlphabetType::BITVECTOR) {
+            assert(rhs.size() == 2);
+            postfix.emplace_back(mata::FormulaNode::Type::OPERAND, rhs[0], rhs[0], mata::FormulaNode::OperandType::SYMBOL);
+            postfix.emplace_back(create_node(aut, rhs[1]));
+        } else if (aut.alphabet_type == mata::IntermediateAut::AlphabetType::BITVECTOR) {
+            // This is a case where rhs state is not separated by a conjunction from the rest of the transitions.
+            std::string last_token{ rhs.back() };
+            rhs.pop_back();
+            postfix = infix_to_postfix(aut, rhs);
+            postfix.emplace_back(create_node(aut, last_token));
+        } else
+            assert(false && "Unknown NFT type");
+
         postfix.emplace_back(mata::FormulaNode::Type::OPERATOR, "&", "&", mata::FormulaNode::OperatorType::AND);
     } else
         postfix = infix_to_postfix(aut, rhs);
@@ -525,7 +544,7 @@ std::vector<mata::IntermediateAut> mata::IntermediateAut::parse_from_mf(const ma
     result.reserve(parsed.size());
 
     for (const parser::ParsedSection& parsed_section: parsed) {
-        if (parsed_section.type.find("FA") == std::string::npos) {
+        if (parsed_section.type.find("FA") == std::string::npos && parsed_section.type.find("FT") == std::string::npos) {
             continue;
         }
         result.push_back(mf_to_aut(parsed_section));
diff --git a/src/nfa/builder.cc b/src/nfa/builder.cc
index 36c063c49..b89295e95 100644
--- a/src/nfa/builder.cc
+++ b/src/nfa/builder.cc
@@ -208,11 +208,11 @@ Nfa builder::create_single_word_nfa(const std::vector<std::string>& word, mata::
 }
 
 Nfa builder::create_empty_string_nfa() {
-    return Nfa{ 1, StateSet{ 0 }, StateSet{ 0 } };
+    return Nfa{ 1, { 0 }, { 0 } };
 }
 
 Nfa builder::create_sigma_star_nfa(mata::Alphabet* alphabet) {
-    Nfa nfa{ 1, StateSet{ 0 }, StateSet{ 0 }, alphabet };
+    Nfa nfa{ 1, { 0 }, { 0 }, alphabet };
     for (const mata::Symbol& symbol : alphabet->get_alphabet_symbols()) {
         nfa.delta.add(0, symbol, 0);
     }
@@ -232,7 +232,7 @@ Nfa builder::create_random_nfa_tabakov_vardi(const size_t num_of_states, const s
         throw std::runtime_error("Final state density must be in range (0, 1]");
     }
 
-    Nfa nfa{ num_of_states, StateSet{ 0 }, StateSet{ 0 }, new OnTheFlyAlphabet{} };
+    Nfa nfa{ num_of_states, { 0 }, { 0 }, new OnTheFlyAlphabet{} };
 
     // Initialize the random number generator
     std::random_device rd;  // Seed for the random number engine
diff --git a/src/nfa/delta.cc b/src/nfa/delta.cc
index fb6d73d78..c9b60bd75 100644
--- a/src/nfa/delta.cc
+++ b/src/nfa/delta.cc
@@ -75,22 +75,19 @@ std::vector<Transition> Delta::get_transitions_to(const State state_to) const {
     return transitions_to_state;
 }
 
-void Delta::add(State source, Symbol symbol, State target) {
-    const State max_state{ std::max(source, target) };
-    if (max_state >= state_posts_.size()) {
+void Delta::add(const State source, Symbol symbol, const State target) {
+    if (const State max_state{ std::max(source, target) }; max_state >= state_posts_.size()) {
         reserve_on_insert(state_posts_, max_state);
         state_posts_.resize(max_state + 1);
     }
 
-    StatePost& state_transitions{ state_posts_[source] };
-
-    if (state_transitions.empty()) {
+    if (StatePost& state_transitions{ state_posts_[source] }; state_transitions.empty()) {
         state_transitions.insert({ symbol, target });
     } else if (state_transitions.back().symbol < symbol) {
         state_transitions.insert({ symbol, target });
     } else {
-        const auto symbol_transitions{ state_transitions.find(SymbolPost{ symbol }) };
-        if (symbol_transitions != state_transitions.end()) {
+        if (const auto symbol_transitions{ state_transitions.find(SymbolPost{ symbol }) };
+            symbol_transitions != state_transitions.end()) {
             // Add transition with symbol already used on transitions from state_from.
             symbol_transitions->insert(target);
         } else {
@@ -102,25 +99,20 @@ void Delta::add(State source, Symbol symbol, State target) {
 }
 
 void Delta::add(const State source, const Symbol symbol, const StateSet& targets) {
-    if(targets.empty()) {
-        return;
-    }
+    if(targets.empty()) { return; }
 
-    const State max_state{ std::max(source, targets.back()) };
-    if (max_state >= state_posts_.size()) {
+    if (const State max_state{ std::max(source, targets.back()) }; max_state >= state_posts_.size()) {
         reserve_on_insert(state_posts_, max_state + 1);
         state_posts_.resize(max_state + 1);
     }
 
-    StatePost& state_transitions{ state_posts_[source] };
-
-    if (state_transitions.empty()) {
+    if (StatePost& state_transitions{ state_posts_[source] }; state_transitions.empty()) {
         state_transitions.insert({ symbol, targets });
     } else if (state_transitions.back().symbol < symbol) {
         state_transitions.insert({ symbol, targets });
     } else {
-        const auto symbol_transitions{ state_transitions.find(symbol) };
-        if (symbol_transitions != state_transitions.end()) {
+        if (const auto symbol_transitions{ state_transitions.find(symbol) };
+            symbol_transitions != state_transitions.end()) {
             // Add transition with symbolOnTransition already used on transitions from state_from.
             symbol_transitions->insert(targets);
 
@@ -132,54 +124,45 @@ void Delta::add(const State source, const Symbol symbol, const StateSet& targets
     }
 }
 
-void Delta::remove(State src, Symbol symb, State tgt) {
-    if (src >= state_posts_.size()) {
-        return;
-    }
+void Delta::remove(const State source, const Symbol symbol, const State target) {
+    if (source >= state_posts_.size()) { return; }
 
-    StatePost& state_transitions{ state_posts_[src] };
-    if (state_transitions.empty()) {
+    if (StatePost& state_transitions{ state_posts_[source] }; state_transitions.empty()) {
         throw std::invalid_argument(
-                "Transition [" + std::to_string(src) + ", " + std::to_string(symb) + ", " +
-                std::to_string(tgt) + "] does not exist.");
-    } else if (state_transitions.back().symbol < symb) {
+                "Transition [" + std::to_string(source) + ", " + std::to_string(symbol) + ", " +
+                std::to_string(target) + "] does not exist.");
+    } else if (state_transitions.back().symbol < symbol) {
         throw std::invalid_argument(
-                "Transition [" + std::to_string(src) + ", " + std::to_string(symb) + ", " +
-                std::to_string(tgt) + "] does not exist.");
+                "Transition [" + std::to_string(source) + ", " + std::to_string(symbol) + ", " +
+                std::to_string(target) + "] does not exist.");
     } else {
-        const auto symbol_transitions{ state_transitions.find(symb) };
-        if (symbol_transitions == state_transitions.end()) {
+        if (const auto symbol_transitions{ state_transitions.find(symbol) };
+            symbol_transitions == state_transitions.end()) {
             throw std::invalid_argument(
-                    "Transition [" + std::to_string(src) + ", " + std::to_string(symb) + ", " +
-                    std::to_string(tgt) + "] does not exist.");
+                    "Transition [" + std::to_string(source) + ", " + std::to_string(symbol) + ", " +
+                    std::to_string(target) + "] does not exist.");
         } else {
-            symbol_transitions->erase(tgt);
+            symbol_transitions->erase(target);
             if (symbol_transitions->empty()) {
-                state_posts_[src].erase(*symbol_transitions);
+                state_posts_[source].erase(*symbol_transitions);
             }
         }
     }
 }
 
-bool Delta::contains(State src, Symbol symb, State tgt) const
+bool Delta::contains(const State source, const Symbol symbol, const State target) const
 { // {{{
-    if (state_posts_.empty()) {
-        return false;
-    }
-
-    if (state_posts_.size() <= src)
-        return false;
+    if (state_posts_.empty()) { return false; }
+    if (state_posts_.size() <= source) { return false; }
 
-    const StatePost& tl = state_posts_[src];
-    if (tl.empty()) {
-        return false;
-    }
-    auto symbol_transitions{ tl.find(SymbolPost{ symb} ) };
+    const StatePost& tl = state_posts_[source];
+    if (tl.empty()) { return false; }
+    const auto symbol_transitions{ tl.find(SymbolPost{ symbol} ) };
     if (symbol_transitions == tl.cend()) {
         return false;
     }
 
-    return symbol_transitions->targets.find(tgt) != symbol_transitions->targets.end();
+    return symbol_transitions->targets.find(target) != symbol_transitions->targets.end();
 }
 
 bool Delta::contains(const Transition& transition) const {
@@ -197,10 +180,7 @@ size_t Delta::num_of_transitions() const {
 }
 
 bool Delta::empty() const {
-    for (const StatePost& state_post: state_posts_) {
-        if (!state_post.empty()) { return false; }
-    }
-    return true;
+    return std::ranges::all_of(state_posts_, [](const StatePost& state_post) { return state_post.empty(); });
 }
 
 Delta::Transitions::const_iterator::const_iterator(const Delta& delta): delta_{ &delta } {
diff --git a/src/nfa/nfa.cc b/src/nfa/nfa.cc
index e276f4c7a..a643ed50f 100644
--- a/src/nfa/nfa.cc
+++ b/src/nfa/nfa.cc
@@ -429,13 +429,21 @@ bool Nfa::is_flat() const {
     return flat;
 }
 
-std::string Nfa::print_to_dot() const {
+std::string Nfa::print_to_dot(const bool ascii) const {
     std::stringstream output;
-    print_to_dot(output);
+    print_to_dot(output, ascii);
     return output.str();
 }
 
-void Nfa::print_to_dot(std::ostream &output) const {
+void Nfa::print_to_dot(std::ostream &output, const bool ascii) const {
+    auto to_ascii = [&](const Symbol symbol) {
+        // Translate only printable ASCII characters.
+        if (symbol < 33) {
+            return std::to_string(symbol);
+        }
+        return "\\'" + std::string(1, static_cast<char>(symbol)) + "\\'";
+    };
+
     output << "digraph finiteAutomaton {" << std::endl
                  << "node [shape=circle];" << std::endl;
 
@@ -450,7 +458,11 @@ void Nfa::print_to_dot(std::ostream &output) const {
             for (State target: move.targets) {
                 output << target << " ";
             }
-            output << "} [label=" << move.symbol << "];" << std::endl;
+            if (ascii) {
+                output << "} [label=\"" << to_ascii(move.symbol) << "\"];" << std::endl;
+            } else {
+                output << "} [label=\"" << move.symbol << "\"];" << std::endl;
+            }
         }
     }
 
@@ -514,7 +526,7 @@ void Nfa::print_to_mata(const std::string& filename, const Alphabet* alphabet) c
 }
 
 Nfa Nfa::get_one_letter_aut(Symbol abstract_symbol) const {
-    Nfa digraph{num_of_states(), StateSet(initial), StateSet(final) };
+    Nfa digraph{num_of_states(), initial, final };
     // Add directed transitions for digraph.
     for (const Transition& transition: delta.transitions()) {
         // Directly try to add the transition. Finding out whether the transition is already in the digraph
@@ -600,6 +612,36 @@ State Nfa::add_state(State state) {
     return state;
 }
 
+State Nfa::insert_word(const State source, const Word &word, const State target) {
+    assert(!word.empty());
+    assert(source < num_of_states());
+    assert(target < num_of_states());
+
+    const size_t word_len = word.size();
+    if (word_len == 1) {
+        delta.add(source, word[0], target);
+        return target;
+    }
+
+    // Add transition source --> inner_state.
+    State inner_state = add_state();
+    delta.add(source, word[0], inner_state);
+
+    // Add transitions inner_state --> inner_state
+    State prev_state = inner_state;
+    for (size_t idx{ 1 }; idx < word_len - 1; idx++) {
+        inner_state = add_state();
+        delta.add(prev_state, word[idx], inner_state);
+        prev_state = inner_state;
+    }
+
+    // Add transition inner_state --> target
+    delta.add(prev_state, word[word_len - 1], target);
+    return target;
+}
+
+State Nfa::insert_word(const State source, const Word &word) { return insert_word(source, word, add_state()); }
+
 size_t Nfa::num_of_states() const {
     return std::max({
         static_cast<size_t>(initial.domain_size()),
@@ -671,8 +713,8 @@ Nfa& Nfa::unite_nondet_with(const mata::nfa::Nfa& aut) {
 }
 
 Nfa Nfa::decode_utf8() const {
-    Nfa result{ this->num_of_states(), StateSet{this->initial}, StateSet{this->final} };
-    BoolVector used(this->num_of_states(), false);
+    Nfa result{ num_of_states(), { initial }, { final } };
+    BoolVector used(num_of_states(), false);
     std::stack<State> worklist;
 
     // Pushes a set of states to the worklist and marks them as used.
@@ -693,9 +735,9 @@ Nfa Nfa::decode_utf8() const {
     // For example, consider the sequences 0xC8 0x80 and 0xC8 0x88.
     // Based solely on the first byte (0xC8), we cannot determine which sequence
     // will result in the higher number.
-    auto add_to_state_post = [&](StatePost &state_post, const SymbolPost &symbol_post, const bool is_nondet) {
+    auto add_to_state_post = [&](StatePost &state_post, const SymbolPost& symbol_post, const bool is_nondet) {
         if (is_nondet) {
-            state_post.insert(std::move(symbol_post));
+            state_post.insert(symbol_post);
         } else {
             state_post.emplace_back(std::move(symbol_post));
         }
diff --git a/src/nfa/operations.cc b/src/nfa/operations.cc
index 347d0b08d..21fe0caf7 100644
--- a/src/nfa/operations.cc
+++ b/src/nfa/operations.cc
@@ -1285,7 +1285,7 @@ Nfa mata::nfa::algorithms::minimize_hopcroft(const Nfa& dfa_trimmed) {
 
     // Construct the minimized automaton using equivalence classes (BRP).
     assert(dfa_trimmed.initial.size() == 1);
-    Nfa result(brp.num_of_sets, StateSet{ brp.set_idx[*dfa_trimmed.initial.begin()] }, StateSet{});
+    Nfa result(brp.num_of_sets, { brp.set_idx[*dfa_trimmed.initial.begin()] }, {});
     for (size_t block_idx = 0; block_idx < brp.num_of_sets; ++block_idx) {
         const State q = brp.get_first(block_idx);
         if (dfa_trimmed.final.contains(q)) {
@@ -1510,7 +1510,7 @@ Run mata::nfa::encode_word(const Alphabet* alphabet, const std::vector<std::stri
     return { .word = alphabet->translate_word(input) };
 }
 
-std::set<mata::Word> mata::nfa::Nfa::get_words(unsigned max_length) const {
+std::set<mata::Word> mata::nfa::Nfa::get_words(size_t max_length) const {
     std::set<mata::Word> result;
 
     // contains a pair: a state s and the word with which we got to the state s
diff --git a/src/nft/builder.cc b/src/nft/builder.cc
new file mode 100644
index 000000000..b3625b589
--- /dev/null
+++ b/src/nft/builder.cc
@@ -0,0 +1,341 @@
+// TODO: Insert header file.
+
+#include "mata/utils/utils.hh"
+#include "mata/utils/sparse-set.hh"
+#include "mata/nft/builder.hh"
+
+#include <fstream>
+
+using namespace mata::nft;
+using mata::nft::Nft;
+using mata::Symbol;
+
+Nft builder::construct(const mata::parser::ParsedSection& parsec, mata::Alphabet* alphabet, NameStateMap* state_map) {
+    Nft aut;
+    assert(nullptr != alphabet);
+
+    // HACK - it should be only "parsec.type != TYPE_NFA" without the conjunction
+    if (parsec.type != TYPE_NFT && parsec.type != TYPE_NFT + "-explicit") {
+        throw std::runtime_error(std::string(__FUNCTION__) + ": expecting type \"" +
+                                 TYPE_NFT + "\"");
+    }
+
+    bool remove_state_map = false;
+    if (nullptr == state_map) {
+        state_map = new NameStateMap();
+        remove_state_map = true;
+    }
+
+    // a lambda for translating state names to identifiers
+    auto get_state_name = [&](const std::string& str) {
+        if (!state_map->contains(str)) {
+            State state = aut.add_state();
+            state_map->insert({str, state});
+            return state;
+        } else {
+            return (*state_map)[str];
+        }
+    };
+
+    // a lambda for cleanup
+    auto clean_up = [&]() {
+        if (remove_state_map) { delete state_map; }
+    };
+
+
+    auto it = parsec.dict.find("Initial");
+    if (parsec.dict.end() != it)
+    {
+        for (const auto& str : it->second)
+        {
+            State state = get_state_name(str);
+            aut.initial.insert(state);
+        }
+    }
+
+    it = parsec.dict.find("Final");
+    if (parsec.dict.end() != it) {
+        for (const auto& str : it->second) {
+            const State state = get_state_name(str);
+            aut.final.insert(state);
+        }
+    }
+
+    it = parsec.dict.find("Levels");
+    if (parsec.dict.end() != it) {
+        for (const auto &str : it->second) {
+            std::stringstream ss(str);
+            std::string level_str;
+            try {
+                std::string state_name;
+                std::getline(ss, state_name, ':');
+                std::getline(ss, level_str, ':');
+                if (!ss.eof()) {
+                    throw std::runtime_error("Bad format of levels: too many colons in " + str);
+                }
+
+                const State state = get_state_name(state_name);
+                const long level = std::stol(level_str);
+                if (level < 0) {
+                    throw std::runtime_error("Bad format of levels: level " + level_str + " is out of range.");
+                }
+                aut.levels[state] = static_cast<Level>(level);
+
+            } catch (const std::invalid_argument&) {
+                throw std::runtime_error("Bad format of levels: unsupported level " + level_str);
+            } catch (const std::out_of_range&) {
+                throw std::runtime_error("Bad format of levels: level " + level_str + " is out of range.");
+            } catch (...) {
+                throw std::runtime_error("Bad format of levels.");
+            }
+        }
+    }
+
+    it = parsec.dict.find("LevelsNum");
+    if (parsec.dict.end() != it) {
+        if (it->second.empty()) {
+            throw std::runtime_error("LevelsNum has to be specified.");
+        }
+        if (it->second.size() > 1) {
+            throw std::runtime_error("Only one LevelsNum can be specified.");
+        }
+        try {
+            const long level = std::stol(it->second[0]);
+            if (level < 0) {
+                throw std::runtime_error("Bad format of levels: level " + it->second[0] + " is out of range.");
+            }
+            aut.num_of_levels = static_cast<Level>(level);
+        } catch (const std::invalid_argument&) {
+            throw std::runtime_error("Bad format of levels: unsupported level " + it->second[0]);
+        } catch (const std::out_of_range&) {
+            throw std::runtime_error("Bad format of levels: level " + it->second[0] + " is out of range.");
+        }
+    }
+
+    for (const auto& body_line : parsec.body) {
+        if (body_line.size() != 3) {
+            // clean up
+            clean_up();
+
+            if (body_line.size() == 2) {
+                throw std::runtime_error("Epsilon transitions not supported: " +
+                                         std::to_string(body_line));
+            } else {
+                throw std::runtime_error("Invalid transition: " +
+                                         std::to_string(body_line));
+            }
+        }
+
+        const State source = get_state_name(body_line[0]);
+        const Symbol symbol = alphabet->translate_symb(body_line[1]);
+        const State target = get_state_name(body_line[2]);
+        aut.delta.add(source, symbol, target);
+    }
+
+    // do the dishes and take out garbage
+    clean_up();
+
+    return aut;
+} // construct().
+
+Nft builder::construct(const mata::IntermediateAut& inter_aut, mata::Alphabet* alphabet, NameStateMap* state_map) {
+    Nft aut;
+    assert(nullptr != alphabet);
+
+    if (!inter_aut.is_nft()) {
+        throw std::runtime_error(std::string(__FUNCTION__) + ": expecting type \"" +
+                                 TYPE_NFT + "\"");
+    }
+
+    NameStateMap tmp_state_map;
+    if (nullptr == state_map) {
+        state_map = &tmp_state_map;
+    }
+
+    // a lambda for translating state names to identifiers
+    auto get_state_name = [&state_map, &aut](const std::string& str) {
+        if (!state_map->contains(str)) {
+            State state = aut.add_state();
+            state_map->insert({str, state});
+            return state;
+        } else {
+            return (*state_map)[str];
+        }
+    };
+
+    for (const auto& str : inter_aut.initial_formula.collect_node_names())
+    {
+        const State state = get_state_name(str);
+        aut.initial.insert(state);
+    }
+
+    for (const auto& [formula_node, formula_graph] : inter_aut.transitions) {
+        if (formula_graph.children.size() != 2) {
+            if (formula_graph.children.size() == 1) {
+                throw std::runtime_error("Epsilon transitions not supported");
+            } else {
+                throw std::runtime_error("Invalid transition");
+            }
+        }
+
+        const State source = get_state_name(formula_node.name);
+        const Symbol symbol = alphabet->translate_symb(formula_graph.children[0].node.name);
+        const State target = get_state_name(formula_graph.children[1].node.name);
+
+        aut.delta.add(source, symbol, target);
+    }
+
+    std::unordered_set<std::string> final_formula_nodes;
+    if (!(inter_aut.final_formula.node.is_constant())) {
+        // we do not want to parse true/false (constant) as a state so we do not collect it
+        final_formula_nodes = inter_aut.final_formula.collect_node_names();
+    }
+
+    // for constant true, we will pretend that final nodes are negated with empty final_formula_nodes
+    if (inter_aut.final_formula.node.is_true() || inter_aut.are_final_states_conjunction_of_negation()) {
+        // Final nodes are negated.
+        // We add all states NOT in final_formula_nodes to final states.
+        for (const auto & [state_name, state] : *state_map) {
+            if (!final_formula_nodes.contains(state_name)) {
+                aut.final.insert(state);
+            }
+        }
+    } else {
+        // we add all states in final_formula_nodes to final states
+        for (const auto& str : final_formula_nodes) { aut.final.insert(get_state_name(str)); }
+    }
+
+    return aut;
+} // construct().
+
+void builder::construct(
+        mata::nft::Nft *result,
+        const mata::IntermediateAut &inter_aut,
+        mata::Alphabet *alphabet,
+        mata::nft::builder::NameStateMap *state_map
+) {
+    *result = construct(inter_aut, alphabet, state_map);
+}
+
+Nft builder::create_single_word_nft(const std::vector<Symbol>& word) {
+    return Nft(mata::nfa::builder::create_single_word_nfa(word));
+}
+
+Nft builder::create_single_word_nft(const std::vector<std::string>& word, mata::Alphabet *alphabet) {
+    return Nft(mata::nfa::builder::create_single_word_nfa(word, alphabet));
+}
+
+Nft builder::create_empty_string_nft(const size_t num_of_levels) {
+    Nft nft{ nfa::builder::create_empty_string_nfa() };
+    nft.num_of_levels = num_of_levels;
+    return nft;
+}
+
+Nft builder::create_sigma_star_nft(const size_t num_of_levels) {
+    Nft nft{ 1, { 0 }, { 0 }, { 0 }, num_of_levels };
+    nft.delta.add(0, DONT_CARE, 0);
+    return nft;
+}
+
+Nft builder::create_sigma_star_nft(const mata::Alphabet* alphabet, const size_t num_of_levels) {
+    Nft nft{1, { 0 }, { 0 }, { 0 }, num_of_levels};
+    nft.insert_identity(0, alphabet->get_alphabet_symbols().to_vector());
+    return nft;
+}
+
+Nft builder::parse_from_mata(std::istream& nft_stream) {
+    const std::string nft_str = "NFT";
+    const parser::Parsed parsed{ parser::parse_mf(nft_stream) };
+    if (parsed.size() != 1) {
+        throw std::runtime_error("The number of sections in the input file is '" + std::to_string(parsed.size())
+            + "'. Required is '1'.\n");
+    }
+    if (const std::string automaton_type{ parsed[0].type }; automaton_type.compare(0, nft_str.length(), nft_str) != 0) {
+        throw std::runtime_error("The type of input automaton is '" + automaton_type + "'. Required is 'NFT'\n");
+    }
+    IntAlphabet alphabet;
+    // return construct(IntermediateAut::parse_from_mf(parsed)[0], &alphabet);
+    return construct(parsed[0], &alphabet);
+}
+
+Nft builder::parse_from_mata(const std::filesystem::path& nft_file) {
+    std::ifstream file_stream{ nft_file };
+    if (!file_stream) {
+        throw std::runtime_error("Could not open file \'" + nft_file.string() + "'\n");
+    }
+
+    Nft nft;
+    try {
+        nft = parse_from_mata(file_stream);
+    } catch (const std::exception&) {
+        file_stream.close();
+        throw;
+    }
+    return nft;
+}
+
+Nft builder::parse_from_mata(const std::string& nft_in_mata) {
+    std::istringstream nft_stream(nft_in_mata);
+    return parse_from_mata(nft_stream);
+}
+
+Nft builder::create_from_nfa(const mata::nfa::Nfa& nfa, const size_t num_of_levels, std::optional<Symbol> next_levels_symbol, const std::set<Symbol>& epsilons) {
+    const Level num_of_additional_states_per_nfa_trans{ static_cast<Level>(num_of_levels) - 1 };
+    Nft nft{};
+    size_t nfa_num_of_states{ nfa.num_of_states() };
+    nft.num_of_levels = num_of_levels;
+    nft.levels.resize(nfa_num_of_states + nfa.delta.num_of_transitions() * num_of_additional_states_per_nfa_trans);
+    std::unordered_map<State, State> state_mapping{};
+    state_mapping.reserve(nfa_num_of_states);
+    State nft_state{ 0 };
+
+    for (State source{ 0 }; source < nfa.num_of_states(); ++source) {
+        if (const auto nft_state_it{ state_mapping.find(source) }; nft_state_it == state_mapping.end()) {
+            state_mapping[source] = nft_state;
+            ++nft_state;
+        }
+        for (const SymbolPost& symbol_post: nfa.delta[source]) {
+            State curr_nft_state = state_mapping[source];
+            Level level{ 0 };
+            if (!epsilons.contains(symbol_post.symbol)) {
+                for (; level < num_of_additional_states_per_nfa_trans; ++level) {
+                    nft.levels[curr_nft_state] = level;
+                    if (level != 0) {
+                        nft.delta.add(curr_nft_state,
+                                      next_levels_symbol.has_value() ? next_levels_symbol.value() : symbol_post.symbol,
+                                      nft_state);
+                    } else {
+                        nft.delta.add(curr_nft_state, symbol_post.symbol, nft_state);
+                    }
+                    curr_nft_state = nft_state;
+                    ++nft_state;
+                }
+            }
+            for (State nft_target; State nfa_target: symbol_post.targets) {
+                auto nft_target_it{ state_mapping.find(nfa_target) };
+                if (nft_target_it == state_mapping.end()) {
+                    nft_target = nft_state;
+                    state_mapping[nfa_target] = nft_target;
+                    ++nft_state;
+                } else {
+                    nft_target = nft_target_it->second;
+                }
+                nft.levels[curr_nft_state] = level;
+                nft.delta.add(curr_nft_state,
+                              next_levels_symbol.has_value() ? next_levels_symbol.value() : symbol_post.symbol,
+                              nft_target);
+            }
+        }
+    }
+    nft.initial.reserve(nfa.initial.size());
+    std::ranges::for_each(nfa.initial,
+                          [&](const State nfa_state) { nft.initial.insert(state_mapping[nfa_state]); });
+    nft.final.reserve(nfa.final.size());
+    std::ranges::for_each(nfa.final,
+                          [&](const State nfa_state) { nft.final.insert(state_mapping[nfa_state]); });
+
+    // TODO(nft): HACK. Levels do not work if the size of delta differs from the size of the vector level.
+    nft.levels.resize(nft.delta.num_of_states());
+
+    return nft;
+}
diff --git a/src/nft/complement.cc b/src/nft/complement.cc
new file mode 100644
index 000000000..5bf724404
--- /dev/null
+++ b/src/nft/complement.cc
@@ -0,0 +1,75 @@
+/* nft-complement.cc -- NFT complement
+ */
+
+// MATA headers
+#include "mata/nft/nft.hh"
+#include "mata/nft/algorithms.hh"
+
+using namespace mata::nft;
+using namespace mata::utils;
+
+Nft mata::nft::algorithms::complement_classical(const Nft& aut, const OrdVector<Symbol>& symbols,
+                                                bool minimize_during_determinization) {
+    Nft result;
+    State sink_state;
+    if (minimize_during_determinization) {
+        result = minimize_brzozowski(aut); // brzozowski minimization makes it deterministic
+        if (result.final.empty() && !result.initial.empty()) {
+            assert(result.initial.size() == 1);
+            // if automaton does not accept anything, then there is only one (initial) state
+            // which can be the sink state (so we do not create unnecessary one)
+            sink_state = *result.initial.begin();
+        } else {
+            sink_state = result.num_of_states();
+        }
+    } else {
+        std::unordered_map<StateSet, State> subset_map;
+        result = determinize(aut, &subset_map);
+        // check if a sink state was not created during determinization
+        auto sink_state_iter = subset_map.find({});
+        if (sink_state_iter != subset_map.end()) {
+            sink_state = sink_state_iter->second;
+        } else {
+            sink_state = result.num_of_states();
+        }
+    }
+
+    result.make_complete(symbols, sink_state);
+    result.final.complement(result.num_of_states());
+    return result;
+}
+
+Nft mata::nft::complement(const Nft& aut, const Alphabet& alphabet, const ParameterMap& params) {
+    return mata::nft::complement(aut, alphabet.get_alphabet_symbols(), params);
+}
+
+Nft mata::nft::complement(const Nft& aut, const mata::utils::OrdVector<mata::Symbol>& symbols, const ParameterMap& params) {
+    Nft result;
+    // Setting the requested algorithm.
+    decltype(algorithms::complement_classical)* algo = algorithms::complement_classical;
+    if (!haskey(params, "algorithm")) {
+        throw std::runtime_error(std::to_string(__func__) +
+                                 " requires setting the \"algorithm\" key in the \"params\" argument; "
+                                 "received: " + std::to_string(params));
+    }
+
+    const std::string& str_algo = params.at("algorithm");
+    if ("classical" == str_algo) {  /* default */ }
+    else {
+        throw std::runtime_error(std::to_string(__func__) +
+                                 " received an unknown value of the \"algorithm\" key: " + str_algo);
+    }
+
+    bool minimize_during_determinization = false;
+    if (params.find("minimize") != params.end()) {
+        const std::string& minimize_arg = params.at("minimize");
+        if ("true" == minimize_arg) { minimize_during_determinization = true; }
+        else if ("false" == minimize_arg) { minimize_during_determinization = false; }
+        else {
+            throw std::runtime_error(std::to_string(__func__) +
+                                     " received an unknown value of the \"minimize\" key: " + str_algo);
+        }
+    }
+
+    return algo(aut, symbols, minimize_during_determinization);
+}
diff --git a/src/nft/composition.cc b/src/nft/composition.cc
new file mode 100644
index 000000000..691ce84f5
--- /dev/null
+++ b/src/nft/composition.cc
@@ -0,0 +1,104 @@
+/* composition.cc -- Composition of two NFTs
+ */
+
+// MATA headers
+#include "mata/nft/nft.hh"
+#include <cassert>
+
+
+using namespace mata::utils;
+
+
+namespace mata::nft
+{
+
+Nft compose(const Nft& lhs, const Nft& rhs, const OrdVector<Level>& lhs_sync_levels, const OrdVector<Level>& rhs_sync_levels, const JumpMode jump_mode) {
+    assert(!lhs_sync_levels.empty());
+    assert(lhs_sync_levels.size() == rhs_sync_levels.size());
+
+    // Inserts loop into the given Nft for each state with level 0.
+    // The loop word is constructed using the EPSILON symbol for all levels, except for the levels
+    // where is_dcare_on_transition is true, in which case the DONT_CARE symbol is used.
+    auto insert_self_loops = [&](Nft &nft, const BoolVector &is_dcare_on_transition) {
+        Word loop_word(nft.num_of_levels, EPSILON);
+        for (size_t i{ 0 }; i < nft.num_of_levels; i++) {
+            if (is_dcare_on_transition[i]) {
+                loop_word[i] = DONT_CARE;
+            }
+        }
+
+        for (State s{ 0 }; s < nft.num_of_states(); s++) {
+            if (nft.levels[s]== 0) {
+                nft.insert_word(s, loop_word, s);
+            }
+        }
+    };
+
+    // Calculate new_levels_mask, which is used for inserting levels to match the synchronization
+    // transitions in lhs and rhs. It also calculates the vector levels_to_project_out, which contains
+    // levels of synchronized transitions. These levels will be projected out from the Nft after the composition.
+    // Example:
+    // lhs_sync_levels: 1 4
+    // rhs_sync_levels: 2 3
+    // lhs_new_levels_mask: 0 1 0 0 0
+    // rhs_new_levels_mask: 0 0 1 1 0
+    // levels_to_project_out: 2 5
+    Level min_level = std::min(*lhs_sync_levels.begin(), *rhs_sync_levels.begin());
+    size_t lhs_suffix_len = lhs.num_of_levels - 1 - *--lhs_sync_levels.end();
+    size_t rhs_suffix_len = rhs.num_of_levels - 1 - *--rhs_sync_levels.end();
+    size_t biggest_suffix_len = std::max(lhs_suffix_len, rhs_suffix_len);
+    BoolVector lhs_new_levels_mask(min_level, false);
+    BoolVector rhs_new_levels_mask(min_level, false);
+    OrdVector<Level> levels_to_project_out;
+    Level lhs_offset{ 0 };
+    Level rhs_offset{ 0 };
+    Level lhs_lvl{ 0 };
+    Level rhs_lvl{ 0 };
+    for (auto lhs_sync_levels_it{ lhs_sync_levels.begin() }, rhssync_levels_it{ rhs_sync_levels.begin ()};
+         lhs_sync_levels_it != lhs_sync_levels.end();
+         ++lhs_sync_levels_it, ++rhssync_levels_it)
+    {
+        lhs_lvl = *lhs_sync_levels_it + lhs_offset;
+        rhs_lvl = *rhssync_levels_it + rhs_offset;
+        if (lhs_lvl < rhs_lvl) {
+            lhs_new_levels_mask.insert(lhs_new_levels_mask.end(), rhs_lvl - lhs_lvl, true);
+            rhs_new_levels_mask.insert(rhs_new_levels_mask.end(), rhs_lvl - lhs_lvl, false);
+            lhs_offset += rhs_lvl - lhs_lvl ;
+        } else if (lhs_lvl > rhs_lvl) {
+            lhs_new_levels_mask.insert(lhs_new_levels_mask.end(), lhs_lvl - rhs_lvl, false);
+            rhs_new_levels_mask.insert(rhs_new_levels_mask.end(), lhs_lvl - rhs_lvl, true);
+            rhs_offset = lhs_lvl - rhs_lvl;
+        } else {
+            lhs_new_levels_mask.resize(lhs_lvl, false);
+            rhs_new_levels_mask.resize(rhs_lvl, false);
+        }
+        lhs_new_levels_mask.push_back(false);
+        rhs_new_levels_mask.push_back(false);
+        levels_to_project_out.push_back(static_cast<Level>(lhs_new_levels_mask.size() - 1));
+    }
+    // Match the size of vectors and num_of_levels in lhs and rhs after the insertion of new levels.
+    lhs_new_levels_mask.insert(lhs_new_levels_mask.end(), lhs_suffix_len, false);
+    rhs_new_levels_mask.insert(rhs_new_levels_mask.end(), rhs_suffix_len, false);
+    lhs_new_levels_mask.insert(lhs_new_levels_mask.end(), biggest_suffix_len - lhs_suffix_len, true);
+    rhs_new_levels_mask.insert(rhs_new_levels_mask.end(), biggest_suffix_len - rhs_suffix_len, true);
+
+    Nft lhs_synced = insert_levels(lhs, lhs_new_levels_mask, jump_mode);
+    Nft rhs_synced = insert_levels(rhs, rhs_new_levels_mask, jump_mode);
+
+    // Two auxiliary states (states from inserted loops) can not create a product state.
+    const State lhs_first_aux_state = lhs_synced.num_of_states();
+    const State rhs_first_aux_state = rhs_synced.num_of_states();
+
+    insert_self_loops(lhs_synced, lhs_new_levels_mask);
+    insert_self_loops(rhs_synced, rhs_new_levels_mask);
+
+    Nft result{ intersection(lhs_synced, rhs_synced, nullptr, jump_mode, lhs_first_aux_state, rhs_first_aux_state) };
+    result = project_out(result, levels_to_project_out, jump_mode);
+    return result;
+}
+
+Nft compose(const Nft& lhs, const Nft& rhs, const Level lhs_sync_level, const Level rhs_sync_level, const JumpMode jump_mode) {
+    return compose(lhs, rhs, OrdVector{ lhs_sync_level }, OrdVector{ rhs_sync_level }, jump_mode);
+}
+
+} // mata::nft
diff --git a/src/nft/concatenation.cc b/src/nft/concatenation.cc
new file mode 100644
index 000000000..18593101b
--- /dev/null
+++ b/src/nft/concatenation.cc
@@ -0,0 +1,135 @@
+/* nft-concatenation.cc -- Concatenation of NFTs
+ */
+
+// MATA headers
+#include "mata/nft/nft.hh"
+#include "mata/nft/algorithms.hh"
+
+using namespace mata::nft;
+
+namespace mata::nft {
+
+Nft concatenate(const Nft& lhs, const Nft& rhs, bool use_epsilon,
+                StateRenaming* lhs_state_renaming, StateRenaming* rhs_state_renaming) {
+    return algorithms::concatenate_eps(lhs, rhs, EPSILON, use_epsilon, lhs_state_renaming, rhs_state_renaming);
+}
+
+Nft& Nft::concatenate(const Nft& aut) {
+    assert(num_of_levels == aut.num_of_levels);
+    size_t n = this->num_of_states();
+    auto upd_fnc = [&](State st) {
+        return st + n;
+    };
+
+    // copy the information about aut to save the case when this is the same object as aut.
+    utils::SparseSet<mata::nft::State> aut_initial = aut.initial;
+    utils::SparseSet<mata::nft::State> aut_final = aut.final;
+    size_t aut_n = aut.num_of_states();
+
+    this->delta.allocate(n);
+    this->delta.append(aut.delta.renumber_targets(upd_fnc));
+    this->levels.append(aut.levels);
+
+    // set accepting states
+    utils::SparseSet<State> new_fin{};
+    new_fin.reserve(n+aut_n);
+    for(const State& aut_fin : aut_final) {
+        new_fin.insert(upd_fnc(aut_fin));
+    }
+
+    // connect both parts
+    for(const State& ini : aut_initial) {
+        const StatePost& ini_post = this->delta[upd_fnc(ini)];
+        // is ini state also final?
+        bool is_final = aut_final[ini];
+        for(const State& fin : this->final) {
+            if(is_final) {
+                new_fin.insert(fin);
+            }
+            for(const SymbolPost& ini_mv : ini_post) {
+                // TODO: this should be done efficiently in a delta method
+                // TODO: in fact it is not efficient for now
+                for(const State& dest : ini_mv.targets) {
+                    this->delta.add(fin, ini_mv.symbol, dest);
+                }
+            }
+        }
+    }
+    this->final = new_fin;
+    return *this;
+}
+
+Nft algorithms::concatenate_eps(const Nft& lhs, const Nft& rhs, const Symbol& epsilon, bool use_epsilon,
+                                StateRenaming* lhs_state_renaming, StateRenaming* rhs_state_renaming) {
+    assert(lhs.num_of_levels == rhs.num_of_levels);
+    // Compute concatenation of given automata.
+    // Concatenation will proceed in the order of the passed automata: Result is 'lhs . rhs'.
+
+    if (lhs.num_of_states() == 0 || rhs.num_of_states() == 0 || lhs.initial.empty() || lhs.final.empty() ||
+        rhs.initial.empty() || rhs.final.empty()) {
+        return Nft::with_levels(lhs.num_of_levels);
+    }
+
+    const unsigned long lhs_states_num{ lhs.num_of_states() };
+    const unsigned long rhs_states_num{ rhs.num_of_states() };
+    Nft result{ Nft::with_levels(lhs.num_of_levels) }; // Concatenated automaton.
+    StateRenaming _lhs_states_renaming{}; // Map mapping rhs states to result states.
+    StateRenaming _rhs_states_renaming{}; // Map mapping rhs states to result states.
+
+    const size_t result_num_of_states{lhs_states_num + rhs_states_num};
+    if (result_num_of_states == 0) { return Nft{ Nft::with_levels(lhs.num_of_levels) }; }
+
+    // Map lhs states to result states.
+    _lhs_states_renaming.reserve(lhs_states_num);
+    Symbol result_state_index{ 0 };
+    for (State lhs_state{ 0 }; lhs_state < lhs_states_num; ++lhs_state) {
+        _lhs_states_renaming.insert(std::make_pair(lhs_state, result_state_index));
+        ++result_state_index;
+    }
+    // Map rhs states to result states.
+    _rhs_states_renaming.reserve(rhs_states_num);
+    for (State rhs_state{ 0 }; rhs_state < rhs_states_num; ++rhs_state) {
+        _rhs_states_renaming.insert(std::make_pair(rhs_state, result_state_index));
+        ++result_state_index;
+    }
+
+    result = Nft::with_levels(lhs.num_of_levels);
+    result.delta = lhs.delta;
+    result.initial = lhs.initial;
+    result.add_state(result_num_of_states-1);
+
+    // Add epsilon transitions connecting lhs and rhs automata.
+    // The epsilon transitions lead from lhs original final states to rhs original initial states.
+    for (const auto& lhs_final_state: lhs.final) {
+        for (const auto& rhs_initial_state: rhs.initial) {
+            result.delta.add(lhs_final_state, epsilon,
+                             _rhs_states_renaming[rhs_initial_state]);
+        }
+    }
+
+    // Make result final states.
+    for (const auto& rhs_final_state: rhs.final)
+    {
+        result.final.insert(_rhs_states_renaming[rhs_final_state]);
+    }
+
+    // Add rhs transitions to the result.
+    for (State rhs_state{ 0 }; rhs_state < rhs_states_num; ++rhs_state)
+    {
+        for (const SymbolPost& rhs_move: rhs.delta.state_post(rhs_state))
+        {
+            for (const State& rhs_state_to: rhs_move.targets)
+            {
+                result.delta.add(_rhs_states_renaming[rhs_state],
+                                 rhs_move.symbol,
+                                 _rhs_states_renaming[rhs_state_to]);
+            }
+        }
+    }
+
+    if (!use_epsilon) { result.remove_epsilon(); }
+    if (lhs_state_renaming != nullptr) { *lhs_state_renaming = _lhs_states_renaming; }
+    if (rhs_state_renaming != nullptr) { *rhs_state_renaming = _rhs_states_renaming; }
+    return result;
+} // concatenate_eps().
+} // Namespace mata::nft.
diff --git a/src/nft/inclusion.cc b/src/nft/inclusion.cc
new file mode 100644
index 000000000..87b0c6520
--- /dev/null
+++ b/src/nft/inclusion.cc
@@ -0,0 +1,119 @@
+/* nft-incl.cc -- NFT language inclusion
+ */
+
+// MATA headers
+#include "mata/nft/nft.hh"
+#include "mata/nft/algorithms.hh"
+#include "mata/nfa/algorithms.hh"
+#include "mata/utils/sparse-set.hh"
+
+using namespace mata::nft;
+using namespace mata::utils;
+
+/// naive language inclusion check (complementation + intersection + emptiness)
+bool mata::nft::algorithms::is_included_naive(
+        const Nft &smaller,
+        const Nft &bigger,
+        const Alphabet *const alphabet,//TODO: this should not be needed, likewise for equivalence
+        Run *cex,
+        const JumpMode jump_mode) { // {{{
+    Nft bigger_cmpl;
+    if (alphabet == nullptr) {
+        bigger_cmpl = complement(bigger, create_alphabet(smaller, bigger));
+    } else {
+        bigger_cmpl = complement(bigger, *alphabet);
+    }
+    Nft nft_isect = intersection(smaller, bigger_cmpl, nullptr, jump_mode);
+
+    return nft_isect.is_lang_empty(cex);
+} // is_included_naive }}}
+
+
+/// language inclusion check using Antichains
+// TODO, what about to construct the separator from this?
+bool mata::nft::algorithms::is_included_antichains(
+    const Nft&             smaller,
+    const Nft&             bigger,
+    const Alphabet* const  alphabet, //TODO: this parameter is not used
+    Run*                   cex,
+    const JumpMode         jump_mode)
+{ // {{{
+    if (smaller.num_of_levels != bigger.num_of_levels) { return false; }
+
+    OrdVector<mata::Symbol> symbols;
+    if (alphabet == nullptr) {
+        symbols = create_alphabet(smaller, bigger).get_alphabet_symbols();
+        if (symbols.contains(DONT_CARE) && symbols.size() > 1) {
+            symbols.erase(DONT_CARE);
+        }
+    } else {
+        symbols = alphabet->get_alphabet_symbols();
+    }
+
+    return nfa::algorithms::is_included_antichains(smaller.get_one_level_aut(symbols, jump_mode),
+                                                   bigger.get_one_level_aut(symbols, jump_mode),
+                                                   alphabet,
+                                                   cex);
+} // }}}
+
+namespace {
+    using AlgoType = decltype(algorithms::is_included_naive)*;
+
+    AlgoType set_algorithm(const std::string &function_name, const ParameterMap &params) {
+        if (!haskey(params, "algorithm")) {
+            throw std::runtime_error(function_name +
+                                     " requires setting the \"algorithm\" key in the \"params\" argument; "
+                                     "received: " + std::to_string(params));
+        }
+
+        decltype(algorithms::is_included_naive) *algo;
+        const std::string &str_algo = params.at("algorithm");
+        if ("naive" == str_algo) {
+            algo = algorithms::is_included_naive;
+        } else if ("antichains" == str_algo) {
+            algo = algorithms::is_included_antichains;
+        } else {
+            throw std::runtime_error(std::to_string(__func__) +
+                                     " received an unknown value of the \"algorithm\" key: " + str_algo);
+        }
+
+        return algo;
+    }
+
+}
+
+// The dispatching method that calls the correct one based on parameters
+bool mata::nft::is_included(
+        const Nft &smaller,
+        const Nft &bigger,
+        Run *cex,
+        const Alphabet *const alphabet,
+        const JumpMode jump_mode,
+        const ParameterMap &params) { // {{{
+    AlgoType algo{set_algorithm(std::to_string(__func__), params)};
+    return algo(smaller, bigger, alphabet, cex, jump_mode);
+} // is_included }}}
+
+bool mata::nft::are_equivalent(const Nft& lhs, const Nft& rhs, const Alphabet *alphabet, const JumpMode jump_mode, const ParameterMap& params)
+{
+    if (lhs.num_of_levels != rhs.num_of_levels) { return false; }
+
+    OrdVector<mata::Symbol> symbols;
+    if (alphabet == nullptr) {
+        symbols = create_alphabet(lhs, rhs).get_alphabet_symbols();
+        if (symbols.contains(DONT_CARE) && symbols.size() > 1) {
+            symbols.erase(DONT_CARE);
+        }
+    } else {
+        symbols = alphabet->get_alphabet_symbols();
+    }
+
+    return nfa::are_equivalent(lhs.get_one_level_aut(symbols, jump_mode),
+                               rhs.get_one_level_aut(symbols, jump_mode),
+                               alphabet,
+                               params);
+}
+
+bool mata::nft::are_equivalent(const Nft& lhs, const Nft& rhs, const JumpMode jump_mode, const ParameterMap& params) {
+    return are_equivalent(lhs, rhs, nullptr, jump_mode, params);
+}
diff --git a/src/nft/intersection.cc b/src/nft/intersection.cc
new file mode 100644
index 000000000..9e7402c5a
--- /dev/null
+++ b/src/nft/intersection.cc
@@ -0,0 +1,283 @@
+/* nft-intersection.cc -- Intersection of NFTs
+ */
+
+// MATA headers
+#include "mata/nft/nft.hh"
+#include "mata/nft/algorithms.hh"
+
+#include <fstream>
+#include <cassert>
+#include <functional>
+
+
+using namespace mata::nft;
+
+namespace {
+
+using ProductMap = std::unordered_map<std::pair<State,State>,State>;
+using MatrixProductStorage = std::vector<std::vector<State>>;
+using VecMapProductStorage = std::vector<std::unordered_map<State,State>>;
+using InvertedProductStorage = std::vector<State>;
+//Unordered map seems to be faster than ordered map here, but still very much slower than matrix.
+
+} // Anonymous namespace.
+
+namespace mata::nft {
+
+Nft intersection(const Nft& lhs, const Nft& rhs, ProductMap *prod_map, const JumpMode jump_mode, const State lhs_first_aux_state, const State rhs_first_aux_state) {
+
+    auto both_final = [&](const State lhs_state,const State rhs_state) {
+        return lhs.final.contains(lhs_state) && rhs.final.contains(rhs_state);
+    };
+
+    if (lhs.final.empty() || lhs.initial.empty() || rhs.initial.empty() || rhs.final.empty())
+        return Nft{};
+
+    return algorithms::product(lhs, rhs, both_final, prod_map, jump_mode, lhs_first_aux_state, rhs_first_aux_state);
+}
+
+//TODO: move this method to nft.hh? It is something one might want to use (e.g. for union, inclusion, equivalence of DFAs).
+Nft mata::nft::algorithms::product(const Nft& lhs, const Nft& rhs, const std::function<bool(State,State)>&& final_condition, ProductMap *product_map, const JumpMode jump_mode, const State lhs_first_aux_state, const State rhs_first_aux_state) {
+
+    Nft product{}; // The product automaton.
+    product.num_of_levels = lhs.num_of_levels;
+
+    // Set of product states to process.
+    std::deque<State> worklist{};
+
+    //The largest matrix (product_matrix) of pairs of states we are brave enough to allocate.
+    // Let's say we are fine with allocating large_product * (about 8 Bytes) space.
+    // So ten million cells is close to 100 MB.
+    // If the number is larger, then we do not allocate a matrix, but use a vector of unordered maps (product_vec_map).
+    // The unordered_map seems to be about twice slower.
+    constexpr size_t MAX_PRODUCT_MATRIX_SIZE = 50'000'000;
+    //constexpr size_t MAX_PRODUCT_MATRIX_SIZE = 0;
+    const bool large_product = lhs.num_of_states() * rhs.num_of_states() > MAX_PRODUCT_MATRIX_SIZE;
+    assert(lhs.num_of_states() < Limits::max_state);
+    assert(rhs.num_of_states() < Limits::max_state);
+    assert(lhs.num_of_levels == rhs.num_of_levels);
+
+    //Two variants of storage for the mapping from pairs of lhs and rhs states to product state, for large and non-large products.
+    MatrixProductStorage matrix_product_storage;
+    VecMapProductStorage vec_map_product_storage;
+    InvertedProductStorage product_to_lhs(lhs.num_of_states()+rhs.num_of_states());
+    InvertedProductStorage product_to_rhs(lhs.num_of_states()+rhs.num_of_states());
+
+
+    //Initialize the storage, according to the number of possible state pairs.
+    if (!large_product)
+        matrix_product_storage = MatrixProductStorage(lhs.num_of_states(), std::vector<State>(rhs.num_of_states(), Limits::max_state));
+    else
+        vec_map_product_storage = VecMapProductStorage(lhs.num_of_states());
+
+    /// Give me the product state for the pair of lhs and rhs states.
+    /// Returns Limits::max_state if not found.
+    auto get_state_from_product_storage = [&](State lhs_state, State rhs_state) {
+        if (!large_product)
+            return matrix_product_storage[lhs_state][rhs_state];
+        else {
+            auto it = vec_map_product_storage[lhs_state].find(rhs_state);
+            if (it == vec_map_product_storage[lhs_state].end())
+                return Limits::max_state;
+            else
+                return it->second;
+        }
+    };
+
+    /// Insert new mapping lhs rhs state pair to product state.
+    auto insert_to_product_storage = [&](State lhs_state, State rhs_state, State product_state) {
+        if (!large_product)
+            matrix_product_storage[lhs_state][rhs_state] = product_state;
+        else
+            vec_map_product_storage[lhs_state][rhs_state] = product_state;
+
+        product_to_lhs.resize(product_state+1);
+        product_to_rhs.resize(product_state+1);
+        product_to_lhs[product_state] = lhs_state;
+        product_to_rhs[product_state] = rhs_state;
+
+        //this thing is not used internally. It is only used if we want to return the mapping. But it is expensive.
+        if (product_map != nullptr)
+            (*product_map)[std::pair<State,State>(lhs_state,rhs_state)] = product_state;
+    };
+
+/**
+ * Create product state if it does not exist in storage yet and fill in its symbol_post from lhs and rhs targets.
+ * @param[in] lhs_target Target state in NFT @c lhs.
+ * @param[in] rhs_target Target state in NFT @c rhs.
+ * @param[out] product_symbol_post New SymbolPost of the product state.
+ */
+    auto create_product_state_and_symbol_post = [&](const State lhs_target, const State rhs_target, SymbolPost& product_symbol_post)
+    {
+        // Two auxiliary states can not create a product state.
+        if (lhs_first_aux_state <= lhs_target && rhs_first_aux_state <= rhs_target) {
+            return;
+        }
+        State product_target = get_state_from_product_storage(lhs_target, rhs_target );
+
+        if (product_target == Limits::max_state)
+        {
+            product_target = product.add_state_with_level((jump_mode == JumpMode::RepeatSymbol || lhs.levels[lhs_target] == 0 || rhs.levels[rhs_target] == 0) ?
+                                                          std::max(lhs.levels[lhs_target], rhs.levels[rhs_target]) :
+                                                          std::min(lhs.levels[lhs_target], rhs.levels[rhs_target]));
+            assert(product_target < Limits::max_state);
+
+            insert_to_product_storage(lhs_target,rhs_target, product_target);
+            worklist.push_back(product_target);
+
+            if (final_condition(lhs_target,rhs_target)) {
+                product.final.insert(product_target);
+            }
+        }
+        //TODO: Push_back all of them and sort at the could be faster.
+        product_symbol_post.insert(product_target);
+    };
+
+    // If DONT_CARE is not present in the given dcare_state_post, no action is taken.
+    // For each transition in specific_state_post and each target found in dcare_state_post using find(DONT_CARE),
+    // a corresponding transition and product state are created.
+    auto process_dont_care = [&](const State dcare_src,
+                                 const State specific_src,
+                                 const bool dcare_on_lhs,
+                                 const State product_source)
+    {
+        const StatePost& dcare_state_post = dcare_on_lhs ? lhs.delta[dcare_src] : rhs.delta[dcare_src];
+        const StatePost& specific_state_post = dcare_on_lhs ? rhs.delta[specific_src] : lhs.delta[specific_src];
+        auto dcare_symbol_post_it = dcare_state_post.find(DONT_CARE);
+        if (dcare_symbol_post_it == dcare_state_post.end()) {
+            return;
+        }
+        for (const auto &specific_symbol_post : specific_state_post) {
+            SymbolPost product_symbol_post{ specific_symbol_post.symbol };
+            for (const State dcare_target : dcare_symbol_post_it->targets) {
+                for (const State specific_target : specific_symbol_post.targets) {
+
+                    const Level dcare_target_level = dcare_on_lhs ? lhs.levels[dcare_target] : rhs.levels[dcare_target];
+                    const Level specific_target_level = dcare_on_lhs ? rhs.levels[specific_target] : lhs.levels[specific_target];
+                    const bool targets_are_on_the_same_level = dcare_target_level == specific_target_level;
+                    const bool dcare_target_is_deeper = specific_target_level != 0 && (specific_target_level < dcare_target_level || dcare_target_level == 0);
+                    const bool specific_target_is_deeper = dcare_target_level != 0 && (dcare_target_level < specific_target_level || specific_target_level == 0);
+
+                    // If jump_mode is AppendDONT_CAREs, we should wait in the deeper state.
+                    // If jump_mode is RepeatSymbol, we should wait in the source state that has a deeper target.
+                    State lhs_target, rhs_target;
+                    if (dcare_on_lhs) {
+                        lhs_target = (jump_mode == JumpMode::AppendDontCares || targets_are_on_the_same_level || specific_target_is_deeper) ? dcare_target : dcare_src;
+                        rhs_target = (jump_mode == JumpMode::AppendDontCares || targets_are_on_the_same_level || dcare_target_is_deeper) ? specific_target : specific_src;
+                    } else {
+                        lhs_target = (jump_mode == JumpMode::AppendDontCares || targets_are_on_the_same_level || dcare_target_is_deeper) ? specific_target : specific_src;
+                        rhs_target = (jump_mode == JumpMode::AppendDontCares || targets_are_on_the_same_level || specific_target_is_deeper) ? dcare_target : dcare_src;
+                    }
+                    create_product_state_and_symbol_post(lhs_target, rhs_target, product_symbol_post);
+                }
+            }
+            if (product_symbol_post.empty()) {
+                continue;
+            }
+            StatePost &product_state_post{product.delta.mutable_state_post(product_source)};
+            const auto product_state_post_find_it = product_state_post.find(product_symbol_post.symbol);
+            if (product_state_post_find_it == product_state_post.end()) {
+                product_state_post.insert(std::move(product_symbol_post));
+            } else {
+                product_state_post_find_it->targets.insert(product_symbol_post.targets);
+            }
+        }
+    };
+
+    // Initialize pairs to process with initial state pairs.
+    for (const State lhs_initial_state : lhs.initial) {
+        for (const State rhs_initial_state : rhs.initial) {
+            // Update product with initial state pairs.
+            const State product_initial_state = product.add_state();
+            insert_to_product_storage(lhs_initial_state,rhs_initial_state,product_initial_state);
+            worklist.push_back(product_initial_state);
+            product.initial.insert(product_initial_state);
+            if (final_condition(lhs_initial_state,rhs_initial_state)) {
+                product.final.insert(product_initial_state);
+            }
+        }
+    }
+
+    while (!worklist.empty()) {
+        State product_source = worklist.back();;
+        worklist.pop_back();
+        State lhs_source =  product_to_lhs[product_source];
+        State rhs_source =  product_to_rhs[product_source];
+
+        const bool sources_are_on_the_same_level = lhs.levels[lhs_source] == rhs.levels[rhs_source];
+        const bool rhs_source_is_deeper = (lhs.levels[lhs_source] < rhs.levels[rhs_source] && lhs.levels[lhs_source] != 0) || (lhs.levels[lhs_source] != 0 && rhs.levels[rhs_source] == 0);
+
+        if (sources_are_on_the_same_level || jump_mode == JumpMode::RepeatSymbol) {
+            // Compute classic product for current state pair.
+            mata::utils::SynchronizedUniversalIterator<mata::utils::OrdVector<SymbolPost>::const_iterator> sync_iterator(2);
+            mata::utils::push_back(sync_iterator, lhs.delta[lhs_source]);
+            mata::utils::push_back(sync_iterator, rhs.delta[rhs_source]);
+            while (sync_iterator.advance()) {
+                const std::vector<StatePost::const_iterator>& same_symbol_posts{ sync_iterator.get_current() };
+                assert(same_symbol_posts.size() == 2); // One move per state in the pair.
+
+                // Compute product for state transitions with same symbols.
+                // Find all transitions that have the same symbol for first and the second state in the pair_to_process.
+                // Create transition from the pair_to_process to all pairs between states to which first transition goes
+                //  and states to which second one goes.
+                Symbol symbol = same_symbol_posts[0]->symbol;
+                SymbolPost product_symbol_post{ symbol };
+                for (const State lhs_target: same_symbol_posts[0]->targets) {
+                    for (const State rhs_target: same_symbol_posts[1]->targets) {
+                        const bool targets_are_on_the_same_level = lhs.levels[lhs_target] == rhs.levels[rhs_target];
+                        const bool lhs_target_is_deeper = rhs.levels[rhs_target] != 0 && (rhs.levels[rhs_target] < lhs.levels[lhs_target] || lhs.levels[lhs_target] == 0);
+                        const bool rhs_target_is_deeper = lhs.levels[lhs_target] != 0 && (lhs.levels[lhs_target] < rhs.levels[rhs_target] || rhs.levels[rhs_target] == 0);
+
+                        // If jump_mode is AppendDONT_CAREs, we should wait in the deeper state.
+                        // If jump_mode is RepeatSymbol, we should wait in the source state that has a deeper target.
+                        const State lhs_state = (jump_mode == JumpMode::AppendDontCares || targets_are_on_the_same_level || rhs_target_is_deeper) ? lhs_target : lhs_source;
+                        const State rhs_state = (jump_mode == JumpMode::AppendDontCares || targets_are_on_the_same_level || lhs_target_is_deeper) ? rhs_target : rhs_source;
+
+                        create_product_state_and_symbol_post(lhs_state, rhs_state, product_symbol_post);
+                    }
+                }
+                if (product_symbol_post.empty()) {
+                    continue;
+                }
+                StatePost &product_state_post{product.delta.mutable_state_post(product_source)};
+                //Here we are sure that we are working with the largest symbol so far, since we iterate through
+                //the symbol posts of the lhs and rhs in order. So we can just push_back (not insert).
+                product_state_post.push_back(std::move(product_symbol_post));
+            }
+
+            process_dont_care(lhs_source, rhs_source, true, product_source);
+            process_dont_care(rhs_source, lhs_source, false, product_source);
+
+        } else if (rhs_source_is_deeper) {
+            // The second state (from rhs) is deeper, so it must wait.
+            for (const auto &symbol_post : lhs.delta[lhs_source]) {
+                SymbolPost product_symbol_post{ symbol_post.symbol };
+                for (const State target : symbol_post.targets) {
+                    create_product_state_and_symbol_post(target, rhs_source, product_symbol_post);
+                }
+                if (product_symbol_post.empty()) {
+                    continue;
+                }
+                StatePost &product_state_post{product.delta.mutable_state_post(product_source)};
+                product_state_post.push_back(std::move(product_symbol_post));
+            }
+        } else {
+            // The first state (from lhs) is deeper, so it must wait.
+            for (const auto &symbol_post : rhs.delta[rhs_source]) {
+                SymbolPost product_symbol_post{ symbol_post.symbol };
+                for (const State target : symbol_post.targets) {
+                    create_product_state_and_symbol_post(lhs_source, target, product_symbol_post);
+                }
+                if (product_symbol_post.empty()) {
+                    continue;
+                }
+                StatePost &product_state_post{product.delta.mutable_state_post(product_source)};
+                product_state_post.push_back(std::move(product_symbol_post));
+            }
+        }
+
+    }
+    return product;
+} // intersection().
+
+} // namespace mata::nft.
diff --git a/src/nft/nft.cc b/src/nft/nft.cc
new file mode 100644
index 000000000..8bcabb42b
--- /dev/null
+++ b/src/nft/nft.cc
@@ -0,0 +1,450 @@
+/* nft.cc -- operations for NFA
+ */
+
+#include <algorithm>
+#include <list>
+#include <optional>
+#include <iterator>
+
+// MATA headers
+#include "mata/utils/sparse-set.hh"
+#include "mata/nft/nft.hh"
+#include "mata/nft/algorithms.hh"
+#include <mata/simlib/explicit_lts.hh>
+
+using namespace mata::utils;
+using namespace mata::nft;
+using mata::Symbol;
+using mata::Word;
+using mata::BoolVector;
+
+using StateBoolArray = std::vector<bool>; ///< Bool array for states in the automaton.
+
+const std::string mata::nft::TYPE_NFT = "NFT";
+
+
+Nft& Nft::trim(StateRenaming* state_renaming) {
+
+#ifdef _STATIC_STRUCTURES_
+    BoolVector useful_states{ useful_states() };
+    useful_states.clear();
+    useful_states = useful_states();
+#else
+    BoolVector useful_states{ get_useful_states() };
+#endif
+    const size_t useful_states_size{ useful_states.size() };
+    std::vector<State> renaming(useful_states_size);
+    for(State new_state{ 0 }, orig_state{ 0 }; orig_state < useful_states_size; ++orig_state) {
+        if (useful_states[orig_state]) {
+            renaming[orig_state] = new_state;
+            ++new_state;
+        }
+    }
+
+    delta.defragment(useful_states, renaming);
+
+    auto is_state_useful = [&](State q){return q < useful_states.size() && useful_states[q];};
+    initial.filter(is_state_useful);
+    final.filter(is_state_useful);
+
+    // Specific for levels
+    //////////////////////
+    State move_index{ 0 };
+    std::erase_if(levels,
+         [&](Level&) -> bool {
+             State prev{ move_index };
+             ++move_index;
+             return !useful_states[prev];
+         }
+    );
+    //////////////////////
+
+    auto rename_state = [&](State q){return renaming[q];};
+    initial.rename(rename_state);
+    final.rename(rename_state);
+    initial.truncate();
+    final.truncate();
+    if (state_renaming != nullptr) {
+        state_renaming->clear();
+        state_renaming->reserve(useful_states_size);
+        for (State q{ 0 }; q < useful_states_size; ++q) {
+            if (useful_states[q]) {
+                (*state_renaming)[q] = renaming[q];
+            }
+        }
+    }
+    return *this;
+}
+
+std::string Nft::print_to_DOT(const bool ascii) const {
+    std::stringstream output;
+    print_to_DOT(output, ascii);
+    return output.str();
+}
+
+void Nft::print_to_DOT(std::ostream &output, const bool ascii) const {
+    auto translate_special_symbols = [&](const Symbol symbol) -> std::string {
+        if (symbol == EPSILON) {
+            return "<eps>";
+        }
+        if (symbol == EPSILON - 99) {
+            return "<marker>";
+        }
+        if (symbol == DONT_CARE) {
+            return "<dcare>";
+        }
+        return std::to_string(symbol);
+    };
+
+    auto to_ascii = [&](const Symbol symbol) {
+        // Translate only printable ASCII characters.
+        if (symbol < 33) {
+            return std::to_string(symbol);
+        }
+        return std::string(1, static_cast<char>(symbol));
+    };
+    output << "digraph finiteAutomaton {" << std::endl
+                 << "node [shape=circle];" << std::endl;
+
+    for (State final_state: final) {
+        output << final_state << " [shape=doublecircle];" << std::endl;
+    }
+
+    const size_t delta_size = delta.num_of_states();
+    for (State source = 0; source != delta_size; ++source) {
+        for (const SymbolPost &move: delta[source]) {
+            output << source << " -> {";
+            for (State target: move.targets) {
+                output << target << " ";
+            }
+
+            if (ascii && move.symbol < 128) {
+                output << "} [label=\"" << to_ascii(move.symbol) << "\"];" << std::endl;
+            } else if (ascii && move.symbol >= 128) {
+                output << "} [label=\"" << translate_special_symbols(move.symbol) << "\"];" << std::endl;
+            } else {
+                output << "} [label=\"" << translate_special_symbols(move.symbol) << "\"];" << std::endl;
+            }
+        }
+    }
+
+    output << "node [shape=none, label=\"\"];" << std::endl;
+    output << "forcelabels=true;" << std::endl;
+    for (State s{ 0 }; s < levels.size(); s++) {
+        output << s << " [label=\"" << s << ":" << levels[s] << "\"];" << std::endl;
+    }
+    for (State init_state: initial) {
+        output << "i" << init_state << " -> " << init_state << ";" << std::endl;
+    }
+
+    output << "}" << std::endl;
+}
+
+std::string Nft::print_to_mata() const {
+    std::stringstream output;
+    print_to_mata(output);
+    return output.str();
+}
+
+void Nft::print_to_mata(std::ostream &output) const {
+    output << "@NFT-explicit" << std::endl
+           << "%Alphabet-auto" << std::endl;
+           // TODO should be this, but we cannot parse %Alphabet-numbers yet
+           //<< "%Alphabet-numbers" << std::endl;
+
+    if (!initial.empty()) {
+        output << "%Initial";
+        for (State init_state : initial) {
+            output << " q" << init_state;
+        }
+        output << std::endl;
+    }
+
+    if (!final.empty()) {
+        output << "%Final";
+        for (State final_state : final) {
+            output << " q" << final_state;
+        }
+        output << std::endl;
+    }
+
+    if (!levels.empty()) {
+        BoolVector live_states(num_of_states(), false);
+        for (const State &s : initial) {
+            live_states[s] = true;
+        }
+        for (const State &s : final) {
+            live_states[s] = true;
+        }
+        for (const Transition &trans: delta.transitions()) {
+            live_states[trans.source] = true;
+            live_states[trans.target] = true;
+        }
+        output << "%Levels";
+        for (State s{ 0 }; s < num_of_states(); s++) {
+            if (live_states[s]) {
+                output << " " << "q" << s << ":" << levels[s];
+            }
+        }
+        output << std::endl;
+    }
+    output << "%LevelsNum " << num_of_levels << std::endl;
+
+    for (const Transition& trans: delta.transitions()) {
+        output << "q" << trans.source << " " << trans.symbol << " q" << trans.target << std::endl;
+    }
+}
+
+Nft Nft::get_one_letter_aut(Symbol abstract_symbol) const {
+    return Nft(mata::nfa::Nfa::get_one_letter_aut(abstract_symbol));
+}
+
+void Nft::get_one_letter_aut(Nft& result) const {
+    result = get_one_letter_aut();
+}
+
+void Nft::make_one_level_aut(const utils::OrdVector<Symbol> &dont_care_symbol_replacements, const JumpMode jump_mode) {
+    const bool dcare_for_dcare = dont_care_symbol_replacements == utils::OrdVector<Symbol>({ DONT_CARE });
+    std::vector<Transition> transitions_to_del;
+    std::vector<Transition> transitions_to_add;
+
+    auto add_inner_transitions = [&](State src, Symbol symbol, State trg) {
+        if (symbol == DONT_CARE && !dcare_for_dcare) {
+            for (const Symbol replace_symbol : dont_care_symbol_replacements) {
+                transitions_to_add.emplace_back( src, replace_symbol, trg );
+            }
+        } else {
+            transitions_to_add.emplace_back( src, symbol, trg );
+        }
+    };
+
+    Level src_lvl, trg_lvl, diff_lvl;
+    for (const auto &transition : delta.transitions()) {
+        src_lvl = levels[transition.source];
+        trg_lvl = levels[transition.target];
+        diff_lvl = (trg_lvl == 0) ? (static_cast<Level>(num_of_levels) - src_lvl) : trg_lvl - src_lvl;
+
+        if (diff_lvl == 1 && transition.symbol == DONT_CARE && !dcare_for_dcare) {
+            transitions_to_del.push_back(transition);
+            for (const Symbol replace_symbol : dont_care_symbol_replacements) {
+                transitions_to_add.emplace_back( transition.source, replace_symbol, transition.target );
+            }
+        } else if (diff_lvl > 1) {
+            transitions_to_del.push_back(transition);
+            State inner_src = transition.source;
+            Level inner_src_lvl = src_lvl;
+
+            // The first iteration connecting original source state with inner state.
+            State inner_trg = add_state();
+            Level inner_trg_lvl = src_lvl + 1;
+            levels[inner_trg] = inner_trg_lvl;
+            add_inner_transitions(inner_src, transition.symbol, inner_trg);
+            inner_src = inner_trg;
+            inner_src_lvl++;
+            inner_trg_lvl++;
+
+            // Iterations 1 to n-1 connecting inner states.
+            Level pre_trg_lvl = (trg_lvl == 0) ? (static_cast<Level>(num_of_levels) - 1) : (trg_lvl - 1);
+            for (; inner_src_lvl < pre_trg_lvl; inner_src_lvl++, inner_trg_lvl++) {
+                inner_trg = add_state();
+                levels[inner_trg] = inner_trg_lvl;
+
+                add_inner_transitions(inner_src, jump_mode == JumpMode::AppendDontCares ? DONT_CARE : transition.symbol, inner_trg);
+                inner_src = inner_trg;
+            }
+
+            // The last iteration connecting last inner state with the original target state.
+            add_inner_transitions(inner_src, jump_mode == JumpMode::AppendDontCares ? DONT_CARE : transition.symbol, transition.target);
+        }
+    }
+
+    for (const Transition &transition : transitions_to_add) {
+        delta.add(transition);
+    }
+    for (const Transition &transition : transitions_to_del) {
+        delta.remove(transition);
+    }
+}
+
+Nft Nft::get_one_level_aut(const utils::OrdVector<Symbol> &dont_care_symbol_replacements, const JumpMode jump_mode) const {
+    Nft result{ *this };
+    // HACK. Works only for automata without levels.
+    if (result.levels.size() != result.num_of_states()) {
+        return result;
+    }
+    result.make_one_level_aut(dont_care_symbol_replacements, jump_mode);
+    return result;
+}
+
+void Nft::get_one_level_aut(Nft& result, const utils::OrdVector<Symbol> &dont_care_symbol_replacements, const JumpMode jump_mode) const {
+    result = get_one_level_aut(dont_care_symbol_replacements, jump_mode);
+}
+
+Nft& Nft::operator=(Nft&& other) noexcept {
+    if (this != &other) {
+        mata::nfa::Nfa::operator=(other);
+        levels = std::move(other.levels);
+        num_of_levels = other.num_of_levels;
+    }
+    return *this;
+}
+
+State Nft::add_state() {
+    const State state{ Nfa::add_state() };
+    levels.set(state);
+    return state;
+}
+
+State Nft::add_state(const State state) {
+    levels.set(state);
+    return Nfa::add_state(state);
+}
+
+State Nft::add_state_with_level(const Level level) {
+    const State state{ Nfa::add_state() };
+    levels.set(state, level);
+    return state;
+}
+
+State Nft::add_state_with_level(const State state, const Level level) {
+    levels.set(state, level);
+    return Nfa::add_state(state);
+}
+
+State Nft::insert_word(const State source, const Word &word, const State target) {
+    // Ensure both states exist in the NFT.
+    add_state(std::max(source, target));
+    if (levels[source] != levels[target]) {
+        throw std::invalid_argument{ "Inserting word between source and target states with different levels." };
+    }
+
+    const State first_new_state = num_of_states();
+    const State word_target = Nfa::insert_word(source, word, target);
+    const size_t num_of_states_after = num_of_states();
+    const Level source_level = levels[source];
+
+    Level lvl = (num_of_levels == 1) ? source_level : (source_level + 1) % static_cast<Level>(num_of_levels);
+    for (State state{ first_new_state }; state < num_of_states_after;
+         ++state, lvl = (lvl + 1) % static_cast<Level>(num_of_levels)) { add_state_with_level(state, lvl); }
+
+    assert(levels[word_target] == 0 || levels[num_of_states_after - 1] < levels[word_target]);
+
+    return word_target;
+}
+
+State Nft::insert_word(const State source, const Word& word) {
+    if (num_of_states() <= source) { add_state(source); }
+    return insert_word(source, word, add_state_with_level(levels[source]));
+}
+
+State Nft::insert_word_by_parts(const State source, const std::vector<Word> &word_parts_on_levels, const State target) {
+    assert(word_parts_on_levels.size() == num_of_levels);
+    assert(source < num_of_states());
+    assert(target < num_of_states());
+    assert(source < levels.size());
+    assert(target < levels.size());
+    assert(levels[source] == levels[target]);
+    const Level from_to_level{ levels[source] };
+
+    if (num_of_levels == 1) {
+        return Nft::insert_word(source, word_parts_on_levels[0], target);
+    }
+
+    std::vector<mata::Word::const_iterator> word_part_it_v(num_of_levels);
+    for (Level lvl{ from_to_level }, i{ 0 }; i < static_cast<Level>(num_of_levels); ++i) {
+        word_part_it_v[lvl] = word_parts_on_levels[lvl].begin();
+        lvl = (lvl + 1) % static_cast<Level>(num_of_levels);
+    }
+
+    // This function retrieves the next symbol from a word part at a specified level and advances the corresponding iterator.
+    // Returns EPSILON when the iterator reaches the end of the word part.
+    auto get_next_symbol = [&](Level lvl) {
+        if (word_part_it_v[lvl] == word_parts_on_levels[lvl].end()) {
+            return EPSILON;
+        }
+        return *(word_part_it_v[lvl]++);
+    };
+
+    // Add transition source --> inner_state.
+    Level inner_lvl = (num_of_levels == 1 ) ? 0 : (from_to_level + 1) % static_cast<Level>(num_of_levels);
+    State inner_state = add_state_with_level(inner_lvl);
+    delta.add(source, get_next_symbol(from_to_level), inner_state);
+
+    // Add transition inner_state --> inner_state
+    State prev_state = inner_state;
+    Level prev_lvl = inner_lvl;
+    const size_t max_word_part_len = std::max_element(
+        word_parts_on_levels.begin(),
+        word_parts_on_levels.end(),
+        [](const Word& a, const Word& b) { return a.size() < b.size(); }
+    )->size();
+    const size_t word_total_len = num_of_levels * max_word_part_len;
+    if (word_total_len != 0) {
+        for (size_t symbol_idx{ 1 }; symbol_idx < word_total_len - 1; symbol_idx++) {
+            inner_lvl = (prev_lvl + 1) % static_cast<Level>(num_of_levels);
+            inner_state = add_state_with_level(inner_lvl);
+            delta.add(prev_state, get_next_symbol(prev_lvl), inner_state);
+            prev_state = inner_state;
+            prev_lvl = inner_lvl;
+        }
+    }
+    // Add transition inner_state --> target.
+    delta.add(prev_state, get_next_symbol(prev_lvl), target);
+    return target;
+}
+
+State Nft::insert_word_by_parts(const State source, const std::vector<Word> &word_parts_on_levels) {
+   assert(source < levels.size());
+   return insert_word_by_parts(source, word_parts_on_levels, add_state_with_level(levels[source]));
+}
+
+State Nft::add_transition(const State source, const std::vector<Symbol>& symbols, const State target) {
+    return insert_word(source, symbols, target);
+}
+State Nft::add_transition(const State source, const std::vector<Symbol>& symbols) {
+    return insert_word(source, symbols);
+}
+
+void Nft::insert_identity(const State state, const std::vector<Symbol> &symbols, const JumpMode jump_mode) {
+    for (const Symbol symbol : symbols) {
+        insert_identity(state, symbol, jump_mode);
+    }
+}
+
+void Nft::insert_identity(const State state, const Symbol symbol, const JumpMode jump_mode) {
+    (void)jump_mode;
+    // TODO(nft): Evaluate the performance difference between adding a jump transition and inserting a transition for each level.
+    // FIXME(nft): Allow symbol jump transitions?
+//    if (jump_mode == JumpMode::RepeatSymbol) {
+//        delta.add(state, symbol, state);
+//        insert_word(state, Word(num_of_levels, symbol), state);
+//    } else {
+        insert_word(state, Word(num_of_levels, symbol), state);
+//    }
+}
+
+void Nft::clear() {
+    mata::nfa::Nfa::clear();
+    levels.clear();
+}
+
+bool Nft::is_identical(const Nft& aut) const {
+    return num_of_levels == aut.num_of_levels && levels == aut.levels && mata::nfa::Nfa::is_identical(aut);
+}
+
+Levels& Levels::set(State state, Level level) {
+    if (size() <= state) { resize(state + 1, DEFAULT_LEVEL); }
+    (*this)[state] = level;
+    return *this;
+}
+
+mata::nfa::Nfa Nft::to_nfa_update_copy(
+    const utils::OrdVector<Symbol>& dont_care_symbol_replacements, const JumpMode jump_mode) const {
+    return get_one_level_aut(dont_care_symbol_replacements, jump_mode).to_nfa_copy();
+}
+
+mata::nfa::Nfa Nft::to_nfa_update_move(
+    const utils::OrdVector<Symbol>& dont_care_symbol_replacements, const JumpMode jump_mode) {
+    make_one_level_aut(dont_care_symbol_replacements, jump_mode);
+    return to_nfa_move();
+}
diff --git a/src/nft/operations.cc b/src/nft/operations.cc
new file mode 100644
index 000000000..c5b1b39ba
--- /dev/null
+++ b/src/nft/operations.cc
@@ -0,0 +1,1049 @@
+/* nft.cc -- operations for NFT
+ */
+
+#include <algorithm>
+#include <list>
+#include <unordered_set>
+#include <iterator>
+#include <algorithm>
+
+// MATA headers
+#include "mata/nft/delta.hh"
+#include "mata/utils/sparse-set.hh"
+#include "mata/nft/nft.hh"
+#include "mata/nft/algorithms.hh"
+#include "mata/nft/builder.hh"
+#include "mata/nft/strings.hh"
+#include <mata/simlib/explicit_lts.hh>
+
+using std::tie;
+
+using namespace mata::utils;
+using namespace mata::nft;
+using namespace mata::nfa;
+using namespace mata;
+using mata::Symbol;
+
+using StateBoolArray = std::vector<bool>; ///< Bool array for states in the automaton.
+
+namespace {
+    Simlib::Util::BinaryRelation compute_fw_direct_simulation(const Nft& aut) {
+        Symbol maxSymbol{ aut.delta.get_max_symbol() };
+        const size_t state_num{ aut.num_of_states() };
+        Simlib::ExplicitLTS LTSforSimulation(state_num);
+
+        for (const Transition& transition : aut.delta.transitions()) {
+            LTSforSimulation.add_transition(transition.source, transition.symbol, transition.target);
+        }
+
+        // final states cannot be simulated by nonfinal -> we add new selfloops over final states with new symbol in LTS
+        for (State finalState : aut.final) {
+            LTSforSimulation.add_transition(finalState, maxSymbol + 1, finalState);
+        }
+
+        LTSforSimulation.init();
+        return LTSforSimulation.compute_simulation();
+    }
+
+    Nft reduce_size_by_simulation(const Nft& aut, StateRenaming &state_renaming) {
+        Nft result;
+        const auto sim_relation = algorithms::compute_relation(
+                aut, ParameterMap{{ "relation", "simulation"}, { "direction", "forward"}});
+
+        auto sim_relation_symmetric = sim_relation;
+        sim_relation_symmetric.restrict_to_symmetric();
+
+        // for State q, quot_proj[q] should be the representative state representing the symmetric class of states in simulation
+        std::vector<size_t> quot_proj;
+        sim_relation_symmetric.get_quotient_projection(quot_proj);
+
+        const size_t num_of_states = aut.num_of_states();
+
+        // map each state q of aut to the state of the reduced automaton representing the simulation class of q
+        for (State q = 0; q < num_of_states; ++q) {
+            const State qReprState = quot_proj[q];
+            if (state_renaming.count(qReprState) == 0) { // we need to map q's class to a new state in reducedAut
+                const State qClass = result.add_state();
+                state_renaming[qReprState] = qClass;
+                state_renaming[q] = qClass;
+            } else {
+                state_renaming[q] = state_renaming[qReprState];
+            }
+        }
+
+        for (State q = 0; q < num_of_states; ++q) {
+            const State q_class_state = state_renaming.at(q);
+
+            if (aut.initial[q]) { // if a symmetric class contains initial state, then the whole class should be initial
+                result.initial.insert(q_class_state);
+            }
+
+            if (quot_proj[q] == q) { // we process only transitions starting from the representative state, this is enough for simulation
+                for (const auto &q_trans : aut.delta.state_post(q)) {
+                    const StateSet representatives_of_states_to = [&]{
+                        StateSet state_set;
+                        for (auto s : q_trans.targets) {
+                            state_set.insert(quot_proj[s]);
+                        }
+                        return state_set;
+                    }();
+
+                    // get the class states of those representatives that are not simulated by another representative in representatives_of_states_to
+                    StateSet representatives_class_states;
+                    for (const State s : representatives_of_states_to) {
+                        bool is_state_important = true; // if true, we need to keep the transition from q to s
+                        for (const State p : representatives_of_states_to) {
+                            if (s != p && sim_relation.get(s, p)) { // if p (different from s) simulates s
+                                is_state_important = false; // as p simulates s, the transition from q to s is not important to keep, as it is subsumed in transition from q to p
+                                break;
+                            }
+                        }
+                        if (is_state_important) {
+                            representatives_class_states.insert(state_renaming.at(s));
+                        }
+                    }
+
+                    // add the transition 'q_class_state-q_trans.symbol->representatives_class_states' at the end of transition list of transitions starting from q_class_state
+                    // as the q_trans.symbol should be the largest symbol we saw (as we iterate trough getTransitionsFromState(q) which is ordered)
+                    result.delta.mutable_state_post(q_class_state).insert(SymbolPost(q_trans.symbol, representatives_class_states));
+                }
+
+                if (aut.final[q]) { // if q is final, then all states in its class are final => we make q_class_state final
+                    result.final.insert(q_class_state);
+                }
+            }
+        }
+
+        return result;
+    }
+}
+
+//TODO: based on the comments inside, this function needs to be rewritten in a more optimal way.
+Nft mata::nft::remove_epsilon(const Nft& aut, Symbol epsilon) {
+    // cannot use multimap, because it can contain multiple occurrences of (a -> a), (a -> a)
+    std::unordered_map<State, StateSet> eps_closure;
+
+    // TODO: grossly inefficient
+    // first we compute the epsilon closure
+    const size_t num_of_states{aut.num_of_states() };
+    for (size_t i{ 0 }; i < num_of_states; ++i)
+    {
+        for (const auto& trans: aut.delta[i])
+        { // initialize
+            const auto it_ins_pair = eps_closure.insert({i, {i}});
+            if (trans.symbol == epsilon)
+            {
+                StateSet& closure = it_ins_pair.first->second;
+                // TODO: Fix possibly insert to OrdVector. Create list already ordered, then merge (do not need to resize each time);
+                closure.insert(trans.targets);
+            }
+        }
+    }
+
+    bool changed = true;
+    while (changed) { // Compute the fixpoint.
+        changed = false;
+        for (size_t i = 0; i < num_of_states; ++i) {
+            const StatePost& post{ aut.delta[i] };
+            const auto eps_move_it { post.find(epsilon) };//TODO: make faster if default epsilon
+            if (eps_move_it != post.end()) {
+                StateSet& src_eps_cl = eps_closure[i];
+                for (const State tgt: eps_move_it->targets) {
+                    const StateSet& tgt_eps_cl = eps_closure[tgt];
+                    for (const State st: tgt_eps_cl) {
+                        if (src_eps_cl.count(st) == 0) {
+                            changed = true;
+                            break;
+                        }
+                    }
+                    src_eps_cl.insert(tgt_eps_cl);
+                }
+            }
+        }
+    }
+
+    // Construct the automaton without epsilon transitions.
+    Nft result{ Delta{}, aut.initial, aut.final, aut.levels, aut.num_of_levels, aut.alphabet };
+    for (const auto& state_closure_pair : eps_closure) { // For every state.
+        State src_state = state_closure_pair.first;
+        for (State eps_cl_state : state_closure_pair.second) { // For every state in its epsilon closure.
+            if (aut.final[eps_cl_state]) result.final.insert(src_state);
+            for (const SymbolPost& move : aut.delta[eps_cl_state]) {
+                if (move.symbol == epsilon) continue;
+                // TODO: this could be done more efficiently if we had a better add method
+                for (State tgt_state : move.targets) {
+                    result.delta.add(src_state, move.symbol, tgt_state);
+                }
+            }
+        }
+    }
+    return result;
+}
+
+Nft mata::nft::project_out(const Nft& nft, const utils::OrdVector<Level>& levels_to_project, const JumpMode jump_mode) {
+    assert(!levels_to_project.empty());
+    assert(*std::max_element(levels_to_project.begin(), levels_to_project.end()) < nft.num_of_levels);
+
+    // Checks if a given state is being projected out based on the levels_to_project vector.
+    auto is_projected_out = [&](State s) {
+        return levels_to_project.find(nft.levels[s]) != levels_to_project.end();
+    };
+
+    // Checks if each level between given states is being projected out.
+    auto is_projected_along_path = [&](State src, State tgt) {
+        Level stop_lvl = (nft.levels[tgt] == 0) ? static_cast<Level>(nft.num_of_levels) : nft.levels[tgt];
+        for (Level lvl{ nft.levels[src] }; lvl < stop_lvl; lvl++) {
+            if (levels_to_project.find(lvl) == levels_to_project.end()) {
+                return false;
+            }
+        }
+        return true;
+    };
+
+    // Determines the transition length between two states based on their levels.
+    auto get_trans_len = [&](State src, State tgt) {
+        return (nft.levels[src] == 0) ? (nft.num_of_levels - nft.levels[src]) : (nft.levels[tgt] - nft.levels[src]);
+    };
+
+    // Returns one-state automaton it the case of projecting all levels.
+    if (nft.num_of_levels == levels_to_project.size()) {
+        if (nft.is_lang_empty()) {
+            return Nft(1, {0}, {}, {}, 0);
+        }
+        return Nft(1, {0}, {0}, {}, 0);
+    }
+
+    // Calculates the smallest level 0 < k < num_of_levels that starts a consecutive ascending sequence
+    // of levels k, k+1, k+2, ..., num_of_levels-1 in the ordered-vector levels_to_project.
+    // If there is no such sequence, then k == num_of_levels.
+    size_t seq_start_idx = nft.num_of_levels;
+    const std::vector<Level> levels_to_proj_v = levels_to_project.to_vector();
+    for (auto levels_to_proj_v_revit = levels_to_proj_v.rbegin();
+         levels_to_proj_v_revit != levels_to_proj_v.rend() && *levels_to_proj_v_revit == seq_start_idx - 1;
+         ++levels_to_proj_v_revit, --seq_start_idx);
+
+    // Only states whose level is part of the sequence (will have level 0) can additionally be marked as final.
+    auto can_be_final = [&](State s) {
+        return seq_start_idx <= nft.levels[s];
+    };
+
+    // Builds a vector of size num_of_levels. Each index k contains a new level for level k.
+    // Sets levels to 0 starting from seq_start_idx.
+    // Example:
+    // old levels    0 1 2 3 4 5 6
+    // project out     x   x   x x
+    // new levels    0 0 1 2 2 0 0
+    std::vector<Level> new_levels(nft.num_of_levels , 0);
+    Level lvl_sub{ 0 };
+    for (Level lvl_old{ 0 }; lvl_old < seq_start_idx; lvl_old++) {
+        new_levels[lvl_old] = static_cast<Level>(lvl_old - lvl_sub);
+        if (levels_to_project.find(lvl_old) != levels_to_project.end()) {
+            lvl_sub++;
+        }
+    }
+
+    // cannot use multimap, because it can contain multiple occurrences of (a -> a), (a -> a)
+    const size_t num_of_states_in_delta{ nft.delta.num_of_states() };
+    std::vector<StateSet> closure = std::vector<StateSet>(num_of_states_in_delta, OrdVector<State>());;
+
+    // TODO: Evaluate efficiency. This might not be as inefficient as the remove_epsilon closure.
+    // Begin by initializing the closure.
+    for (State source{ 0 }; source < num_of_states_in_delta; ++source)
+    {
+        closure[source].insert(source);
+        if (!is_projected_out(source)) {
+            continue;
+        }
+        for (const auto& trans: nft.delta[source])
+        {
+            for (const auto& target : trans.targets) {
+                if (is_projected_along_path(source, target)) {
+                    closure[source].insert(target);
+                }
+            }
+        }
+    }
+
+    // We will focus only on those states that will be affected by projection.
+    std::vector<State> states_to_project;
+    for (State s{ 0 }; s < num_of_states_in_delta; s++) {
+        if (closure[s].size() > 1) {
+            states_to_project.push_back(s);
+        }
+    }
+
+    // Compute transitive closure.
+    bool changed = true;
+    while (changed) { // Compute the fixpoint.
+        changed = false;
+        for (const State s : states_to_project) {
+            for (const State cls_state : closure[s]) {
+                if (!closure[cls_state].is_subset_of(closure[s])) {
+                    closure[s].insert(closure[cls_state]);
+                    changed = true;
+                }
+            }
+        }
+    }
+
+    // Construct the automaton with projected levels.
+    Nft result{ Delta{}, nft.initial, nft.final, nft.levels, nft.num_of_levels, nft.alphabet };
+    for (State src_state{ 0 }; src_state < num_of_states_in_delta; src_state++) { // For every state.
+        for (State cls_state : closure[src_state]) { // For every state in its epsilon closure.
+            if (nft.final[cls_state] && can_be_final(src_state)) result.final.insert(src_state);
+            for (const SymbolPost& move : nft.delta[cls_state]) {
+                // TODO: this could be done more efficiently if we had a better add method
+                for (State tgt_state : move.targets) {
+                    bool is_loop_on_target = cls_state == tgt_state;
+                    if (is_projected_along_path(cls_state, tgt_state)) continue;
+                    if (is_projected_out(cls_state) && get_trans_len(cls_state, tgt_state) == 1 && !is_loop_on_target) continue;
+
+                    if (is_projected_out(cls_state)) {
+                        // If there are remaining levels between cls_state and tgt_state
+                        // on a transition with a length greater than 1, then these levels must be preserved.
+                        if (jump_mode == JumpMode::RepeatSymbol) {
+                            result.delta.add(src_state, move.symbol, tgt_state);
+                        } else {
+                            result.delta.add(src_state, DONT_CARE, tgt_state);
+                        }
+                    } else if (is_loop_on_target) {
+                        // Instead of creating a transition to tgt_state and
+                        // then a self-loop, establish the self-loop directly on src_state.
+                        result.delta.add(src_state, move.symbol, src_state);
+                    } else {
+                        result.delta.add(src_state, move.symbol, tgt_state);
+                    }
+                }
+            }
+        }
+    }
+    // TODO(nft): Sometimes, unreachable states are left in the automaton.
+    // These states typically contain projected levels with a self-loop.
+    result = result.trim();
+
+    // Repare levels
+    for (State s{ 0 }; s < result.levels.size(); s++) {
+        result.levels[s] = new_levels[result.levels[s]];
+    }
+    result.num_of_levels = static_cast<Level>(result.num_of_levels - levels_to_project.size());
+
+    return result;
+}
+
+Nft mata::nft::project_out(const Nft& nft, const Level level_to_project, const JumpMode jump_mode) {
+    return project_out(nft, utils::OrdVector{ level_to_project }, jump_mode);
+}
+
+Nft mata::nft::project_to(const Nft& nft, const OrdVector<Level>& levels_to_project, const JumpMode jump_mode) {
+    OrdVector<Level> all_levels{ OrdVector<Level>::with_reserved(nft.num_of_levels) };
+    for (Level level{ 0 }; level < nft.num_of_levels; ++level) { all_levels.push_back(level); }
+    OrdVector<Level> levels_to_project_out{ OrdVector<Level>::with_reserved(nft.num_of_levels) };
+    std::set_difference(all_levels.begin(), all_levels.end(), levels_to_project.begin(),
+                        levels_to_project.end(), std::back_inserter(levels_to_project_out) );
+    return project_out(nft, levels_to_project_out, jump_mode);
+}
+
+Nft mata::nft::project_to(const Nft& nft, Level level_to_project, const JumpMode jump_mode) {
+    return project_to(nft, OrdVector<Level>{ level_to_project }, jump_mode);
+}
+
+Nft Nft::apply(const nfa::Nfa& nfa, Level level_to_apply_on, JumpMode jump_mode) const {
+    Nft nft_from_nfa{ nft::builder::create_from_nfa(nfa, this->num_of_levels) };
+    return compose(nft_from_nfa, *this, static_cast<Level>(this->num_of_levels) - 1, level_to_apply_on, jump_mode);
+}
+
+
+Nft mata::nft::insert_levels(const Nft& nft, const BoolVector& new_levels_mask, const JumpMode jump_mode) {
+    assert(0 < nft.num_of_levels);
+    assert(nft.num_of_levels <= new_levels_mask.size());
+    assert(static_cast<size_t>(std::count(new_levels_mask.begin(), new_levels_mask.end(), false)) == nft.num_of_levels);
+
+    if (nft.num_of_levels == new_levels_mask.size()) {
+         return { nft };
+    }
+
+    // Construct a vector of size equal to num_of_levels. Each index k in this vector represents a new level for the original level k.
+    // Note: The 0th index always remains zero.
+    // Here are a couple of examples to illustrate this:
+    // Example 1:
+    //   Input (levels)      : 0 1 2
+    //   Mask                : 0 0 1 1 0 1
+    //   Output (new levels) : 0 1 4
+    // Example 2:
+    //   Input (levels)      : 0 1 2
+    //   Mask                : 1 1 0 1 1 0 0
+    //   Output (new levels) : 0 3 6
+    std::vector<Level> updated_levels(nft.num_of_levels, 0);
+    Level old_lvl = 0;
+    Level new_lvl = 0;
+    auto mask_it = std::find(new_levels_mask.begin(), new_levels_mask.end(), false);
+    if (new_levels_mask[0]) {
+        old_lvl = 1;
+        new_lvl = static_cast<Level>(std::distance(new_levels_mask.begin(), mask_it) + 1);
+        ++mask_it;
+    }
+    for (; mask_it != new_levels_mask.end(); ++mask_it, ++new_lvl) {
+        if (!*mask_it) {
+            updated_levels[old_lvl++] = new_lvl;
+        }
+    }
+    // Repare state levels
+    std::vector<Level> new_state_levels(nft.levels.size());
+    for (State s{ 0 }; s < nft.levels.size(); s++) {
+        new_state_levels[s] = updated_levels[nft.levels[s]];
+    }
+
+    // Construct vector of next inner levels for usable inner states.
+    // This allows us to create only the important inner states, rather than all of them.
+    // Example:
+    //  Input (new_levels_mask):               0 1 1 0 1 0 1 1  0  1  1  1
+    //  Output (JumpMode::RepeatSymbol):       1 3 3 4 5 6 8 8  9 12 12 12
+    //  Output (JumpMode::AppendDontCares):    3 3 3 5 5 8 8 8 12 12 12 12
+    const size_t mask_size = new_levels_mask.size();
+    std::vector<Level> next_inner_levels(mask_size);
+    Level next_level = static_cast<Level>(mask_size);
+    size_t i = mask_size - 1;
+    for (auto it = new_levels_mask.rbegin(); it != new_levels_mask.rend(); ++it, --i) {
+        next_inner_levels[i] = next_level;
+        if (!*it) {
+            if (jump_mode == JumpMode::RepeatSymbol) {
+                next_inner_levels[i] = static_cast<Level>(i+1);
+            }
+            next_level = static_cast<Level>(i);
+        }
+    }
+
+    // Construct an empty automaton with updated levels.
+    Nft result(Delta(nft.num_of_states()), nft.initial, nft.final, new_state_levels, static_cast<unsigned int>(new_levels_mask.size()), nft.alphabet);
+
+    // Function to create a transition between source and target states.
+    // The transition symbol is determined based on the parameters:
+    // it could be a specific symb, DONT_CARE, or a default symbol.
+    auto create_transition = [&](const State src, const Symbol symb, const State tgt, const bool is_inserted_level, const bool is_old_level_processed) {
+        if (!is_inserted_level && (jump_mode == JumpMode::RepeatSymbol || !is_old_level_processed)) {
+            result.delta.add(src, symb, tgt);
+        } else {
+            result.delta.add(src, DONT_CARE, tgt);
+        }
+    };
+
+    std::vector<std::vector<State>> state_level_matrix(nft.num_of_states(), std::vector<State>());
+    // Creates an inner state for a given source state and inner level.
+    // If the inner state already exists, then it is reused.
+    auto get_inner_state = [&](const State src, const Level inner_level, const bool is_inserted_level, const bool is_old_level_processed) {
+        if (!is_old_level_processed && is_inserted_level) {
+            const size_t inner_state_idx = inner_level - result.levels[src] - 1;
+
+            if (state_level_matrix[src].size() <= inner_state_idx) {
+                state_level_matrix[src].resize((inner_state_idx + 1) * 2, Limits::max_state);
+            }
+
+            if (state_level_matrix[src][inner_state_idx] != Limits::max_state) {
+                return state_level_matrix[src][inner_state_idx];
+            }
+
+            State inner_state = result.add_state_with_level(inner_level);
+            state_level_matrix[src][inner_state_idx] = inner_state;
+            return inner_state;
+
+        }
+        return result.add_state_with_level(inner_level);
+    };
+
+    //Construct delta with inserted levels and auxiliary states.
+    for (const auto &trans : nft.delta.transitions()) {
+        State src = trans.source;
+        Level src_lvl = result.levels[trans.source];
+        State inner;
+        const Level stop_level = static_cast<Level>((result.levels[trans.target] == 0) ? (new_levels_mask.size() - 1) : (result.levels[trans.target] - 1));
+
+        // Construct the first n-1 parts of the original transition.
+        bool is_old_level_processed = false;
+        while (next_inner_levels[src_lvl] < next_inner_levels[stop_level]) {
+            inner = get_inner_state(trans.source, next_inner_levels[src_lvl], new_levels_mask[src_lvl], is_old_level_processed);
+            create_transition(src, trans.symbol, inner, new_levels_mask[src_lvl], is_old_level_processed);
+            if (!new_levels_mask[src_lvl]) {
+                is_old_level_processed = true;
+            }
+            src = inner;
+            src_lvl = result.levels[src];
+            assert(src_lvl == result.levels[inner]);
+        }
+        // Construct the n-th part of the transition.
+        create_transition(src, trans.symbol, trans.target, new_levels_mask[src_lvl], is_old_level_processed);
+    }
+
+    return result;
+}
+
+Nft mata::nft::insert_level(const Nft& nft, const Level new_level, const JumpMode jump_mode) {
+    // TODO(nft): Optimize the insertion of just one level by using move.
+    BoolVector new_levels_mask(nft.num_of_levels + 1, false);
+    if (new_level < new_levels_mask.size()) {
+        new_levels_mask[new_level] = true;
+    } else {
+        new_levels_mask[nft.num_of_levels] = true;
+        new_levels_mask.resize(new_level + 1, true);
+    }
+    return insert_levels(nft, new_levels_mask, jump_mode);
+}
+
+Nft mata::nft::fragile_revert(const Nft& aut) {
+    const size_t num_of_states{ aut.num_of_states() };
+
+    Nft result(num_of_states);
+
+    result.initial = aut.final;
+    result.final = aut.initial;
+
+    // Compute non-epsilon symbols.
+    OrdVector<Symbol> symbols = aut.delta.get_used_symbols();
+    if (symbols.empty()) { return result; }
+    if (symbols.back() == EPSILON) { symbols.pop_back(); }
+    // size of the "used alphabet", i.e. max symbol+1 or 0
+    Symbol alphasize =  (symbols.empty()) ? 0 : (symbols.back()+1);
+
+#ifdef _STATIC_STRUCTURES_
+    //STATIC DATA STRUCTURES:
+    // Not sure that it works ideally, whether the space for the inner vectors stays there.
+    static std::vector<std::vector<State>> sources;
+    static std::vector<std::vector<State>> targets;
+    static std::vector<State> e_sources;
+    static std::vector<State> e_targets;
+    if (alphasize>sources.size()) {
+        sources.resize(alphasize);
+        targets.resize(alphasize);
+    }
+
+    e_sources.clear();
+    e_targets.clear();
+
+    //WHEN ONLY MAX SYMBOL IS COMPUTED
+    // for (int i = 0;i<alphasize;i++) {
+    //     for (int i = 0;i<alphasize;i++) {
+    //         if (!sources[i].empty())
+    //         {
+    //             sources[i].resize(0);
+    //             targets[i].resize(0);
+    //         }
+    //     }
+    // }
+
+    //WHEN ALL SYMBOLS ARE COMPUTED
+    for (Symbol symbol: symbols) {
+        if(!sources[symbol].empty()) {
+            sources[symbol].clear();
+            targets[symbol].clear();
+        }
+    }
+#else
+    // NORMAL, NON STATIC DATA STRUCTURES:
+    //All transition of delta are to be copied here, into two arrays of transition sources and targets indexed by the transition symbol.
+    // There is a special treatment for epsilon, since we want the arrays to be only as long as the largest symbol in the automaton,
+    // and epsilon is the maximum (so we don't want to have the maximum array lenght whenever epsilon is present)
+    std::vector<std::vector<State>> sources (alphasize);
+    std::vector<std::vector<State>> targets (alphasize);
+    std::vector<State> e_sources;
+    std::vector<State> e_targets;
+#endif
+
+    //Copy all transition with non-e symbols to the arrays of sources and targets indexed by symbols.
+    //Targets and sources of e-transitions go to the special place.
+    //Important: since we are going through delta in order of sources, the sources arrays are all ordered.
+    for (State sourceState{ 0 }; sourceState < num_of_states; ++sourceState) {
+        for (const SymbolPost &move: aut.delta[sourceState]) {
+            if (move.symbol == EPSILON) {
+                for (const State targetState: move.targets) {
+                    //reserve_on_insert(e_sources);
+                    e_sources.push_back(sourceState);
+                    //reserve_on_insert(e_targets);
+                    e_targets.push_back(targetState);
+                }
+            }
+            else {
+                for (const State targetState: move.targets) {
+                    //reserve_on_insert(sources[move.symbol]);
+                    sources[move.symbol].push_back(sourceState);
+                    //reserve_on_insert(targets[move.symbol]);
+                    targets[move.symbol].push_back(targetState);
+                }
+            }
+        }
+    }
+
+    //Now make the delta of the reversed automaton.
+    //Important: since sources are ordered, when adding them as targets, we can just push them back.
+    result.delta.reserve(num_of_states);
+
+    // adding non-e transitions
+    for (const Symbol symbol: symbols) {
+        for (size_t i{ 0 }; i < sources[symbol].size(); ++i) {
+            State tgt_state =sources[symbol][i];
+            State src_state =targets[symbol][i];
+            StatePost & src_post = result.delta.mutable_state_post(src_state);
+            if (src_post.empty() || src_post.back().symbol != symbol) {
+                src_post.push_back(SymbolPost(symbol));
+            }
+            src_post.back().push_back(tgt_state);
+        }
+    }
+
+    // adding e-transitions
+    for (size_t i{ 0 }; i < e_sources.size(); ++i) {
+        State tgt_state =e_sources[i];
+        State src_state =e_targets[i];
+        StatePost & src_post = result.delta.mutable_state_post(src_state);
+        if (src_post.empty() || src_post.back().symbol != EPSILON) {
+            src_post.push_back(SymbolPost(EPSILON));
+        }
+        src_post.back().push_back(tgt_state);
+    }
+
+    //sorting the targets
+    //Hm I don't know why I put this here, but it should not be needed ...
+    //for (State q = 0, states_num = result.delta.post_size(); q<states_num;++q) {
+    //    for (auto m = result.delta.get_mutable_post(q).begin(); m != result.delta.get_mutable_post(q).end(); ++m) {
+    //        sort_and_rmdupl(m->targets);
+    //    }
+    //}
+
+    return result;
+}
+
+Nft mata::nft::simple_revert(const Nft& aut) {
+    Nft result;
+    result.clear();
+
+    const size_t num_of_states{ aut.num_of_states() };
+    result.delta.allocate(num_of_states);
+
+    for (State sourceState{ 0 }; sourceState < num_of_states; ++sourceState) {
+        for (const SymbolPost &transition: aut.delta[sourceState]) {
+            for (const State targetState: transition.targets) {
+                result.delta.add(targetState, transition.symbol, sourceState);
+            }
+        }
+    }
+
+    result.initial = aut.final;
+    result.final = aut.initial;
+
+    return result;
+}
+
+//not so great, can be removed
+Nft mata::nft::somewhat_simple_revert(const Nft& aut) {
+    const size_t num_of_states{ aut.num_of_states() };
+
+    Nft result(num_of_states);
+
+    result.initial = aut.final;
+    result.final = aut.initial;
+
+    for (State sourceState{ 0 }; sourceState < num_of_states; ++sourceState) {
+        for (const SymbolPost &transition: aut.delta[sourceState]) {
+            for (const State targetState: transition.targets) {
+                StatePost & post = result.delta.mutable_state_post(targetState);
+                //auto move = std::find(post.begin(),post.end(),Move(transition.symbol));
+                auto move = post.find(SymbolPost(transition.symbol));
+                if (move == post.end()) {
+                    //post.push_back(Move(transition.symbol,sourceState));
+                    post.insert(SymbolPost(transition.symbol, sourceState));
+                }
+                else
+                    move->push_back(sourceState);
+                    //move->insert(sourceState);
+            }
+        }
+    }
+
+    //sorting the targets
+    for (State q = 0, states_num = result.delta.num_of_states(); q < states_num; ++q) {
+        //Post & post = result.delta.get_mutable_post(q);
+        //utils::sort_and_rmdupl(post);
+        for (SymbolPost& m: result.delta.mutable_state_post(q)) { sort_and_rmdupl(m.targets); }
+    }
+
+    return result;
+}
+
+Nft mata::nft::revert(const Nft& aut) {
+    return simple_revert(aut);
+    //return fragile_revert(aut);
+    //return somewhat_simple_revert(aut);
+}
+
+std::pair<Run, bool> mata::nft::Nft::get_word_for_path(const Run& run) const {
+    if (run.path.empty()) { return {{}, true}; }
+
+    Run word;
+    State cur = run.path[0];
+    for (size_t i = 1; i < run.path.size(); ++i) {
+        State newSt = run.path[i];
+        bool found = false;
+        if (!this->delta.empty()) {
+            for (const auto &symbolMap: this->delta[cur]) {
+                for (State st: symbolMap.targets) {
+                    if (st == newSt) {
+                        word.word.push_back(symbolMap.symbol);
+                        found = true;
+                        break;
+                    }
+                }
+                if (found) { break; }
+            }
+        }
+        if (!found) { return {{}, false}; }
+        cur = newSt;    // update current state
+    }
+    return {word, true};
+}
+
+//TODO: this is not efficient
+bool mata::nft::Nft::is_in_lang(const Run& run) const {
+    StateSet current_post(this->initial);
+    for (const Symbol sym : run.word) {
+        current_post = this->post(current_post, sym);
+        if (current_post.empty()) { return false; }
+    }
+    return this->final.intersects_with(current_post);
+}
+
+/// Checks whether the prefix of a string is in the language of an automaton
+// TODO: slow and it should share code with is_in_lang
+bool mata::nft::Nft::is_prfx_in_lang(const Run& run) const {
+    StateSet current_post{ this->initial };
+    for (const Symbol sym : run.word) {
+        if (this->final.intersects_with(current_post)) { return true; }
+        current_post = this->post(current_post, sym);
+        if (current_post.empty()) { return false; }
+    }
+    return this->final.intersects_with(current_post);
+}
+
+Nft mata::nft::algorithms::minimize_brzozowski(const Nft& aut) {
+    //compute the minimal deterministic automaton, Brzozovski algorithm
+    return determinize(revert(determinize(revert(aut))));
+}
+
+Nft mata::nft::minimize(
+                const Nft& aut,
+                const ParameterMap& params)
+{
+    Nft result;
+    // setting the default algorithm
+    decltype(algorithms::minimize_brzozowski)* algo = algorithms::minimize_brzozowski;
+    if (!haskey(params, "algorithm")) {
+        throw std::runtime_error(std::to_string(__func__) +
+            " requires setting the \"algorithm\" key in the \"params\" argument; "
+            "received: " + std::to_string(params));
+    }
+
+    const std::string& str_algo = params.at("algorithm");
+    if ("brzozowski" == str_algo) {  /* default */ }
+    else {
+        throw std::runtime_error(std::to_string(__func__) +
+            " received an unknown value of the \"algorithm\" key: " + str_algo);
+    }
+
+    return algo(aut);
+}
+
+Nft mata::nft::uni(const Nft &lhs, const Nft &rhs) {
+    Nft union_nft{ lhs };
+    return union_nft.uni(rhs);
+}
+
+Nft& Nft::uni(const Nft& aut) {
+    size_t n = this->num_of_states();
+    auto upd_fnc = [&](State st) {
+        return st + n;
+    };
+
+    // copy the information about aut to save the case when this is the same object as aut.
+    size_t aut_states = aut.num_of_states();
+    SparseSet<mata::nft::State> aut_final_copy = aut.final;
+    SparseSet<mata::nft::State> aut_initial_copy = aut.initial;
+
+    this->delta.allocate(n);
+    this->delta.append(aut.delta.renumber_targets(upd_fnc));
+
+    // set accepting states
+    this->final.reserve(n+aut_states);
+    for(const State& aut_fin : aut_final_copy) {
+        this->final.insert(upd_fnc(aut_fin));
+    }
+    // set unitial states
+    this->initial.reserve(n+aut_states);
+    for(const State& aut_ini : aut_initial_copy) {
+        this->initial.insert(upd_fnc(aut_ini));
+    }
+
+    return *this;
+}
+
+Simlib::Util::BinaryRelation mata::nft::algorithms::compute_relation(const Nft& aut, const ParameterMap& params) {
+    if (!haskey(params, "relation")) {
+        throw std::runtime_error(std::to_string(__func__) +
+                                 " requires setting the \"relation\" key in the \"params\" argument; "
+                                 "received: " + std::to_string(params));
+    }
+    if (!haskey(params, "direction")) {
+        throw std::runtime_error(std::to_string(__func__) +
+                                 " requires setting the \"direction\" key in the \"params\" argument; "
+                                 "received: " + std::to_string(params));
+    }
+
+    const std::string& relation = params.at("relation");
+    const std::string& direction = params.at("direction");
+    if ("simulation" == relation && direction == "forward") {
+        return compute_fw_direct_simulation(aut);
+    }
+    else {
+        throw std::runtime_error(std::to_string(__func__) +
+                                 " received an unknown value of the \"relation\" key: " + relation);
+    }
+}
+
+Nft mata::nft::reduce(const Nft &aut, StateRenaming *state_renaming, const ParameterMap& params) {
+    if (!haskey(params, "algorithm")) {
+        throw std::runtime_error(std::to_string(__func__) +
+                                 " requires setting the \"algorithm\" key in the \"params\" argument; "
+                                 "received: " + std::to_string(params));
+    }
+
+    Nft result;
+    std::unordered_map<State,State> reduced_state_map;
+    const std::string& algorithm = params.at("algorithm");
+    if ("simulation" == algorithm) {
+        result = reduce_size_by_simulation(aut, reduced_state_map);
+    } else {
+        throw std::runtime_error(std::to_string(__func__) +
+                                 " received an unknown value of the \"algorithm\" key: " + algorithm);
+    }
+
+    if (state_renaming) {
+        state_renaming->clear();
+        *state_renaming = reduced_state_map;
+    }
+    return result;
+}
+
+Nft mata::nft::determinize(
+        const Nft&  aut,
+        std::unordered_map<StateSet, State> *subset_map) {
+
+    Nft result;
+    //assuming all sets targets are non-empty
+    std::vector<std::pair<State, StateSet>> worklist;
+    bool deallocate_subset_map = false;
+    if (subset_map == nullptr) {
+        subset_map = new std::unordered_map<StateSet,State>();
+        deallocate_subset_map = true;
+    }
+
+    result.clear();
+
+    const StateSet S0 =  StateSet(aut.initial);
+    const State S0id = result.add_state();
+    result.initial.insert(S0id);
+
+    if (aut.final.intersects_with(S0)) {
+        result.final.insert(S0id);
+    }
+    worklist.emplace_back(S0id, S0);
+
+    (*subset_map)[mata::utils::OrdVector<State>(S0)] = S0id;
+
+    if (aut.delta.empty())
+        return result;
+
+    using Iterator = mata::utils::OrdVector<SymbolPost>::const_iterator;
+    SynchronizedExistentialSymbolPostIterator synchronized_iterator;
+
+    while (!worklist.empty()) {
+        const auto Spair = worklist.back();
+        worklist.pop_back();
+        const StateSet S = Spair.second;
+        const State Sid = Spair.first;
+        if (S.empty()) {
+            // This should not happen assuming all sets targets are non-empty.
+            break;
+        }
+
+        // add moves of S to the sync ex iterator
+        // TODO: shouldn't we also reset first?
+        for (State q: S) {
+            mata::utils::push_back(synchronized_iterator, aut.delta[q]);
+        }
+
+        while (synchronized_iterator.advance()) {
+
+            // extract post from the sychronized_iterator iterator
+            const std::vector<Iterator>& moves = synchronized_iterator.get_current();
+            Symbol currentSymbol = (*moves.begin())->symbol;
+            StateSet T = synchronized_iterator.unify_targets();
+
+            const auto existingTitr = subset_map->find(T);
+            State Tid;
+            if (existingTitr != subset_map->end()) {
+                Tid = existingTitr->second;
+            } else {
+                Tid = result.add_state();
+                (*subset_map)[mata::utils::OrdVector<State>(T)] = Tid;
+                if (aut.final.intersects_with(T)) {
+                    result.final.insert(Tid);
+                }
+                worklist.emplace_back(Tid, T);
+            }
+            result.delta.mutable_state_post(Sid).insert(SymbolPost(currentSymbol, Tid));
+        }
+    }
+
+    if (deallocate_subset_map) { delete subset_map; }
+
+    return result;
+}
+
+std::ostream& std::operator<<(std::ostream& os, const Nft& nft) {
+    nft.print_to_mata(os);
+    return os;
+}
+
+Run mata::nft::encode_word(const Alphabet* alphabet, const std::vector<std::string>& input) {
+    return mata::nfa::encode_word(alphabet, input);
+}
+
+std::set<mata::Word> mata::nft::Nft::get_words(size_t max_length) const {
+    std::set<mata::Word> result;
+
+    // contains a pair: a state s and the word with which we got to the state s
+    std::vector<std::pair<State, mata::Word>> worklist;
+    // initializing worklist
+    for (State init_state : initial) {
+        worklist.push_back({init_state, {}});
+        if (final.contains(init_state)) {
+            result.insert(mata::Word());
+        }
+    }
+
+    // will be used during the loop
+    std::vector<std::pair<State, mata::Word>> new_worklist;
+
+    unsigned cur_length = 0;
+    while (!worklist.empty() && cur_length < max_length) {
+        new_worklist.clear();
+        for (const auto& state_and_word : worklist) {
+            State s_from = state_and_word.first;
+            const mata::Word& word = state_and_word.second;
+            for (const SymbolPost& sp : delta[s_from]) {
+                mata::Word new_word = word;
+                new_word.push_back(sp.symbol);
+                for (State s_to : sp.targets) {
+                    new_worklist.push_back({s_to, new_word});
+                    if (final.contains(s_to)) {
+                        result.insert(new_word);
+                    }
+                }
+            }
+        }
+        worklist.swap(new_worklist);
+        ++cur_length;
+    }
+
+    return result;
+}
+
+bool Nft::is_tuple_in_lang(const std::vector<Word>& track_words) {
+    if (track_words.size() != num_of_levels) {
+        throw std::runtime_error("Invalid number of tracks. Expected " + std::to_string(num_of_levels) + ".");
+    }
+    std::vector<Word::const_iterator> track_words_begins(num_of_levels);
+    for (size_t track{ 0 }; track < num_of_levels; ++track) {
+        track_words_begins[track] = track_words[track].begin();
+    }
+
+    const std::vector<Word::const_iterator> track_words_ends{
+        [&](){
+            std::vector<Word::const_iterator> track_words_ends(num_of_levels);
+            for (size_t track{ 0 }; track < num_of_levels; ++track) {
+                track_words_ends[track] = track_words[track].end();
+            }
+            return track_words_ends;
+        }()
+    };
+
+    auto are_all_track_words_read = [&](const std::vector<Word::const_iterator>& word_begins){
+       for (size_t i{ 0 }; Word::const_iterator word_it: word_begins) {
+           if (word_it != track_words_ends[i]) { return false; }
+           ++i;
+       }
+       return true;
+    };
+
+    if (are_all_track_words_read(track_words_begins) && final.intersects_with(initial)) { return true; }
+
+    using StateWordBeginsPair = std::pair<State, std::vector<Word::const_iterator>>;
+    std::deque<StateWordBeginsPair> worklist{};
+    for (const State state: initial) {
+        worklist.emplace_back(state, track_words_begins);
+    }
+    Level level;
+    while (!worklist.empty()) {
+        const auto [state, words_its]{ std::move(worklist.front()) };
+        worklist.pop_front();
+        level = levels[state];
+        const StatePost& state_post{ delta[state] };
+        const auto state_post_end{ state_post.end() };
+        const Word::const_iterator word_symbol_it{ words_its[level] };
+
+        auto symbol_post_it{ state_post.find(EPSILON) };
+        if (symbol_post_it != state_post_end) {
+            for (State target: symbol_post_it->targets) {
+                if (are_all_track_words_read(words_its) && final.contains(target)) { return true; }
+                worklist.emplace_back(target, words_its);
+            }
+        }
+
+        if (word_symbol_it != track_words_ends[level]) {
+//            auto symbol_post_it{ state_post.find(EPSILON) };
+//            if (symbol_post_it != state_post_end) {
+//                for (State target: symbol_post_it->targets) {
+//                    if (are_all_track_words_read(words_its) && final.contains(target)) { return true; }
+//                    worklist.emplace_back(target, words_its);
+//                }
+//            }
+
+            symbol_post_it = state_post.find(DONT_CARE);
+            if (*word_symbol_it != EPSILON && symbol_post_it != state_post_end) {
+                for (const State target: symbol_post_it->targets) {
+                    bool continue_to_next_target{ false };
+                    std::vector<Word::const_iterator> next_words_its{ words_its };
+                    Level level_in_transition{ level };
+                    do {
+                        if (next_words_its[level_in_transition] == track_words_ends[level_in_transition]) {
+                            continue_to_next_target = true;
+                        }
+                        ++next_words_its[level_in_transition];
+                        level_in_transition = (level_in_transition + 1) % static_cast<Level>(num_of_levels);
+                    } while(level_in_transition % num_of_levels != levels[target] && !continue_to_next_target);
+                    if (continue_to_next_target) { continue; }
+                    if (are_all_track_words_read(next_words_its) && final.contains(target)) { return true; }
+                    worklist.emplace_back(target, next_words_its);
+                }
+            }
+
+            symbol_post_it = state_post.find(*word_symbol_it);
+            if (*word_symbol_it != DONT_CARE && *word_symbol_it != EPSILON && symbol_post_it != state_post_end) {
+                for (State target: symbol_post_it->targets) {
+                    std::vector<Word::const_iterator> next_words_its{ words_its };
+                    ++next_words_its[level];
+                    if (are_all_track_words_read(next_words_its) && final.contains(target)) { return true; }
+                    worklist.emplace_back(target, next_words_its);
+                }
+            }
+            // TODO(nft): Input words may contain epsilons and dont cares, theoretically. Handle that.
+        }
+    }
+    return false;
+}
diff --git a/src/nft/strings.cc b/src/nft/strings.cc
new file mode 100644
index 000000000..527a30b7f
--- /dev/null
+++ b/src/nft/strings.cc
@@ -0,0 +1,533 @@
+/* nfa-strings.hh -- Operations on NFAs for string solving.
+ */
+
+#include <utility>
+
+#include "mata/nft/strings.hh"
+#include "mata/parser/re2parser.hh"
+#include "mata/nft/nft.hh"
+#include "mata/nft/builder.hh"
+
+using namespace mata;
+using mata::Symbol;
+using mata::nfa::SymbolPost;
+using mata::nfa::StatePost;
+using mata::nfa::Nfa;
+using namespace mata::nft;
+using mata::nft::State;
+using mata::nft::Level;
+using mata::nft::Nft;
+using nft::strings::ReplaceMode;
+using nft::strings::ReluctantReplace;
+
+namespace {
+    template<class Sequence>
+    bool is_subsequence(const Sequence& subsequence, const Sequence& sequence) {
+        assert(subsequence.size() <= sequence.size());
+        for (size_t i{ 0 }; const Symbol symbol: subsequence) {
+            if (symbol != sequence[i]) { return false; }
+            ++i;
+        }
+        return true;
+    }
+
+    void add_end_marker_transitions_from_literal_states(
+        const Symbol end_marker, const std::vector<std::pair<State, Word>>& state_word_pairs, Nft& nft) {
+        auto state_pair_end{ state_word_pairs.end() };
+        State state_lvl0, state_lvl1;
+        // Skip the last state-word pair for the whole literal as it must first apply the replacement before accepting the end marker.
+        for (auto state_word_pair_it{ state_word_pairs.begin() };
+             state_word_pair_it + 1 < state_pair_end; ++state_word_pair_it) {
+            const auto& [state, word]{ *state_word_pair_it };
+            auto word_it{ word.begin() };
+            auto word_end{ word.end() };
+            state_lvl0 = state;
+            state_lvl1 = nft.add_state_with_level(1);
+            nft.delta.add(state_lvl0, end_marker, state_lvl1);
+            for (; word_it + 1 < word_end; ++word_it) {
+                state_lvl0 = nft.add_state_with_level(0);
+                nft.delta.add(state_lvl1, *word_it, state_lvl0);
+                state_lvl1 = nft.add_state_with_level(1);
+                nft.delta.add(state_lvl0, EPSILON, state_lvl1);
+            }
+            nft.delta.add(state_lvl1, word_it == word_end ? EPSILON : *word_it, *nft.final.begin());
+        }
+    }
+
+    void handle_last_symbol_replacement(const Symbol end_marker, const ReplaceMode& replace_mode, Nft& nft,
+                                        const utils::OrdVector<Symbol>& alphabet_symbols,
+                                        State state_lvl1, Symbol replacement) {
+        switch (replace_mode) {
+            case ReplaceMode::All: {
+                nft.delta.add(state_lvl1, replacement, *nft.initial.begin());
+                break;
+            }
+            case ReplaceMode::Single: {
+                State state_lvl0{ nft.add_state_with_level(0) };
+                nft.delta.add(state_lvl1, replacement, state_lvl0);
+                nft.insert_identity(state_lvl0, alphabet_symbols.to_vector());
+                state_lvl1 = nft.add_state_with_level(1);
+                nft.delta.add(state_lvl0, end_marker, state_lvl1);
+                nft.delta.add(state_lvl1, EPSILON, *nft.final.begin());
+                break;
+            }
+            default: {
+                throw std::runtime_error("Unhandled replace mode.");
+            }
+        }
+    }
+
+    void add_replacement_transitions(const Word& replacement, const Symbol end_marker, const ReplaceMode& replace_mode,
+                                     const std::vector<std::pair<State, Word>>& state_word_pairs, Nft& nft,
+                                     const utils::OrdVector<Symbol>& alphabet_symbols) {
+        auto replacement_end{ replacement.end() };
+        State state_lvl0{ state_word_pairs.back().first };
+        if (replacement.empty()) {
+            State state_lvl1{ nft.add_state_with_level(1) };
+            nft.delta.add(state_lvl0, EPSILON, state_lvl1);
+            handle_last_symbol_replacement(end_marker, replace_mode, nft, alphabet_symbols, state_lvl1, EPSILON);
+        }
+        for (auto replacement_it{ replacement.begin() }; replacement_it < replacement_end; ++replacement_it) {
+            State state_lvl1{ nft.add_state_with_level(1) };
+            nft.delta.add(state_lvl0, EPSILON, state_lvl1);
+            if (replacement_it + 1 == replacement_end) {
+                handle_last_symbol_replacement(end_marker, replace_mode, nft, alphabet_symbols, state_lvl1, *replacement_it);
+            } else {
+                state_lvl0 = nft.add_state_with_level(0);
+                nft.delta.add(state_lvl1, *replacement_it, state_lvl0);
+            }
+        }
+    }
+
+    void add_generic_literal_transitions(const Word& literal, const std::vector<std::pair<State, Word>>& state_word_pairs,
+                                    Nft& nft, const utils::OrdVector<Symbol>& alphabet_symbols) {
+        const size_t literal_size{ literal.size() };
+        Symbol literal_symbol;
+        for (size_t i{ 0 }; i < literal_size; ++i) {
+            literal_symbol = literal[i];
+            const auto& [word_state, subword] = state_word_pairs[i];
+            for (Symbol symbol: alphabet_symbols) {
+                State target_state{ 0 };
+                if (symbol == literal_symbol) { // Handle transition to next subword init_state.
+                    State middle{ nft.add_state_with_level(1) };
+                    nft.delta.add(word_state, literal_symbol, middle);
+                    nft.delta.add(middle, EPSILON, state_word_pairs[i + 1].first);
+                } else { // Add back transitions.
+                    Word subword_next_symbol = subword;
+                    subword_next_symbol.push_back(symbol);
+                    const auto subword_next_symbol_begin{ subword_next_symbol.begin() };
+                    const auto subword_next_symbol_end{ subword_next_symbol.end() };
+                    auto subword_next_symbol_it{ subword_next_symbol_begin };
+                    while (subword_next_symbol_it != subword_next_symbol_end) {
+                        const Word subsubword{ subword_next_symbol_it, subword_next_symbol_end };
+                        if (is_subsequence(subsubword, literal)) {
+                            // it...end is a valid literal subvector. Transition should therefore lead to the corresponding
+                            //  subvector init_state.
+                            target_state = subsubword.size();
+                            break;
+                        }
+                        ++subword_next_symbol_it;
+                    }
+
+                    // Output all buffered symbols up until the new buffered content (subsubword).
+                    auto subword_next_symbol_it_from_begin = subword_next_symbol.begin();
+                    State state_lvl0{ word_state };
+                    for (;
+                        subword_next_symbol_it_from_begin <
+                        subword_next_symbol_it; ++subword_next_symbol_it_from_begin) {
+                        State state_lvl1 = nft.add_state_with_level(1);
+                        nft.delta.add(state_lvl0, symbol, state_lvl1);
+                        symbol = EPSILON;
+                        if (subword_next_symbol_it_from_begin + 1 == subword_next_symbol_it) {
+                            nft.delta.add(state_lvl1, *subword_next_symbol_it_from_begin,
+                                          target_state);
+                        } else {
+                            state_lvl0 = nft.add_state_with_level(0);
+                            nft.delta.add(state_lvl1, *subword_next_symbol_it_from_begin, state_lvl0);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /// Add transitions, optionally add @p source to @p dfa_generic_end_marker.final, and update @p labeling and @p labeling_inv functions.
+    void process_source(const nfa::Nfa& regex, const utils::OrdVector<Symbol>& alphabet_symbols, nfa::Nfa& dfa_generic_end_marker,
+                        std::map<State, StateSet>& labeling,
+                        std::unordered_map<StateSet, State>& labeling_inv, State source,
+                        StateSet& source_label, std::vector<State>& worklist) {
+        const State generic_initial_state{ *regex.initial.begin() };
+        for (const Symbol symbol: alphabet_symbols) {
+            StateSet target_label{ generic_initial_state };
+            for (const State regex_state: source_label) {
+                const StatePost& state_post{ regex.delta[regex_state] };
+                auto symbol_post_it{ state_post.find(symbol) };
+                if (symbol_post_it == state_post.end()) { continue; }
+                target_label.insert(symbol_post_it->targets);
+            }
+            auto target_it{ labeling_inv.find(target_label) };
+            State target;
+            if (target_it == labeling_inv.end()) {
+                target = dfa_generic_end_marker.add_state();
+                labeling.emplace(target, target_label);
+                labeling_inv.emplace(target_label, target);
+                worklist.push_back(target);
+            } else {
+                target = target_it->second;
+            }
+            dfa_generic_end_marker.delta.add(source, symbol, target);
+        }
+        dfa_generic_end_marker.final.insert(source);
+    }
+}
+
+Nft mata::nft::strings::create_identity(mata::Alphabet* alphabet, const size_t num_of_levels) {
+    if (num_of_levels == 0) { throw std::runtime_error("NFT must have at least one level"); }
+    const auto alphabet_symbols{ alphabet->get_alphabet_symbols() };
+//    const size_t additional_states_per_symbol_num{ num_of_levels - 1 };
+//    const size_t num_of_states{ alphabet_symbols.size() * additional_states_per_symbol_num + 1 };
+    Nft nft{ 1, { 0 }, { 0 }, { 0 }, num_of_levels, alphabet };
+    nft.insert_identity(0, alphabet_symbols.to_vector());
+    return nft;
+}
+
+Nft mata::nft::strings::create_identity_with_single_symbol_replace(
+    Alphabet* alphabet, const Symbol from_symbol, const Symbol replacement, const ReplaceMode replace_mode) {
+    return create_identity_with_single_symbol_replace(alphabet, from_symbol, Word{ replacement }, replace_mode);
+}
+
+Nft nft::strings::create_identity_with_single_symbol_replace(Alphabet* alphabet, const Symbol from_symbol,
+                                                             const Word& replacement, const ReplaceMode replace_mode) {
+    Nft nft{ create_identity(alphabet) };
+    if (alphabet->empty()) { throw std::runtime_error("Alphabet does not contain symbol being replaced."); }
+    auto symbol_post_to_state_with_replace{ nft.delta.mutable_state_post(0).find(from_symbol) };
+    State state_lvl1{ symbol_post_to_state_with_replace->targets.front() };
+    nft.delta.mutable_state_post(state_lvl1).clear();
+    const auto replacement_end{ replacement.end() };
+    auto replacement_it{ replacement.begin() };
+    State state_lvl0;
+    for (; replacement_it < replacement_end; ++replacement_it) {
+        if (replacement_it + 1 == replacement_end) { break; }
+        state_lvl0 = nft.add_state_with_level(0);
+        nft.delta.add(state_lvl1, *replacement_it, state_lvl0);
+        state_lvl1 = nft.add_state_with_level(1);
+        nft.delta.add(state_lvl0, EPSILON, state_lvl1);
+    }
+    switch (replace_mode) {
+        case ReplaceMode::All: {
+            nft.delta.add(state_lvl1,
+                          replacement_it == replacement_end ? EPSILON : *replacement_it,
+                          *nft.initial.begin());
+            break;
+        }
+        case ReplaceMode::Single: {
+            const State after_replace_state{ nft.add_state_with_level(0) };
+            nft.delta.add(state_lvl1,
+                          replacement_it == replacement_end ? EPSILON : *replacement_it,
+                          after_replace_state);
+            nft.insert_identity(after_replace_state, alphabet->get_alphabet_symbols().to_vector());
+            nft.final.insert(after_replace_state);
+            break;
+        }
+        default: {
+            throw std::runtime_error("Unhandled replace mode.");
+        }
+    }
+    return nft;
+}
+
+Nft mata::nft::strings::replace_reluctant_regex(
+    const std::string& regex,
+    const Word& replacement,
+    Alphabet* alphabet,
+    ReplaceMode replace_mode,
+    Symbol begin_marker
+) {
+    nfa::Nfa regex_nfa{};
+    parser::create_nfa(&regex_nfa, regex);
+    return replace_reluctant_regex(std::move(regex_nfa), replacement, alphabet, replace_mode, begin_marker);
+}
+
+Nft mata::nft::strings::replace_reluctant_regex(
+    nfa::Nfa regex,
+    const Word& replacement,
+    Alphabet* alphabet,
+    ReplaceMode replace_mode,
+    Symbol begin_marker
+) {
+    return ReluctantReplace::replace_regex(std::move(regex), replacement, alphabet, replace_mode, begin_marker);
+}
+
+nfa::Nfa ReluctantReplace::end_marker_dfa(nfa::Nfa regex) {
+    if (!regex.is_deterministic()) { regex = determinize(regex); }
+    State new_final;
+    for (State orig_final: regex.final) {
+        new_final = regex.add_state();
+        regex.final.insert(new_final);
+        regex.final.erase(orig_final);
+        StatePost::Moves orig_moves{ regex.delta[orig_final].moves() };
+        std::vector<Move> moves{ orig_moves.begin(), orig_moves.end() };
+        for (const Move& move: moves) {
+            regex.delta.remove(orig_final, move.symbol, move.target);
+            regex.delta.add(new_final, move.symbol, move.target);
+        }
+        regex.delta.add(orig_final, EPSILON, new_final);
+    }
+    return regex;
+}
+
+Nft ReluctantReplace::marker_nft(const nfa::Nfa& marker_dfa, Symbol marker) {
+    Nft dft_marker{ nft::builder::create_from_nfa(marker_dfa) };
+    const size_t dft_marker_num_of_states{ dft_marker.num_of_states() };
+    for (State source{ 0 }; source < dft_marker_num_of_states; ++source) {
+        StatePost& state_post = dft_marker.delta.mutable_state_post(source);
+        for (const Move& move: state_post.moves_epsilons()) {
+            const State marker_state{ dft_marker.add_state() };
+            dft_marker.levels.resize(marker_state + 1);
+            dft_marker.levels[marker_state] = 1;
+            SymbolPost& symbol_post{ *state_post.find(move.symbol) };
+            symbol_post.targets.erase(move.target);
+            symbol_post.targets.insert(marker_state);
+            dft_marker.delta.add(marker_state, marker, move.target);
+        }
+    }
+    return dft_marker;
+}
+
+nfa::Nfa ReluctantReplace::generic_marker_dfa(const std::string& regex, Alphabet* alphabet) {
+    nfa::Nfa nfa{};
+    parser::create_nfa(&nfa, regex);
+    return generic_marker_dfa(std::move(nfa), alphabet);
+}
+
+nfa::Nfa ReluctantReplace::generic_marker_dfa(nfa::Nfa regex, Alphabet* alphabet) {
+    if (!regex.is_deterministic()) { regex = determinize(regex); }
+
+    const utils::OrdVector<Symbol> alphabet_symbols{ alphabet->get_alphabet_symbols() };
+    nfa::Nfa dfa_generic_end_marker{};
+    dfa_generic_end_marker.initial.insert(0);
+    std::map<State, StateSet> labeling{};
+    std::unordered_map<StateSet, State> labeling_inv{};
+    labeling.emplace(0, *regex.initial.begin());
+    labeling_inv.emplace(*regex.initial.begin(), 0);
+
+    std::vector<State> worklist{ 0 };
+    while (!worklist.empty()) {
+        State source{ worklist.back() };
+        worklist.pop_back();
+        StateSet& source_label{ labeling[source] };
+
+        if (regex.final.intersects_with(source_label)) {
+            const State end_marker_target{ dfa_generic_end_marker.add_state() };
+            dfa_generic_end_marker.delta.add(source, EPSILON, end_marker_target);
+            process_source(regex, alphabet_symbols, dfa_generic_end_marker, labeling, labeling_inv, end_marker_target,
+                           source_label, worklist);
+        } else {
+            process_source(regex, alphabet_symbols, dfa_generic_end_marker, labeling, labeling_inv, source, source_label,
+                           worklist);
+        }
+
+    }
+
+    return dfa_generic_end_marker;
+}
+
+nfa::Nfa ReluctantReplace::begin_marker_nfa(const std::string& regex, Alphabet* alphabet) {
+    nfa::Nfa nfa{};
+    parser::create_nfa(&nfa, regex);
+    return begin_marker_nfa(std::move(nfa), alphabet);
+}
+
+nfa::Nfa ReluctantReplace::begin_marker_nfa(nfa::Nfa regex, Alphabet* alphabet) {
+    regex = revert(regex);
+    nfa::Nfa dfa_generic_end_marker{ generic_marker_dfa(std::move(regex), alphabet) };
+    dfa_generic_end_marker = revert(dfa_generic_end_marker);
+    std::swap(dfa_generic_end_marker.initial, dfa_generic_end_marker.final);
+    return dfa_generic_end_marker;
+}
+
+Nft ReluctantReplace::begin_marker_nft(const nfa::Nfa& marker_nfa, Symbol begin_marker) {
+    Nft begin_marker_nft{ marker_nft(marker_nfa, begin_marker) };
+    const State new_initial{ begin_marker_nft.add_state_with_level(0) };
+    for (const State orig_final: begin_marker_nft.final) {
+        begin_marker_nft.delta.add(new_initial, EPSILON, orig_final);
+    }
+    begin_marker_nft.final = begin_marker_nft.initial;
+    begin_marker_nft.initial = { new_initial };
+    return begin_marker_nft;
+}
+
+Nft ReluctantReplace::end_marker_dft(const nfa::Nfa& end_marker_dfa, const Symbol end_marker) {
+    return marker_nft(end_marker_dfa, end_marker);
+}
+
+nfa::Nfa ReluctantReplace::reluctant_nfa_with_marker(nfa::Nfa nfa, const Symbol marker, Alphabet* alphabet) {
+    // Convert to reluctant NFA.
+    nfa = mata::strings::reluctant_nfa(nfa);
+
+    // Add marker self-loops to accept begin markers inside the shortest match.
+    for (State state{ 0 }; state < nfa.num_of_states(); ++state) {
+        nfa.delta.add(state, marker, state);
+    }
+
+    // Intersect with NFA to avoid removing the next begin marker which might be used for the next replace.
+    // TODO(nft): Could be optimised.
+    nfa::Nfa nfa_avoid_removing_next_begin_marker{ 2, { 0 }, { 0 } };
+    StatePost& initial{ nfa_avoid_removing_next_begin_marker.delta.mutable_state_post(0) };
+    const utils::OrdVector<Symbol> alphabet_symbols{ alphabet->get_alphabet_symbols() };
+    for (const Symbol symbol: alphabet_symbols) {
+        initial.emplace_back(symbol, 0);
+    }
+    StatePost& marker_state{ nfa_avoid_removing_next_begin_marker.delta.mutable_state_post(1) };
+    nfa_avoid_removing_next_begin_marker.delta.add(0, marker, 1);
+    for (const Symbol symbol: alphabet_symbols) {
+        marker_state.emplace_back(symbol, 0);
+    }
+    nfa_avoid_removing_next_begin_marker.delta.add(1, marker, 1);
+    // TODO(nft): Leaves a non-terminating begin_marker transitions in a form of a lasso from final states.
+    //  These lassos should be removed to further optimize NFT creation.
+    return mata::strings::reluctant_nfa(reduce(intersection(nfa, nfa_avoid_removing_next_begin_marker)));
+}
+
+Nft ReluctantReplace::reluctant_leftmost_nft(const std::string& regex, Alphabet* alphabet, Symbol begin_marker,
+                                         const Word& replacement, ReplaceMode replace_mode) {
+    nfa::Nfa nfa{};
+    parser::create_nfa(&nfa, regex);
+    return reluctant_leftmost_nft(std::move(nfa), alphabet, begin_marker, replacement, replace_mode);
+}
+
+Nft ReluctantReplace::reluctant_leftmost_nft(nfa::Nfa nfa, Alphabet* alphabet, Symbol begin_marker,
+                                         const Word& replacement, ReplaceMode replace_mode) {
+    nfa = reluctant_nfa_with_marker(std::move(nfa), begin_marker, alphabet);
+    Nft nft_reluctant_leftmost{
+        nft::builder::create_from_nfa(nfa, 2, { EPSILON }, { EPSILON }) };
+    const size_t regex_num_of_states{ nft_reluctant_leftmost.num_of_states() };
+    assert(nft_reluctant_leftmost.is_deterministic());
+    const utils::OrdVector<Symbol> alphabet_symbols{ alphabet->get_alphabet_symbols() };
+    nft_reluctant_leftmost.levels.resize(regex_num_of_states + replacement.size() * 2 + alphabet_symbols.size() + 4);
+
+    // Create self-loop on the new initial state.
+    const State initial{ nft_reluctant_leftmost.add_state_with_level(0) };
+    nft_reluctant_leftmost.insert_identity(initial, alphabet_symbols.to_vector());
+
+    // Move to replace mode when begin marker is encountered.
+    State curr_state{ nft_reluctant_leftmost.num_of_states() };
+    nft_reluctant_leftmost.delta.add(initial, begin_marker, curr_state);
+    nft_reluctant_leftmost.delta.mutable_state_post(curr_state).push_back(
+        SymbolPost{ EPSILON, StateSet{ nft_reluctant_leftmost.initial } }
+    );
+    nft_reluctant_leftmost.levels[curr_state] = 1;
+    ++curr_state;
+
+    // Start outputting replacement by epsilon transition from all regex final states.
+    for (const State regex_final: nft_reluctant_leftmost.final) {
+        nft_reluctant_leftmost.delta.add(regex_final, EPSILON, curr_state);
+    }
+    nft_reluctant_leftmost.levels[curr_state] = 1;
+    // Output the replacement.
+    curr_state = nft_reluctant_leftmost.insert_word_by_parts(curr_state, { {}, replacement });
+    State next_state{ nft_reluctant_leftmost.add_state_with_level(0) };
+    nft_reluctant_leftmost.delta.add(curr_state, EPSILON, next_state);
+    nft_reluctant_leftmost.final.insert(next_state);
+    nft_reluctant_leftmost.final.clear();
+    switch (replace_mode) {
+        case ReplaceMode::All: { // Return to beginning to possibly repeat replacement.
+            nft_reluctant_leftmost.insert_word_by_parts(next_state, { { EPSILON }, { EPSILON } }, initial);
+            break;
+        };
+        case ReplaceMode::Single: { // End the replacement mode.
+            nft_reluctant_leftmost.levels.resize(nft_reluctant_leftmost.levels.size() + alphabet_symbols.size() + 1);
+            const State final{ next_state };
+            nft_reluctant_leftmost.final.insert(final);
+            nft_reluctant_leftmost.insert_identity(final, alphabet_symbols.to_vector());
+            nft_reluctant_leftmost.insert_word_by_parts(final, { { begin_marker }, { EPSILON } }, final);
+            break;
+        };
+        default: {
+            throw std::runtime_error("Unimplemented replace mode");
+        }
+    }
+
+    nft_reluctant_leftmost.initial = { initial };
+    nft_reluctant_leftmost.final.insert(initial);
+
+    return nft_reluctant_leftmost;
+}
+
+Nft nft::strings::replace_reluctant_literal(const Word& literal, const Word& replacement, Alphabet* alphabet,
+                                            strings::ReplaceMode replace_mode, Symbol end_marker) {
+    return ReluctantReplace::replace_literal(literal, replacement, alphabet, replace_mode, end_marker);
+}
+
+Nft ReluctantReplace::replace_literal_nft(const Word& literal, const Word& replacement, const Alphabet* alphabet,
+                                      const Symbol end_marker, ReplaceMode replace_mode) {
+    Nft nft{};
+    nft.num_of_levels = 2;
+    State init_state{ nft.add_state_with_level(0) };
+    nft.initial.insert(init_state);
+    const std::vector<std::pair<State, Word>> state_word_pairs{
+        [&]() {
+            std::vector<std::pair<State, Word>> state_word_pairs{};
+            state_word_pairs.emplace_back(init_state, Word{});
+            const auto literal_begin{ literal.begin() };
+            const auto literal_end{ literal.end() };
+            for (auto literal_it{ literal_begin }; literal_it < literal_end; ++literal_it) {
+                state_word_pairs.emplace_back(nft.add_state_with_level(0), Word{ literal_begin, literal_it + 1 });
+            }
+            return state_word_pairs;
+        }() };
+    const utils::OrdVector<Symbol> alphabet_symbols{ alphabet->get_alphabet_symbols() };
+    add_generic_literal_transitions(literal, state_word_pairs, nft, alphabet_symbols);
+    State final{ nft.add_state_with_level(0) };
+    nft.final.insert(final);
+    add_replacement_transitions(replacement, end_marker, replace_mode, state_word_pairs, nft, alphabet_symbols);
+    add_end_marker_transitions_from_literal_states(end_marker, state_word_pairs, nft);
+    return nft;
+}
+
+Nft nft::strings::replace_reluctant_single_symbol(Symbol from_symbol, const Word& replacement, mata::Alphabet* alphabet,
+                                                  ReplaceMode replace_mode) {
+    return ReluctantReplace::replace_symbol(from_symbol, replacement, alphabet, replace_mode);
+}
+
+Nft nft::strings::replace_reluctant_single_symbol(Symbol from_symbol, Symbol replacement, mata::Alphabet* alphabet,
+                                                  ReplaceMode replace_mode) {
+    return ReluctantReplace::replace_symbol(from_symbol, replacement, alphabet, replace_mode);
+}
+
+Nft ReluctantReplace::replace_regex(nfa::Nfa regex, const Word& replacement, Alphabet* alphabet,
+                                              ReplaceMode replace_mode, Symbol begin_marker) {
+    ReluctantReplace reluctant_replace{};
+    // TODO(nft): Add optional bool parameter to revert whether to swap initial and final states.
+    Nft dft_begin_marker{ reluctant_replace.begin_marker_nft(reluctant_replace.begin_marker_nfa(regex, alphabet), begin_marker) };
+    Nft nft_reluctant_replace{
+        reluctant_replace.reluctant_leftmost_nft(std::move(regex), alphabet, begin_marker, replacement, replace_mode) };
+    return compose(dft_begin_marker, nft_reluctant_replace);
+}
+
+Nft ReluctantReplace::replace_literal(const Word& literal, const Word& replacement, Alphabet* alphabet,
+                                                ReplaceMode replace_mode, Symbol end_marker) {
+    ReluctantReplace reluctant_replace{};
+    Nft nft_end_marker{ [&]() {
+        Nft nft_end_marker{ create_identity(alphabet) };
+        State middle{ nft_end_marker.add_state_with_level(1) };
+        State end_marker_state{ nft_end_marker.add_state_with_level(0) };
+        nft_end_marker.delta.add(*nft_end_marker.initial.begin(), EPSILON, middle);
+        nft_end_marker.delta.add(middle, end_marker, end_marker_state);
+        nft_end_marker.final.clear();
+        nft_end_marker.final.insert(end_marker_state);
+        return nft_end_marker;
+    }() };
+    Nft nft_literal_replace{ reluctant_replace.replace_literal_nft(literal, replacement, alphabet, end_marker, replace_mode) };
+    return compose(nft_end_marker, nft_literal_replace);
+}
+
+Nft ReluctantReplace::replace_symbol(Symbol from_symbol, Symbol replacement, mata::Alphabet* alphabet,
+                                     ReplaceMode replace_mode) {
+    return create_identity_with_single_symbol_replace(alphabet, from_symbol, replacement, replace_mode);
+}
+
+Nft ReluctantReplace::replace_symbol(Symbol from_symbol, const Word& replacement, mata::Alphabet* alphabet,
+                                     ReplaceMode replace_mode) {
+    return create_identity_with_single_symbol_replace(alphabet, from_symbol, replacement, replace_mode);
+}
diff --git a/src/nft/universal.cc b/src/nft/universal.cc
new file mode 100644
index 000000000..2ce02c916
--- /dev/null
+++ b/src/nft/universal.cc
@@ -0,0 +1,156 @@
+/* nft-universal.cc -- NFT universality
+ */
+
+// MATA headers
+#include "mata/nft/nft.hh"
+#include "mata/nft/algorithms.hh"
+#include "mata/utils/sparse-set.hh"
+
+using namespace mata::nft;
+using namespace mata::utils;
+
+//TODO: this could be merged with inclusion, or even removed, universality could be implemented using inclusion,
+// it is not something needed in practice, so some little overhead is ok
+
+
+/// naive universality check (complementation + emptiness)
+bool mata::nft::algorithms::is_universal_naive(
+	const Nft&         aut,
+	const Alphabet&    alphabet,
+	Run*               cex)
+{ // {{{
+	Nft cmpl = complement(aut, alphabet);
+
+	return cmpl.is_lang_empty(cex);
+} // is_universal_naive }}}
+
+
+/// universality check using Antichains
+bool mata::nft::algorithms::is_universal_antichains(
+	const Nft&         aut,
+	const Alphabet&    alphabet,
+	Run*               cex)
+{ // {{{
+
+	using WorklistType = std::list<StateSet>;
+	using ProcessedType = std::list<StateSet>;
+
+	auto subsumes = [](const StateSet& lhs, const StateSet& rhs) {
+		if (lhs.size() > rhs.size()) { // bigger set cannot be subset
+			return false;
+		}
+
+		return std::includes(rhs.begin(), rhs.end(), lhs.begin(), lhs.end());
+	};
+
+	// process parameters
+	// TODO: set correctly!!!!
+	bool is_dfs = true;
+
+	// check the initial state
+	if (are_disjoint(aut.initial, aut.final)) {
+		if (nullptr != cex) { cex->word.clear(); }
+		return false;
+	}
+
+	// initialize
+	WorklistType worklist = { StateSet(aut.initial) };
+	ProcessedType processed = { StateSet(aut.initial) };
+	mata::utils::OrdVector<Symbol> alph_symbols = alphabet.get_alphabet_symbols();
+
+	// 'paths[s] == t' denotes that state 's' was accessed from state 't',
+	// 'paths[s] == s' means that 's' is an initial state
+	std::map<StateSet, std::pair<StateSet, Symbol>> paths =
+		{ {StateSet(aut.initial), {StateSet(aut.initial), 0}} };
+
+	while (!worklist.empty()) {
+		// get a next state
+		StateSet state;
+		if (is_dfs) {
+			state = *worklist.rbegin();
+			worklist.pop_back();
+		} else { // BFS
+			state = *worklist.begin();
+			worklist.pop_front();
+		}
+
+		// process it
+		for (Symbol symb : alph_symbols) {
+			StateSet succ = aut.post(state, symb);
+			if (!aut.final.intersects_with(succ)) {
+				if (nullptr != cex) {
+					cex->word.clear();
+					cex->word.push_back(symb);
+					StateSet trav = state;
+					while (paths[trav].first != trav)
+					{ // go back until initial state
+						cex->word.push_back(paths[trav].second);
+						trav = paths[trav].first;
+					}
+
+					std::reverse(cex->word.begin(), cex->word.end());
+				}
+
+				return false;
+			}
+
+			bool is_subsumed = false;
+			for (const auto& anti_state : processed) {
+				// trying to find a smaller state in processed
+				if (subsumes(anti_state, succ)) {
+					is_subsumed = true;
+					break;
+				}
+			}
+
+			if (is_subsumed) { continue; }
+
+			// prune data structures and insert succ inside
+			for (std::list<StateSet>* ds : {&processed, &worklist}) {
+				auto it = ds->begin();
+				while (it != ds->end()) {
+					if (subsumes(succ, *it)) {
+						auto to_remove = it;
+						++it;
+						ds->erase(to_remove);
+					} else {
+						++it;
+					}
+				}
+
+				// TODO: set pushing strategy
+				ds->push_back(succ);
+			}
+
+			// also set that succ was accessed from state
+			paths[succ] = {state, symb};
+		}
+	}
+
+	return true;
+} // }}}
+
+// The dispatching method that calls the correct one based on parameters.
+bool mata::nft::Nft::is_universal(const Alphabet& alphabet, Run* cex, const ParameterMap& params) const {
+	// setting the default algorithm
+	decltype(algorithms::is_universal_naive)* algo = algorithms::is_universal_naive;
+	if (!haskey(params, "algorithm")) {
+		throw std::runtime_error(std::to_string(__func__) +
+			" requires setting the \"algorithm\" key in the \"params\" argument; "
+			"received: " + std::to_string(params));
+	}
+
+	const std::string& str_algo = params.at("algorithm");
+	if ("naive" == str_algo) { /* default */ }
+	else if ("antichains" == str_algo) {
+		algo = algorithms::is_universal_antichains;
+	} else {
+		throw std::runtime_error(std::to_string(__func__) +
+			" received an unknown value of the \"algorithm\" key: " + str_algo);
+	}
+	return algo(*this, alphabet, cex);
+} // is_universal()
+
+bool mata::nft::Nft::is_universal(const Alphabet& alphabet, const ParameterMap& params) const {
+    return this->is_universal(alphabet, nullptr, params);
+}
diff --git a/src/strings/nfa-strings.cc b/src/strings/nfa-strings.cc
index db44597e1..48664dd7f 100644
--- a/src/strings/nfa-strings.cc
+++ b/src/strings/nfa-strings.cc
@@ -4,6 +4,8 @@
 #include "mata/nfa/strings.hh"
 #include "mata/nfa/builder.hh"
 
+#include <optional>
+
 using namespace mata::nfa;
 using namespace mata::strings;
 
@@ -208,3 +210,10 @@ bool mata::strings::is_lang_eps(const Nfa& aut) {
     }
     return true;
 }
+
+Nfa mata::strings::reluctant_nfa(Nfa nfa) {
+    for (const State final: nfa.final) {
+        nfa.delta.mutable_state_post(final).clear();
+    }
+    return nfa;
+}


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index f36986382..eb695cff6 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -13,6 +13,15 @@ add_executable(tests
 		nfa/nfa-product.cc
 		nfa/nfa-profiling.cc
 		nfa/nfa-plumbing.cc
+		nft/delta.cc
+		nft/nft.cc
+		nft/builder.cc
+		nft/nft-composition.cc
+		nft/nft-concatenation.cc
+		nft/nft-intersection.cc
+		nft/nft-profiling.cc
+		nft/nft-plumbing.cc
+		nft/strings.cc
 		strings/nfa-noodlification.cc
 		strings/nfa-segmentation.cc
 		strings/nfa-string-solving.cc
diff --git a/tests/nfa/nfa.cc b/tests/nfa/nfa.cc
index 21a5f99a4..2213f4edb 100644
--- a/tests/nfa/nfa.cc
+++ b/tests/nfa/nfa.cc
@@ -4284,6 +4284,298 @@ TEST_CASE("mata::nfa::Nfa::get_word()") {
     }
 }
 
+TEST_CASE("mata::nfa::Nfa::insert_word()") {
+    Delta delta;
+    delta.add(0, 0, 1);
+    delta.add(0, 4, 0);
+    delta.add(1, 1, 2);
+    delta.add(1, 5, 1);
+    delta.add(2, 2, 3);
+    delta.add(2, 6, 2);
+    delta.add(3, 3, 4);
+    delta.add(3, 7, 3);
+
+    Nfa nfa, expected;
+
+    SECTION("Insert 'a'") {
+        SECTION("src < tgt") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(1, {'a'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(1, 'a', 3);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("src < tgt && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(1, {'a'}, 4);
+            CHECK(new_tgt == 4);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(1, 'a', 4);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("tgt < src") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(3, {'a'}, 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(3, 'a', 1);
+
+            CHECK(are_equivalent(nfa, expected));
+
+        }
+
+        SECTION("tgt < src && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(4, {'a'}, 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(4, 'a', 1);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("self-loop") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(3, {'a'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(3, 'a', 3);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("self-loop && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(4, {'a'}, 4);
+            CHECK(new_tgt == 4);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(4, 'a', 4);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("tgt is not specified") {
+            nfa = Nfa(delta, { 0 });
+            State new_tgt = nfa.insert_word(1, {'a'});
+            nfa.final.insert(new_tgt);
+
+            expected = Nfa(3, { 0 }, { 2 });
+            expected.delta.add(0, 0, 1);
+            expected.delta.add(0, 4, 0);
+            expected.delta.add(1, 5, 1);
+            expected.delta.add(1, 'a', 2);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+    }
+
+    SECTION("Insert 'ab'") {
+        SECTION("src < tgt") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(1, {'a', 'b'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(1, 'a', 5);
+            expected.delta.add(5, 'b', 3);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("src < tgt && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(1, {'a', 'b'}, 4);
+            CHECK(new_tgt == 4);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(1, 'a', 5);
+            expected.delta.add(5, 'b', 4);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("tgt < src") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(3, {'a', 'b'}, 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(3, 'a', 5);
+            expected.delta.add(5, 'b', 1);
+
+            CHECK(are_equivalent(nfa, expected));
+
+        }
+
+        SECTION("tgt < src && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(4, {'a', 'b'}, 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(4, 'a', 5);
+            expected.delta.add(5, 'b', 1);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("self-loop") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(3, {'a', 'b'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(3, 'a', 5);
+            expected.delta.add(5, 'b', 3);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("self-loop && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(4, {'a', 'b'}, 4);
+            CHECK(new_tgt == 4);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(4, 'a', 5);
+            expected.delta.add(5, 'b', 4);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("tgt is not specified") {
+            nfa = Nfa(delta, { 0 });
+            State new_tgt = nfa.insert_word(1, {'a', 'b'});
+            nfa.final.insert(new_tgt);
+
+            expected = Nfa(4, { 0 }, { 3 });
+            expected.delta.add(0, 0, 1);
+            expected.delta.add(0, 4, 0);
+            expected.delta.add(1, 5, 1);
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'b', 3);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+    }
+
+    SECTION("Insert 'abcd'") {
+        SECTION("src < tgt") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(1, {'a', 'b', 'c', 'd'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(1, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 3);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("src < tgt && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(1, {'a', 'b', 'c', 'd'}, 4);
+            CHECK(new_tgt == 4);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(1, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 4);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("tgt < src") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(3, {'a', 'b', 'c', 'd'}, 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(3, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 1);
+
+            CHECK(are_equivalent(nfa, expected));
+
+        }
+
+        SECTION("tgt < src && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(4, {'a', 'b', 'c', 'd'}, 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(4, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 1);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("self-loop") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(3, {'a', 'b', 'c', 'd'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(3, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 3);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("self-loop && final.contains(tgt)") {
+            nfa = Nfa(delta, { 0 }, { 4 });
+            State new_tgt = nfa.insert_word(4, {'a', 'b', 'c', 'd'}, 4);
+            CHECK(new_tgt == 4);
+
+            expected = Nfa(delta, { 0 }, { 4 });
+            expected.delta.add(4, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 4);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+
+        SECTION("tgt is not specified") {
+            nfa = Nfa(delta, { 0 });
+            State new_tgt = nfa.insert_word(1, {'a', 'b', 'c', 'd'});
+            nfa.final.insert(new_tgt);
+
+            expected = Nfa(6, { 0 }, { 5 });
+            expected.delta.add(0, 0, 1);
+            expected.delta.add(0, 4, 0);
+            expected.delta.add(1, 5, 1);
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'b', 3);
+            expected.delta.add(3, 'c', 4);
+            expected.delta.add(4, 'd', 5);
+
+            CHECK(are_equivalent(nfa, expected));
+        }
+    }
+}
+
 TEST_CASE("mata::nfa::Nfa::decode_utf8") {
     SECTION("Empty") {
         Nfa aut;
diff --git a/tests/nft/builder.cc b/tests/nft/builder.cc
new file mode 100644
index 000000000..3a910e6a3
--- /dev/null
+++ b/tests/nft/builder.cc
@@ -0,0 +1,384 @@
+// TODO: some header
+
+#include <unordered_set>
+#include <vector>
+#include <fstream>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+#include "mata/nft/nft.hh"
+#include "mata/nft/builder.hh"
+
+using namespace mata::nft;
+using mata::nfa::Nfa;
+using Symbol = mata::Symbol;
+using IntAlphabet = mata::IntAlphabet;
+using OnTheFlyAlphabet = mata::OnTheFlyAlphabet;
+
+using Word = std::vector<Symbol>;
+
+TEST_CASE("nft::create_from_nfa()") {
+    Nft nft{};
+    Nft expected{};
+    Nfa nfa{};
+
+    SECTION("small nfa to 2 level NFT") {
+        constexpr Level NUM_OF_LEVELS{ 2 };
+        nfa.initial = { 0 };
+        nfa.final = { 3 };
+        nfa.delta.add(0, 1, 2);
+        nfa.delta.add(1, EPSILON, 3);
+        nfa.delta.add(3, 2, 3);
+        nfa.delta.add(2, 3, 1);
+        nfa.delta.add(2, 3, 0);
+        nft = builder::create_from_nfa(nfa, NUM_OF_LEVELS);
+        expected = mata::nft::builder::parse_from_mata(
+            std::string("@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q4\n%Levels q0:0 q1:1 q2:0 q3:0 q4:0 q5:1 q6:1\n%LevelsNum 2\nq0 1 q1\nq1 1 q2\nq2 3 q5\nq3 4294967295 q4\nq4 2 q6\nq5 3 q0\nq5 3 q3\nq6 2 q4\n")
+        );
+        expected.num_of_levels = NUM_OF_LEVELS;
+        CHECK(mata::nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("small nfa to 3 level NFT") {
+        constexpr Level NUM_OF_LEVELS{ 3 };
+        nfa.initial = { 0 };
+        nfa.final = { 3 };
+        nfa.delta.add(0, 1, 2);
+        nfa.delta.add(1, EPSILON, 3);
+        nfa.delta.add(3, 2, 3);
+        nfa.delta.add(2, 3, 1);
+        nfa.delta.add(2, 3, 0);
+        nft = builder::create_from_nfa(nfa, NUM_OF_LEVELS);
+        expected = mata::nft::builder::parse_from_mata(
+            std::string("@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q5\n%Levels q0:0 q1:1 q2:2 q3:0 q4:0 q5:0 q6:1 q7:2 q8:1 q9:2\n%LevelsNum 3\nq0 1 q1\nq1 1 q2\nq2 1 q3\nq3 3 q6\nq4 4294967295 q5\nq5 2 q8\nq6 3 q7\nq7 3 q0\nq7 3 q4\nq8 2 q9\nq9 2 q5\n")
+        );
+        expected.num_of_levels = NUM_OF_LEVELS;
+        CHECK(mata::nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("regex cb+a+") {
+        constexpr Level NUM_OF_LEVELS{ 2 };
+        nfa.initial = { 0 };
+        nfa.final = { 3 };
+        nfa.delta.add(0, 'c', 1);
+        nfa.delta.add(1, 'b', 1);
+        nfa.delta.add(1, 'b', 2);
+        nfa.delta.add(2, 'a', 2);
+        nfa.delta.add(2, 'a', 3);
+        nft = builder::create_from_nfa(nfa, NUM_OF_LEVELS);
+        expected = mata::nft::builder::parse_from_mata(
+            std::string("@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q6\n%Levels q0:0 q1:1 q2:0 q3:1 q4:0 q5:1 q6:0\n%LevelsNum 2\nq0 99 q1\nq1 99 q2\nq2 98 q3\nq3 98 q2\nq3 98 q4\nq4 97 q5\nq5 97 q4\nq5 97 q6\n")
+        );
+        expected.num_of_levels = NUM_OF_LEVELS;
+        CHECK(mata::nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("regex cb+a+ with epsilon on added levels") {
+        constexpr Level NUM_OF_LEVELS{ 2 };
+        nfa.initial = { 0 };
+        nfa.final = { 3 };
+        nfa.delta.add(0, 'c', 1);
+        nfa.delta.add(1, 'b', 1);
+        nfa.delta.add(1, 'b', 2);
+        nfa.delta.add(2, 'a', 2);
+        nfa.delta.add(2, 'a', 3);
+        nft = builder::create_from_nfa(nfa, NUM_OF_LEVELS, { EPSILON }, { EPSILON });
+        expected = mata::nft::builder::parse_from_mata(
+            std::string("@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q6\n%Levels q0:0 q1:1 q2:0 q3:1 q4:0 q5:1 q6:0\n%LevelsNum 2\nq0 99 q1\nq1 4294967295 q2\nq2 98 q3\nq3 4294967295 q2\nq3 4294967295 q4\nq4 97 q5\nq5 4294967295 q4\nq5 4294967295 q6\n")
+        );
+        expected.num_of_levels = NUM_OF_LEVELS;
+        CHECK(mata::nft::are_equivalent(nft, expected));
+    }
+}
+
+TEST_CASE("mata::nft::parse_from_mata()") {
+    Delta delta;
+
+    SECTION("Simple automaton") {
+        delta.add(0, 0, 0);
+        delta.add(0, 1, 1);
+        delta.add(1, 2, 0);
+        Nft nft{ delta, { 0 }, { 1 }, { 0, 0 }, 1};
+
+        SECTION("from string") {
+            Nft parsed{ mata::nft::builder::parse_from_mata(nft.print_to_mata()) };
+            CHECK(are_equivalent(parsed, nft));
+        }
+
+        SECTION("from stream") {
+            std::stringstream nft_stream;
+            nft.print_to_mata(nft_stream);
+            Nft parsed{ mata::nft::builder::parse_from_mata(nft_stream) };
+            CHECK(are_equivalent(parsed, nft));
+        }
+
+        SECTION("from file") {
+            std::filesystem::path nft_file{ "./temp-test-parse_from_mata-simple_nft.mata" };
+            std::fstream file{ nft_file, std::fstream::in | std::fstream::out | std::fstream::trunc};
+            nft.print_to_mata(file);
+            Nft parsed{ mata::nft::builder::parse_from_mata(nft_file) };
+            file.close();
+            std::filesystem::remove(nft_file);
+
+            CHECK(are_equivalent(parsed, nft));
+        }
+
+    }
+
+    SECTION("larger automaton") {
+        Nft nft;
+        nft.initial = { 1, 2, 50 };
+        nft.delta.add(1, 'a', 2);
+        nft.delta.add(1, 'a', 3);
+        nft.delta.add(1, 'b', 4);
+        nft.delta.add(2, 'a', 2);
+        nft.delta.add(2, 'b', 2);
+        nft.delta.add(2, 'a', 3);
+        nft.delta.add(2, 'b', 4);
+        nft.delta.add(3, 'b', 4);
+        nft.delta.add(3, 'c', 7);
+        nft.delta.add(3, 'b', 2);
+        nft.delta.add(5, 'c', 3);
+        nft.delta.add(7, 'a', 8);
+        nft.delta.add(12, 'b', 15);
+        nft.delta.add(1, 'b', 40);
+        nft.delta.add(51, 'z', 42);
+        nft.final = { 3, 103 };
+        nft.levels = Levels(nft.num_of_states(), 0);
+        nft.levels[3] = 42;
+        nft.levels[103] = 42;
+        nft.num_of_levels = 43;
+
+        SECTION("from string") {
+            Nft parsed{ mata::nft::builder::parse_from_mata(nft.print_to_mata()) };
+            parsed.final.contains(103);
+            parsed.initial.contains(50);
+            parsed.delta.contains(51, 'z', 42);
+            CHECK(parsed.num_of_levels == 43);
+
+            Levels test_levels(parsed.levels);
+            for (const State &s : parsed.final) {
+                CHECK(test_levels[s] == 42);
+                test_levels[s] = 0;
+            }
+            CHECK(std::all_of(test_levels.begin(), test_levels.end(), [](Level l) { return l==0; }));
+
+            CHECK(are_equivalent(parsed, nft));
+        }
+
+        SECTION("from stream") {
+            std::stringstream nft_stream;
+            nft.print_to_mata(nft_stream);
+            Nft parsed{ mata::nft::builder::parse_from_mata(nft_stream) };
+            parsed.final.contains(103);
+            parsed.initial.contains(50);
+            parsed.delta.contains(51, 'z', 42);
+            CHECK(parsed.num_of_levels == 43);
+
+            std::vector test_levels(parsed.levels);
+            for (const State &s : parsed.final) {
+                CHECK(test_levels[s] == 42);
+                test_levels[s] = 0;
+            }
+            CHECK(std::all_of(test_levels.begin(), test_levels.end(), [](Level l) { return l==0; }));
+
+            CHECK(are_equivalent(parsed, nft));
+        }
+
+        SECTION("from file") {
+            std::filesystem::path nft_file{ "./temp-test-parse_from_mata-larger_nft.mata" };
+            std::fstream file{ nft_file, std::fstream::in | std::fstream::out | std::fstream::trunc };
+            nft.print_to_mata(file);
+            Nft parsed{ mata::nft::builder::parse_from_mata(nft_file) };
+            file.close();
+            std::filesystem::remove(nft_file);
+
+            parsed.final.contains(103);
+            parsed.initial.contains(50);
+            parsed.delta.contains(51, 'z', 42);
+            CHECK(parsed.num_of_levels == 43);
+
+            std::vector test_levels(parsed.levels);
+            for (const State &s : parsed.final) {
+                CHECK(test_levels[s] == 42);
+                test_levels[s] = 0;
+            }
+            CHECK(std::all_of(test_levels.begin(), test_levels.end(), [](Level l) { return l==0; }));
+
+            CHECK(are_equivalent(parsed, nft));
+        }
+    }
+
+    SECTION("levels testing") {
+        SECTION("ascending") {
+            Nft nft;
+            nft.delta.add(0, 1, 1);
+            nft.delta.add(1, 1, 2);
+            nft.delta.add(2, 1, 3);
+            nft.delta.add(3, 1, 4);
+            nft.delta.add(4, 1, 5);
+            nft.delta.add(5, 1, 6);
+            nft.delta.add(6, 1, 7);
+            nft.delta.add(7, 1, 8);
+            nft.delta.add(8, 1, 9);
+            nft.delta.add(9, 1, 10);
+            nft.initial.insert(0);
+            nft.final.insert(10);
+            nft.levels = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+            nft.num_of_levels = 11;
+
+            SECTION("from string") {
+                Nft parsed{ mata::nft::builder::parse_from_mata(nft.print_to_mata()) };
+
+                REQUIRE(parsed.initial.size() == 1);
+                REQUIRE(parsed.final.size() == 1);
+                CHECK(parsed.num_of_levels == 11);
+                State s{ *parsed.initial.begin() };
+                Level level = 0;
+                while (s != *parsed.final.begin()) {
+                    CHECK(parsed.levels[s] == level);
+                    REQUIRE(parsed.delta[s].size() == 1);
+                    SymbolPost symbol_post = *parsed.delta[s].begin();
+                    REQUIRE(symbol_post.targets.size() == 1);
+                    s = *symbol_post.targets.begin();
+                    level++;
+                }
+                CHECK(parsed.final.contains(s));
+                CHECK(parsed.levels[s] == 10);
+            }
+
+            SECTION("from stream") {
+                std::stringstream nft_stream;
+                nft.print_to_mata(nft_stream);
+                Nft parsed{ mata::nft::builder::parse_from_mata(nft_stream) };
+
+                REQUIRE(parsed.initial.size() == 1);
+                REQUIRE(parsed.final.size() == 1);
+                CHECK(parsed.num_of_levels == 11);
+                State s{ *parsed.initial.begin() };
+                Level level = 0;
+                while (s != *parsed.final.begin()) {
+                    CHECK(parsed.levels[s] == level);
+                    REQUIRE(parsed.delta[s].size() == 1);
+                    SymbolPost symbol_post = *parsed.delta[s].begin();
+                    REQUIRE(symbol_post.targets.size() == 1);
+                    s = *symbol_post.targets.begin();
+                    level++;
+                }
+                CHECK(parsed.final.contains(s));
+                CHECK(parsed.levels[s] == 10);
+            }
+
+            SECTION("from file") {
+                std::filesystem::path nft_file{ "./temp-test-parse_from_mata-levels_testing.mata" };
+                std::fstream file{ nft_file, std::fstream::in | std::fstream::out | std::fstream::trunc };
+                nft.print_to_mata(file);
+                Nft parsed{ mata::nft::builder::parse_from_mata(nft_file) };
+                file.close();
+                std::filesystem::remove(nft_file);
+
+                REQUIRE(parsed.initial.size() == 1);
+                REQUIRE(parsed.final.size() == 1);
+                CHECK(parsed.num_of_levels == 11);
+                State s{ *parsed.initial.begin() };
+                Level level = 0;
+                while (s != *parsed.final.begin()) {
+                    CHECK(parsed.levels[s] == level);
+                    REQUIRE(parsed.delta[s].size() == 1);
+                    SymbolPost symbol_post = *parsed.delta[s].begin();
+                    REQUIRE(symbol_post.targets.size() == 1);
+                    s = *symbol_post.targets.begin();
+                    level++;
+                }
+                CHECK(parsed.final.contains(s));
+                CHECK(parsed.levels[s] == 10);
+            }
+        }
+
+        SECTION("descending") {
+            Nft nft;
+            nft.delta.add(0, 1, 1);
+            nft.delta.add(1, 1, 2);
+            nft.delta.add(2, 1, 3);
+            nft.delta.add(3, 1, 4);
+            nft.delta.add(4, 1, 5);
+            nft.delta.add(5, 1, 6);
+            nft.delta.add(6, 1, 7);
+            nft.delta.add(7, 1, 8);
+            nft.delta.add(8, 1, 9);
+            nft.delta.add(9, 1, 10);
+            nft.initial.insert(0);
+            nft.final.insert(10);
+            nft.levels = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+            nft.num_of_levels = 11;
+
+            SECTION("from string") {
+                Nft parsed{ mata::nft::builder::parse_from_mata(nft.print_to_mata()) };
+
+                REQUIRE(parsed.initial.size() == 1);
+                REQUIRE(parsed.final.size() == 1);
+                CHECK(parsed.num_of_levels == 11);
+                State s{ *parsed.initial.begin() };
+                Level level = 10;
+                while (s != *parsed.final.begin()) {
+                    CHECK(parsed.levels[s] == level);
+                    REQUIRE(parsed.delta[s].size() == 1);
+                    SymbolPost symbol_post = *parsed.delta[s].begin();
+                    REQUIRE(symbol_post.targets.size() == 1);
+                    s = *symbol_post.targets.begin();
+                    level--;
+                }
+                CHECK(parsed.final.contains(s));
+                CHECK(parsed.levels[s] == 0);
+            }
+
+            SECTION("from stream") {
+                std::stringstream nft_stream;
+                nft.print_to_mata(nft_stream);
+                Nft parsed{ mata::nft::builder::parse_from_mata(nft_stream) };
+
+                REQUIRE(parsed.initial.size() == 1);
+                REQUIRE(parsed.final.size() == 1);
+                CHECK(parsed.num_of_levels == 11);
+                State s{ *parsed.initial.begin() };
+                Level level = 10;
+                while (s != *parsed.final.begin()) {
+                    CHECK(parsed.levels[s] == level);
+                    REQUIRE(parsed.delta[s].size() == 1);
+                    SymbolPost symbol_post = *parsed.delta[s].begin();
+                    REQUIRE(symbol_post.targets.size() == 1);
+                    s = *symbol_post.targets.begin();
+                    level--;
+                }
+                CHECK(parsed.final.contains(s));
+                CHECK(parsed.levels[s] == 0);
+            }
+
+            SECTION("from file") {
+                std::filesystem::path nft_file{ "./temp-test-parse_from_mata-levels_testing.mata" };
+                std::fstream file{ nft_file, std::fstream::in | std::fstream::out | std::fstream::trunc };
+                nft.print_to_mata(file);
+                Nft parsed{ mata::nft::builder::parse_from_mata(nft_file) };
+                file.close();
+                std::filesystem::remove(nft_file);
+
+                REQUIRE(parsed.initial.size() == 1);
+                REQUIRE(parsed.final.size() == 1);
+                CHECK(parsed.num_of_levels == 11);
+                State s{ *parsed.initial.begin() };
+                Level level = 10;
+                while (s != *parsed.final.begin()) {
+                    CHECK(parsed.levels[s] == level);
+                    REQUIRE(parsed.delta[s].size() == 1);
+                    SymbolPost symbol_post = *parsed.delta[s].begin();
+                    REQUIRE(symbol_post.targets.size() == 1);
+                    s = *symbol_post.targets.begin();
+                    level--;
+                }
+                CHECK(parsed.final.contains(s));
+                CHECK(parsed.levels[s] == 0);
+            }
+        }
+    }
+}
diff --git a/tests/nft/delta.cc b/tests/nft/delta.cc
new file mode 100644
index 000000000..7785f124d
--- /dev/null
+++ b/tests/nft/delta.cc
@@ -0,0 +1,468 @@
+// TODO: some header
+
+#include "mata/alphabet.hh"
+#include "mata/nft/types.hh"
+#include "mata/nft/delta.hh"
+#include "mata/nft/nft.hh"
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+using namespace mata::nft;
+
+using Symbol = mata::Symbol;
+
+TEST_CASE("mata::nft::SymbolPost") {
+    CHECK(SymbolPost{ 0, StateSet{} } == SymbolPost{ 0, StateSet{ 0, 1 } });
+    CHECK(SymbolPost{ 1, StateSet{} } != SymbolPost{ 0, StateSet{} });
+    CHECK(SymbolPost{ 0, StateSet{ 1 } } < SymbolPost{ 1, StateSet{} });
+    CHECK(SymbolPost{ 0, StateSet{ 1 } } <= SymbolPost{ 1, StateSet{} });
+    CHECK(SymbolPost{ 0, StateSet{ 1 } } <= SymbolPost{ 0, StateSet{} });
+    CHECK(SymbolPost{ 1, StateSet{ 0 } } > SymbolPost{ 0, StateSet{ 1 } });
+    CHECK(SymbolPost{ 1, StateSet{ 0 } } >= SymbolPost{ 0, StateSet{ 1 } });
+    CHECK(SymbolPost{ 1, StateSet{ 0 } } >= SymbolPost{ 0, StateSet{ 1 } });
+}
+
+TEST_CASE("mata::nft::Delta::state_post()") {
+    Nft aut{};
+
+    SECTION("Add new states within the limit") {
+        aut.add_state(19);
+        aut.initial.insert(0);
+        aut.initial.insert(1);
+        aut.initial.insert(2);
+        REQUIRE_NOTHROW(aut.delta.state_post(0));
+        REQUIRE_NOTHROW(aut.delta.state_post(1));
+        REQUIRE_NOTHROW(aut.delta.state_post(2));
+        REQUIRE(aut.delta.state_post(0).empty());
+        REQUIRE(aut.delta.state_post(1).empty());
+        REQUIRE(aut.delta.state_post(2).empty());
+
+        CHECK(&aut.delta.state_post(4) == &aut.delta[4]);
+    }
+
+    SECTION("Add new states over the limit") {
+        aut.add_state(1);
+        REQUIRE_NOTHROW(aut.initial.insert(0));
+        REQUIRE_NOTHROW(aut.initial.insert(1));
+        REQUIRE_NOTHROW(aut.delta.state_post(0));
+        REQUIRE_NOTHROW(aut.delta.state_post(1));
+        REQUIRE_NOTHROW(aut.delta.state_post(2));
+        CHECK(aut.delta.state_post(0).empty());
+        CHECK(aut.delta.state_post(1).empty());
+        CHECK(aut.delta.state_post(2).empty());
+    }
+
+    SECTION("Add new states without specifying the number of states") {
+        CHECK_NOTHROW(aut.initial.insert(0));
+        CHECK_NOTHROW(aut.delta.state_post(2));
+        CHECK(aut.delta.state_post(0).empty());
+        CHECK(aut.delta.state_post(2).empty());
+    }
+
+    SECTION("Add new initial without specifying the number of states with over +1 number") {
+        REQUIRE_NOTHROW(aut.initial.insert(25));
+        CHECK_NOTHROW(aut.delta.state_post(25));
+        CHECK_NOTHROW(aut.delta.state_post(26));
+        CHECK(aut.delta.state_post(25).empty());
+        CHECK(aut.delta.state_post(26).empty());
+    }
+
+    SECTION("Add multiple targets at once") {
+        CHECK_NOTHROW(aut.delta.add(0, 1, { 3, 4, 5, 6 }));
+        CHECK_NOTHROW(aut.delta.add(26, 1, StateSet{}));
+        CHECK_NOTHROW(aut.delta.add(42, 1, StateSet{ 43 }));
+        CHECK(aut.delta.num_of_transitions() == 5);
+    }
+}
+
+TEST_CASE("mata::nft::Delta::contains()") {
+    Nft nft;
+    CHECK(!nft.delta.contains(0, 1, 0));
+    CHECK(!nft.delta.contains(Transition{ 0, 1, 0 }));
+    nft.delta.add(0, 1, 0);
+    CHECK(nft.delta.contains(0, 1, 0));
+    CHECK(nft.delta.contains(Transition{ 0, 1, 0 }));
+}
+
+TEST_CASE("mata::nft::Delta::remove()") {
+    Nft nft;
+
+    SECTION("Simple remove") {
+        nft.delta.add(0, 1, 0);
+        CHECK_NOTHROW(nft.delta.remove(3, 5, 6));
+        CHECK_NOTHROW(nft.delta.remove(0, 1, 0));
+        CHECK(nft.delta.empty());
+        nft.delta.add(10, 1, 0);
+        CHECK_THROWS_AS(nft.delta.remove(3, 5, 6), std::invalid_argument);
+    }
+}
+
+TEST_CASE("mata::nft::Delta::mutable_post()") {
+    Nft nft;
+
+    SECTION("Default initialized") {
+        CHECK(nft.delta.num_of_states() == 0);
+        CHECK(!nft.delta.uses_state(0));
+        CHECK(nft.delta.mutable_state_post(0).empty());
+        CHECK(nft.delta.num_of_states() == 1);
+        CHECK(nft.delta.uses_state(0));
+
+        CHECK(nft.delta.mutable_state_post(9).empty());
+        CHECK(nft.delta.num_of_states() == 10);
+        CHECK(nft.delta.uses_state(1));
+        CHECK(nft.delta.uses_state(2));
+        CHECK(nft.delta.uses_state(9));
+        CHECK(!nft.delta.uses_state(10));
+
+        CHECK(nft.delta.mutable_state_post(9).empty());
+        CHECK(nft.delta.num_of_states() == 10);
+        CHECK(nft.delta.uses_state(9));
+        CHECK(!nft.delta.uses_state(10));
+    }
+}
+
+TEST_CASE("mata::nft::StatePost iteration over moves") {
+    Nft nft;
+    std::vector<Move> iterated_moves{};
+    std::vector<Move> expected_moves{};
+    StatePost state_post{};
+
+    SECTION("Simple NFT") {
+        nft.initial.insert(0);
+        nft.final.insert(3);
+        nft.delta.add(0, 1, 1);
+        nft.delta.add(0, 2, 1);
+        nft.delta.add(0, 5, 1);
+        nft.delta.add(1, 3, 2);
+        nft.delta.add(2, 0, 1);
+        nft.delta.add(2, 0, 3);
+
+        state_post = nft.delta.state_post(0);
+        expected_moves = std::vector<Move>{ { 1, 1 }, { 2, 1 }, { 5, 1 } };
+        StatePost::Moves moves{ state_post.moves() };
+        iterated_moves.clear();
+        for (auto move_it{ moves.begin() }; move_it != moves.end(); ++move_it) {
+            iterated_moves.push_back(*move_it);
+        }
+        CHECK(iterated_moves == expected_moves);
+
+        iterated_moves = { moves.begin(), moves.end() };
+        CHECK(iterated_moves == expected_moves);
+
+        iterated_moves.clear();
+        for (const Move& move: state_post.moves()) { iterated_moves.push_back(move); }
+        CHECK(iterated_moves == expected_moves);
+
+        StatePost::Moves epsilon_moves{ state_post.moves_epsilons() };
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() }.empty());
+
+        state_post = nft.delta.state_post(1);
+        moves = state_post.moves();
+        StatePost::Moves moves_custom;
+        moves_custom = moves;
+        CHECK(std::vector<Move>{ moves.begin(), moves.end() }
+              == std::vector<Move>{ moves_custom.begin(), moves_custom.end() });
+        moves_custom = state_post.moves(state_post.begin(), state_post.end());
+        CHECK(std::vector<Move>{ moves.begin(), moves.end() }
+              == std::vector<Move>{ moves_custom.begin(), moves_custom.end() });
+        iterated_moves.clear();
+        for (auto move_it{ moves.begin() }; move_it != moves.end(); ++move_it) {
+            iterated_moves.push_back(*move_it);
+        }
+        expected_moves = std::vector<Move>{ { 3, 2 } };
+        CHECK(iterated_moves == expected_moves);
+        iterated_moves = { moves.begin(), moves.end() };
+        CHECK(iterated_moves == expected_moves);
+        iterated_moves.clear();
+        for (const Move& move: state_post.moves()) { iterated_moves.push_back(move); }
+        CHECK(iterated_moves == expected_moves);
+        epsilon_moves = state_post.moves_epsilons();
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() }.empty());
+
+        state_post = nft.delta.state_post(2);
+        moves = state_post.moves();
+        iterated_moves.clear();
+        for (auto move_it{ moves.begin() }; move_it != moves.end(); ++move_it) {
+            iterated_moves.push_back(*move_it);
+        }
+        expected_moves = std::vector<Move>{ { 0, 1 }, { 0, 3 } };
+        CHECK(iterated_moves == expected_moves);
+        iterated_moves = { moves.begin(), moves.end() };
+        CHECK(iterated_moves == expected_moves);
+        iterated_moves.clear();
+        for (const Move& move: state_post.moves()) { iterated_moves.push_back(move); }
+        CHECK(iterated_moves == expected_moves);
+        epsilon_moves = state_post.moves_epsilons();
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() }.empty());
+
+        state_post = nft.delta.state_post(3);
+        moves = state_post.moves();
+        iterated_moves.clear();
+        for (auto move_it{ moves.begin() }; move_it != moves.end(); ++move_it) {
+            iterated_moves.push_back(*move_it);
+        }
+        CHECK(iterated_moves.empty());
+        CHECK(StatePost::Moves::const_iterator{ state_post } == moves.end());
+        iterated_moves = { moves.begin(), moves.end() };
+        CHECK(iterated_moves.empty());
+        iterated_moves.clear();
+        for (const Move& move: state_post.moves()) { iterated_moves.push_back(move); }
+        CHECK(iterated_moves.empty());
+        epsilon_moves = state_post.moves_epsilons();
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() }.empty());
+
+        state_post = nft.delta.state_post(4);
+        moves = state_post.moves();
+        iterated_moves.clear();
+        for (auto move_it{ moves.begin() }; move_it != moves.end(); ++move_it) {
+            iterated_moves.push_back(*move_it);
+        }
+        CHECK(iterated_moves.empty());
+        iterated_moves = { moves.begin(), moves.end() };
+        CHECK(iterated_moves.empty());
+        iterated_moves.clear();
+        for (const Move& move: state_post.moves()) { iterated_moves.push_back(move); }
+        CHECK(iterated_moves.empty());
+        epsilon_moves = state_post.moves_epsilons();
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() }.empty());
+
+        nft.delta.add(0, EPSILON, 2);
+        state_post = nft.delta.state_post(0);
+        epsilon_moves = state_post.moves_epsilons();
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() } == std::vector<Move>{ { EPSILON, 2 } });
+        nft.delta.add(1, EPSILON, 3);
+        state_post = nft.delta.state_post(1);
+        epsilon_moves = state_post.moves_epsilons();
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() } == std::vector<Move>{ { EPSILON, 3 } });
+        nft.delta.add(4, EPSILON, 4);
+        state_post = nft.delta.state_post(4);
+        epsilon_moves = state_post.moves_epsilons();
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() } == std::vector<Move>{ { EPSILON, 4 } });
+
+        state_post = nft.delta.state_post(0);
+        epsilon_moves = state_post.moves_epsilons(3);
+        iterated_moves.clear();
+        for (const Move& move: epsilon_moves) { iterated_moves.push_back(move); }
+        CHECK(iterated_moves == std::vector<Move>{ { 5, 1 }, { EPSILON, 2 }});
+        state_post = nft.delta.state_post(1);
+        epsilon_moves = state_post.moves_epsilons(3);
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() } == std::vector<Move>{ { 3, 2 }, { EPSILON, 3 } });
+
+        state_post = nft.delta.state_post(2);
+        epsilon_moves = state_post.moves_epsilons(3);
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() }.empty());
+        state_post = nft.delta.state_post(4);
+        epsilon_moves = state_post.moves_epsilons(3);
+        CHECK(std::vector<Move>{ epsilon_moves.begin(), epsilon_moves.end() } == std::vector<Move>{ { EPSILON, 4 } });
+
+        state_post = nft.delta.state_post(0);
+        StatePost::Moves symbol_moves = state_post.moves_symbols(3);
+        iterated_moves.clear();
+        for (const Move& move: symbol_moves) { iterated_moves.push_back(move); }
+        CHECK(iterated_moves == std::vector<Move>{ { 1, 1 }, { 2, 1 } });
+        symbol_moves = state_post.moves_symbols(0);
+        iterated_moves.clear();
+        for (const Move& move: symbol_moves) { iterated_moves.push_back(move); }
+        CHECK(iterated_moves.empty());
+
+        state_post = nft.delta.state_post(1);
+        symbol_moves = state_post.moves_symbols(3);
+        CHECK(std::vector<Move>{ symbol_moves.begin(), symbol_moves.end() } == std::vector<Move>{ { 3, 2 } });
+        state_post = nft.delta.state_post(2);
+        symbol_moves = state_post.moves_symbols(3);
+        CHECK(std::vector<Move>{ symbol_moves.begin(), symbol_moves.end() } == std::vector<Move>{ { 0, 1 }, { 0 , 3 } });
+        state_post = nft.delta.state_post(4);
+        symbol_moves = state_post.moves_symbols(3);
+        CHECK(std::vector<Move>{ symbol_moves.begin(), symbol_moves.end() }.empty());
+
+        // Create custom moves iterator.
+        state_post = nft.delta[0];
+        moves = { state_post, state_post.cbegin(), state_post.cbegin() + 2 };
+        iterated_moves = { moves.begin(), moves.end() };
+        CHECK(iterated_moves == std::vector<Move>{ { 1, 1 }, { 2, 1 } });
+
+        state_post = nft.delta[20];
+        moves = { state_post, state_post.cbegin(), state_post.cend() };
+        iterated_moves = { moves.begin(), moves.end() };
+        CHECK(iterated_moves.empty());
+    }
+}
+
+TEST_CASE("mata::nft::Delta iteration over transitions") {
+    Nft nft;
+    std::vector<Transition> iterated_transitions{};
+    std::vector<Transition> expected_transitions{};
+
+    SECTION("empty automaton") {
+        Delta::Transitions transitions{ nft.delta.transitions() };
+        CHECK(transitions.begin() == transitions.end());
+        Delta::Transitions::const_iterator transition_it{ nft.delta };
+        CHECK(transition_it == transitions.end());
+        transition_it = { nft.delta, 0 };
+        CHECK(transition_it == transitions.end());
+    }
+
+    SECTION("Simple NFT") {
+        nft.initial.insert(0);
+        nft.final.insert(3);
+        nft.delta.add(0, 1, 1);
+        nft.delta.add(0, 2, 1);
+        nft.delta.add(0, 5, 1);
+        nft.delta.add(1, 3, 2);
+        nft.delta.add(2, 0, 1);
+        nft.delta.add(2, 0, 3);
+
+        Delta::Transitions transitions{ nft.delta.transitions() };
+        iterated_transitions.clear();
+        for (auto transitions_it{ transitions.begin() };
+             transitions_it != transitions.end(); ++transitions_it) {
+            iterated_transitions.push_back(*transitions_it);
+        }
+        expected_transitions = std::vector<Transition>{
+            { 0, 1, 1 }, { 0, 2, 1 }, { 0, 5, 1 }, { 1, 3, 2 }, { 2, 0, 1 }, { 2, 0, 3 }
+        };
+        CHECK(iterated_transitions == expected_transitions);
+
+        iterated_transitions = { transitions.begin(), transitions.end() };
+        CHECK(iterated_transitions == expected_transitions);
+
+        iterated_transitions.clear();
+        for (const Transition& transition: nft.delta.transitions()) { iterated_transitions.push_back(transition); }
+        CHECK(iterated_transitions == expected_transitions);
+
+        Delta::Transitions::const_iterator transitions_it{ nft.delta.transitions().begin() };
+        CHECK(*transitions_it == Transition{ 0, 1, 1 });
+        ++transitions_it;
+        CHECK(*transitions_it == Transition{ 0, 2, 1 });
+        ++transitions_it;
+        ++transitions_it;
+        CHECK(*transitions_it == Transition{ 1, 3, 2 });
+
+        Delta::Transitions::const_iterator transitions_from_1_to_end_it{ nft.delta, 1 };
+        iterated_transitions.clear();
+        while (transitions_from_1_to_end_it != nft.delta.transitions().end()) {
+            iterated_transitions.push_back(*transitions_from_1_to_end_it);
+            ++transitions_from_1_to_end_it;
+        }
+        expected_transitions = std::vector<Transition>{ { 1, 3, 2 }, { 2, 0, 1 }, { 2, 0, 3 } };
+        CHECK(iterated_transitions == expected_transitions);
+    }
+
+    SECTION("Sparse automaton") {
+        constexpr size_t state_num = 'r'+1;
+        nft.delta.reserve(state_num);
+
+        nft.delta.add('q', 'a', 'r');
+        nft.delta.add('q', 'b', 'r');
+        const Delta::Transitions transitions{ nft.delta.transitions() };
+        Delta::Transitions::const_iterator it{ transitions.begin() };
+        Delta::Transitions::const_iterator jt{ transitions.begin() };
+        CHECK(it == jt);
+        ++it;
+        CHECK(it != jt);
+        CHECK((it != transitions.begin() && it != transitions.end()));
+        CHECK(jt == transitions.begin());
+
+        ++jt;
+        CHECK(it == jt);
+        CHECK((jt != transitions.begin() && jt != transitions.end()));
+
+        jt = transitions.end();
+        CHECK(it != jt);
+        CHECK((jt != transitions.begin() && jt == transitions.end()));
+
+        it = transitions.end();
+        CHECK(it == jt);
+        CHECK((it != transitions.begin() && it == transitions.end()));
+    }
+}
+
+TEST_CASE("mata::nft::Delta::operator=()") {
+    Nft nft{};
+    nft.initial.insert(0);
+    nft.final.insert(1);
+    nft.delta.add(0, 'a', 1);
+
+    Nft copied_nft{ nft };
+    nft.delta.add(1, 'b', 0);
+    CHECK(nft.delta.num_of_transitions() == 2);
+    CHECK(copied_nft.delta.num_of_transitions() == 1);
+}
+
+TEST_CASE("mata::nft::StatePost::Moves") {
+    Nft nft{};
+    nft.initial.insert(0);
+    nft.final.insert(5);
+    nft.delta.add(0, 'a', 1);
+    nft.delta.add(1, 'b', 2);
+    nft.delta.add(1, 'c', 2);
+    nft.delta.add(1, 'd', 2);
+    nft.delta.add(2, 'e', 3);
+    nft.delta.add(3, 'e', 4);
+    nft.delta.add(4, 'f', 5);
+    // TODO: rewrite in a check of moves.
+    StatePost::Moves moves_from_source{ nft.delta[0].moves() };
+
+    CHECK(std::vector<Move>{ moves_from_source.begin(), moves_from_source.end() } == std::vector<Move>{ { 'a', 1 }});
+    moves_from_source = nft.delta[1].moves();
+    CHECK(std::vector<Move>{ moves_from_source.begin(), moves_from_source.end() } ==
+        std::vector<Move>{ { 'b', 2 }, { 'c', 2 }, { 'd', 2 } });
+    StatePost::Moves::const_iterator move_incremented_it{ moves_from_source.begin() };
+    ++move_incremented_it;
+    CHECK(*move_incremented_it == Move{ 'c', 2 });
+    CHECK(*StatePost::Moves::const_iterator{ nft.delta.state_post(1) } == Move{ 'b', 2 });
+    CHECK(move_incremented_it != moves_from_source.begin());
+    CHECK(move_incremented_it == ++moves_from_source.begin());
+    StatePost::Moves moves_from_source_copy_constructed{ nft.delta[12].moves() };
+    CHECK(
+        std::vector<Move>{ moves_from_source_copy_constructed.begin(), moves_from_source_copy_constructed.end() }
+            .empty()
+     );
+
+}
+
+TEST_CASE("mata::nft::Delta::operator==()") {
+    Delta delta{};
+    Delta delta2{};
+    CHECK(delta == delta2);
+    delta.add(0, 0, 0);
+    CHECK(delta != delta2);
+    delta2.add(0, 0, 0);
+    CHECK(delta == delta2);
+    delta.add(0, 0, 1);
+    delta2.add(0, 0, 2);
+    CHECK(delta != delta2);
+    delta2.add(0, 0, 1);
+    CHECK(delta != delta2);
+    delta.add(0, 0, 2);
+    CHECK(delta == delta2);
+    delta2.add(0, 0, 3);
+    CHECK(delta != delta2);
+    delta.add(0, 0, 3);
+    CHECK(delta == delta2);
+}
+
+TEST_CASE("mata::nft::Delta::add_symbols_to()") {
+    mata::OnTheFlyAlphabet empty_alphabet{};
+    mata::OnTheFlyAlphabet alphabet{};
+    Delta delta{};
+    delta.add_symbols_to(alphabet);
+    CHECK(alphabet.get_symbol_map().empty());
+    delta.add(0, 0, 0);
+    delta.add_symbols_to(alphabet);
+    CHECK(alphabet.get_symbol_map().size() == 1);
+    delta.add(0, 0, 0);
+    delta.add_symbols_to(alphabet);
+    CHECK(alphabet.get_symbol_map().size() == 1);
+    delta.add(0, 1, 0);
+    delta.add_symbols_to(alphabet);
+    CHECK(alphabet.get_symbol_map().size() == 2);
+    delta.add(0, 2, 0);
+    delta.add(0, 3, 0);
+    delta.add_symbols_to(alphabet);
+    CHECK(alphabet.get_symbol_map().size() == 4);
+    CHECK(alphabet.get_symbol_map() == std::unordered_map<std::string, mata::Symbol>{
+        { "0", 0 }, { "1", 1 }, { "2", 2 }, { "3", 3 }
+    });
+}
diff --git a/tests/nft/nft-composition.cc b/tests/nft/nft-composition.cc
new file mode 100644
index 000000000..725f6ea04
--- /dev/null
+++ b/tests/nft/nft-composition.cc
@@ -0,0 +1,423 @@
+/* tests-nft-concatenation.cc -- Tests for concatenation of NFAs
+ */
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+#include "mata/nft/nft.hh"
+#include "mata/utils/ord-vector.hh"
+
+
+using namespace mata::nft;
+using namespace mata::utils;
+
+
+TEST_CASE("Mata::nft::compose()") {
+
+    Nft lhs, rhs, expected, result;
+    SECTION("levels_cnt == 2") {
+
+        SECTION("Linear structure") {
+
+            SECTION("Epsilon free") {
+                lhs = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                lhs.delta.add(0, 'e', 1);
+                lhs.delta.add(1, 'a', 2);
+                lhs.delta.add(2, 'g', 3);
+                lhs.delta.add(3, 'b', 4);
+                lhs.delta.add(4, 'i', 5);
+                lhs.delta.add(5, 'c', 6);
+
+                rhs = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                rhs.delta.add(0, 'a', 1);
+                rhs.delta.add(1, 'f', 2);
+                rhs.delta.add(2, 'b', 3);
+                rhs.delta.add(3, 'h', 4);
+                rhs.delta.add(4, 'c', 5);
+                rhs.delta.add(5, 'j', 6);
+
+                expected = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                expected.delta.add(0, 'e', 1);
+                expected.delta.add(1, 'f', 2);
+                expected.delta.add(2, 'g', 3);
+                expected.delta.add(3, 'h', 4);
+                expected.delta.add(4, 'i', 5);
+                expected.delta.add(5, 'j', 6);
+
+                result = compose(lhs, rhs);
+
+                CHECK(are_equivalent(result, expected));
+            }
+
+            SECTION("Epsilon perfectly matches.") {
+                lhs = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                lhs.delta.add(0, 'e', 1);
+                lhs.delta.add(1, 'a', 2);
+                lhs.delta.add(2, 'g', 3);
+                lhs.delta.add(3, EPSILON, 4);
+                lhs.delta.add(4, EPSILON, 5);
+                lhs.delta.add(5, 'c', 6);
+
+                rhs = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                rhs.delta.add(0, 'a', 1);
+                rhs.delta.add(1, EPSILON, 2);
+                rhs.delta.add(2, EPSILON, 3);
+                rhs.delta.add(3, 'h', 4);
+                rhs.delta.add(4, 'c', 5);
+                rhs.delta.add(5, 'j', 6);
+
+                expected = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                expected.delta.add(0, 'e', 1);
+                expected.delta.add(1, EPSILON, 2);
+                expected.delta.add(2, 'g', 3);
+                expected.delta.add(3, 'h', 4);
+                expected.delta.add(4, EPSILON, 5);
+                expected.delta.add(5, 'j', 6);
+                expected.delta.add(2, EPSILON, 7);
+                expected.delta.add(7, 'h', 8);
+                expected.delta.add(8, 'g', 9);
+                expected.delta.add(9, EPSILON, 4);
+                expected.delta.add(2, 'g', 10);
+                expected.delta.add(10, EPSILON, 11);
+                expected.delta.add(11, EPSILON, 12);
+                expected.delta.add(12, 'h', 4);
+
+                result = compose(lhs, rhs, 1, 0);
+
+                CHECK(are_equivalent(result, expected));
+            }
+        }
+
+        SECTION("Branching") {
+
+            SECTION("Epsilon free") {
+                lhs = Nft(8, { 0 }, { 6, 7 }, { 0, 1, 0, 0, 1, 1, 0, 0 }, 2);
+                lhs.delta.add(0, 'e', 1);
+                lhs.delta.add(1, 'a', 2);
+                lhs.delta.add(2, 'c', 4);
+                lhs.delta.add(4, 'e', 6);
+                lhs.delta.add(1, 'b', 3);
+                lhs.delta.add(3, 'd', 5);
+                lhs.delta.add(5, 'f', 7);
+
+                rhs = Nft(11, { 0 }, { 8, 9, 10 }, { 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0 }, 2);
+                rhs.delta.add(0, 'e', 1);
+                rhs.delta.add(1, 'a', 3);
+                rhs.delta.add(3, 'c', 5);
+                rhs.delta.add(5, 'e', 8);
+                rhs.delta.add(0, 'b', 2);
+                rhs.delta.add(2, 'd', 4);
+                rhs.delta.add(4, 'f', 6);
+                rhs.delta.add(6, 'g', 9);
+                rhs.delta.add(4, 'f', 7);
+                rhs.delta.add(7, 'h', 10);
+
+                expected = Nft(5, { 0 }, { 4 }, { 0, 1, 0, 1, 0 }, 2);
+                expected.delta.add(0, 'e', 1);
+                expected.delta.add(1, 'd', 2);
+                expected.delta.add(2, 'd', 3);
+                expected.delta.add(3, 'g', 4);
+                expected.delta.add(3, 'h', 4);
+
+                result = compose(lhs, rhs, OrdVector<Level>{ 1 }, OrdVector<Level>{ 0 });
+
+                CHECK(are_equivalent(result, expected));
+            }
+
+            SECTION("Epsilon perfectly matches.") {
+                lhs = Nft(8, { 0 }, { 6, 7 }, { 0, 1, 0, 0, 1, 1, 0, 0 }, 2);
+                lhs.delta.add(0, 'e', 1);
+                lhs.delta.add(1, 'a', 2);
+                lhs.delta.add(2, 'c', 4);
+                lhs.delta.add(4, EPSILON, 6);
+                lhs.delta.add(1, 'b', 3);
+                lhs.delta.add(3, EPSILON, 5);
+                lhs.delta.add(5, 'f', 7);
+
+                rhs = Nft(11, { 0 }, { 8, 9, 10 }, { 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0 }, 2);
+                rhs.delta.add(0, EPSILON, 1);
+                rhs.delta.add(1, 'a', 3);
+                rhs.delta.add(3, 'c', 5);
+                rhs.delta.add(5, 'e', 8);
+                rhs.delta.add(0, 'b', 2);
+                rhs.delta.add(2, 'd', 4);
+                rhs.delta.add(4, 'f', 6);
+                rhs.delta.add(6, 'g', 9);
+                rhs.delta.add(4, 'f', 7);
+                rhs.delta.add(7, 'h', 10);
+
+                expected = Nft(5, { 0 }, { 4 }, { 0, 1, 0, 1, 0 }, 2);
+                expected.delta.add(0, 'e', 1);
+                expected.delta.add(1, 'd', 2);
+                expected.delta.add(2, EPSILON, 3);
+                expected.delta.add(3, 'g', 4);
+                expected.delta.add(3, 'h', 4);
+
+                result = compose(lhs, rhs, OrdVector<Level>{ 1 }, OrdVector<Level>{ 0 });
+
+                CHECK(are_equivalent(result, expected));
+            }
+        }
+
+        SECTION("Cycle") {
+            lhs = Nft(5, { 0 }, { 2, 4 }, { 0, 1, 0, 1, 0 }, 2);
+            lhs.delta.add(0, 'a', 1);
+            lhs.delta.add(1, 'b', 2);
+            lhs.delta.add(2, 'c', 3);
+            lhs.delta.add(3, 'e', 4);
+            lhs.delta.add(3, 'd', 2);
+
+            rhs = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+            rhs.delta.add(0, 'b', 1);
+            rhs.delta.add(1, 'x', 2);
+            rhs.delta.add(2, 'd', 3);
+            rhs.delta.add(3, 'y', 4);
+            rhs.delta.add(4, 'f', 4);
+            rhs.delta.add(4, 'd', 5);
+            rhs.delta.add(5, 'z', 6);
+
+            expected = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'x', 2);
+            expected.delta.add(2, 'c', 3);
+            expected.delta.add(3, 'y', 4);
+            expected.delta.add(4, 'c', 5);
+            expected.delta.add(5, 'z', 6);
+
+            result = compose(lhs, rhs);
+
+            CHECK(are_equivalent(result, expected));
+
+        }
+
+        SECTION("Epsilon does not match on synchronization level.") {
+
+            SECTION("Epsilon only on synchronization levels") {
+                lhs = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                lhs.delta.add(0, 'x', 1);
+                lhs.delta.add(1, EPSILON, 2);
+                lhs.delta.add(2, 'y', 3);
+                lhs.delta.add(3, 'a', 4);
+                lhs.delta.add(4, 'x', 5);
+                lhs.delta.add(5, 'c', 6);
+
+                rhs = Nft(5, { 0 }, { 4 }, { 0, 1, 0, 1, 0 }, 2);
+                rhs.delta.add(0, 'a', 1);
+                rhs.delta.add(1, 'b', 2);
+                rhs.delta.add(2, 'c', 3);
+                rhs.delta.add(3, 'd', 4);
+
+                expected = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                expected.delta.add(0, 'x', 1);
+                expected.delta.add(1, EPSILON, 2);
+                expected.delta.add(2, 'y', 3);
+                expected.delta.add(3, 'b', 4);
+                expected.delta.add(4, 'x', 5);
+                expected.delta.add(5, 'd', 6);
+
+                result = compose(lhs, rhs);
+
+                CHECK(are_equivalent(result, expected));
+
+            }
+
+            SECTION("Epsilon is even on non-synchronization levels") {
+                lhs = Nft(7, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2);
+                lhs.delta.add(0, 'x', 1);
+                lhs.delta.add(1, EPSILON, 2);
+                lhs.delta.add(2, EPSILON, 3);
+                lhs.delta.add(3, 'a', 4);
+                lhs.delta.add(4, 'x', 5);
+                lhs.delta.add(5, EPSILON, 6);
+
+                rhs = Nft(5, { 0 }, { 4 }, { 0, 1, 0, 1, 0 }, 2);
+                rhs.delta.add(0, 'a', 1);
+                rhs.delta.add(1, 'b', 2);
+                rhs.delta.add(2, EPSILON, 3);
+                rhs.delta.add(3, 'd', 4);
+
+                expected = Nft(13, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1 }, 2);
+                expected.delta.add(0, 'x', 1);
+                expected.delta.add(1, EPSILON, 2);
+                expected.delta.add(2, EPSILON, 3);
+                expected.delta.add(3, 'b', 4);
+                expected.delta.add(4, 'x', 5);
+                expected.delta.add(4, EPSILON, 10);
+                expected.delta.add(10, 'd', 11);
+                expected.delta.add(11, 'x', 12);
+                expected.delta.add(12, EPSILON, 6);
+                expected.delta.add(4, 'x', 7);
+                expected.delta.add(7, EPSILON, 8);
+                expected.delta.add(8, EPSILON, 9);
+                expected.delta.add(9, 'd', 6);
+                expected.delta.add(5, 'd', 6);
+
+                result = compose(lhs, rhs);
+
+                CHECK(are_equivalent(result, expected));
+            }
+
+        }
+    }
+
+    SECTION("lhs.num_of_levels != rhs.num_of_levels") {
+        SECTION("lhs.num_of_levels > rhs.num_of_levels") {
+            lhs = Nft(6, { 0 }, { 5 }, { 0, 1, 2, 3, 4, 0 }, 5);
+            lhs.delta.add(0, 'a', 1);
+            lhs.delta.add(1, 'b', 2);
+            lhs.delta.add(2, 'c', 3);
+            lhs.delta.add(3, 'e', 4);
+            lhs.delta.add(4, 'd', 5);
+
+            rhs = Nft(4, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+            rhs.delta.add(0, 'b', 1);
+            rhs.delta.add(1, 'd', 2);
+            rhs.delta.add(2, 'f', 3);
+
+            expected = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'c', 2);
+            expected.delta.add(2, 'e', 3);
+            expected.delta.add(3, 'f', 4);
+
+            result = compose(lhs, rhs, { 1, 4 }, { 0, 1 });
+
+            CHECK(are_equivalent(result, expected));
+        }
+
+        SECTION("lhs.num_of_levels < rhs.num_of_levels") {
+            lhs = Nft(4, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+            lhs.delta.add(0, 'b', 1);
+            lhs.delta.add(1, 'd', 2);
+            lhs.delta.add(2, 'f', 3);
+
+            rhs = Nft(6, { 0 }, { 5 }, { 0, 1, 2, 3, 4, 0 }, 5);
+            rhs.delta.add(0, 'a', 1);
+            rhs.delta.add(1, 'b', 2);
+            rhs.delta.add(2, 'c', 3);
+            rhs.delta.add(3, 'e', 4);
+            rhs.delta.add(4, 'd', 5);
+
+            expected = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'c', 2);
+            expected.delta.add(2, 'e', 3);
+            expected.delta.add(3, 'f', 4);
+
+            result = compose(lhs, rhs, { 0, 1 }, { 1, 4 });
+
+            CHECK(are_equivalent(result, expected));
+        }
+    }
+
+    SECTION("num_of_levels == 4") {
+
+        SECTION("Epsilon free") {
+            lhs = Nft(9, { 0 }, { 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            lhs.delta.add(0, 'i', 1);
+            lhs.delta.add(1, 'b', 2);
+            lhs.delta.add(2, 'c', 3);
+            lhs.delta.add(3, 'j', 4);
+            lhs.delta.add(4, 'k', 5);
+            lhs.delta.add(5, 'f', 6);
+            lhs.delta.add(6, 'g', 7);
+            lhs.delta.add(7, 'l', 8);
+
+            rhs = Nft(9, { 0 }, { 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            rhs.delta.add(0, 'a', 1);
+            rhs.delta.add(1, 'i', 2);
+            rhs.delta.add(2, 'j', 3);
+            rhs.delta.add(3, 'd', 4);
+            rhs.delta.add(4, 'e', 5);
+            rhs.delta.add(5, 'k', 6);
+            rhs.delta.add(6, 'l', 7);
+            rhs.delta.add(7, 'h', 8);
+
+            expected= Nft(9, { 0 }, { 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'b', 2);
+            expected.delta.add(2, 'c', 3);
+            expected.delta.add(3, 'd', 4);
+            expected.delta.add(4, 'e', 5);
+            expected.delta.add(5, 'f', 6);
+            expected.delta.add(6, 'g', 7);
+            expected.delta.add(7, 'h', 8);
+
+            result = compose(lhs, rhs, { 0, 3 }, { 1, 2 });
+
+            CHECK(are_equivalent(result, expected));
+        }
+
+        SECTION("Epsilon perfectly matches") {
+            lhs = Nft(9, { 0 }, { 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            lhs.delta.add(0, 'i', 1);
+            lhs.delta.add(1, 'b', 2);
+            lhs.delta.add(2, 'c', 3);
+            lhs.delta.add(3, EPSILON, 4);
+            lhs.delta.add(4, 'k', 5);
+            lhs.delta.add(5, 'f', 6);
+            lhs.delta.add(6, 'g', 7);
+            lhs.delta.add(7, 'l', 8);
+
+            rhs = Nft(9, { 0 }, { 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            rhs.delta.add(0, 'a', 1);
+            rhs.delta.add(1, 'i', 2);
+            rhs.delta.add(2, EPSILON, 3);
+            rhs.delta.add(3, EPSILON, 4);
+            rhs.delta.add(4, 'e', 5);
+            rhs.delta.add(5, 'k', 6);
+            rhs.delta.add(6, 'l', 7);
+            rhs.delta.add(7, 'h', 8);
+
+            expected= Nft(9, { 0 }, { 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'b', 2);
+            expected.delta.add(2, 'c', 3);
+            expected.delta.add(3, EPSILON, 4);
+            expected.delta.add(4, 'e', 5);
+            expected.delta.add(5, 'f', 6);
+            expected.delta.add(6, 'g', 7);
+            expected.delta.add(7, 'h', 8);
+
+            result = compose(lhs, rhs, { 0, 3 }, { 1, 2 });
+
+            CHECK(are_equivalent(result, expected));
+        }
+
+        SECTION("Epsilon only on synchronization levels") {
+            lhs = Nft(9, { 0 }, { 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            lhs.delta.add(0, EPSILON, 1);
+            lhs.delta.add(1, 'c', 2);
+            lhs.delta.add(2, 'd', 3);
+            lhs.delta.add(3, EPSILON, 4);
+            lhs.delta.add(4, 'b', 5);
+            lhs.delta.add(5, 'g', 6);
+            lhs.delta.add(6, 'h', 7);
+            lhs.delta.add(7, 'a', 8);
+
+            rhs = Nft(9, { 0 }, { 4, 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            rhs.delta.add(0, 'e', 1);
+            rhs.delta.add(1, 'b', 2);
+            rhs.delta.add(2, 'a', 3);
+            rhs.delta.add(3, 'f', 4);
+            rhs.delta.add(4, 'i', 5);
+            rhs.delta.add(5, 'x', 6);
+            rhs.delta.add(6, 'y', 7);
+            rhs.delta.add(7, 'j', 8);
+
+            expected= Nft(9, { 0 }, { 8 }, { 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+            expected.delta.add(0, EPSILON, 1);
+            expected.delta.add(1, 'c', 2);
+            expected.delta.add(2, 'd', 3);
+            expected.delta.add(3, EPSILON, 4);
+            expected.delta.add(4, 'e', 5);
+            expected.delta.add(5, 'g', 6);
+            expected.delta.add(6, 'h', 7);
+            expected.delta.add(7, 'f', 8);
+
+            result = compose(lhs, rhs, { 0, 3 }, { 1, 2 });
+
+            CHECK(are_equivalent(result, expected));
+        }
+    }
+}
diff --git a/tests/nft/nft-concatenation.cc b/tests/nft/nft-concatenation.cc
new file mode 100644
index 000000000..da2a45124
--- /dev/null
+++ b/tests/nft/nft-concatenation.cc
@@ -0,0 +1,1082 @@
+/* tests-nft-concatenation.cc -- Tests for concatenation of NFAs
+ */
+
+
+#include <unordered_set>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+#include "mata/nft/nft.hh"
+#include "mata/nft/strings.hh"
+#include "mata/parser/re2parser.hh"
+
+using namespace mata::nft;
+using namespace mata::strings;
+using namespace mata::utils;
+using namespace mata::parser;
+
+using Symbol = mata::Symbol;
+
+// Some common automata {{{
+
+// Automaton A
+#define FILL_WITH_AUT_A(x) \
+    x.initial = {1, 3}; \
+    x.final = {5}; \
+    x.delta.add(1, 'a', 3); \
+    x.delta.add(1, 'a', 10); \
+    x.delta.add(1, 'b', 7); \
+    x.delta.add(3, 'a', 7); \
+    x.delta.add(3, 'b', 9); \
+    x.delta.add(9, 'a', 9); \
+    x.delta.add(7, 'b', 1); \
+    x.delta.add(7, 'a', 3); \
+    x.delta.add(7, 'c', 3); \
+    x.delta.add(10, 'a', 7); \
+    x.delta.add(10, 'b', 7); \
+    x.delta.add(10, 'c', 7); \
+    x.delta.add(7, 'a', 5); \
+    x.delta.add(5, 'a', 5); \
+    x.delta.add(5, 'c', 9); \
+
+
+// Automaton B
+#define FILL_WITH_AUT_B(x) \
+    x.initial = {4}; \
+    x.final = {2, 12}; \
+    x.delta.add(4, 'c', 8); \
+    x.delta.add(4, 'a', 8); \
+    x.delta.add(8, 'b', 4); \
+    x.delta.add(4, 'a', 6); \
+    x.delta.add(4, 'b', 6); \
+    x.delta.add(6, 'a', 2); \
+    x.delta.add(2, 'b', 2); \
+    x.delta.add(2, 'a', 0); \
+    x.delta.add(0, 'a', 2); \
+    x.delta.add(2, 'c', 12); \
+    x.delta.add(12, 'a', 14); \
+    x.delta.add(14, 'b', 12); \
+
+// }}}
+
+TEST_CASE("mata::nft::concatenate()") {
+    Nft lhs{};
+    Nft rhs{};
+    Nft result{};
+
+    SECTION("Empty automaton without states") {
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("One empty automaton without states") {
+        rhs.add_state();
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Other empty automaton without states") {
+        lhs.add_state();
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("One empty automaton without states with other with initial states") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Other empty automaton without states with other with initial states") {
+        rhs.add_state();
+        rhs.initial.insert(0);
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("One empty automaton without states with other non-empty automaton") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Other empty automaton without states with other non-empty automaton") {
+        rhs.add_state();
+        rhs.initial.insert(0);
+        rhs.final.insert(0);
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Empty automaton") {
+        lhs.add_state();
+        rhs.add_state();
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Empty language") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        rhs.add_state();
+        rhs.initial.insert(0);
+
+        result = concatenate(lhs, rhs);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+    }
+
+    SECTION("Empty language rhs automaton") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        rhs.add_state();
+        rhs.initial.insert(0);
+
+        result = concatenate(lhs, rhs);
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Single state automata accepting an empty string") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        rhs.add_state();
+        rhs.initial.insert(0);
+        rhs.final.insert(0);
+
+        result = concatenate(lhs, rhs);
+
+        CHECK(!result.is_lang_empty());
+        CHECK(result.is_in_lang(Run{ {}, {} }));
+        CHECK(result.delta.empty());
+    }
+
+    SECTION("Empty language rhs automaton") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        rhs.add_state(1);
+        rhs.initial.insert(0);
+        rhs.final.insert(1);
+
+        result = concatenate(lhs, rhs);
+
+        CHECK(!result.initial.empty());
+        CHECK(!result.final.empty());
+        CHECK(result.delta.empty());
+    }
+
+    SECTION("Simple two state rhs automaton") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        rhs.add_state(1);
+        rhs.initial.insert(0);
+        rhs.final.insert(1);
+        rhs.delta.add(0, 'a', 1);
+
+        result = concatenate(lhs, rhs);
+
+        CHECK(!result.initial.empty());
+        CHECK(!result.final.empty());
+    }
+
+    SECTION("Simple two state automata") {
+        lhs.add_state(1);
+        lhs.initial.insert(0);
+        lhs.final.insert(1);
+        lhs.delta.add(0, 'b', 1);
+        rhs.add_state(1);
+        rhs.initial.insert(0);
+        rhs.final.insert(1);
+        rhs.delta.add(0, 'a', 1);
+
+        result = concatenate(lhs, rhs);
+
+        CHECK(!result.initial.empty());
+        CHECK(!result.final.empty());
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 1);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a' }) != shortest_words.end());
+    }
+
+    SECTION("Simple two state automata with higher state num for non-final state") {
+        lhs.add_state(1);
+        lhs.initial.insert(0);
+        lhs.final.insert(1);
+        lhs.delta.add(0, 'b', 1);
+        rhs.add_state(3);
+        rhs.initial.insert(0);
+        rhs.final.insert(1);
+        rhs.delta.add(0, 'a', 1);
+        rhs.delta.add(0, 'c', 3);
+
+        result = concatenate(lhs, rhs);
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 1);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a' }) != shortest_words.end());
+    }
+
+    SECTION("Simple two state lhs automaton") {
+        lhs.add_state(1);
+        lhs.initial.insert(0);
+        lhs.final.insert(1);
+        lhs.delta.add(0, 'b', 1);
+        rhs.add_state();
+        rhs.initial.insert(0);
+        rhs.final.insert(0);
+        rhs.delta.add(0, 'a', 0);
+
+        result = concatenate(lhs, rhs);
+        CHECK(result.is_in_lang(Run{ { 'b' }, {} }));
+        CHECK(result.is_in_lang(Run{ { 'b', 'a' }, {} }));
+        CHECK(result.is_in_lang(Run{ { 'b', 'a', 'a' }, {} }));
+        CHECK(!result.is_in_lang(Run{ { 'a' }, {} }));
+        CHECK(!result.is_in_lang(Run{ { 'a', 'b' }, {} }));
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 1);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b' }) != shortest_words.end());
+    }
+
+    SECTION("Automaton A concatenate automaton B") {
+        lhs.add_state(10);
+        FILL_WITH_AUT_A(lhs);
+        rhs.add_state(14);
+        FILL_WITH_AUT_B(rhs);
+
+        result = concatenate(lhs, rhs);
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 4);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', 'b', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', 'b', 'a' }) != shortest_words.end());
+    }
+
+    SECTION("Automaton B concatenate automaton A") {
+        lhs.add_state(10);
+        FILL_WITH_AUT_A(lhs);
+        rhs.add_state(14);
+        FILL_WITH_AUT_B(rhs);
+
+        result = concatenate(rhs, lhs);
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 4);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', 'b', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', 'b', 'a' }) != shortest_words.end());
+    }
+
+    SECTION("Sample automata") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        lhs.delta.add(0, 58, 0);
+        lhs.delta.add(0, 65, 0);
+        lhs.delta.add(0, 102, 0);
+        lhs.delta.add(0, 112, 0);
+        lhs.delta.add(0, 115, 0);
+        lhs.delta.add(0, 116, 0);
+
+        rhs.add_state(5);
+        rhs.final.insert({0, 5});
+        rhs.initial.insert(5);
+        rhs.delta.add(1, 112, 0);
+        rhs.delta.add(2, 116, 1);
+        rhs.delta.add(3, 102, 2);
+        rhs.delta.add(4, 115, 3);
+        rhs.delta.add(5, 102, 2);
+        rhs.delta.add(5, 112, 0);
+        rhs.delta.add(5, 115, 3);
+        rhs.delta.add(5, 116, 1);
+
+        result = concatenate(lhs, rhs);
+        CHECK(!result.is_lang_empty());
+        // TODO: Add more checks.
+    }
+}
+
+TEST_CASE("mata::nft::concatenate() over epsilon symbol") {
+    Nft lhs{};
+    Nft rhs{};
+    Nft result{};
+
+    SECTION("Empty automaton") {
+        lhs.add_state();
+        rhs.add_state();
+        result = concatenate(lhs, rhs, true);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Empty language") {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        rhs.add_state();
+        rhs.initial.insert(0);
+
+        result = concatenate(lhs, rhs, true);
+
+        CHECK(result.num_of_states() == 0);
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+    }
+
+    SECTION("Empty language rhs automaton")
+    {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        rhs.add_state();
+        rhs.initial.insert(0);
+
+        result = concatenate(lhs, rhs, true);
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Single state automata accepting an empty string")
+    {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        rhs.add_state();
+        rhs.initial.insert(0);
+        rhs.final.insert(0);
+
+        result = concatenate(lhs, rhs, true);
+
+        CHECK(result.initial[0]);
+        CHECK(result.final[1]);
+        CHECK(result.num_of_states() == 2);
+        CHECK(result.delta.num_of_transitions() == 1);
+        CHECK(result.delta.contains(0, EPSILON, 1));
+    }
+
+    SECTION("Empty language rhs automaton")
+    {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        rhs.add_state(1);
+        rhs.initial.insert(0);
+        rhs.final.insert(1);
+
+        result = concatenate(lhs, rhs, true);
+
+        CHECK(result.initial[0]);
+        CHECK(result.final[2]);
+        CHECK(result.num_of_states() == 3);
+        CHECK(result.delta.num_of_transitions() == 1);
+        CHECK(result.delta.contains(0, EPSILON, 1));
+    }
+
+    SECTION("Simple two state rhs automaton")
+    {
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        rhs.add_state(1);
+        rhs.initial.insert(0);
+        rhs.final.insert(1);
+        rhs.delta.add(0, 'a', 1);
+
+        result = concatenate(lhs, rhs, true);
+
+        CHECK(result.initial[0]);
+        CHECK(result.final[2]);
+        CHECK(result.num_of_states() == 3);
+        CHECK(result.delta.num_of_transitions() == 2);
+        CHECK(result.delta.contains(1, 'a', 2));
+        CHECK(result.delta.contains(0, EPSILON, 1));
+    }
+
+    SECTION("Simple two state automata")
+    {
+        lhs.add_state(1);
+        lhs.initial.insert(0);
+        lhs.final.insert(1);
+        lhs.delta.add(0, 'b', 1);
+        rhs.add_state(1);
+        rhs.initial.insert(0);
+        rhs.final.insert(1);
+        rhs.delta.add(0, 'a', 1);
+
+        result = concatenate(lhs, rhs, true);
+
+        CHECK(result.initial[0]);
+        CHECK(result.final[3]);
+        CHECK(result.num_of_states() == 4);
+        CHECK(result.delta.num_of_transitions() == 3);
+        CHECK(result.delta.contains(0, 'b', 1));
+        CHECK(result.delta.contains(2, 'a', 3));
+        CHECK(result.delta.contains(1, EPSILON, 2));
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 1);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', EPSILON, 'a' }) != shortest_words.end());
+    }
+
+    SECTION("Simple two state automata with higher state num for non-final state")
+    {
+        lhs.add_state(1);
+        lhs.initial.insert(0);
+        lhs.final.insert(1);
+        lhs.delta.add(0, 'b', 1);
+        rhs.add_state(3);
+        rhs.initial.insert(0);
+        rhs.final.insert(1);
+        rhs.delta.add(0, 'a', 1);
+        rhs.delta.add(0, 'c', 3);
+
+        result = concatenate(lhs, rhs, true);
+
+        CHECK(result.initial[0]);
+        CHECK(result.final[3]);
+        CHECK(result.num_of_states() == 6);
+        CHECK(result.delta.num_of_transitions() == 4);
+        CHECK(result.delta.contains(0, 'b', 1));
+        CHECK(result.delta.contains(2, 'a', 3));
+        CHECK(result.delta.contains(2, 'c', 5));
+        CHECK(result.delta.contains(1, EPSILON, 2));
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 1);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', EPSILON, 'a' }) != shortest_words.end());
+    }
+
+    SECTION("Simple two state lhs automaton")
+    {
+        lhs.add_state(1);
+        lhs.initial.insert(0);
+        lhs.final.insert(1);
+        lhs.delta.add(0, 'b', 1);
+        rhs.add_state();
+        rhs.initial.insert(0);
+        rhs.final.insert(0);
+        rhs.delta.add(0, 'a', 0);
+
+        StateRenaming lhs_renaming{};
+        StateRenaming rhs_renaming{};
+        result = concatenate(lhs, rhs, true, &lhs_renaming, &rhs_renaming);
+
+        CHECK(rhs_renaming == StateRenaming{{ 0, 2 } });
+
+        CHECK(result.initial[0]);
+        CHECK(result.final[2]);
+        CHECK(result.num_of_states() == 3);
+        CHECK(result.delta.num_of_transitions() == 3);
+        CHECK(result.delta.contains(0, 'b', 1));
+        CHECK(result.delta.contains(2, 'a', 2));
+        CHECK(result.delta.contains(1, EPSILON, 2));
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 1);
+    }
+
+    SECTION("Automaton A concatenate automaton B")
+    {
+        lhs.add_state(10);
+        FILL_WITH_AUT_A(lhs);
+        rhs.add_state(14);
+        FILL_WITH_AUT_B(rhs);
+
+        result = concatenate(lhs, rhs, true);
+
+        CHECK(result.initial.size() == 2);
+        CHECK(result.initial[1]);
+        CHECK(result.initial[3]);
+
+        CHECK(result.num_of_states() == 26);
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 4);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', EPSILON, 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', EPSILON, 'b', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', EPSILON, 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', EPSILON, 'b', 'a' }) != shortest_words.end());
+    }
+
+    SECTION("Automaton B concatenate automaton A")
+    {
+        lhs.add_state(10);
+        FILL_WITH_AUT_A(lhs);
+        rhs.add_state(14);
+        FILL_WITH_AUT_B(rhs);
+
+        result = concatenate(rhs, lhs, true);
+
+        CHECK(result.num_of_states() == 26);
+
+        CHECK(result.initial.size() == 1);
+        CHECK(result.initial[4]);
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 4);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', EPSILON, 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', EPSILON, 'b', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', EPSILON, 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', EPSILON, 'b', 'a' }) != shortest_words.end());
+    }
+}
+
+TEST_CASE("mata::nft::(a|b)*") {
+    Nft aut1{ Nft::with_levels(1) };
+    mata::parser::create_nfa(&aut1, "a*");
+    Nft aut2{ Nft::with_levels(1) };
+    mata::parser::create_nfa(&aut2, "b*");
+    Nft aut3{ Nft::with_levels(1) };
+    mata::parser::create_nfa(&aut3, "a*b*");
+    const Nft concatenated_aut{ concatenate(aut1, aut2) };
+    CHECK(are_equivalent(concatenated_aut, aut3));
+}
+
+TEST_CASE("mata::nft::Bug with epsilon transitions") {
+    Nft nft1{ Nft::with_levels(1, 4) };
+    nft1.initial.insert(0);
+    nft1.final.insert(3);
+    nft1.delta.add(0, 97, 0);
+    nft1.delta.add(0, 98, 0);
+    nft1.delta.add(0, 99, 0);
+    nft1.delta.add(0, 100, 0);
+    nft1.delta.add(0, EPSILON, 1);
+    nft1.delta.add(1, 97, 2);
+    nft1.delta.add(2, 98, 3);
+
+    Nft nft2{ Nft::with_levels(1, 1) };
+    nft2.initial.insert(0);
+    nft2.final.insert(0);
+    nft2.delta.add(0, 97, 0);
+    nft2.delta.add(0, 98, 0);
+    nft2.delta.add(0, 99, 0);
+    nft2.delta.add(0, 100, 0);
+
+    const Nft result{ concatenate(nft1, nft2, true) };
+
+    Nft expected{ nft1 };
+    assert(expected.num_of_levels == nft1.num_of_levels);
+    expected.delta.add(3, EPSILON, 4);
+    expected.delta.add(4, 97, 4);
+    expected.delta.add(4, 98, 4);
+    expected.delta.add(4, 99, 4);
+    expected.delta.add(4, 100, 4);
+    expected.final = { 4 };
+
+    CHECK(are_equivalent(result, expected));
+}
+
+TEST_CASE("mata::nft::concatenate() inplace") {
+    SECTION("Empty automaton without states") {
+        Nft lhs{};
+        Nft rhs{};
+        Nft result{};
+        result = lhs.concatenate(rhs);
+
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("One empty automaton without states") {
+        Nft lhs{};
+        Nft rhs{};
+        Nft result{};
+        rhs.add_state();
+        result = lhs.concatenate(rhs);
+
+        CHECK(result.initial.empty());
+        CHECK(result.final.empty());
+        CHECK(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Automaton A concatenate automaton B") {
+        Nft lhs{};
+        Nft rhs{};
+        Nft result{};
+        lhs.add_state(10);
+        FILL_WITH_AUT_A(lhs);
+        rhs.add_state(14);
+        FILL_WITH_AUT_B(rhs);
+
+        result = lhs.concatenate(rhs);
+
+        auto shortest_words{ get_shortest_words(result) };
+        CHECK(shortest_words.size() == 4);
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'b', 'a', 'b', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', 'a', 'a' }) != shortest_words.end());
+        CHECK(shortest_words.find(std::vector<Symbol>{ 'a', 'a', 'b', 'a' }) != shortest_words.end());
+    }
+
+    SECTION("Sample automata") {
+        Nft lhs{};
+        Nft rhs{};
+        Nft result{};
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        lhs.delta.add(0, 58, 0);
+        lhs.delta.add(0, 65, 0);
+        lhs.delta.add(0, 102, 0);
+        lhs.delta.add(0, 112, 0);
+        lhs.delta.add(0, 115, 0);
+        lhs.delta.add(0, 116, 0);
+
+        rhs.add_state(5);
+        rhs.final.insert({0, 5});
+        rhs.initial.insert(5);
+        rhs.delta.add(1, 112, 0);
+        rhs.delta.add(2, 116, 1);
+        rhs.delta.add(3, 102, 2);
+        rhs.delta.add(4, 115, 3);
+        rhs.delta.add(5, 102, 2);
+        rhs.delta.add(5, 112, 0);
+        rhs.delta.add(5, 115, 3);
+        rhs.delta.add(5, 116, 1);
+
+        result = lhs.concatenate(rhs);
+        CHECK(!result.is_lang_empty());
+        // TODO: Add more checks.
+    }
+
+    SECTION("Delta smaller than states") {
+        Nft lhs{};
+        Nft rhs{};
+
+        lhs.delta.add(0, 65, 5);
+        lhs.initial.insert(0);
+        lhs.initial.insert(7);
+        lhs.final.insert(5);
+        lhs.final.insert(7);
+
+        rhs.delta.add(0, 65, 7);
+        rhs.initial.insert(0);
+        rhs.final.insert(7);
+
+        Nft result = lhs.concatenate(rhs);
+        CHECK(!result.is_lang_empty());
+    }
+
+    SECTION("the same automata") {
+        Nft lhs{};
+
+        lhs.add_state();
+        lhs.initial.insert(0);
+        lhs.final.insert(0);
+        lhs.delta.add(0, 58, 0);
+        lhs.delta.add(0, 65, 0);
+        lhs.delta.add(0, 102, 0);
+        lhs.delta.add(0, 112, 0);
+        lhs.delta.add(0, 115, 0);
+        lhs.delta.add(0, 116, 0);
+
+        size_t lhs_size = lhs.num_of_states();
+        Nft result = lhs.concatenate(lhs);
+        CHECK(result.num_of_states() == lhs_size * 2);
+    }
+
+}
+
+TEST_CASE("mata::nft::Concat_inplace performance", "[.profiling]") {
+    Nft base;
+    base.initial.insert(0);
+    base.final.insert(4);
+    base.delta.add(0, 45, 1);
+    base.delta.add(0, 46, 1);
+    base.delta.add(0, 48, 1);
+    base.delta.add(0, 49, 1);
+    base.delta.add(0, 50, 1);
+    base.delta.add(0, 51, 1);
+    base.delta.add(0, 52, 1);
+    base.delta.add(0, 53, 1);
+    base.delta.add(0, 54, 1);
+    base.delta.add(0, 55, 1);
+    base.delta.add(0, 56, 1);
+    base.delta.add(0, 57, 1);
+    base.delta.add(0, 65, 1);
+    base.delta.add(0, 66, 1);
+    base.delta.add(0, 67, 1);
+    base.delta.add(0, 68, 1);
+    base.delta.add(0, 69, 1);
+    base.delta.add(0, 70, 1);
+    base.delta.add(0, 71, 1);
+    base.delta.add(0, 72, 1);
+    base.delta.add(0, 73, 1);
+    base.delta.add(0, 74, 1);
+    base.delta.add(0, 75, 1);
+    base.delta.add(0, 76, 1);
+    base.delta.add(0, 77, 1);
+    base.delta.add(0, 78, 1);
+    base.delta.add(0, 79, 1);
+    base.delta.add(0, 80, 1);
+    base.delta.add(0, 81, 1);
+    base.delta.add(0, 82, 1);
+    base.delta.add(0, 83, 1);
+    base.delta.add(0, 84, 1);
+    base.delta.add(0, 85, 1);
+    base.delta.add(0, 86, 1);
+    base.delta.add(0, 87, 1);
+    base.delta.add(0, 88, 1);
+    base.delta.add(0, 89, 1);
+    base.delta.add(0, 90, 1);
+    base.delta.add(0, 95, 1);
+    base.delta.add(0, 97, 1);
+    base.delta.add(0, 98, 1);
+    base.delta.add(0, 99, 1);
+    base.delta.add(0, 100, 1);
+    base.delta.add(0, 101, 1);
+    base.delta.add(0, 102, 1);
+    base.delta.add(0, 103, 1);
+    base.delta.add(0, 104, 1);
+    base.delta.add(0, 105, 1);
+    base.delta.add(0, 106, 1);
+    base.delta.add(0, 107, 1);
+    base.delta.add(0, 108, 1);
+    base.delta.add(0, 109, 1);
+    base.delta.add(0, 110, 1);
+    base.delta.add(0, 111, 1);
+    base.delta.add(0, 112, 1);
+    base.delta.add(0, 113, 1);
+    base.delta.add(0, 114, 1);
+    base.delta.add(0, 115, 1);
+    base.delta.add(0, 116, 1);
+    base.delta.add(0, 117, 1);
+    base.delta.add(0, 118, 1);
+    base.delta.add(0, 119, 1);
+    base.delta.add(0, 120, 1);
+    base.delta.add(0, 121, 1);
+    base.delta.add(0, 122, 1);
+    base.delta.add(0, 124, 1);
+    base.delta.add(1, 45, 2);
+    base.delta.add(1, 46, 2);
+    base.delta.add(1, 48, 2);
+    base.delta.add(1, 49, 2);
+    base.delta.add(1, 50, 2);
+    base.delta.add(1, 51, 2);
+    base.delta.add(1, 52, 2);
+    base.delta.add(1, 53, 2);
+    base.delta.add(1, 54, 2);
+    base.delta.add(1, 55, 2);
+    base.delta.add(1, 56, 2);
+    base.delta.add(1, 57, 2);
+    base.delta.add(1, 65, 2);
+    base.delta.add(1, 66, 2);
+    base.delta.add(1, 67, 2);
+    base.delta.add(1, 68, 2);
+    base.delta.add(1, 69, 2);
+    base.delta.add(1, 70, 2);
+    base.delta.add(1, 71, 2);
+    base.delta.add(1, 72, 2);
+    base.delta.add(1, 73, 2);
+    base.delta.add(1, 74, 2);
+    base.delta.add(1, 75, 2);
+    base.delta.add(1, 76, 2);
+    base.delta.add(1, 77, 2);
+    base.delta.add(1, 78, 2);
+    base.delta.add(1, 79, 2);
+    base.delta.add(1, 80, 2);
+    base.delta.add(1, 81, 2);
+    base.delta.add(1, 82, 2);
+    base.delta.add(1, 83, 2);
+    base.delta.add(1, 84, 2);
+    base.delta.add(1, 85, 2);
+    base.delta.add(1, 86, 2);
+    base.delta.add(1, 87, 2);
+    base.delta.add(1, 88, 2);
+    base.delta.add(1, 89, 2);
+    base.delta.add(1, 90, 2);
+    base.delta.add(1, 95, 2);
+    base.delta.add(1, 97, 2);
+    base.delta.add(1, 98, 2);
+    base.delta.add(1, 99, 2);
+    base.delta.add(1, 100, 2);
+    base.delta.add(1, 101, 2);
+    base.delta.add(1, 102, 2);
+    base.delta.add(1, 103, 2);
+    base.delta.add(1, 104, 2);
+    base.delta.add(1, 105, 2);
+    base.delta.add(1, 106, 2);
+    base.delta.add(1, 107, 2);
+    base.delta.add(1, 108, 2);
+    base.delta.add(1, 109, 2);
+    base.delta.add(1, 110, 2);
+    base.delta.add(1, 111, 2);
+    base.delta.add(1, 112, 2);
+    base.delta.add(1, 113, 2);
+    base.delta.add(1, 114, 2);
+    base.delta.add(1, 115, 2);
+    base.delta.add(1, 116, 2);
+    base.delta.add(1, 117, 2);
+    base.delta.add(1, 118, 2);
+    base.delta.add(1, 119, 2);
+    base.delta.add(1, 120, 2);
+    base.delta.add(1, 121, 2);
+    base.delta.add(1, 122, 2);
+    base.delta.add(1, 124, 2);
+    base.delta.add(2, 45, 3);
+    base.delta.add(2, 46, 3);
+    base.delta.add(2, 48, 3);
+    base.delta.add(2, 49, 3);
+    base.delta.add(2, 50, 3);
+    base.delta.add(2, 51, 3);
+    base.delta.add(2, 52, 3);
+    base.delta.add(2, 53, 3);
+    base.delta.add(2, 54, 3);
+    base.delta.add(2, 55, 3);
+    base.delta.add(2, 56, 3);
+    base.delta.add(2, 57, 3);
+    base.delta.add(2, 65, 3);
+    base.delta.add(2, 66, 3);
+    base.delta.add(2, 67, 3);
+    base.delta.add(2, 68, 3);
+    base.delta.add(2, 69, 3);
+    base.delta.add(2, 70, 3);
+    base.delta.add(2, 71, 3);
+    base.delta.add(2, 72, 3);
+    base.delta.add(2, 73, 3);
+    base.delta.add(2, 74, 3);
+    base.delta.add(2, 75, 3);
+    base.delta.add(2, 76, 3);
+    base.delta.add(2, 77, 3);
+    base.delta.add(2, 78, 3);
+    base.delta.add(2, 79, 3);
+    base.delta.add(2, 80, 3);
+    base.delta.add(2, 81, 3);
+    base.delta.add(2, 82, 3);
+    base.delta.add(2, 83, 3);
+    base.delta.add(2, 84, 3);
+    base.delta.add(2, 85, 3);
+    base.delta.add(2, 86, 3);
+    base.delta.add(2, 87, 3);
+    base.delta.add(2, 88, 3);
+    base.delta.add(2, 89, 3);
+    base.delta.add(2, 90, 3);
+    base.delta.add(2, 95, 3);
+    base.delta.add(2, 97, 3);
+    base.delta.add(2, 98, 3);
+    base.delta.add(2, 99, 3);
+    base.delta.add(2, 100, 3);
+    base.delta.add(2, 101, 3);
+    base.delta.add(2, 102, 3);
+    base.delta.add(2, 103, 3);
+    base.delta.add(2, 104, 3);
+    base.delta.add(2, 105, 3);
+    base.delta.add(2, 106, 3);
+    base.delta.add(2, 107, 3);
+    base.delta.add(2, 108, 3);
+    base.delta.add(2, 109, 3);
+    base.delta.add(2, 110, 3);
+    base.delta.add(2, 111, 3);
+    base.delta.add(2, 112, 3);
+    base.delta.add(2, 113, 3);
+    base.delta.add(2, 114, 3);
+    base.delta.add(2, 115, 3);
+    base.delta.add(2, 116, 3);
+    base.delta.add(2, 117, 3);
+    base.delta.add(2, 118, 3);
+    base.delta.add(2, 119, 3);
+    base.delta.add(2, 120, 3);
+    base.delta.add(2, 121, 3);
+    base.delta.add(2, 122, 3);
+    base.delta.add(2, 124, 3);
+    base.delta.add(3, 45, 4);
+    base.delta.add(3, 46, 4);
+    base.delta.add(3, 48, 4);
+    base.delta.add(3, 49, 4);
+    base.delta.add(3, 50, 4);
+    base.delta.add(3, 51, 4);
+    base.delta.add(3, 52, 4);
+    base.delta.add(3, 53, 4);
+    base.delta.add(3, 54, 4);
+    base.delta.add(3, 55, 4);
+    base.delta.add(3, 56, 4);
+    base.delta.add(3, 57, 4);
+    base.delta.add(3, 65, 4);
+    base.delta.add(3, 66, 4);
+    base.delta.add(3, 67, 4);
+    base.delta.add(3, 68, 4);
+    base.delta.add(3, 69, 4);
+    base.delta.add(3, 70, 4);
+    base.delta.add(3, 71, 4);
+    base.delta.add(3, 72, 4);
+    base.delta.add(3, 73, 4);
+    base.delta.add(3, 74, 4);
+    base.delta.add(3, 75, 4);
+    base.delta.add(3, 76, 4);
+    base.delta.add(3, 77, 4);
+    base.delta.add(3, 78, 4);
+    base.delta.add(3, 79, 4);
+    base.delta.add(3, 80, 4);
+    base.delta.add(3, 81, 4);
+    base.delta.add(3, 82, 4);
+    base.delta.add(3, 83, 4);
+    base.delta.add(3, 84, 4);
+    base.delta.add(3, 85, 4);
+    base.delta.add(3, 86, 4);
+    base.delta.add(3, 87, 4);
+    base.delta.add(3, 88, 4);
+    base.delta.add(3, 89, 4);
+    base.delta.add(3, 90, 4);
+    base.delta.add(3, 95, 4);
+    base.delta.add(3, 97, 4);
+    base.delta.add(3, 98, 4);
+    base.delta.add(3, 99, 4);
+    base.delta.add(3, 100, 4);
+    base.delta.add(3, 101, 4);
+    base.delta.add(3, 102, 4);
+    base.delta.add(3, 103, 4);
+    base.delta.add(3, 104, 4);
+    base.delta.add(3, 105, 4);
+    base.delta.add(3, 106, 4);
+    base.delta.add(3, 107, 4);
+    base.delta.add(3, 108, 4);
+    base.delta.add(3, 109, 4);
+    base.delta.add(3, 110, 4);
+    base.delta.add(3, 111, 4);
+    base.delta.add(3, 112, 4);
+    base.delta.add(3, 113, 4);
+    base.delta.add(3, 114, 4);
+    base.delta.add(3, 115, 4);
+    base.delta.add(3, 116, 4);
+    base.delta.add(3, 117, 4);
+    base.delta.add(3, 118, 4);
+    base.delta.add(3, 119, 4);
+    base.delta.add(3, 120, 4);
+    base.delta.add(3, 121, 4);
+    base.delta.add(3, 122, 4);
+    base.delta.add(3, 124, 4);
+
+    Nft concat;
+    concat.initial.insert(1);
+    concat.final.insert(0);
+    concat.final.insert(1);
+    concat.delta.add(1, 45, 0);
+    concat.delta.add(1, 46, 0);
+    concat.delta.add(1, 48, 0);
+    concat.delta.add(1, 49, 0);
+    concat.delta.add(1, 50, 0);
+    concat.delta.add(1, 51, 0);
+    concat.delta.add(1, 52, 0);
+    concat.delta.add(1, 53, 0);
+    concat.delta.add(1, 54, 0);
+    concat.delta.add(1, 55, 0);
+    concat.delta.add(1, 56, 0);
+    concat.delta.add(1, 57, 0);
+    concat.delta.add(1, 65, 0);
+    concat.delta.add(1, 66, 0);
+    concat.delta.add(1, 67, 0);
+    concat.delta.add(1, 68, 0);
+    concat.delta.add(1, 69, 0);
+    concat.delta.add(1, 70, 0);
+    concat.delta.add(1, 71, 0);
+    concat.delta.add(1, 72, 0);
+    concat.delta.add(1, 73, 0);
+    concat.delta.add(1, 74, 0);
+    concat.delta.add(1, 75, 0);
+    concat.delta.add(1, 76, 0);
+    concat.delta.add(1, 77, 0);
+    concat.delta.add(1, 78, 0);
+    concat.delta.add(1, 79, 0);
+    concat.delta.add(1, 80, 0);
+    concat.delta.add(1, 81, 0);
+    concat.delta.add(1, 82, 0);
+    concat.delta.add(1, 83, 0);
+    concat.delta.add(1, 84, 0);
+    concat.delta.add(1, 85, 0);
+    concat.delta.add(1, 86, 0);
+    concat.delta.add(1, 87, 0);
+    concat.delta.add(1, 88, 0);
+    concat.delta.add(1, 89, 0);
+    concat.delta.add(1, 90, 0);
+    concat.delta.add(1, 95, 0);
+    concat.delta.add(1, 97, 0);
+    concat.delta.add(1, 98, 0);
+    concat.delta.add(1, 99, 0);
+    concat.delta.add(1, 100, 0);
+    concat.delta.add(1, 101, 0);
+    concat.delta.add(1, 102, 0);
+    concat.delta.add(1, 103, 0);
+    concat.delta.add(1, 104, 0);
+    concat.delta.add(1, 105, 0);
+    concat.delta.add(1, 106, 0);
+    concat.delta.add(1, 107, 0);
+    concat.delta.add(1, 108, 0);
+    concat.delta.add(1, 109, 0);
+    concat.delta.add(1, 110, 0);
+    concat.delta.add(1, 111, 0);
+    concat.delta.add(1, 112, 0);
+    concat.delta.add(1, 113, 0);
+    concat.delta.add(1, 114, 0);
+    concat.delta.add(1, 115, 0);
+    concat.delta.add(1, 116, 0);
+    concat.delta.add(1, 117, 0);
+    concat.delta.add(1, 118, 0);
+    concat.delta.add(1, 119, 0);
+    concat.delta.add(1, 120, 0);
+    concat.delta.add(1, 121, 0);
+    concat.delta.add(1, 122, 0);
+    concat.delta.add(1, 124, 0);
+
+    for (auto i=0;i<1000;i++) {
+        base.concatenate(concat);
+    }
+}
diff --git a/tests/nft/nft-intersection.cc b/tests/nft/nft-intersection.cc
new file mode 100644
index 000000000..0363009e4
--- /dev/null
+++ b/tests/nft/nft-intersection.cc
@@ -0,0 +1,875 @@
+/* tests-nft-intersection.cc -- Tests for intersection of NFTs
+ */
+
+
+#include <unordered_set>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+#include "mata/nft/nft.hh"
+
+using namespace mata::nft;
+using namespace mata::utils;
+using namespace mata::parser;
+
+// Some common automata {{{
+
+// Automaton A
+#define FILL_WITH_AUT_A(x) \
+    x.initial = {1, 3}; \
+    x.final = {5}; \
+    x.delta.add(1, 'a', 3); \
+    x.delta.add(1, 'a', 10); \
+    x.delta.add(1, 'b', 7); \
+    x.delta.add(3, 'a', 7); \
+    x.delta.add(3, 'b', 9); \
+    x.delta.add(9, 'a', 9); \
+    x.delta.add(7, 'b', 1); \
+    x.delta.add(7, 'a', 3); \
+    x.delta.add(7, 'c', 3); \
+    x.delta.add(10, 'a', 7); \
+    x.delta.add(10, 'b', 7); \
+    x.delta.add(10, 'c', 7); \
+    x.delta.add(7, 'a', 5); \
+    x.delta.add(5, 'a', 5); \
+    x.delta.add(5, 'c', 9); \
+
+
+// Automaton B
+#define FILL_WITH_AUT_B(x) \
+    x.initial = {4}; \
+    x.final = {2, 12}; \
+    x.delta.add(4, 'c', 8); \
+    x.delta.add(4, 'a', 8); \
+    x.delta.add(8, 'b', 4); \
+    x.delta.add(4, 'a', 6); \
+    x.delta.add(4, 'b', 6); \
+    x.delta.add(6, 'a', 2); \
+    x.delta.add(2, 'b', 2); \
+    x.delta.add(2, 'a', 0); \
+    x.delta.add(0, 'a', 2); \
+    x.delta.add(2, 'c', 12); \
+    x.delta.add(12, 'a', 14); \
+    x.delta.add(14, 'b', 12); \
+
+// }}}
+
+TEST_CASE("mata::nft::intersection() with jump_mode == JumpMode::AppendDontCares")
+{ // {{{
+    Nft a, b, res;
+    std::unordered_map<std::pair<State, State>, State> prod_map;
+
+    SECTION("Intersection of empty automata")
+    {
+        res = intersection(a, b, &prod_map, JumpMode::AppendDontCares);
+
+        REQUIRE(res.initial.empty());
+        REQUIRE(res.final.empty());
+        REQUIRE(res.delta.empty());
+        REQUIRE(prod_map.empty());
+    }
+
+    SECTION("Intersection of empty automata 2")
+    {
+        res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+        REQUIRE(res.initial.empty());
+        REQUIRE(res.final.empty());
+        REQUIRE(res.delta.empty());
+    }
+
+    a.add_state(5);
+    b.add_state(6);
+
+    SECTION("Intersection of automata with no transitions")
+    {
+        a.initial = {1, 3};
+        a.final = {3, 5};
+
+        b.initial = {4, 6};
+        b.final = {4, 2};
+
+        REQUIRE(!a.initial.empty());
+        REQUIRE(!b.initial.empty());
+        REQUIRE(!a.final.empty());
+        REQUIRE(!b.final.empty());
+
+        res = intersection(a, b, &prod_map, JumpMode::AppendDontCares);
+
+        REQUIRE(!res.initial.empty());
+        REQUIRE(!res.final.empty());
+
+        State init_fin_st = prod_map[{3, 4}];
+
+        REQUIRE(res.initial[init_fin_st]);
+        REQUIRE(res.final[init_fin_st]);
+    }
+
+    a.add_state(10);
+    b.add_state(14);
+
+    SECTION("Intersection of automata with some transitions")
+    {
+        FILL_WITH_AUT_A(a);
+        FILL_WITH_AUT_B(b);
+
+        res = intersection(a, b, &prod_map, JumpMode::AppendDontCares);
+
+        REQUIRE(res.initial[prod_map[{1, 4}]]);
+        REQUIRE(res.initial[prod_map[{3, 4}]]);
+        REQUIRE(res.final[prod_map[{5, 2}]]);
+
+        //for (const auto& c : prod_map) std::cout << c.first.first << "," << c.first.second << " -> " << c.second << "\n";
+        //std::cout << prod_map[{7, 2}] << " " <<  prod_map[{1, 2}] << '\n';
+        REQUIRE(res.delta.contains(prod_map[{1, 4}], 'a', prod_map[{3, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 4}], 'a', prod_map[{10, 8}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 4}], 'a', prod_map[{10, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 4}], 'b', prod_map[{7, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 6}], 'a', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 2}], 'a', prod_map[{3, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 2}], 'a', prod_map[{5, 0}]));
+        // REQUIRE(res.delta.contains(prod_map[{7, 2}], 'b', prod_map[{1, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 0}], 'a', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 2}], 'a', prod_map[{10, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 2}], 'a', prod_map[{3, 0}]));
+        // REQUIRE(res.delta.contains(prod_map[{1, 2}], 'b', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{10, 0}], 'a', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{5, 0}], 'a', prod_map[{5, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{5, 2}], 'a', prod_map[{5, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{10, 6}], 'a', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 6}], 'a', prod_map[{5, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 6}], 'a', prod_map[{3, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{10, 8}], 'b', prod_map[{7, 4}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 4}], 'a', prod_map[{3, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 4}], 'a', prod_map[{3, 8}]));
+        // REQUIRE(res.delta.contains(prod_map[{7, 4}], 'b', prod_map[{1, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 4}], 'a', prod_map[{5, 6}]));
+        // REQUIRE(res.delta.contains(prod_map[{7, 4}], 'b', prod_map[{1, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 6}], 'a', prod_map[{3, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 6}], 'a', prod_map[{10, 2}]));
+        // REQUIRE(res.delta.contains(prod_map[{10, 2}], 'b', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{10, 2}], 'a', prod_map[{7, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 0}], 'a', prod_map[{5, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 0}], 'a', prod_map[{3, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 2}], 'a', prod_map[{7, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{5, 6}], 'a', prod_map[{5, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 4}], 'a', prod_map[{7, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 4}], 'a', prod_map[{7, 8}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 8}], 'b', prod_map[{1, 4}]));
+    }
+
+    SECTION("Intersection of automata with some transitions but without a final state")
+    {
+        FILL_WITH_AUT_A(a);
+        FILL_WITH_AUT_B(b);
+        b.final = {12};
+
+        res = intersection(a, b, &prod_map, JumpMode::AppendDontCares);
+
+        REQUIRE(res.initial[prod_map[{1, 4}]]);
+        REQUIRE(res.initial[prod_map[{3, 4}]]);
+        REQUIRE(res.is_lang_empty());
+    }
+
+    Nft expected;
+    SECTION("Intersection of transducers with epsilon transitions.") {
+        SECTION("The intersection results in an empty language.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            a.delta.add(0, EPSILON, 1);
+            a.delta.add(1, 'b', 2);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            b.delta.add(0, 'b', 1);
+            b.delta.add(1, EPSILON, 2);
+
+            res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+            CHECK(!res.initial.empty());
+            CHECK(res.final.empty());
+            CHECK(res.is_lang_empty());
+        }
+
+        SECTION("Epsilon is treated as an alphabet symbol.") {
+            a = Nft(5, { 0 }, { 3, 4 }, { 0, 1, 1, 0, 0 }, 2);
+            a.delta.add(0, EPSILON, 1);
+            a.delta.add(0, 'b', 2);
+            a.delta.add(1, 'a', 3);
+            a.delta.add(2, 'a', 4);
+            a.delta.add(4, EPSILON, 4);
+
+            b = Nft(4, { 0 }, { 3 }, { 0, 1, 1, 0 }, 2);
+            b.delta.add(0, EPSILON, 1);
+            b.delta.add(0, 'b', 2);
+            b.delta.add(1, 'a', 3);
+            b.delta.add(1, 'b', 3);
+            b.delta.add(2, 'a', 3);
+
+            expected = Nft(4, { 0 }, { 3 }, { 0, 1, 1, 0 }, 2);
+            expected.delta.add(0, EPSILON, 1);
+            expected.delta.add(0, 'b', 2);
+            expected.delta.add(1, 'a', 3);
+            expected.delta.add(2, 'a', 3);
+
+            res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+            CHECK(are_equivalent(res, expected));
+        }
+    }
+
+    SECTION("Intersection of linear transducers with multiple levels.") {
+        SECTION("Intersection 1") {
+            a = Nft(4, { 0 }, { 3 }, { 0, 1, 3, 0 }, 4);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, 'b', 2);
+            a.delta.add(2, 'c', 3);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 4);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, 'b', 2);
+
+            expected = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'b', 2);
+            expected.delta.add(2, 'b', 3);
+            expected.delta.add(3, 'c', 4);
+
+            res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("Intersection 2") {
+            a = Nft(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            a.delta.add(0, 'a', 1);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 1, 0}, 1);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, 'b', 2);
+
+            expected = b;
+
+            res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("Intersection 3") {
+            a = Nft(4, { 0 }, { 3 }, { 0, 2, 3, 0 }, 5);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, 'b', 2);
+            a.delta.add(2, 'a', 3);
+
+            b = Nft(5, { 0 }, { 4 }, { 0, 1, 3, 4, 0 }, 5);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, 'c', 2);
+            b.delta.add(2, 'b', 3);
+            b.delta.add(3, 'a', 4);
+
+            std::unordered_map<std::pair<State, State>, State> prod_map;
+            Nft res = intersection(a, b, &prod_map, JumpMode::AppendDontCares);
+
+            REQUIRE(!res.initial.empty());
+            REQUIRE(res.final.empty());
+            REQUIRE(res.delta.contains(prod_map[{0, 0}], 'a', prod_map[{1, 1}]));
+            REQUIRE(res.delta.contains(prod_map[{1, 1}], 'c', prod_map[{1, 2}]));
+            REQUIRE(res.delta.contains(prod_map[{1, 2}], 'b', prod_map[{2, 2}]));
+            CHECK(res.is_lang_empty());
+        }
+    }
+
+    SECTION("Intersection of complex transducers with multiple levels and an epsilon transition") {
+        a = Nft(8, { 0 }, { 5, 6, 7 }, { 0, 1, 1, 2, 2, 0, 0, 0 }, 3);
+        a.delta.add(0, 'a', 1);
+        a.delta.add(0, 'b', 2);
+        a.delta.add(0, 'a', 4);
+        a.delta.add(1, 'c', 3);
+        a.delta.add(2, 'a', 4);
+        a.delta.add(2, 'c', 7);
+        a.delta.add(3, 'a', 5);
+        a.delta.add(4, 'b', 6);
+        a.delta.add(5, 'a', 3);
+        a.delta.add(6, EPSILON, 4);
+        a.delta.add(7, 'c', 2);
+
+        b = Nft(5, { 0 }, { 3, 4 }, { 0, 1, 2, 0, 0 }, 3);
+        b.delta.add(0, 'a', 1);
+        b.delta.add(0, 'b', 1);
+        b.delta.add(0, 'a', 3);
+        b.delta.add(1, 'a', 2);
+        b.delta.add(1, 'c', 4);
+        b.delta.add(2, 'b', 4);
+        b.delta.add(3, 'c', 3);
+        b.delta.add(4, EPSILON, 4);
+
+        expected = Nft(12, { 0 }, { 4, 5, 9, 11 }, { 0, 1, 1, 2, 0, 0, 2, 1, 2, 0, 2, 0 }, 3);
+        expected.delta.add(0, 'b', 1);
+        expected.delta.add(0, 'a', 2);
+        expected.delta.add(0, 'a', 7);
+        expected.delta.add(0, 'a', 10);
+        expected.delta.add(1, 'a', 3);
+        expected.delta.add(1, 'c', 4);
+        expected.delta.add(2, 'a', 3);
+        expected.delta.add(2, 'c', 6);
+        expected.delta.add(3, 'b', 5);
+        expected.delta.add(5, EPSILON, 6);
+        expected.delta.add(6, 'b', 5);
+        expected.delta.add(7, 'c', 8);
+        expected.delta.add(8, 'a', 9);
+        expected.delta.add(10, 'b', 11);
+
+        res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+        CHECK(are_equivalent(res, expected));
+    }
+
+    SECTION("Intersection of transducers with the DONT_CARE symbol") {
+        SECTION("DONT_CARE is in the lhs.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 3);
+            a.delta.add(0, DONT_CARE, 1);
+            a.delta.add(1, 'c', 2);
+
+            b = Nft(7, { 0 }, { 4, 5, 6 }, { 0, 1, 1, 1, 0, 0, 0 }, 3);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(0, 'b', 2);
+            b.delta.add(0, 'a', 3);
+            b.delta.add(1, 'c', 4);
+            b.delta.add(2, 'd', 5);
+            b.delta.add(3, 'e', 6);
+
+            expected = Nft(10, { 0 }, { 7, 8, 9 }, { 0, 1, 1, 1, 2, 2, 2, 0, 0, 0 }, 3);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(0, 'b', 2);
+            expected.delta.add(0, 'a', 3);
+            expected.delta.add(1, 'c', 4);
+            expected.delta.add(2, 'd', 5);
+            expected.delta.add(3, 'e', 6);
+            expected.delta.add(4, 'c', 7);
+            expected.delta.add(5, 'c', 8);
+            expected.delta.add(6, 'c', 9);
+
+            res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("DONT_CARE is in the rhs.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 3);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, 'c', 2);
+
+            b = Nft(7, { 0 }, { 4, 5, 6 }, { 0, 1, 1, 1, 0, 0, 0 }, 3);
+            b.delta.add(0, DONT_CARE, 1);
+            b.delta.add(0, DONT_CARE, 2);
+            b.delta.add(0, DONT_CARE, 3);
+            b.delta.add(1, 'c', 4);
+            b.delta.add(2, 'd', 5);
+            b.delta.add(3, 'e', 6);
+
+            expected = Nft(8, { 0 }, { 5, 6, 7 }, { 0, 1, 2, 2, 2, 0, 0, 0 }, 3);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'c', 2);
+            expected.delta.add(1, 'd', 3);
+            expected.delta.add(1, 'e', 4);
+            expected.delta.add(2, 'c', 5);
+            expected.delta.add(3, 'c', 6);
+            expected.delta.add(4, 'c', 7);
+
+            res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("DONT_CARE is in both lhs and rhs. In lhs, DONT_CARE is at a higher level than it is in rhs.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 3);
+            a.delta.add(0, DONT_CARE, 1);
+            a.delta.add(1, DONT_CARE, 2);
+
+            b = Nft(7, { 0 }, { 4, 5, 6 }, { 0, 1, 1, 1, 0, 0, 0 }, 3);
+            b.delta.add(0, DONT_CARE, 1);
+            b.delta.add(0, DONT_CARE, 2);
+            b.delta.add(0, DONT_CARE, 3);
+            b.delta.add(1, 'c', 4);
+            b.delta.add(2, 'd', 5);
+            b.delta.add(3, 'e', 6);
+
+            expected = Nft(10, { 0 }, { 7, 8, 9 }, { 0, 1, 1, 1, 2, 2, 2, 0, 0, 0 }, 3);
+            expected.delta.add(0, DONT_CARE, 1);
+            expected.delta.add(0, DONT_CARE, 2);
+            expected.delta.add(0, DONT_CARE, 3);
+            expected.delta.add(1, 'c', 4);
+            expected.delta.add(2, 'd', 5);
+            expected.delta.add(3, 'e', 6);
+            expected.delta.add(4, DONT_CARE, 7);
+            expected.delta.add(5, DONT_CARE, 8);
+            expected.delta.add(6, DONT_CARE, 9);
+
+            res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("DONT_CARE is in the rhs at a higher level than it is in the lhs.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 1, 0 }, 3);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, DONT_CARE, 2);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 3);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, DONT_CARE, 2);
+
+            expected = Nft(4, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, DONT_CARE, 2);
+            expected.delta.add(2, DONT_CARE, 3);
+
+            res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+            CHECK(are_equivalent(res, expected));
+        }
+    }
+} // }}}
+
+
+TEST_CASE("mata::nft::intersection() with jump_mode == JumpMode::RepeatSymbol") {
+    Nft a, b, res;
+    std::unordered_map<std::pair<State, State>, State> prod_map;
+
+    SECTION("Intersection of empty automata")
+    {
+        res = intersection(a, b, &prod_map, JumpMode::AppendDontCares);
+
+        REQUIRE(res.initial.empty());
+        REQUIRE(res.final.empty());
+        REQUIRE(res.delta.empty());
+        REQUIRE(prod_map.empty());
+    }
+
+    SECTION("Intersection of empty automata 2")
+    {
+        res = intersection(a, b, nullptr, JumpMode::AppendDontCares);
+
+        REQUIRE(res.initial.empty());
+        REQUIRE(res.final.empty());
+        REQUIRE(res.delta.empty());
+    }
+
+    a.add_state(5);
+    b.add_state(6);
+
+    SECTION("Intersection of automata with no transitions")
+    {
+        a.initial = {1, 3};
+        a.final = {3, 5};
+
+        b.initial = {4, 6};
+        b.final = {4, 2};
+
+        REQUIRE(!a.initial.empty());
+        REQUIRE(!b.initial.empty());
+        REQUIRE(!a.final.empty());
+        REQUIRE(!b.final.empty());
+
+        res = intersection(a, b, &prod_map);
+
+        REQUIRE(!res.initial.empty());
+        REQUIRE(!res.final.empty());
+
+        State init_fin_st = prod_map[{3, 4}];
+
+        REQUIRE(res.initial[init_fin_st]);
+        REQUIRE(res.final[init_fin_st]);
+    }
+
+    a.add_state(10);
+    b.add_state(14);
+
+    SECTION("Intersection of automata with some transitions")
+    {
+        FILL_WITH_AUT_A(a);
+        FILL_WITH_AUT_B(b);
+
+        res = intersection(a, b, &prod_map);
+
+        REQUIRE(res.initial[prod_map[{1, 4}]]);
+        REQUIRE(res.initial[prod_map[{3, 4}]]);
+        REQUIRE(res.final[prod_map[{5, 2}]]);
+
+        //for (const auto& c : prod_map) std::cout << c.first.first << "," << c.first.second << " -> " << c.second << "\n";
+        //std::cout << prod_map[{7, 2}] << " " <<  prod_map[{1, 2}] << '\n';
+        REQUIRE(res.delta.contains(prod_map[{1, 4}], 'a', prod_map[{3, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 4}], 'a', prod_map[{10, 8}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 4}], 'a', prod_map[{10, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 4}], 'b', prod_map[{7, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 6}], 'a', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 2}], 'a', prod_map[{3, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 2}], 'a', prod_map[{5, 0}]));
+        // REQUIRE(res.delta.contains(prod_map[{7, 2}], 'b', prod_map[{1, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 0}], 'a', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 2}], 'a', prod_map[{10, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 2}], 'a', prod_map[{3, 0}]));
+        // REQUIRE(res.delta.contains(prod_map[{1, 2}], 'b', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{10, 0}], 'a', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{5, 0}], 'a', prod_map[{5, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{5, 2}], 'a', prod_map[{5, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{10, 6}], 'a', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 6}], 'a', prod_map[{5, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 6}], 'a', prod_map[{3, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{10, 8}], 'b', prod_map[{7, 4}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 4}], 'a', prod_map[{3, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 4}], 'a', prod_map[{3, 8}]));
+        // REQUIRE(res.delta.contains(prod_map[{7, 4}], 'b', prod_map[{1, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 4}], 'a', prod_map[{5, 6}]));
+        // REQUIRE(res.delta.contains(prod_map[{7, 4}], 'b', prod_map[{1, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 6}], 'a', prod_map[{3, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{1, 6}], 'a', prod_map[{10, 2}]));
+        // REQUIRE(res.delta.contains(prod_map[{10, 2}], 'b', prod_map[{7, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{10, 2}], 'a', prod_map[{7, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 0}], 'a', prod_map[{5, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 0}], 'a', prod_map[{3, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 2}], 'a', prod_map[{7, 0}]));
+        REQUIRE(res.delta.contains(prod_map[{5, 6}], 'a', prod_map[{5, 2}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 4}], 'a', prod_map[{7, 6}]));
+        REQUIRE(res.delta.contains(prod_map[{3, 4}], 'a', prod_map[{7, 8}]));
+        REQUIRE(res.delta.contains(prod_map[{7, 8}], 'b', prod_map[{1, 4}]));
+    }
+
+    SECTION("Intersection of automata with some transitions but without a final state")
+    {
+        FILL_WITH_AUT_A(a);
+        FILL_WITH_AUT_B(b);
+        b.final = {12};
+
+        res = intersection(a, b, &prod_map);
+
+        REQUIRE(res.initial[prod_map[{1, 4}]]);
+        REQUIRE(res.initial[prod_map[{3, 4}]]);
+        REQUIRE(res.is_lang_empty());
+    }
+
+    Nft expected;
+    SECTION("Intersection of transducers with epsilon transitions.") {
+        SECTION("The intersection results in an empty language.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            a.delta.add(0, EPSILON, 1);
+            a.delta.add(1, 'b', 2);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            b.delta.add(0, 'b', 1);
+            b.delta.add(1, EPSILON, 2);
+
+            res = intersection(a, b);
+
+            CHECK(!res.initial.empty());
+            CHECK(res.final.empty());
+            CHECK(res.is_lang_empty());
+        }
+
+        SECTION("Epsilon is treated as an alphabet symbol.") {
+            a = Nft(5, { 0 }, { 3, 4 }, { 0, 1, 1, 0, 0 }, 2);
+            a.delta.add(0, EPSILON, 1);
+            a.delta.add(0, 'b', 2);
+            a.delta.add(1, 'a', 3);
+            a.delta.add(2, 'a', 4);
+            a.delta.add(4, EPSILON, 4);
+
+            b = Nft(4, { 0 }, { 3 }, { 0, 1, 1, 0 }, 2);
+            b.delta.add(0, EPSILON, 1);
+            b.delta.add(0, 'b', 2);
+            b.delta.add(1, 'a', 3);
+            b.delta.add(1, 'b', 3);
+            b.delta.add(2, 'a', 3);
+
+            expected = Nft(4, { 0 }, { 3 }, { 0, 1, 1, 0 }, 2);
+            expected.delta.add(0, EPSILON, 1);
+            expected.delta.add(0, 'b', 2);
+            expected.delta.add(1, 'a', 3);
+            expected.delta.add(2, 'a', 3);
+
+            res = intersection(a, b);
+
+            CHECK(are_equivalent(res, expected));
+        }
+    }
+
+    SECTION("Intersection of linear transducers with multiple levels.") {
+        SECTION("Intersection 1") {
+            a = Nft(4, { 0 }, { 3 }, { 0, 1, 3, 0 }, 4);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, 'b', 2);
+            a.delta.add(2, 'c', 3);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 4);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, 'b', 2);
+
+            expected = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'b', 2);
+            expected.delta.add(2, 'b', 3);
+            expected.delta.add(3, 'c', 4);
+
+            res = intersection(a, b);
+
+            CHECK(res.is_lang_empty());
+            CHECK(!are_equivalent(res, expected));
+        }
+
+        SECTION("Intersection 2") {
+            a = Nft(4, { 0 }, { 3 }, { 0, 1, 3, 0 }, 4);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, 'a', 2);
+            a.delta.add(2, 'a', 3);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 4);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, 'a', 2);
+
+            expected = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'a', 3);
+            expected.delta.add(3, 'a', 4);
+
+            res = intersection(a, b);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("Intersection 3") {
+            a = Nft(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            a.delta.add(0, 'a', 1);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 1, 0}, 1);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, 'b', 2);
+
+            expected = b;
+
+            res = intersection(a, b);
+
+            CHECK(!are_equivalent(res, expected));
+        }
+
+        SECTION("Intersection 4") {
+            a = Nft(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            a.delta.add(0, 'a', 1);
+
+            b = Nft(3, { 0 }, { 2 }, { 0, 1, 0}, 1);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, 'a', 2);
+
+            expected = b;
+
+            res = intersection(a, b);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("Intersection 5") {
+            a = Nft(4, { 0 }, { 3 }, { 0, 2, 3, 0 }, 5);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, 'a', 2);
+            a.delta.add(2, 'b', 3);
+
+            b = Nft(5, { 0 }, { 4 }, { 0, 1, 3, 4, 0 }, 5);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(1, 'a', 2);
+            b.delta.add(2, 'b', 3);
+            b.delta.add(3, 'b', 4);
+
+            expected = Nft(6, { 0 }, { 5 }, { 0, 1, 2, 3, 4, 0 }, 5);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'a', 3);
+            expected.delta.add(3, 'b', 4);
+            expected.delta.add(4, 'b', 5);
+
+            res = intersection(a, b);
+
+            CHECK(are_equivalent(res, expected));
+        }
+    }
+
+    SECTION("Intersection of complex transducers with multiple levels and an epsilon transition") {
+        a = Nft(8, { 0 }, { 5, 6, 7 }, { 0, 1, 1, 2, 2, 0, 0, 0 }, 3);
+        a.delta.add(0, 'a', 1);
+        a.delta.add(0, 'b', 2);
+        a.delta.add(0, 'a', 4);
+        a.delta.add(1, 'c', 3);
+        a.delta.add(2, 'a', 4);
+        a.delta.add(2, 'c', 7);
+        a.delta.add(3, 'a', 5);
+        a.delta.add(4, 'b', 6);
+        a.delta.add(5, 'a', 3);
+        a.delta.add(6, EPSILON, 4);
+        a.delta.add(7, 'c', 2);
+
+        b = Nft(5, { 0 }, { 3, 4 }, { 0, 1, 2, 0, 0 }, 3);
+        b.delta.add(0, 'a', 1);
+        b.delta.add(0, 'b', 1);
+        b.delta.add(0, 'a', 3);
+        b.delta.add(1, 'a', 2);
+        b.delta.add(1, 'c', 4);
+        b.delta.add(2, 'b', 4);
+        b.delta.add(3, EPSILON, 3);
+        b.delta.add(4, 'c', 4);
+
+        expected = Nft(6, { 0 }, { 3, 5 }, { 0, 1, 2, 0, 1, 0}, 3);
+        expected.delta.add(0, 'a', 1);
+        expected.delta.add(0, 'b', 1);
+        expected.delta.add(0, 'b', 4);
+        expected.delta.add(1, 'a', 2);
+        expected.delta.add(2, 'b', 3);
+        expected.delta.add(4, 'c', 5);
+        expected.delta.add(5, 'c', 4);
+
+        res = intersection(a, b);
+
+        CHECK(are_equivalent(res, expected));
+    }
+
+    SECTION("Intersection of transducers with the DONT_CARE symbol") {
+        SECTION("DONT_CARE is in the lhs.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 3);
+            a.delta.add(0, DONT_CARE, 1);
+            a.delta.add(1, 'c', 2);
+
+            b = Nft(7, { 0 }, { 4, 5, 6 }, { 0, 1, 1, 1, 0, 0, 0 }, 3);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(0, 'b', 2);
+            b.delta.add(0, 'a', 3);
+            b.delta.add(1, 'c', 4);
+            b.delta.add(2, 'd', 5);
+            b.delta.add(3, 'e', 6);
+
+            expected = Nft(4, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'c', 2);
+            expected.delta.add(2, 'c', 3);
+
+            res = intersection(a, b);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("DONT_CARE is in the rhs.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 3);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, 'c', 2);
+
+            b = Nft(7, { 0 }, { 4, 5, 6 }, { 0, 1, 1, 1, 0, 0, 0 }, 3);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(0, 'b', 2);
+            b.delta.add(0, 'a', 3);
+            b.delta.add(1, DONT_CARE, 4);
+            b.delta.add(2, DONT_CARE, 5);
+            b.delta.add(3, DONT_CARE, 6);
+
+            expected = Nft(4, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'c', 3);
+
+            res = intersection(a, b);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("DONT_CARE is in both lhs and rhs. In lhs, DONT_CARE is at a smaller level than it is in rhs.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 3);
+            a.delta.add(0, DONT_CARE, 1);
+            a.delta.add(1, 'c', 2);
+
+            b = Nft(7, { 0 }, { 4, 5, 6 }, { 0, 1, 1, 1, 0, 0, 0 }, 3);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(0, 'b', 2);
+            b.delta.add(0, 'a', 3);
+            b.delta.add(1, DONT_CARE, 4);
+            b.delta.add(2, DONT_CARE, 5);
+            b.delta.add(3, DONT_CARE, 6);
+
+            expected = Nft(3, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(0, 'b', 1);
+            expected.delta.add(1, DONT_CARE, 2);
+            expected.delta.add(2, 'c', 3);
+
+            res = intersection(a, b);
+
+            CHECK(are_equivalent(res, expected));
+        }
+
+        SECTION("DONT_CARE is in the rhs at a smaller level than it is in the lhs.") {
+            a = Nft(3, { 0 }, { 2 }, { 0, 2, 0 }, 3);
+            a.delta.add(0, 'a', 1);
+            a.delta.add(1, DONT_CARE, 2);
+
+            b = Nft(7, { 0 }, { 4, 5, 6 }, { 0, 1, 1, 1, 0, 0, 0 }, 3);
+            b.delta.add(0, 'a', 1);
+            b.delta.add(0, 'b', 2);
+            b.delta.add(0, 'a', 3);
+            b.delta.add(1, DONT_CARE, 4);
+            b.delta.add(2, DONT_CARE, 5);
+            b.delta.add(3, DONT_CARE, 6);
+
+            expected = Nft(4, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+            expected.delta.add(0, 'a', 1);
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, DONT_CARE, 3);
+
+            res = intersection(a, b);
+
+            CHECK(are_equivalent(res, expected));
+        }
+    }
+}
+
+
+TEST_CASE("mata::nft::intersection() for profiling", "[.profiling],[intersection]")
+{
+    Nft a{4};
+    a.initial.insert(0);
+    a.final.insert({ 0, 2, 3});
+    a.delta.add(0, 'a', 0);
+    a.delta.add(0, 'b', 0);
+    a.delta.add(0, 'c', 1);
+    a.delta.add(1, 'a', 3);
+    a.delta.add(1, 'b', 2);
+
+    Nft b{9};
+    b.initial.insert(0);
+    b.final.insert({2, 4, 8, 7});
+    b.delta.add(0, 'b', 1);
+    b.delta.add(0, 'a', 2);
+    b.delta.add(0, 'c', 3);
+    b.delta.add(2, 'a', 8);
+    b.delta.add(2, 'b', 8);
+    b.delta.add(3, 'a', 6);
+    b.delta.add(3, 'a', 4);
+    b.delta.add(3, 'a', 7);
+
+    for (size_t i{ 0 }; i < 10000; ++i) {
+        Nft result{intersection(a, b, nullptr, JumpMode::AppendDontCares) };
+    }
+}
+
+TEST_CASE("mata::nft::Move semantics", "[.profiling][std::move]") {
+    Nft b{10};
+    b.initial.insert(0);
+    b.final.insert({2, 4, 8, 7});
+    b.delta.add(0, 'b', 1);
+    b.delta.add(0, 'a', 2);
+    b.delta.add(0, 'c', 3);
+    b.delta.add(2, 'a', 8);
+    b.delta.add(2, 'b', 8);
+    b.delta.add(3, 'a', 6);
+    b.delta.add(3, 'a', 4);
+    b.delta.add(3, 'a', 7);
+
+    for (size_t i{ 0 }; i < 1'000'000; ++i) {
+        Nft a{ std::move(b) };
+        a.initial.insert(1);
+        b = std::move(a);
+    }
+}
diff --git a/tests/nft/nft-plumbing.cc b/tests/nft/nft-plumbing.cc
new file mode 100644
index 000000000..2bb66df5b
--- /dev/null
+++ b/tests/nft/nft-plumbing.cc
@@ -0,0 +1,121 @@
+/* tests-nft-plumbing.cc -- Tests plumbing versions of functions
+ */
+
+
+#include <unordered_set>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+#include "mata/nft/nft.hh"
+#include "mata/nft/plumbing.hh"
+
+using Symbol = mata::Symbol;
+using OnTheFlyAlphabet = mata::OnTheFlyAlphabet;
+
+// Some common automata {{{
+
+// Automaton A
+#define FILL_WITH_AUT_A(x) \
+    x.initial = {1, 3}; \
+    x.final = {5}; \
+    x.delta.add(1, 'a', 3); \
+    x.delta.add(1, 'a', 10); \
+    x.delta.add(1, 'b', 7); \
+    x.delta.add(3, 'a', 7); \
+    x.delta.add(3, 'b', 9); \
+    x.delta.add(9, 'a', 9); \
+    x.delta.add(7, 'b', 1); \
+    x.delta.add(7, 'a', 3); \
+    x.delta.add(7, 'c', 3); \
+    x.delta.add(10, 'a', 7); \
+    x.delta.add(10, 'b', 7); \
+    x.delta.add(10, 'c', 7); \
+    x.delta.add(7, 'a', 5); \
+    x.delta.add(5, 'a', 5); \
+    x.delta.add(5, 'c', 9); \
+
+
+// Automaton B
+#define FILL_WITH_AUT_B(x) \
+    x.initial = {4}; \
+    x.final = {2, 12}; \
+    x.delta.add(4, 'c', 8); \
+    x.delta.add(4, 'a', 8); \
+    x.delta.add(8, 'b', 4); \
+    x.delta.add(4, 'a', 6); \
+    x.delta.add(4, 'b', 6); \
+    x.delta.add(6, 'a', 2); \
+    x.delta.add(2, 'b', 2); \
+    x.delta.add(2, 'a', 0); \
+    x.delta.add(0, 'a', 2); \
+    x.delta.add(2, 'c', 12); \
+    x.delta.add(12, 'a', 14); \
+    x.delta.add(14, 'b', 12); \
+
+// }}}
+
+TEST_CASE("Mata::nft::Plumbing") {
+    mata::nft::Nft lhs{};
+    mata::nft::Nft rhs{};
+    mata::nft::Nft result{};
+    OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b", "c" } };
+
+    SECTION("Mata::nft::Plumbing::concatenate") {
+        FILL_WITH_AUT_A(lhs);
+        FILL_WITH_AUT_B(lhs);
+        mata::nft::plumbing::concatenate(&result, lhs, rhs);
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Mata::nft::Plumbing::intersection") {
+        FILL_WITH_AUT_A(lhs);
+        FILL_WITH_AUT_B(lhs);
+        mata::nft::plumbing::intersection(&result, lhs, rhs);
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("Mata::nft::Plumbing::union") {
+        FILL_WITH_AUT_A(lhs);
+        FILL_WITH_AUT_B(lhs);
+        mata::nft::plumbing::uni(&result, lhs, rhs);
+        CHECK(!result.is_lang_empty());
+    }
+
+    SECTION("Mata::nft::Plumbing::remove_epsilon") {
+        FILL_WITH_AUT_A(lhs);
+        mata::nft::plumbing::remove_epsilon(&result, lhs);
+        CHECK(!result.is_lang_empty());
+    }
+
+    SECTION("Mata::nft::Plumbing::revert") {
+        FILL_WITH_AUT_A(lhs);
+        mata::nft::plumbing::revert(&result, lhs);
+        CHECK(!result.is_lang_empty());
+    }
+
+    SECTION("Mata::nft::Plumbing::reduce") {
+        FILL_WITH_AUT_A(lhs);
+        mata::nft::plumbing::reduce(&result, lhs);
+        CHECK(!result.is_lang_empty());
+        CHECK(result.num_of_states() <= lhs.num_of_states());
+    }
+
+    SECTION("Mata::nft::Plumbing::determinize") {
+        FILL_WITH_AUT_A(lhs);
+        mata::nft::plumbing::determinize(&result, lhs);
+        CHECK(!result.is_lang_empty());
+    }
+
+    SECTION("Mata::nft::Plumbing::minimize") {
+        FILL_WITH_AUT_A(lhs);
+        mata::nft::plumbing::minimize(&result, lhs);
+        CHECK(!result.is_lang_empty());
+    }
+
+    SECTION("Mata::nft::Plumbing::complement") {
+        FILL_WITH_AUT_A(lhs);
+        mata::nft::plumbing::complement(&result, lhs, alph);
+        CHECK(!result.is_lang_empty());
+    }
+}
diff --git a/tests/nft/nft-profiling.cc b/tests/nft/nft-profiling.cc
new file mode 100644
index 000000000..63b88241a
--- /dev/null
+++ b/tests/nft/nft-profiling.cc
@@ -0,0 +1,127 @@
+// TODO: some header
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+#include "utils.hh"
+
+#include "mata/nft/nft.hh"
+#include "mata/parser/re2parser.hh"
+
+using namespace mata::nft;
+using namespace mata::parser;
+
+/////////////////////////////
+// Profiling revert and trim
+/////////////////////////////
+
+TEST_CASE("mata::nft::fragile_revert() speed, simple ", "[.profiling]") {
+    Nft B;
+    FILL_WITH_AUT_D(B);
+    for (int i = 0; i < 300000; i++) {
+        B = fragile_revert(B);
+    }
+}
+
+TEST_CASE("mata::nft::simple_revert() speed, simple ", "[.profiling]") {
+    Nft B;
+    FILL_WITH_AUT_B(B);
+    for (int i = 0; i < 300000; i++) {
+        B = simple_revert(B);
+    }
+}
+
+TEST_CASE("mata::nft::simple_revert() speed, harder", "[.profiling]") {
+    Nft B;
+//this gives an interesting test case if the parser is not trimming and reducing
+    mata::parser::create_nfa(&B, "((.*){10})*");
+    for (int i = 0; i < 200; i++) {
+        B = simple_revert(B);
+    }
+}
+
+TEST_CASE("mata::nft::fragile_revert() speed, harder", "[.profiling]") {
+    Nft B;
+//this gives an interesting test case if the parser is not trimming and reducing
+    create_nfa(&B, "((.*){10})*");
+    for (int i = 0; i < 200; i++) {
+        B = fragile_revert(B);
+    }
+}
+
+TEST_CASE("mata::nft::somewhat_simple_revert() speed, harder", "[.profiling]") {
+    Nft B;
+//this gives an interesting test case if the parser is not trimming and reducing
+    create_nfa(&B, "((.*){10})*");
+//FILL_WITH_AUT_C(B);
+    for (int i = 0; i < 200; i++) {
+        B = somewhat_simple_revert(B);
+    }
+}
+
+TEST_CASE("mata::nft::trim_inplace() speed, simple", "[.profiling]") {
+    Nft A, B;
+//this gives an interesting test case if the parser is not trimming and reducing
+    FILL_WITH_AUT_B(B);
+    for (int i = 0; i < 300000; i++) {
+        A = B;
+        A.trim();
+    }
+}
+
+TEST_CASE("mata::nft::trim_inplace() speed, harder", "[.profiling]") {
+    Nft A, B;
+//this gives an interesting test case if the parser is not trimming and reducing
+    create_nfa(&B, "((.*){10})*");
+    for (int i = 0; i < 200; i++) {
+        A = B;
+        A.trim();
+    }
+}
+
+//////////////////////////////
+// Profiling get_used_symbols
+//////////////////////////////
+
+TEST_CASE("mata::nft::get_used_symbols speed, harder", "[.profiling]") {
+    Nft A;
+    create_nfa(&A, "((.*){10})*");
+    for (int i = 0; i < 2000000; i++) {
+        A.delta.get_used_symbols();
+    }
+}
+
+TEST_CASE("mata::nft::get_used_symbols_bv speed, harder", "[.profiling]") {
+    Nft A;
+    create_nfa(&A, "((.*){10})*");
+    for (int i = 0; i < 2000000; i++) {
+        A.delta.get_used_symbols_bv();
+    }
+}
+
+TEST_CASE("mata::nft::get_used_symbols_vec speed, harder", "[.profiling]") {
+    Nft A;
+    create_nfa(&A, "((.*){10})*");
+    for (int i = 0; i < 2000000; i++) {
+        A.delta.get_used_symbols_vec();
+    }
+}
+
+TEST_CASE("mata::nft::get_used_symbols_set speed, harder", "[.profiling]") {
+    Nft A;
+    create_nfa(&A, "((.*){10})*");
+    for (int i = 0; i < 2000000; i++) {
+        A.delta.get_used_symbols_set();
+    }
+}
+
+TEST_CASE("mata::nft::get_used_symbols_sps speed, harder", "[.profiling]") {
+    Nft A;
+    create_nfa(&A, "((.*){10})*");
+    for (int i = 0; i < 2000000; i++) {
+        A.delta.get_used_symbols_sps();
+    }
+}
+
+/////////////////////////////
+/////////////////////////////
diff --git a/tests/nft/nft.cc b/tests/nft/nft.cc
new file mode 100644
index 000000000..239c7b498
--- /dev/null
+++ b/tests/nft/nft.cc
@@ -0,0 +1,4934 @@
+// TODO: some header
+
+#include <unordered_set>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+#include "utils.hh"
+
+#include "mata/utils/sparse-set.hh"
+#include "mata/nft/delta.hh"
+#include "mata/nft/nft.hh"
+#include "mata/nft/strings.hh"
+#include "mata/nft/builder.hh"
+#include "mata/nft/plumbing.hh"
+#include "mata/nft/algorithms.hh"
+#include "mata/nfa/nfa.hh"
+#include "mata/parser/re2parser.hh"
+
+using namespace mata;
+using namespace mata::nft::algorithms;
+using namespace mata::nft;
+using namespace mata::strings;
+using namespace mata::nft::plumbing;
+using namespace mata::utils;
+using namespace mata::parser;
+using Symbol = mata::Symbol;
+using Word = mata::Word;
+using IntAlphabet = mata::IntAlphabet;
+using OnTheFlyAlphabet = mata::OnTheFlyAlphabet;
+
+TEST_CASE("mata::nft::Nft()") {
+    Nft nft{};
+    nft.levels.resize(3);
+    nft.num_of_levels = 5;
+    CHECK(nft.num_of_levels == 5);
+    CHECK(nft.levels.size() == 3);
+    nft.levels[0] = 0;
+    nft.levels[1] = 3;
+    nft.levels[2] = 1;
+    CHECK(nft.levels[2] == 1);
+    CHECK(nft.levels == std::vector<Level>{ 0, 3, 1 });
+}
+
+TEST_CASE("mata::nft::size()") {
+    Nft nft{};
+    CHECK(nft.num_of_states() == 0);
+
+    nft.add_state(3);
+    CHECK(nft.num_of_states() == 4);
+
+    nft.clear();
+    nft.add_state();
+    CHECK(nft.num_of_states() == 1);
+
+    nft.clear();
+    FILL_WITH_AUT_A(nft);
+    CHECK(nft.num_of_states() == 11);
+
+    nft.clear();
+    FILL_WITH_AUT_B(nft);
+    CHECK(nft.num_of_states() == 15);
+
+    nft = Nft{ 0, {}, {} };
+    CHECK(nft.num_of_states() == 0);
+}
+
+TEST_CASE("mata::nft::Trans::operator<<") {
+    Transition trans(1, 2, 3);
+    REQUIRE(std::to_string(trans) == "(1, 2, 3)");
+}
+
+TEST_CASE("mata::nft::create_alphabet()") {
+    Nft a{1};
+    a.delta.add(0, 'a', 0);
+
+    Nft b{1};
+    b.delta.add(0, 'b', 0);
+    b.delta.add(0, 'a', 0);
+    Nft c{1};
+    b.delta.add(0, 'c', 0);
+
+    auto alphabet{ create_alphabet(a, b, c) };
+
+    auto symbols{alphabet.get_alphabet_symbols() };
+    CHECK(symbols == mata::utils::OrdVector<Symbol>{ 'c', 'b', 'a' });
+
+    // create_alphabet(1, 3, 4); // Will not compile: '1', '3', '4' are not of the required type.
+    // create_alphabet(a, b, 4); // Will not compile: '4' is not of the required type.
+}
+
+TEST_CASE("mata::nft::Nft::delta.add()/delta.contains()")
+{ // {{{
+    Nft a(3);
+
+    SECTION("Empty automata have now transitions")
+    {
+        REQUIRE(!a.delta.contains(1, 'a', 1));
+    }
+
+    SECTION("If I add a transition, it is in the automaton")
+    {
+        a.delta.add(1, 'a', 1);
+
+        REQUIRE(a.delta.contains(1, 'a', 1));
+    }
+
+    SECTION("If I add a transition, only it is added")
+    {
+        a.delta.add(1, 'a', 1);
+
+        REQUIRE(a.delta.contains(1, 'a', 1));
+        REQUIRE(!a.delta.contains(1, 'a', 2));
+        REQUIRE(!a.delta.contains(1, 'b', 2));
+        REQUIRE(!a.delta.contains(2, 'a', 1));
+    }
+
+    SECTION("Adding multiple transitions")
+    {
+        a.delta.add(2, 'b', {2,1,0});
+        REQUIRE(a.delta.contains(2, 'b', 0));
+        REQUIRE(a.delta.contains(2, 'b', 1));
+        REQUIRE(a.delta.contains(2, 'b', 2));
+        REQUIRE(!a.delta.contains(0, 'b', 0));
+
+        a.delta.add(0, 'b', StateSet({0}));
+        REQUIRE(a.delta.contains(0, 'b', 0));
+    }
+
+    SECTION("Iterating over transitions") {
+        Transition t1{ 0, 0, 0};
+        Transition t2{ 0, 1, 0};
+        Transition t3{ 1, 1, 1};
+        Transition t4{ 2, 2, 2};
+        a.delta.add(t1);
+        a.delta.add(t2);
+        a.delta.add(t3);
+        a.delta.add(t4);
+        a.delta.add(t3);
+        size_t transitions_cnt{ 0 };
+        std::vector<Transition> expected_transitions{ t1, t2, t3, t4 };
+        std::vector<Transition> iterated_transitions{};
+        const Delta::Transitions transitions{ a.delta.transitions() };
+        const Delta::Transitions::const_iterator transitions_end{ transitions.end() };
+        for (Delta::Transitions::const_iterator trans_it{ transitions.begin()}; trans_it != transitions_end; ++trans_it) {
+            iterated_transitions.push_back(*trans_it);
+            ++transitions_cnt;
+        }
+        CHECK(transitions_cnt == 4);
+        CHECK(expected_transitions == iterated_transitions);
+
+        transitions_cnt = 0;
+        iterated_transitions.clear();
+        for (const Transition& trans: a.delta.transitions()) {
+            iterated_transitions.push_back(trans);
+            ++transitions_cnt;
+        }
+        CHECK(transitions_cnt == 4);
+        CHECK(expected_transitions == iterated_transitions);
+    }
+
+} // }}}
+
+TEST_CASE("mata::nft::Delta.transform/append")
+{ // {{{
+    Nft a(3);
+    a.delta.add(1, 'a', 1);
+    a.delta.add(2, 'b', {2,1,0});
+
+    SECTION("transform")
+    {
+        auto upd_fnc = [&](State st) {
+            return st + 5;
+        };
+        std::vector<StatePost> state_posts = a.delta.renumber_targets(upd_fnc);
+        a.delta.append(state_posts);
+
+        REQUIRE(a.delta.contains(4, 'a', 6));
+        REQUIRE(a.delta.contains(5, 'b', 7));
+        REQUIRE(a.delta.contains(5, 'b', 5));
+        REQUIRE(a.delta.contains(5, 'b', 6));
+    }
+
+} // }}}
+
+TEST_CASE("mata::nft::is_lang_empty()")
+{ // {{{
+    Nft aut(14);
+    Run cex;
+
+    SECTION("An empty automaton has an empty language")
+    {
+        REQUIRE(aut.is_lang_empty());
+    }
+
+    SECTION("An automaton with a state that is both initial and final does not have an empty language")
+    {
+        aut.initial = {1, 2};
+        aut.final = {2, 3};
+
+        bool is_empty = aut.is_lang_empty(&cex);
+        REQUIRE(!is_empty);
+    }
+
+    SECTION("More complicated automaton")
+    {
+        aut.initial = {1, 2};
+        aut.delta.add(1, 'a', 2);
+        aut.delta.add(1, 'a', 3);
+        aut.delta.add(1, 'b', 4);
+        aut.delta.add(2, 'a', 2);
+        aut.delta.add(2, 'a', 3);
+        aut.delta.add(2, 'b', 4);
+        aut.delta.add(3, 'b', 4);
+        aut.delta.add(3, 'c', 7);
+        aut.delta.add(3, 'b', 2);
+        aut.delta.add(7, 'a', 8);
+
+        SECTION("with final states")
+        {
+            aut.final = {7};
+            REQUIRE(!aut.is_lang_empty());
+        }
+
+        SECTION("without final states")
+        {
+            REQUIRE(aut.is_lang_empty());
+        }
+
+        SECTION("another complicated automaton")
+        {
+            FILL_WITH_AUT_A(aut);
+
+            REQUIRE(!aut.is_lang_empty());
+        }
+
+        SECTION("a complicated automaton with unreachable final states")
+        {
+            FILL_WITH_AUT_A(aut);
+            aut.final = {13};
+
+            REQUIRE(aut.is_lang_empty());
+        }
+    }
+
+    SECTION("An automaton with a state that is both initial and final does not have an empty language")
+    {
+        aut.initial = {1, 2};
+        aut.final = {2, 3};
+
+        bool is_empty = aut.is_lang_empty(&cex);
+        REQUIRE(!is_empty);
+
+        // check the counterexample
+        REQUIRE(cex.path.size() == 1);
+        REQUIRE(cex.path[0] == 2);
+    }
+
+    SECTION("Counterexample of an automaton with non-empty language")
+    {
+        aut.initial = {1, 2};
+        aut.final = {8, 9};
+        aut.delta.add(1, 'c', 2);
+        aut.delta.add(2, 'a', 4);
+        aut.delta.add(2, 'c', 1);
+        aut.delta.add(2, 'c', 3);
+        aut.delta.add(3, 'e', 5);
+        aut.delta.add(4, 'c', 8);
+
+        bool is_empty = aut.is_lang_empty(&cex);
+        REQUIRE(!is_empty);
+
+        // check the counterexample
+        REQUIRE(cex.path.size() == 3);
+        REQUIRE(cex.path[0] == 2);
+        REQUIRE(cex.path[1] == 4);
+        REQUIRE(cex.path[2] == 8);
+    }
+} // }}}
+
+TEST_CASE("mata::nft::is_acyclic")
+{ // {{{
+    Nft aut(14);
+
+    SECTION("An empty automaton is acyclic")
+    {
+        REQUIRE(aut.is_acyclic());
+    }
+
+    SECTION("An automaton with a state that is both initial and final is acyclic")
+    {
+        aut.initial = {1, 2};
+        aut.final = {2, 3};
+        REQUIRE(aut.is_acyclic());
+    }
+
+    SECTION("More complicated automaton")
+    {
+        aut.initial = {1, 2};
+        aut.delta.add(1, 'a', 2);
+        aut.delta.add(1, 'a', 3);
+        aut.delta.add(1, 'b', 4);
+        aut.delta.add(2, 'a', 3);
+        aut.delta.add(2, 'b', 4);
+        aut.delta.add(3, 'b', 4);
+        aut.delta.add(3, 'c', 7);
+        aut.delta.add(7, 'a', 8);
+
+        SECTION("without final states")
+        {
+            REQUIRE(aut.is_lang_empty());
+        }
+    }
+
+    SECTION("Cyclic automaton")
+    {
+        aut.initial = {1, 2};
+        aut.final = {8, 9};
+        aut.delta.add(1, 'c', 2);
+        aut.delta.add(2, 'a', 4);
+        aut.delta.add(2, 'c', 1);
+        aut.delta.add(2, 'c', 3);
+        aut.delta.add(3, 'e', 5);
+        aut.delta.add(4, 'c', 8);
+        REQUIRE(!aut.is_acyclic());
+    }
+
+    SECTION("Automaton with self-loops")
+    {
+        Nft aut(2);
+        aut.initial = {0};
+        aut.final = {1};
+        aut.delta.add(0, 'c', 1);
+        aut.delta.add(1, 'a', 1);
+        REQUIRE(!aut.is_acyclic());
+    }
+} // }}}
+
+TEST_CASE("mata::nft::get_word_for_path()")
+{ // {{{
+    Nft aut(5);
+    Run path;
+    Word word;
+
+    SECTION("empty word")
+    {
+        path = { };
+
+        auto word_bool_pair = aut.get_word_for_path(path);
+        REQUIRE(word_bool_pair.second);
+        REQUIRE(word_bool_pair.first.word.empty());
+    }
+
+    SECTION("empty word 2")
+    {
+        aut.initial = {1};
+        path.path = {1};
+
+        auto word_bool_pair = aut.get_word_for_path(path);
+        REQUIRE(word_bool_pair.second);
+        REQUIRE(word_bool_pair.first.word.empty());
+    }
+
+    SECTION("nonempty word")
+    {
+        aut.initial = {1};
+        aut.delta.add(1, 'c', 2);
+        aut.delta.add(2, 'a', 4);
+        aut.delta.add(2, 'c', 1);
+        aut.delta.add(2, 'b', 3);
+
+        path.path = {1,2,3};
+
+        auto word_bool_pair = aut.get_word_for_path(path);
+        REQUIRE(word_bool_pair.second);
+        REQUIRE(word_bool_pair.first.word == Word({'c', 'b'}));
+    }
+
+    SECTION("longer word")
+    {
+        aut.initial = {1};
+        aut.delta.add(1, 'a', 2);
+        aut.delta.add(1, 'c', 2);
+        aut.delta.add(2, 'a', 4);
+        aut.delta.add(2, 'c', 1);
+        aut.delta.add(2, 'b', 3);
+        aut.delta.add(3, 'd', 2);
+
+        path.path = {1,2,3,2,4};
+
+        auto word_bool_pair = aut.get_word_for_path(path);
+        std::set<Word> possible({
+            Word({'c', 'b', 'd', 'a'}),
+            Word({'a', 'b', 'd', 'a'})});
+        REQUIRE(word_bool_pair.second);
+        REQUIRE(haskey(possible, word_bool_pair.first.word));
+    }
+
+    SECTION("invalid path")
+    {
+        aut.initial = {1};
+        aut.delta.add(1, 'a', 2);
+        aut.delta.add(1, 'c', 2);
+        aut.delta.add(2, 'a', 4);
+        aut.delta.add(2, 'c', 1);
+        aut.delta.add(2, 'b', 3);
+        aut.delta.add(3, 'd', 2);
+
+        path.path = {1,2,3,1,2};
+
+        auto word_bool_pair = aut.get_word_for_path(path);
+        REQUIRE(!word_bool_pair.second);
+    }
+}
+
+
+TEST_CASE("mata::nft::is_lang_empty_cex()")
+{
+    Nft aut(10);
+    Run cex;
+
+    SECTION("Counterexample of an automaton with non-empty language")
+    {
+        aut.initial = {1, 2};
+        aut.final = {8, 9};
+        aut.delta.add(1, 'c', 2);
+        aut.delta.add(2, 'a', 4);
+        aut.delta.add(2, 'c', 1);
+        aut.delta.add(2, 'c', 3);
+        aut.delta.add(3, 'e', 5);
+        aut.delta.add(4, 'c', 8);
+
+        bool is_empty = aut.is_lang_empty(&cex);
+        REQUIRE(!is_empty);
+
+        // check the counterexample
+        REQUIRE(cex.word.size() == 2);
+        REQUIRE(cex.word[0] == 'a');
+        REQUIRE(cex.word[1] == 'c');
+    }
+}
+
+
+TEST_CASE("mata::nft::determinize()")
+{
+    Nft aut(3);
+    Nft result;
+    std::unordered_map<StateSet, State> subset_map;
+
+    SECTION("empty automaton")
+    {
+        result = determinize(aut);
+
+        REQUIRE(result.final.empty());
+        REQUIRE(result.delta.empty());
+        CHECK(result.is_lang_empty());
+    }
+
+    SECTION("simple automaton 1")
+    {
+        aut.initial = {1 };
+        aut.final = {1 };
+        result = determinize(aut, &subset_map);
+
+        REQUIRE(result.initial[subset_map[{1}]]);
+        REQUIRE(result.final[subset_map[{1}]]);
+        REQUIRE(result.delta.empty());
+    }
+
+    SECTION("simple automaton 2")
+    {
+        aut.initial = {1 };
+        aut.final = {2 };
+        aut.delta.add(1, 'a', 2);
+        result = determinize(aut, &subset_map);
+
+        REQUIRE(result.initial[subset_map[{1}]]);
+        REQUIRE(result.final[subset_map[{2}]]);
+        REQUIRE(result.delta.contains(subset_map[{1}], 'a', subset_map[{2}]));
+    }
+
+    SECTION("This broke Delta when delta[q] could cause re-allocation of post")
+    {
+        Nft x{};
+        x.initial.insert(0);
+        x.final.insert(4);
+        x.delta.add(0, 1, 3);
+        x.delta.add(3, 1, 3);
+        x.delta.add(3, 2, 3);
+        x.delta.add(3, 0, 1);
+        x.delta.add(1, 1, 1);
+        x.delta.add(1, 2, 1);
+        x.delta.add(1, 0, 2);
+        x.delta.add(2, 0, 2);
+        x.delta.add(2, 1, 2);
+        x.delta.add(2, 2, 2);
+        x.delta.add(2, 0, 4);
+        OnTheFlyAlphabet alphabet{};
+        auto complement_result{determinize(x)};
+    }
+} // }}}
+
+TEST_CASE("mata::nft::minimize() for profiling", "[.profiling],[minimize]") {
+    Nft aut(4);
+    Nft result;
+    std::unordered_map<StateSet, State> subset_map;
+
+    aut.initial.insert(0);
+    aut.final.insert(3);
+    aut.delta.add(0, 46, 0);
+    aut.delta.add(0, 47, 0);
+    aut.delta.add(0, 58, 0);
+    aut.delta.add(0, 58, 1);
+    aut.delta.add(0, 64, 0);
+    aut.delta.add(0, 64, 0);
+    aut.delta.add(0, 82, 0);
+    aut.delta.add(0, 92, 0);
+    aut.delta.add(0, 98, 0);
+    aut.delta.add(0, 100, 0);
+    aut.delta.add(0, 103, 0);
+    aut.delta.add(0, 109, 0);
+    aut.delta.add(0, 110, 0);
+    aut.delta.add(0, 111, 0);
+    aut.delta.add(0, 114, 0);
+    aut.delta.add(1, 47, 2);
+    aut.delta.add(2, 47, 3);
+    aut.delta.add(3, 46, 3);
+    aut.delta.add(3, 47, 3);
+    aut.delta.add(3, 58, 3);
+    aut.delta.add(3, 64, 3);
+    aut.delta.add(3, 82, 3);
+    aut.delta.add(3, 92, 3);
+    aut.delta.add(3, 98, 3);
+    aut.delta.add(3, 100, 3);
+    aut.delta.add(3, 103, 3);
+    aut.delta.add(3, 109, 3);
+    aut.delta.add(3, 110, 3);
+    aut.delta.add(3, 111, 3);
+    aut.delta.add(3, 114, 3);
+    minimize(&result, aut);
+}
+
+TEST_CASE("mata::nft::construct() correct calls")
+{ // {{{
+    Nft aut(10);
+    mata::parser::ParsedSection parsec;
+    OnTheFlyAlphabet alphabet;
+
+    SECTION("construct an empty automaton")
+    {
+        parsec.type = nft::TYPE_NFT;
+
+        aut = builder::construct(parsec);
+
+        REQUIRE(aut.is_lang_empty());
+    }
+
+    SECTION("construct a simple non-empty automaton accepting the empty word")
+    {
+        parsec.type = nft::TYPE_NFT;
+        parsec.dict.insert({"Initial", {"q1"}});
+        parsec.dict.insert({"Final", {"q1"}});
+
+        aut = builder::construct(parsec);
+
+        REQUIRE(!aut.is_lang_empty());
+    }
+
+    SECTION("construct an automaton with more than one initial/final states")
+    {
+        parsec.type = nft::TYPE_NFT;
+        parsec.dict.insert({"Initial", {"q1", "q2"}});
+        parsec.dict.insert({"Final", {"q1", "q2", "q3"}});
+
+        aut = builder::construct(parsec);
+
+        REQUIRE(aut.initial.size() == 2);
+        REQUIRE(aut.final.size() == 3);
+    }
+
+    SECTION("construct a simple non-empty automaton accepting only the word 'a'")
+    {
+        parsec.type = nft::TYPE_NFT;
+        parsec.dict.insert({"Initial", {"q1"}});
+        parsec.dict.insert({"Final", {"q2"}});
+        parsec.body = { {"q1", "a", "q2"} };
+
+        aut = builder::construct(parsec, &alphabet);
+
+        Run cex;
+        REQUIRE(!aut.is_lang_empty(&cex));
+        auto word_bool_pair = aut.get_word_for_path(cex);
+        REQUIRE(word_bool_pair.second);
+        REQUIRE(word_bool_pair.first.word == encode_word(&alphabet, { "a"}).word);
+
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "a"})));
+    }
+
+    SECTION("construct a more complicated non-empty automaton")
+    {
+        parsec.type = nft::TYPE_NFT;
+        parsec.dict.insert({"Initial", {"q1", "q3"}});
+        parsec.dict.insert({"Final", {"q5"}});
+        parsec.body.push_back({"q1", "a", "q3"});
+        parsec.body.push_back({"q1", "a", "q10"});
+        parsec.body.push_back({"q1", "b", "q7"});
+        parsec.body.push_back({"q3", "a", "q7"});
+        parsec.body.push_back({"q3", "b", "q9"});
+        parsec.body.push_back({"q9", "a", "q9"});
+        parsec.body.push_back({"q7", "b", "q1"});
+        parsec.body.push_back({"q7", "a", "q3"});
+        parsec.body.push_back({"q7", "c", "q3"});
+        parsec.body.push_back({"q10", "a", "q7"});
+        parsec.body.push_back({"q10", "b", "q7"});
+        parsec.body.push_back({"q10", "c", "q7"});
+        parsec.body.push_back({"q7", "a", "q5"});
+        parsec.body.push_back({"q5", "a", "q5"});
+        parsec.body.push_back({"q5", "c", "q9"});
+
+        aut = builder::construct(parsec, &alphabet);
+
+        // some samples
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "b", "a"})));
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "a", "c", "a", "a"})));
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet,
+                                            {"a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a"})));
+        // some wrong samples
+        REQUIRE(!aut.is_in_lang(encode_word(&alphabet, { "b", "c"})));
+        REQUIRE(!aut.is_in_lang(encode_word(&alphabet, { "a", "c", "c", "a"})));
+        REQUIRE(!aut.is_in_lang(encode_word(&alphabet, { "b", "a", "c", "b"})));
+    }
+} // }}}
+
+TEST_CASE("mata::nft::construct() invalid calls")
+{ // {{{
+    Nft aut;
+    mata::parser::ParsedSection parsec;
+
+    SECTION("construct() call with invalid ParsedSection object")
+    {
+        parsec.type = "FA";
+
+        CHECK_THROWS_WITH(builder::construct(parsec),
+                          Catch::Matchers::ContainsSubstring("expecting type"));
+    }
+
+    SECTION("construct() call with an epsilon transition")
+    {
+        parsec.type = nft::TYPE_NFT;
+        parsec.body = { {"q1", "q2"} };
+
+        CHECK_THROWS_WITH(builder::construct(parsec), Catch::Matchers::ContainsSubstring("Epsilon transition"));
+    }
+
+    SECTION("construct() call with a nonsense transition")
+    {
+        parsec.type = nft::TYPE_NFT;
+        parsec.body = { {"q1", "a", "q2", "q3"} };
+
+        CHECK_THROWS_WITH(plumbing::construct(&aut, parsec), Catch::Matchers::ContainsSubstring("Invalid transition"));
+    }
+} // }}}
+
+TEST_CASE("mata::nft::construct() from IntermediateAut correct calls")
+{ // {{{
+    Nft aut;
+    mata::IntermediateAut inter_aut;
+    OnTheFlyAlphabet alphabet;
+
+    SECTION("construct an empty automaton")
+    {
+        inter_aut.automaton_type = mata::IntermediateAut::AutomatonType::NFT;
+        REQUIRE(aut.is_lang_empty());
+        aut = builder::construct(inter_aut);
+        REQUIRE(aut.is_lang_empty());
+    }
+
+    SECTION("construct a simple non-empty automaton accepting the empty word from intermediate automaton")
+    {
+        std::string file =
+                "@NFT-explicit\n"
+                "%States-enum p q r\n"
+                "%Alphabet-auto\n"
+                "%Initial p | q\n"
+                "%Final p | q\n";
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+
+        aut = builder::construct(inter_aut);
+
+        REQUIRE(!aut.is_lang_empty());
+    }
+
+    SECTION("construct an automaton with more than one initial/final states from intermediate automaton")
+    {
+        std::string file =
+                "@NFT-explicit\n"
+                "%States-enum p q 3\n"
+                "%Alphabet-auto\n"
+                "%Initial p | q\n"
+                "%Final p | q | r\n";
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+
+        plumbing::construct(&aut, inter_aut);
+
+        REQUIRE(aut.initial.size() == 2);
+        REQUIRE(aut.final.size() == 3);
+    }
+
+    SECTION("construct an automaton with implicit operator completion one initial/final states from intermediate automaton")
+    {
+        std::string file =
+                "@NFT-explicit\n"
+                "%States-enum p q r\n"
+                "%Alphabet-auto\n"
+                "%Initial p q\n"
+                "%Final p q r\n";
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+
+        plumbing::construct(&aut, inter_aut);
+
+        REQUIRE(aut.initial.size() == 2);
+        REQUIRE(aut.final.size() == 3);
+    }
+
+    SECTION("construct an automaton with implicit operator completion one initial/final states from intermediate automaton")
+    {
+        std::string file =
+                "@NFT-explicit\n"
+                "%States-enum p q r m n\n"
+                "%Alphabet-auto\n"
+                "%Initial p q r\n"
+                "%Final p q m n\n";
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+
+        plumbing::construct(&aut, inter_aut);
+
+        REQUIRE(aut.initial.size() == 3);
+        REQUIRE(aut.final.size() == 4);
+    }
+
+    SECTION("construct a simple non-empty automaton accepting only the word 'a' from intermediate automaton")
+    {
+        std::string file =
+                "@NFT-explicit\n"
+                "%States-enum p q 3\n"
+                "%Alphabet-auto\n"
+                "%Initial q1\n"
+                "%Final q2\n"
+                "q1 a q2\n";
+
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+        plumbing::construct(&aut, inter_aut, &alphabet);
+
+        Run cex;
+        REQUIRE(!aut.is_lang_empty(&cex));
+        auto word_bool_pair = aut.get_word_for_path(cex);
+        REQUIRE(word_bool_pair.second);
+        REQUIRE(word_bool_pair.first.word == encode_word(&alphabet, { "a" }).word);
+
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "a" })));
+    }
+
+    SECTION("construct a more complicated non-empty automaton from intermediate automaton")
+    {
+        std::string file =
+                "@NFT-explicit\n"
+                "%States-enum p q 3\n"
+                "%Alphabet-auto\n"
+                "%Initial q1 | q3\n"
+                "%Final q5\n"
+                "q1 a q3\n"
+                "q1 a q10\n"
+                "q1 b q7\n"
+                "q3 a q7\n"
+                "q3 b q9\n"
+                "q9 a q9\n"
+                "q7 b q1\n"
+                "q7 a q3\n"
+                "q7 c q3\n"
+                "q10 a q7\n"
+                "q10 b q7\n"
+                "q10 c q7\n"
+                "q7 a q5\n"
+                "q5 c q9\n";
+
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+
+        plumbing::construct(&aut, inter_aut, &alphabet);
+
+        // some samples
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "b", "a"})));
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "a", "c", "a", "a"})));
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet,
+                                            {"a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a"})));
+        // some wrong samples
+        REQUIRE(!aut.is_in_lang(encode_word(&alphabet, { "b", "c"})));
+        REQUIRE(!aut.is_in_lang(encode_word(&alphabet, { "a", "c", "c", "a"})));
+        REQUIRE(!aut.is_in_lang(encode_word(&alphabet, { "b", "a", "c", "b"})));
+    }
+
+    SECTION("construct - final states from negation")
+    {
+        std::string file =
+                "@NFT-bits\n"
+                "%Alphabet-auto\n"
+                "%Initial q0 q8\n"
+                "%Final !q0 & !q1 & !q4 & !q5 & !q6\n"
+                "q0 a1 q1\n"
+                "q1 a2 q2\n"
+                "q2 a3 q3\n"
+                "q2 a4 q4\n"
+                "q3 a5 q5\n"
+                "q3 a6 q6\n"
+                "q5 a7 q7\n";
+
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+
+        plumbing::construct(&aut, inter_aut, &alphabet);
+        REQUIRE(aut.final.size() == 4);
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "a1", "a2"})));
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "a1", "a2", "a3"})));
+        REQUIRE(!aut.is_in_lang(encode_word(&alphabet, { "a1", "a2", "a3", "a4"})));
+        REQUIRE(aut.is_in_lang(encode_word(&alphabet, { "a1", "a2", "a3", "a5", "a7"})));
+    }
+
+    SECTION("construct - final states given as true")
+    {
+        std::string file =
+                "@NFT-bits\n"
+                "%Alphabet-auto\n"
+                "%Initial q0 q8\n"
+                "%Final \\true\n"
+                "q0 a1 q1\n"
+                "q1 a2 q2\n"
+                "q2 a3 q3\n"
+                "q2 a4 q4\n"
+                "q3 a5 q5\n"
+                "q3 a6 q6\n"
+                "q5 a7 q7\n";
+
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+
+        nft::builder::NameStateMap state_map;
+        plumbing::construct(&aut, inter_aut, &alphabet, &state_map);
+        CHECK(aut.final.size() == 9);
+        CHECK(aut.final[state_map.at("0")]);
+        CHECK(aut.final[state_map.at("1")]);
+        CHECK(aut.final[state_map.at("2")]);
+        CHECK(aut.final[state_map.at("3")]);
+        CHECK(aut.final[state_map.at("4")]);
+        CHECK(aut.final[state_map.at("5")]);
+        CHECK(aut.final[state_map.at("6")]);
+        CHECK(aut.final[state_map.at("7")]);
+        CHECK(aut.final[state_map.at("8")]);
+    }
+
+    SECTION("construct - final states given as false")
+    {
+        std::string file =
+                "@NFT-bits\n"
+                "%Alphabet-auto\n"
+                "%Initial q0 q8\n"
+                "%Final \\false\n"
+                "q0 a1 q1\n"
+                "q1 a2 q2\n"
+                "q2 a3 q3\n"
+                "q2 a4 q4\n"
+                "q3 a5 q5\n"
+                "q3 a6 q6\n"
+                "q5 a7 q7\n";
+
+        const auto auts = mata::IntermediateAut::parse_from_mf(parse_mf(file));
+        inter_aut = auts[0];
+
+        nft::builder::NameStateMap state_map;
+        plumbing::construct(&aut, inter_aut, &alphabet, &state_map);
+        CHECK(aut.final.empty());
+    }
+} // }}}
+
+TEST_CASE("mata::nft::make_complete()")
+{ // {{{
+    Nft aut(11);
+
+    SECTION("empty automaton, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+
+        aut.make_complete(&alph, 0);
+
+        REQUIRE(aut.initial.empty());
+        REQUIRE(aut.final.empty());
+        REQUIRE(aut.delta.empty());
+    }
+
+    SECTION("empty automaton")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b" } };
+
+        aut.make_complete(&alph, 0);
+
+        REQUIRE(aut.initial.empty());
+        REQUIRE(aut.final.empty());
+        REQUIRE(aut.delta.contains(0, alph["a"], 0));
+        REQUIRE(aut.delta.contains(0, alph["b"], 0));
+    }
+
+    SECTION("non-empty automaton, empty alphabet")
+    {
+        OnTheFlyAlphabet alphabet{};
+
+        aut.initial = {1};
+
+        aut.make_complete(&alphabet, 0);
+
+        REQUIRE(aut.initial.size() == 1);
+        REQUIRE(*aut.initial.begin() == 1);
+        REQUIRE(aut.final.empty());
+        REQUIRE(aut.delta.empty());
+    }
+
+    SECTION("one-state automaton")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b" } };
+        constexpr State SINK = 10;
+
+        aut.initial = {1};
+
+        aut.make_complete(&alph, SINK);
+
+        REQUIRE(aut.initial.size() == 1);
+        REQUIRE(*aut.initial.begin() == 1);
+        REQUIRE(aut.final.empty());
+        REQUIRE(aut.delta.contains(1, alph["a"], SINK));
+        REQUIRE(aut.delta.contains(1, alph["b"], SINK));
+        REQUIRE(aut.delta.contains(SINK, alph["a"], SINK));
+        REQUIRE(aut.delta.contains(SINK, alph["b"], SINK));
+    }
+
+    SECTION("bigger automaton")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b", "c" } };
+        constexpr State SINK = 9;
+
+        aut.initial = {1, 2};
+        aut.final = {8};
+        aut.delta.add(1, alph["a"], 2);
+        aut.delta.add(2, alph["a"], 4);
+        aut.delta.add(2, alph["c"], 1);
+        aut.delta.add(2, alph["c"], 3);
+        aut.delta.add(3, alph["b"], 5);
+        aut.delta.add(4, alph["c"], 8);
+
+        aut.make_complete(&alph, SINK);
+
+        REQUIRE(aut.delta.contains(1, alph["a"], 2));
+        REQUIRE(aut.delta.contains(1, alph["b"], SINK));
+        REQUIRE(aut.delta.contains(1, alph["c"], SINK));
+        REQUIRE(aut.delta.contains(2, alph["a"], 4));
+        REQUIRE(aut.delta.contains(2, alph["c"], 1));
+        REQUIRE(aut.delta.contains(2, alph["c"], 3));
+        REQUIRE(aut.delta.contains(2, alph["b"], SINK));
+        REQUIRE(aut.delta.contains(3, alph["b"], 5));
+        REQUIRE(aut.delta.contains(3, alph["a"], SINK));
+        REQUIRE(aut.delta.contains(3, alph["c"], SINK));
+        REQUIRE(aut.delta.contains(4, alph["c"], 8));
+        REQUIRE(aut.delta.contains(4, alph["a"], SINK));
+        REQUIRE(aut.delta.contains(4, alph["b"], SINK));
+        REQUIRE(aut.delta.contains(5, alph["a"], SINK));
+        REQUIRE(aut.delta.contains(5, alph["b"], SINK));
+        REQUIRE(aut.delta.contains(5, alph["c"], SINK));
+        REQUIRE(aut.delta.contains(8, alph["a"], SINK));
+        REQUIRE(aut.delta.contains(8, alph["b"], SINK));
+        REQUIRE(aut.delta.contains(8, alph["c"], SINK));
+        REQUIRE(aut.delta.contains(SINK, alph["a"], SINK));
+        REQUIRE(aut.delta.contains(SINK, alph["b"], SINK));
+        REQUIRE(aut.delta.contains(SINK, alph["c"], SINK));
+    }
+} // }}}
+
+TEST_CASE("mata::nft::complement()")
+{ // {{{
+    Nft aut(3);
+    Nft cmpl;
+
+    SECTION("empty automaton, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+
+        cmpl = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "false"}});
+        Nft empty_string_nft{ nft::builder::create_sigma_star_nft(&alph) };
+        CHECK(are_equivalent(cmpl, empty_string_nft));
+    }
+
+    SECTION("empty automaton")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b" } };
+
+        cmpl = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "false"}});
+
+        REQUIRE(cmpl.is_in_lang({}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"] }, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["b"] }, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"], alph["a"]}, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"], alph["b"], alph["b"], alph["a"] }, {}}));
+
+        Nft sigma_star_nft{ nft::builder::create_sigma_star_nft(&alph) };
+        CHECK(are_equivalent(cmpl, sigma_star_nft));
+    }
+
+    SECTION("empty automaton accepting epsilon, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+        aut.initial = {1};
+        aut.final = {1};
+
+        cmpl = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "false"}});
+
+        CHECK(cmpl.is_lang_empty());
+    }
+
+    SECTION("empty automaton accepting epsilon")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b" } };
+        aut.initial = {1};
+        aut.final = {1};
+
+        cmpl = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "false"}});
+
+        REQUIRE(!cmpl.is_in_lang({}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"]}, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["b"]}, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"], alph["a"]}, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"], alph["b"], alph["b"], alph["a"]}, {}}));
+        REQUIRE(cmpl.initial.size() == 1);
+        REQUIRE(cmpl.final.size() == 1);
+        REQUIRE(cmpl.delta.num_of_transitions() == 4);
+    }
+
+    SECTION("non-empty automaton accepting a*b*")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b" } };
+        aut.initial = {1, 2};
+        aut.final = {1, 2};
+
+        aut.delta.add(1, alph["a"], 1);
+        aut.delta.add(1, alph["a"], 2);
+        aut.delta.add(2, alph["b"], 2);
+
+        cmpl = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "false"}});
+
+        REQUIRE(!cmpl.is_in_lang(Word{}));
+        REQUIRE(!cmpl.is_in_lang(Word{ alph["a"] }));
+        REQUIRE(!cmpl.is_in_lang(Word{ alph["b"] }));
+        REQUIRE(!cmpl.is_in_lang(Word{ alph["a"], alph["a"] }));
+        REQUIRE(cmpl.is_in_lang(Word{ alph["a"], alph["b"], alph["b"], alph["a"] }));
+        REQUIRE(!cmpl.is_in_lang(Word{ alph["a"], alph["a"], alph["b"], alph["b"] }));
+        REQUIRE(cmpl.is_in_lang(Word{ alph["b"], alph["a"], alph["a"], alph["a"] }));
+
+        REQUIRE(cmpl.initial.size() == 1);
+        REQUIRE(cmpl.final.size() == 1);
+        REQUIRE(cmpl.delta.num_of_transitions() == 6);
+    }
+
+    SECTION("empty automaton, empty alphabet, minimization")
+    {
+        OnTheFlyAlphabet alph{};
+
+        cmpl = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "true"}});
+        Nft empty_string_nft{ nft::builder::create_sigma_star_nft(&alph) };
+        CHECK(are_equivalent(empty_string_nft, cmpl));
+    }
+
+    SECTION("empty automaton, minimization")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b" } };
+
+        cmpl = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "true"}});
+
+        REQUIRE(cmpl.is_in_lang({}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"] }, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["b"] }, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"], alph["a"]}, {}}));
+        REQUIRE(cmpl.is_in_lang(Run{{ alph["a"], alph["b"], alph["b"], alph["a"] }, {}}));
+
+        Nft sigma_star_nft{ nft::builder::create_sigma_star_nft(&alph) };
+        CHECK(are_equivalent(sigma_star_nft, cmpl));
+    }
+
+    SECTION("minimization vs no minimization")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b" } };
+        aut.initial = {0, 1};
+        aut.final = {1, 2};
+
+        aut.delta.add(1, alph["b"], 1);
+        aut.delta.add(1, alph["a"], 2);
+        aut.delta.add(2, alph["b"], 2);
+        aut.delta.add(0, alph["a"], 1);
+        aut.delta.add(0, alph["a"], 2);
+
+        cmpl = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "false"}});
+
+        Nft cmpl_min = complement(aut, alph, {{"algorithm", "classical"},
+                                    {"minimize", "true"}});
+
+        CHECK(are_equivalent(cmpl, cmpl_min, &alph));
+        CHECK(cmpl_min.num_of_states() == 4);
+        CHECK(cmpl.num_of_states() == 5);
+    }
+
+} // }}}
+
+TEST_CASE("mata::nft::is_universal()")
+{ // {{{
+    Nft aut(6);
+    Run cex;
+    ParameterMap params;
+
+    const std::unordered_set<std::string> ALGORITHMS = {
+        "naive",
+        "antichains",
+    };
+
+    SECTION("empty automaton, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, params);
+
+            REQUIRE(!is_univ);
+        }
+    }
+
+    SECTION("empty automaton accepting epsilon, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+        aut.initial = {1};
+        aut.final = {1};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, &cex, params);
+
+            REQUIRE(is_univ);
+            REQUIRE(cex.word.empty());
+        }
+    }
+
+    SECTION("empty automaton accepting epsilon")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a" } };
+        aut.initial = {1};
+        aut.final = {1};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, &cex, params);
+
+            REQUIRE(!is_univ);
+            REQUIRE(((cex.word == Word{alph["a"]}) || (cex.word == Word{alph["b"]})));
+        }
+    }
+
+    SECTION("automaton for a*b*")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b" } };
+        aut.initial = {1, 2};
+        aut.final = {1, 2};
+
+        aut.delta.add(1, alph["a"], 1);
+        aut.delta.add(1, alph["a"], 2);
+        aut.delta.add(2, alph["b"], 2);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, params);
+
+            REQUIRE(!is_univ);
+        }
+    }
+
+    SECTION("automaton for a* + b*")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        aut.initial = {1, 2};
+        aut.final = {1, 2};
+
+        aut.delta.add(1, alph["a"], 1);
+        aut.delta.add(2, alph["b"], 2);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, params);
+
+            REQUIRE(!is_univ);
+        }
+    }
+
+    SECTION("automaton for (a + b)*")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        aut.initial = {1};
+        aut.final = {1};
+
+        aut.delta.add(1, alph["a"], 1);
+        aut.delta.add(1, alph["b"], 1);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, params);
+
+            REQUIRE(is_univ);
+        }
+    }
+
+    SECTION("automaton for eps + (a+b) + (a+b)(a+b)(a* + b*)")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        aut.initial = {1};
+        aut.final = {1, 2, 3, 4, 5};
+
+        aut.delta.add(1, alph["a"], 2);
+        aut.delta.add(1, alph["b"], 2);
+        aut.delta.add(2, alph["a"], 3);
+        aut.delta.add(2, alph["b"], 3);
+
+        aut.delta.add(3, alph["a"], 4);
+        aut.delta.add(4, alph["a"], 4);
+
+        aut.delta.add(3, alph["b"], 5);
+        aut.delta.add(5, alph["b"], 5);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, &cex, params);
+
+            REQUIRE(!is_univ);
+
+            REQUIRE(cex.word.size() == 4);
+            REQUIRE((cex.word[0] == alph["a"] || cex.word[0] == alph["b"]));
+            REQUIRE((cex.word[1] == alph["a"] || cex.word[1] == alph["b"]));
+            REQUIRE((cex.word[2] == alph["a"] || cex.word[2] == alph["b"]));
+            REQUIRE((cex.word[3] == alph["a"] || cex.word[3] == alph["b"]));
+            REQUIRE(cex.word[2] != cex.word[3]);
+        }
+    }
+
+    SECTION("automaton for epsilon + a(a + b)* + b(a + b)*")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        aut.initial = {1, 3};
+        aut.final = {1, 2, 4};
+
+        aut.delta.add(1, alph["a"], 2);
+        aut.delta.add(2, alph["a"], 2);
+        aut.delta.add(2, alph["b"], 2);
+        aut.delta.add(3, alph["b"], 4);
+        aut.delta.add(4, alph["a"], 4);
+        aut.delta.add(4, alph["b"], 4);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, &cex, params);
+
+            REQUIRE(is_univ);
+        }
+    }
+
+    SECTION("example from Abdulla et al. TACAS'10")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        aut.initial = {1, 2};
+        aut.final = {1, 2, 3};
+
+        aut.delta.add(1, alph["b"], 1);
+        aut.delta.add(1, alph["a"], 2);
+        aut.delta.add(1, alph["b"], 4);
+        aut.delta.add(2, alph["b"], 2);
+        aut.delta.add(2, alph["a"], 3);
+        aut.delta.add(3, alph["b"], 3);
+        aut.delta.add(3, alph["a"], 1);
+        aut.delta.add(4, alph["b"], 2);
+        aut.delta.add(4, alph["b"], 3);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, &cex, params);
+
+            REQUIRE(is_univ);
+        }
+    }
+
+    SECTION("subsumption-pruning in processed")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a" } };
+        aut.initial = {1, 2};
+        aut.final = {1};
+
+        aut.delta.add(1, alph["a"], 1);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_univ = aut.is_universal(alph, &cex, params);
+
+            REQUIRE(is_univ);
+        }
+    }
+
+    SECTION("wrong parameters 1")
+    {
+        OnTheFlyAlphabet alph{};
+
+        CHECK_THROWS_WITH(aut.is_universal(alph, params),
+            Catch::Matchers::ContainsSubstring("requires setting the \"algorithm\" key"));
+    }
+
+    SECTION("wrong parameters 2")
+    {
+        OnTheFlyAlphabet alph{};
+        params["algorithm"] = "foo";
+
+        CHECK_THROWS_WITH(aut.is_universal(alph, params),
+            Catch::Matchers::ContainsSubstring("received an unknown value"));
+    }
+} // }}}
+
+TEST_CASE("mata::nft::is_included()")
+{ // {{{
+    Nft smaller(10);
+    Nft bigger(16);
+    Run cex;
+    ParameterMap params;
+
+    const std::unordered_set<std::string> ALGORITHMS = {
+        "naive",
+        "antichains",
+    };
+
+    SECTION("{} <= {}, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_incl = is_included(smaller, bigger, &alph, params);
+            CHECK(is_incl);
+
+            is_incl = is_included(bigger, smaller, &alph, params);
+            CHECK(is_incl);
+        }
+    }
+
+    SECTION("{} <= {epsilon}, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+        bigger.initial = {1};
+        bigger.final = {1};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_incl = is_included(smaller, bigger, &cex, &alph, params);
+            CHECK(is_incl);
+
+            is_incl = is_included(bigger, smaller, &cex, &alph, params);
+            CHECK(!is_incl);
+        }
+    }
+
+    SECTION("{epsilon} <= {epsilon}, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+        smaller.initial = {1};
+        smaller.final = {1};
+        bigger.initial = {11};
+        bigger.final = {11};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_incl = is_included(smaller, bigger, &cex, &alph, params);
+            CHECK(is_incl);
+
+            is_incl = is_included(bigger, smaller, &cex, &alph, params);
+            CHECK(is_incl);
+        }
+    }
+
+    SECTION("{epsilon} !<= {}, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+        smaller.initial = {1};
+        smaller.final = {1};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_incl = is_included(smaller, bigger, &cex, &alph, params);
+
+            REQUIRE(!is_incl);
+            REQUIRE(cex.word.empty());
+
+            is_incl = is_included(bigger, smaller, &cex, &alph, params);
+            REQUIRE(cex.word.empty());
+            REQUIRE(is_incl);
+        }
+    }
+
+    SECTION("a* + b* <= (a+b)*")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        smaller.initial = {1, 2};
+        smaller.final = {1, 2};
+        smaller.delta.add(1, alph["a"], 1);
+        smaller.delta.add(2, alph["b"], 2);
+
+        bigger.initial = {11};
+        bigger.final = {11};
+        bigger.delta.add(11, alph["a"], 11);
+        bigger.delta.add(11, alph["b"], 11);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_incl = is_included(smaller, bigger, &alph, params);
+            REQUIRE(is_incl);
+
+            is_incl = is_included(bigger, smaller, &alph, params);
+            REQUIRE(!is_incl);
+        }
+    }
+
+    SECTION("(a+b)* !<= a* + b*")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        smaller.initial = {1};
+        smaller.final = {1};
+        smaller.delta.add(1, alph["a"], 1);
+        smaller.delta.add(1, alph["b"], 1);
+
+        bigger.initial = {11, 12};
+        bigger.final = {11, 12};
+        bigger.delta.add(11, alph["a"], 11);
+        bigger.delta.add(12, alph["b"], 12);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+
+            bool is_incl = is_included(smaller, bigger, &cex, &alph, params);
+
+            REQUIRE(!is_incl);
+            REQUIRE((
+                cex.word == Word{alph["a"], alph["b"]} ||
+                cex.word == Word{alph["b"], alph["a"]}));
+
+            is_incl = is_included(bigger, smaller, &cex, &alph, params);
+            REQUIRE(is_incl);
+            REQUIRE((
+                cex.word == Word{alph["a"], alph["b"]} ||
+                cex.word == Word{alph["b"], alph["a"]}));
+        }
+    }
+
+    SECTION("(a+b)* !<= eps + (a+b) + (a+b)(a+b)(a* + b*)")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        smaller.initial = {1};
+        smaller.final = {1};
+        smaller.delta.add(1, alph["a"], 1);
+        smaller.delta.add(1, alph["b"], 1);
+
+        bigger.initial = {11};
+        bigger.final = {11, 12, 13, 14, 15};
+
+        bigger.delta.add(11, alph["a"], 12);
+        bigger.delta.add(11, alph["b"], 12);
+        bigger.delta.add(12, alph["a"], 13);
+        bigger.delta.add(12, alph["b"], 13);
+
+        bigger.delta.add(13, alph["a"], 14);
+        bigger.delta.add(14, alph["a"], 14);
+
+        bigger.delta.add(13, alph["b"], 15);
+        bigger.delta.add(15, alph["b"], 15);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+            bool is_incl = is_included(smaller, bigger, &cex, &alph, params);
+            REQUIRE(!is_incl);
+
+            REQUIRE(cex.word.size() == 4);
+            REQUIRE((cex.word[0] == alph["a"] || cex.word[0] == alph["b"]));
+            REQUIRE((cex.word[1] == alph["a"] || cex.word[1] == alph["b"]));
+            REQUIRE((cex.word[2] == alph["a"] || cex.word[2] == alph["b"]));
+            REQUIRE((cex.word[3] == alph["a"] || cex.word[3] == alph["b"]));
+            REQUIRE(cex.word[2] != cex.word[3]);
+
+            is_incl = is_included(bigger, smaller, &cex, &alph, params);
+            REQUIRE(is_incl);
+
+            REQUIRE(cex.word.size() == 4);
+            REQUIRE((cex.word[0] == alph["a"] || cex.word[0] == alph["b"]));
+            REQUIRE((cex.word[1] == alph["a"] || cex.word[1] == alph["b"]));
+            REQUIRE((cex.word[2] == alph["a"] || cex.word[2] == alph["b"]));
+            REQUIRE((cex.word[3] == alph["a"] || cex.word[3] == alph["b"]));
+            REQUIRE(cex.word[2] != cex.word[3]);
+        }
+    }
+
+    SECTION("wrong parameters 1")
+    {
+        OnTheFlyAlphabet alph{};
+
+        CHECK_THROWS_WITH(is_included(smaller, bigger, &alph, params),
+            Catch::Matchers::ContainsSubstring("requires setting the \"algorithm\" key"));
+        CHECK_NOTHROW(is_included(smaller, bigger, &alph));
+    }
+
+    SECTION("wrong parameters 2")
+    {
+        OnTheFlyAlphabet alph{};
+        params["algorithm"] = "foo";
+
+        CHECK_THROWS_WITH(is_included(smaller, bigger, &alph, params),
+            Catch::Matchers::ContainsSubstring("received an unknown value"));
+        CHECK_NOTHROW(is_included(smaller, bigger, &alph));
+    }
+} // }}}
+
+TEST_CASE("mata::nft::are_equivalent")
+{
+    Nft smaller(10);
+    Nft bigger(16);
+    Word cex;
+    ParameterMap params;
+
+    const std::unordered_set<std::string> ALGORITHMS = {
+            "naive",
+            "antichains",
+    };
+
+    SECTION("{} == {}, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+
+            CHECK(are_equivalent(smaller, bigger, &alph, params));
+            CHECK(are_equivalent(smaller, bigger, params));
+            CHECK(are_equivalent(smaller, bigger));
+
+            CHECK(are_equivalent(bigger, smaller, &alph, params));
+            CHECK(are_equivalent(bigger, smaller, params));
+            CHECK(are_equivalent(bigger, smaller));
+        }
+    }
+
+    SECTION("{} == {epsilon}, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+        bigger.initial = {1};
+        bigger.final = {1};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+
+            CHECK(!are_equivalent(smaller, bigger, &alph, params));
+            CHECK(!are_equivalent(smaller, bigger, params));
+            CHECK(!are_equivalent(smaller, bigger));
+
+            CHECK(!are_equivalent(bigger, smaller, &alph, params));
+            CHECK(!are_equivalent(bigger, smaller, params));
+            CHECK(!are_equivalent(bigger, smaller));
+        }
+    }
+
+    SECTION("{epsilon} == {epsilon}, empty alphabet")
+    {
+        OnTheFlyAlphabet alph{};
+        smaller.initial = {1};
+        smaller.final = {1};
+        bigger.initial = {11};
+        bigger.final = {11};
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+
+            CHECK(are_equivalent(smaller, bigger, &alph, params));
+            CHECK(are_equivalent(smaller, bigger, params));
+            CHECK(are_equivalent(smaller, bigger));
+
+            CHECK(are_equivalent(bigger, smaller, &alph, params));
+            CHECK(are_equivalent(bigger, smaller, params));
+            CHECK(are_equivalent(bigger, smaller));
+        }
+    }
+
+    SECTION("a* + b* == (a+b)*")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        smaller.initial = {1, 2};
+        smaller.final = {1, 2};
+        smaller.delta.add(1, alph["a"], 1);
+        smaller.delta.add(2, alph["b"], 2);
+
+        bigger.initial = {11};
+        bigger.final = {11};
+        bigger.delta.add(11, alph["a"], 11);
+        bigger.delta.add(11, alph["b"], 11);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+
+            //TODO:what about we test the plumbing versions primarily?
+            // Debugging with the dispatcher is annoying.
+
+            CHECK(!are_equivalent(smaller, bigger, &alph, params));
+            CHECK(!are_equivalent(smaller, bigger, params));
+            CHECK(!are_equivalent(smaller, bigger));
+
+            CHECK(!are_equivalent(bigger, smaller, &alph, params));
+            CHECK(!are_equivalent(bigger, smaller, params));
+            CHECK(!are_equivalent(bigger, smaller));
+        }
+    }
+
+    SECTION("a* != (a|b)*, was throwing exception")
+    {
+        Nft aut;
+        mata::parser::create_nfa(&aut, "a*");
+        Nft aut2;
+        mata::parser::create_nfa(&aut2, "(a|b)*");
+        CHECK(!are_equivalent(aut, aut2));
+    }
+
+    SECTION("(a+b)* !<= eps + (a+b) + (a+b)(a+b)(a* + b*)")
+    {
+        OnTheFlyAlphabet alph{ std::vector<std::string>{ "a", "b"} };
+        smaller.initial = {1};
+        smaller.final = {1};
+        smaller.delta.add(1, alph["a"], 1);
+        smaller.delta.add(1, alph["b"], 1);
+
+        bigger.initial = {11};
+        bigger.final = {11, 12, 13, 14, 15};
+
+        bigger.delta.add(11, alph["a"], 12);
+        bigger.delta.add(11, alph["b"], 12);
+        bigger.delta.add(12, alph["a"], 13);
+        bigger.delta.add(12, alph["b"], 13);
+
+        bigger.delta.add(13, alph["a"], 14);
+        bigger.delta.add(14, alph["a"], 14);
+
+        bigger.delta.add(13, alph["b"], 15);
+        bigger.delta.add(15, alph["b"], 15);
+
+        for (const auto& algo : ALGORITHMS) {
+            params["algorithm"] = algo;
+
+            CHECK(!are_equivalent(smaller, bigger, &alph, params));
+            CHECK(!are_equivalent(smaller, bigger, params));
+            CHECK(!are_equivalent(smaller, bigger));
+
+            CHECK(!are_equivalent(bigger, smaller, &alph, params));
+            CHECK(!are_equivalent(bigger, smaller, params));
+            CHECK(!are_equivalent(bigger, smaller));
+        }
+    }
+
+    SECTION("wrong parameters 1")
+    {
+        OnTheFlyAlphabet alph{};
+
+        CHECK_THROWS_WITH(are_equivalent(smaller, bigger, &alph, params),
+                          Catch::Matchers::ContainsSubstring("requires setting the \"algorithm\" key"));
+        CHECK_THROWS_WITH(are_equivalent(smaller, bigger, params),
+                          Catch::Matchers::ContainsSubstring("requires setting the \"algorithm\" key"));
+        CHECK_NOTHROW(are_equivalent(smaller, bigger));
+    }
+
+    SECTION("wrong parameters 2")
+    {
+        OnTheFlyAlphabet alph{};
+        params["algorithm"] = "foo";
+
+        CHECK_THROWS_WITH(are_equivalent(smaller, bigger, &alph, params),
+                          Catch::Matchers::ContainsSubstring("received an unknown value"));
+        CHECK_THROWS_WITH(are_equivalent(smaller, bigger, params),
+                          Catch::Matchers::ContainsSubstring("received an unknown value"));
+        CHECK_NOTHROW(are_equivalent(smaller, bigger));
+    }
+}
+
+TEST_CASE("mata::nft::revert()")
+{ // {{{
+    Nft aut(9);
+
+    SECTION("empty automaton")
+    {
+        Nft result = revert(aut);
+
+        REQUIRE(result.delta.empty());
+        REQUIRE(result.initial.empty());
+        REQUIRE(result.final.empty());
+    }
+
+    SECTION("no-transition automaton")
+    {
+        aut.initial.insert(1);
+        aut.initial.insert(3);
+
+        aut.final.insert(2);
+        aut.final.insert(5);
+
+        Nft result = revert(aut);
+
+        REQUIRE(result.delta.empty());
+        REQUIRE(result.initial[2]);
+        REQUIRE(result.initial[5]);
+        REQUIRE(result.final[1]);
+        REQUIRE(result.final[3]);
+    }
+
+    SECTION("one-transition automaton")
+    {
+        aut.initial.insert(1);
+        aut.final.insert(2);
+        aut.delta.add(1, 'a', 2);
+
+        Nft result = revert(aut);
+
+        REQUIRE(result.initial[2]);
+        REQUIRE(result.final[1]);
+        REQUIRE(result.delta.contains(2, 'a', 1));
+        REQUIRE(result.delta.num_of_transitions() == aut.delta.num_of_transitions());
+    }
+
+    SECTION("bigger automaton")
+    {
+        aut.initial = {1, 2};
+        aut.delta.add(1, 'a', 2);
+        aut.delta.add(1, 'a', 3);
+        aut.delta.add(1, 'b', 4);
+        aut.delta.add(2, 'a', 2);
+        aut.delta.add(2, 'a', 3);
+        aut.delta.add(2, 'b', 4);
+        aut.delta.add(3, 'b', 4);
+        aut.delta.add(3, 'c', 7);
+        aut.delta.add(3, 'b', 2);
+        aut.delta.add(7, 'a', 8);
+        aut.final = {3};
+
+        Nft result = revert(aut);
+        //REQUIRE(result.final == StateSet({1, 2}));
+        REQUIRE(StateSet(result.final) == StateSet({1, 2}));
+        REQUIRE(result.delta.contains(2, 'a', 1));
+        REQUIRE(result.delta.contains(3, 'a', 1));
+        REQUIRE(result.delta.contains(4, 'b', 1));
+        REQUIRE(result.delta.contains(2, 'a', 2));
+        REQUIRE(result.delta.contains(3, 'a', 2));
+        REQUIRE(result.delta.contains(4, 'b', 2));
+        REQUIRE(result.delta.contains(4, 'b', 3));
+        REQUIRE(result.delta.contains(7, 'c', 3));
+        REQUIRE(result.delta.contains(2, 'b', 3));
+        REQUIRE(result.delta.contains(8, 'a', 7));
+        REQUIRE(StateSet(result.initial) == StateSet({3}));
+    }
+
+    SECTION("Automaton A") {
+        Nft nft{ 11 };
+        FILL_WITH_AUT_A(nft);
+        Nft res = revert(nft);
+        CHECK(res.initial[5]);
+        CHECK(res.final[1]);
+        CHECK(res.final[3]);
+        CHECK(res.delta.num_of_transitions() == 15);
+        CHECK(res.delta.contains(5, 'a', 5));
+        CHECK(res.delta.contains(5, 'a', 7));
+        CHECK(res.delta.contains(9, 'a', 9));
+        CHECK(res.delta.contains(9, 'c', 5));
+        CHECK(res.delta.contains(9, 'b', 3));
+        CHECK(res.delta.contains(7, 'a', 3));
+        CHECK(res.delta.contains(7, 'a', 10));
+        CHECK(res.delta.contains(7, 'b', 10));
+        CHECK(res.delta.contains(7, 'c', 10));
+        CHECK(res.delta.contains(7, 'b', 1));
+        CHECK(res.delta.contains(3, 'a', 7));
+        CHECK(res.delta.contains(3, 'c', 7));
+        CHECK(res.delta.contains(3, 'a', 1));
+        CHECK(res.delta.contains(1, 'b', 7));
+        CHECK(res.delta.contains(10, 'a', 1));
+    }
+
+    SECTION("Automaton B") {
+        Nft nft{ 15 };
+        FILL_WITH_AUT_B(nft);
+        Nft res = revert(nft);
+        CHECK(res.initial[2]);
+        CHECK(res.initial[12]);
+        CHECK(res.final[4]);
+        CHECK(res.delta.num_of_transitions() == 12);
+        CHECK(res.delta.contains(8, 'a', 4));
+        CHECK(res.delta.contains(8, 'c', 4));
+        CHECK(res.delta.contains(4, 'b', 8));
+        CHECK(res.delta.contains(6, 'b', 4));
+        CHECK(res.delta.contains(6, 'a', 4));
+        CHECK(res.delta.contains(2, 'a', 6));
+        CHECK(res.delta.contains(2, 'a', 0));
+        CHECK(res.delta.contains(2, 'b', 2));
+        CHECK(res.delta.contains(0, 'a', 2));
+        CHECK(res.delta.contains(12, 'c', 2));
+        CHECK(res.delta.contains(12, 'b', 14));
+        CHECK(res.delta.contains(14, 'a', 12));
+    }
+} // }}}
+
+
+TEST_CASE("mata::nft::Nft::is_deterministic()")
+{ // {{{
+    Nft aut('s'+1);
+
+    SECTION("(almost) empty automaton") {
+        // no initial states
+        REQUIRE(!aut.is_deterministic());
+
+        // add an initial state
+        aut.initial.insert('q');
+        REQUIRE(aut.is_deterministic());
+
+        // add the same initial state
+        aut.initial.insert('q');
+        REQUIRE(aut.is_deterministic());
+
+        // add another initial state
+        aut.initial.insert('r');
+        REQUIRE(!aut.is_deterministic());
+
+        // add a final state
+        aut.final.insert('q');
+        REQUIRE(!aut.is_deterministic());
+    }
+
+    SECTION("trivial automata") {
+        aut.initial.insert('q');
+        aut.delta.add('q', 'a', 'r');
+        REQUIRE(aut.is_deterministic());
+
+        // unreachable states
+        aut.delta.add('s', 'a', 'r');
+        REQUIRE(aut.is_deterministic());
+
+        // transitions over a different symbol
+        aut.delta.add('q', 'b', 'h');
+        REQUIRE(aut.is_deterministic());
+
+        // nondeterminism
+        aut.delta.add('q', 'a', 's');
+        REQUIRE(!aut.is_deterministic());
+    }
+
+    SECTION("larger automaton 1") {
+        FILL_WITH_AUT_A(aut);
+        REQUIRE(!aut.is_deterministic());
+    }
+
+    SECTION("larger automaton 2") {
+        FILL_WITH_AUT_B(aut);
+        REQUIRE(!aut.is_deterministic());
+    }
+} // }}}
+
+TEST_CASE("mata::nft::is_complete()")
+{ // {{{
+    Nft aut('q'+1);
+
+    SECTION("empty automaton")
+    {
+        OnTheFlyAlphabet alph{};
+
+        // is complete for the empty alphabet
+        REQUIRE(aut.is_complete(&alph));
+
+        alph.translate_symb("a1");
+        alph.translate_symb("a2");
+
+        // the empty automaton is complete even for a non-empty alphabet
+        REQUIRE(aut.is_complete(&alph));
+
+        // add a non-reachable state (the automaton should still be complete)
+        aut.delta.add('q', alph["a1"], 'q');
+        REQUIRE(aut.is_complete(&alph));
+    }
+
+    SECTION("small automaton")
+    {
+        OnTheFlyAlphabet alph{};
+
+        aut.initial.insert(4);
+        aut.delta.add(4, alph["a"], 8);
+        aut.delta.add(4, alph["c"], 8);
+        aut.delta.add(4, alph["a"], 6);
+        aut.delta.add(4, alph["b"], 6);
+        aut.delta.add(8, alph["b"], 4);
+        aut.delta.add(6, alph["a"], 2);
+        aut.delta.add(2, alph["b"], 2);
+        aut.delta.add(2, alph["a"], 0);
+        aut.delta.add(2, alph["c"], 12);
+        aut.delta.add(0, alph["a"], 2);
+        aut.delta.add(12, alph["a"], 14);
+        aut.delta.add(14, alph["b"], 12);
+        aut.final.insert({2, 12});
+
+        REQUIRE(!aut.is_complete(&alph));
+
+        aut.make_complete(&alph, 100);
+        REQUIRE(aut.is_complete(&alph));
+    }
+
+    SECTION("using a non-alphabet symbol")
+    {
+        OnTheFlyAlphabet alph{};
+
+        aut.initial.insert(4);
+        aut.delta.add(4, alph["a"], 8);
+        aut.delta.add(4, alph["c"], 8);
+        aut.delta.add(4, alph["a"], 6);
+        aut.delta.add(4, alph["b"], 6);
+        aut.delta.add(6, 100, 4);
+
+        CHECK_THROWS_WITH(aut.is_complete(&alph),
+            Catch::Matchers::ContainsSubstring("symbol that is not in the provided alphabet"));
+    }
+} // }}}
+
+TEST_CASE("mata::nft::is_prfx_in_lang()")
+{ // {{{
+    Nft aut('q'+1);
+
+    SECTION("empty automaton")
+    {
+        Run w;
+        w.word = {'a', 'b', 'd'};
+        REQUIRE(!aut.is_prfx_in_lang(w));
+
+        w.word = { };
+        REQUIRE(!aut.is_prfx_in_lang(w));
+    }
+
+    SECTION("automaton accepting only epsilon")
+    {
+        aut.initial.insert('q');
+        aut.final.insert('q');
+
+        Run w;
+        w.word = { };
+        REQUIRE(aut.is_prfx_in_lang(w));
+
+        w.word = {'a', 'b'};
+        REQUIRE(aut.is_prfx_in_lang(w));
+    }
+
+    SECTION("small automaton")
+    {
+        FILL_WITH_AUT_B(aut);
+
+        Run w;
+        w.word = {'b', 'a'};
+        REQUIRE(aut.is_prfx_in_lang(w));
+
+        w.word = { };
+        REQUIRE(!aut.is_prfx_in_lang(w));
+
+        w.word = {'c', 'b', 'a'};
+        REQUIRE(!aut.is_prfx_in_lang(w));
+
+        w.word = {'c', 'b', 'a', 'a'};
+        REQUIRE(aut.is_prfx_in_lang(w));
+
+        w.word = {'a', 'a'};
+        REQUIRE(aut.is_prfx_in_lang(w));
+
+        w.word = {'c', 'b', 'b', 'a', 'c', 'b'};
+        REQUIRE(aut.is_prfx_in_lang(w));
+
+        w.word = Word(100000, 'a');
+        REQUIRE(aut.is_prfx_in_lang(w));
+
+        w.word = Word(100000, 'b');
+        REQUIRE(!aut.is_prfx_in_lang(w));
+    }
+} // }}}
+
+TEST_CASE("mata::nft::fw-direct-simulation()")
+{ // {{{
+    Nft aut;
+
+    SECTION("empty automaton")
+    {
+        Simlib::Util::BinaryRelation result = compute_relation(aut);
+
+        REQUIRE(result.size() == 0);
+    }
+
+    aut.add_state(8);
+    SECTION("no-transition automaton")
+    {
+        aut.initial.insert(1);
+        aut.initial.insert(3);
+
+        aut.final.insert(2);
+        aut.final.insert(5);
+
+        Simlib::Util::BinaryRelation result = compute_relation(aut);
+        REQUIRE(result.get(1,3));
+        REQUIRE(result.get(2,5));
+        REQUIRE(!result.get(5,1));
+        REQUIRE(!result.get(2,3));
+    }
+
+    SECTION("small automaton")
+    {
+        aut.initial.insert(1);
+        aut.final.insert(2);
+        aut.delta.add(1, 'a', 4);
+        aut.delta.add(4, 'b', 5);
+        aut.delta.add(2, 'b', 5);
+        aut.delta.add(1, 'b', 4);
+
+        Simlib::Util::BinaryRelation result = compute_relation(aut);
+        REQUIRE(result.get(4,1));
+        REQUIRE(!result.get(2,5));
+
+    }
+
+    Nft aut_big(9);
+
+    SECTION("bigger automaton")
+    {
+        aut_big.initial = {1, 2};
+        aut_big.delta.add(1, 'a', 2);
+        aut_big.delta.add(1, 'a', 3);
+        aut_big.delta.add(1, 'b', 4);
+        aut_big.delta.add(2, 'a', 2);
+        aut_big.delta.add(2, 'b', 2);
+        aut_big.delta.add(2, 'a', 3);
+        aut_big.delta.add(2, 'b', 4);
+        aut_big.delta.add(3, 'b', 4);
+        aut_big.delta.add(3, 'c', 7);
+        aut_big.delta.add(3, 'b', 2);
+        aut_big.delta.add(5, 'c', 3);
+        aut_big.delta.add(7, 'a', 8);
+        aut_big.final = {3};
+
+        Simlib::Util::BinaryRelation result = compute_relation(aut_big);
+        REQUIRE(result.get(1,2));
+        REQUIRE(!result.get(2,1));
+        REQUIRE(!result.get(3,1));
+        REQUIRE(!result.get(3,2));
+        REQUIRE(result.get(4,1));
+        REQUIRE(result.get(4,2));
+        REQUIRE(result.get(4,5));
+        REQUIRE(!result.get(5,2));
+        REQUIRE(!result.get(5,1));
+        REQUIRE(result.get(7,1));
+        REQUIRE(result.get(7,2));
+        REQUIRE(result.get(8,1));
+        REQUIRE(result.get(8,2));
+        REQUIRE(result.get(8,5));
+    }
+} // }}
+
+TEST_CASE("mata::nft::reduce_size_by_simulation()")
+{
+    Nft aut;
+    StateRenaming state_renaming;
+
+    SECTION("empty automaton")
+    {
+        Nft result = reduce(aut, &state_renaming);
+
+        REQUIRE(result.delta.empty());
+        REQUIRE(result.initial.empty());
+        REQUIRE(result.final.empty());
+    }
+
+    SECTION("simple automaton")
+    {
+        aut.add_state(2);
+        aut.initial.insert(1);
+
+        aut.final.insert(2);
+        Nft result = reduce(aut, &state_renaming);
+
+        REQUIRE(result.delta.empty());
+        REQUIRE(result.initial[state_renaming[1]]);
+        REQUIRE(result.final[state_renaming[2]]);
+        REQUIRE(result.num_of_states() == 2);
+        REQUIRE(state_renaming[1] == state_renaming[0]);
+        REQUIRE(state_renaming[2] != state_renaming[0]);
+    }
+
+    SECTION("big automaton")
+    {
+        aut.add_state(9);
+        aut.initial = {1, 2};
+        aut.delta.add(1, 'a', 2);
+        aut.delta.add(1, 'a', 3);
+        aut.delta.add(1, 'b', 4);
+        aut.delta.add(2, 'a', 2);
+        aut.delta.add(2, 'b', 2);
+        aut.delta.add(2, 'a', 3);
+        aut.delta.add(2, 'b', 4);
+        aut.delta.add(3, 'b', 4);
+        aut.delta.add(3, 'c', 7);
+        aut.delta.add(3, 'b', 2);
+        aut.delta.add(5, 'c', 3);
+        aut.delta.add(7, 'a', 8);
+        aut.delta.add(9, 'b', 2);
+        aut.delta.add(9, 'c', 0);
+        aut.delta.add(0, 'a', 4);
+        aut.final = {3, 9};
+
+
+        Nft result = reduce(aut, &state_renaming);
+
+        REQUIRE(result.num_of_states() == 6);
+        REQUIRE(result.initial[state_renaming[1]]);
+        REQUIRE(result.initial[state_renaming[2]]);
+        REQUIRE(result.delta.contains(state_renaming[9], 'c', state_renaming[0]));
+        REQUIRE(result.delta.contains(state_renaming[9], 'c', state_renaming[7]));
+        REQUIRE(result.delta.contains(state_renaming[3], 'c', state_renaming[0]));
+        REQUIRE(result.delta.contains(state_renaming[0], 'a', state_renaming[8]));
+        REQUIRE(result.delta.contains(state_renaming[7], 'a', state_renaming[4]));
+        REQUIRE(result.delta.contains(state_renaming[1], 'a', state_renaming[3]));
+        REQUIRE(!result.delta.contains(state_renaming[3], 'b', state_renaming[4]));
+        REQUIRE(result.delta.contains(state_renaming[2], 'a', state_renaming[2]));
+        REQUIRE(result.final[state_renaming[9]]);
+        REQUIRE(result.final[state_renaming[3]]);
+
+        result = reduce(aut.trim(), &state_renaming);
+        CHECK(result.num_of_states() == 3);
+        CHECK(result.initial == SparseSet<State>{ 0, 1 });
+        CHECK(result.final == SparseSet<State>{ 2 });
+        CHECK(result.delta.num_of_transitions() == 6);
+        CHECK(result.delta.contains(state_renaming[0], 'a', state_renaming[2]));
+        CHECK(result.delta.contains(state_renaming[0], 'a', state_renaming[1]));
+        CHECK(result.delta.contains(state_renaming[1], 'a', state_renaming[1]));
+        CHECK(result.delta.contains(state_renaming[1], 'b', state_renaming[1]));
+        CHECK(result.delta.contains(state_renaming[1], 'a', state_renaming[2]));
+        CHECK(result.delta.contains(state_renaming[2], 'b', state_renaming[1]));
+    }
+
+    SECTION("no transitions from non-final state")
+    {
+        aut.delta.add(0, 'a', 1);
+        aut.initial = { 0 };
+        Nft result = reduce(aut.trim(), &state_renaming);
+        CHECK(are_equivalent(result, aut));
+    }
+}
+
+TEST_CASE("mata::nft::union_norename()") {
+    Run one{{1},{}};
+    Run zero{{0}, {}};
+
+    Nft lhs(2);
+    lhs.initial.insert(0);
+    lhs.delta.add(0, 0, 1);
+    lhs.final.insert(1);
+    REQUIRE(!lhs.is_in_lang(one));
+    REQUIRE(lhs.is_in_lang(zero));
+
+    Nft rhs(2);
+    rhs.initial.insert(0);
+    rhs.delta.add(0, 1, 1);
+    rhs.final.insert(1);
+    REQUIRE(rhs.is_in_lang(one));
+    REQUIRE(!rhs.is_in_lang(zero));
+
+    SECTION("failing minimal scenario") {
+        Nft result = uni(lhs, rhs);
+        REQUIRE(result.is_in_lang(one));
+        REQUIRE(result.is_in_lang(zero));
+    }
+}
+
+TEST_CASE("mata::nft::union_inplace") {
+    Run one{{1},{}};
+    Run zero{{0}, {}};
+
+    Nft lhs(2);
+    lhs.initial.insert(0);
+    lhs.delta.add(0, 0, 1);
+    lhs.final.insert(1);
+    REQUIRE(!lhs.is_in_lang(one));
+    REQUIRE(lhs.is_in_lang(zero));
+
+    Nft rhs(2);
+    rhs.initial.insert(0);
+    rhs.delta.add(0, 1, 1);
+    rhs.final.insert(1);
+    REQUIRE(rhs.is_in_lang(one));
+    REQUIRE(!rhs.is_in_lang(zero));
+
+    SECTION("failing minimal scenario") {
+        Nft result = lhs.uni(rhs);
+        REQUIRE(result.is_in_lang(one));
+        REQUIRE(result.is_in_lang(zero));
+    }
+
+    SECTION("same automata") {
+        size_t lhs_states = lhs.num_of_states();
+        Nft result = lhs.uni(lhs);
+        REQUIRE(result.num_of_states() == lhs_states * 2);
+    }
+}
+
+TEST_CASE("mata::nft::remove_final()")
+{
+    Nft aut('q' + 1);
+
+    SECTION("Automaton B")
+    {
+        FILL_WITH_AUT_B(aut);
+        REQUIRE(aut.final[2]);
+        REQUIRE(aut.final[12]);
+        aut.final.erase(12);
+        REQUIRE(aut.final[2]);
+        REQUIRE(!aut.final[12]);
+    }
+}
+
+TEST_CASE("mata::nft::delta.remove()")
+{
+    Nft aut('q' + 1);
+
+    SECTION("Automaton B")
+    {
+        FILL_WITH_AUT_B(aut);
+        aut.delta.add(1, 3, 4);
+        aut.delta.add(1, 3, 5);
+
+        SECTION("Simple remove")
+        {
+            REQUIRE(aut.delta.contains(1, 3, 4));
+            REQUIRE(aut.delta.contains(1, 3, 5));
+            aut.delta.remove(1, 3, 5);
+            REQUIRE(aut.delta.contains(1, 3, 4));
+            REQUIRE(!aut.delta.contains(1, 3, 5));
+        }
+
+        SECTION("Remove missing transition")
+        {
+            REQUIRE_THROWS_AS(aut.delta.remove(1, 1, 5), std::invalid_argument);
+        }
+
+        SECTION("Remove the last state_to from targets") {
+            REQUIRE(aut.delta.contains(6, 'a', 2));
+            aut.delta.remove(6, 'a', 2);
+            REQUIRE(!aut.delta.contains(6, 'a', 2));
+            REQUIRE(aut.delta[6].empty());
+
+            REQUIRE(aut.delta.contains(4, 'a', 8));
+            REQUIRE(aut.delta.contains(4, 'c', 8));
+            REQUIRE(aut.delta.contains(4, 'a', 6));
+            REQUIRE(aut.delta.contains(4, 'b', 6));
+            REQUIRE(aut.delta[4].size() == 3);
+            aut.delta.remove(4, 'a', 6);
+            REQUIRE(!aut.delta.contains(4, 'a', 6));
+            REQUIRE(aut.delta.contains(4, 'b', 6));
+            REQUIRE(aut.delta[4].size() == 3);
+
+            aut.delta.remove(4, 'a', 8);
+            REQUIRE(!aut.delta.contains(4, 'a', 8));
+            REQUIRE(aut.delta.contains(4, 'c', 8));
+            REQUIRE(aut.delta[4].size() == 2);
+
+            aut.delta.remove(4, 'c', 8);
+            REQUIRE(!aut.delta.contains(4, 'a', 8));
+            REQUIRE(!aut.delta.contains(4, 'c', 8));
+            REQUIRE(aut.delta[4].size() == 1);
+        }
+    }
+}
+
+TEST_CASE("mata::nft::get_trans_as_sequence(}") {
+    Nft aut('q' + 1);
+    std::vector<Transition> expected{};
+
+    aut.delta.add(1, 2, 3);
+    expected.emplace_back(1, 2, 3);
+    aut.delta.add(1, 3, 4);
+    expected.emplace_back(1, 3, 4);
+    aut.delta.add(2, 3, 4);
+    expected.emplace_back(2, 3, 4);
+
+
+    const Delta::Transitions transitions{ aut.delta.transitions() };
+    REQUIRE(std::vector<Transition>{ transitions.begin(), transitions.end() } == expected);
+}
+
+TEST_CASE("mata::nft::remove_epsilon()")
+{
+    Nft aut{20};
+    FILL_WITH_AUT_A(aut);
+    aut.remove_epsilon('c');
+    REQUIRE(aut.delta.contains(10, 'a', 7));
+    REQUIRE(aut.delta.contains(10, 'b', 7));
+    REQUIRE(!aut.delta.contains(10, 'c', 7));
+    REQUIRE(aut.delta.contains(7, 'a', 5));
+    REQUIRE(aut.delta.contains(7, 'a', 3));
+    REQUIRE(!aut.delta.contains(7, 'c', 3));
+    REQUIRE(aut.delta.contains(7, 'b', 9));
+    REQUIRE(aut.delta.contains(7, 'a', 7));
+    REQUIRE(aut.delta.contains(5, 'a', 5));
+    REQUIRE(!aut.delta.contains(5, 'c', 9));
+    REQUIRE(aut.delta.contains(5, 'a', 9));
+}
+
+TEST_CASE("Profile mata::nft::remove_epsilon()", "[.profiling]")
+{
+    for (size_t n{}; n < 100000; ++n) {
+        Nft aut{20};
+        FILL_WITH_AUT_A(aut);
+        aut.remove_epsilon('c');
+    }
+}
+
+TEST_CASE("mata::nft::get_num_of_trans()")
+{
+    Nft aut{20};
+    FILL_WITH_AUT_A(aut);
+    REQUIRE(aut.delta.num_of_transitions() == 15);
+}
+
+TEST_CASE("mata::nft::get_one_letter_aut()")
+{
+    Nft aut(11);
+    Symbol abstract_symbol{'x'};
+    FILL_WITH_AUT_A(aut);
+
+    Nft digraph{aut.get_one_letter_aut() };
+
+    REQUIRE(digraph.num_of_states() == aut.num_of_states());
+    REQUIRE(digraph.delta.num_of_transitions() == 12);
+    REQUIRE(digraph.delta.contains(1, abstract_symbol, 10));
+    REQUIRE(digraph.delta.contains(10, abstract_symbol, 7));
+    REQUIRE(!digraph.delta.contains(10, 'a', 7));
+    REQUIRE(!digraph.delta.contains(10, 'b', 7));
+    REQUIRE(!digraph.delta.contains(10, 'c', 7));
+}
+
+TEST_CASE("mata::nft::get_reachable_states()") {
+    Nft aut{20};
+
+    SECTION("Automaton A") {
+        FILL_WITH_AUT_A(aut);
+        aut.delta.remove(3, 'b', 9);
+        aut.delta.remove(5, 'c', 9);
+        aut.delta.remove(1, 'a', 10);
+
+        StateSet reachable{ aut.get_reachable_states() };
+        CHECK(!reachable.contains(0));
+        CHECK(reachable.contains(1));
+        CHECK(!reachable.contains(2));
+        CHECK(reachable.contains(3));
+        CHECK(!reachable.contains(4));
+        CHECK(reachable.contains(5));
+        CHECK(!reachable.contains(6));
+        CHECK(reachable.contains(7));
+        CHECK(!reachable.contains(8));
+        CHECK(!reachable.contains(9));
+        CHECK(!reachable.contains(10));
+
+        aut.initial.erase(1);
+        aut.initial.erase(3);
+        reachable = aut.get_reachable_states();
+        CHECK(reachable.empty());
+    }
+
+    SECTION("Automaton B")
+    {
+        FILL_WITH_AUT_B(aut);
+        aut.delta.remove(2, 'c', 12);
+        aut.delta.remove(4, 'c', 8);
+        aut.delta.remove(4, 'a', 8);
+
+        auto reachable{aut.get_reachable_states()};
+        CHECK(reachable.find(0) != reachable.end());
+        CHECK(reachable.find(1) == reachable.end());
+        CHECK(reachable.find(2) != reachable.end());
+        CHECK(reachable.find(3) == reachable.end());
+        CHECK(reachable.find(4) != reachable.end());
+        CHECK(reachable.find(5) == reachable.end());
+        CHECK(reachable.find(6) != reachable.end());
+        CHECK(reachable.find(7) == reachable.end());
+        CHECK(reachable.find(8) == reachable.end());
+        CHECK(reachable.find(9) == reachable.end());
+        CHECK(reachable.find(10) == reachable.end());
+        CHECK(reachable.find(11) == reachable.end());
+        CHECK(reachable.find(12) == reachable.end());
+        CHECK(reachable.find(13) == reachable.end());
+        CHECK(reachable.find(14) == reachable.end());
+
+        aut.final.erase(2);
+        reachable = aut.get_reachable_states();
+        CHECK(reachable.size() == 4);
+        CHECK(reachable.find(0) != reachable.end());
+        CHECK(reachable.find(2) != reachable.end());
+        CHECK(reachable.find(4) != reachable.end());
+        CHECK(reachable.find(6) != reachable.end());
+        CHECK(aut.get_useful_states().count() == 0);
+
+        aut.final.insert(4);
+        reachable = aut.get_reachable_states();
+        CHECK(reachable.find(4) != reachable.end());
+    }
+}
+
+TEST_CASE("mata::nft::trim() for profiling", "[.profiling],[trim]")
+{
+    Nft aut{20};
+    FILL_WITH_AUT_A(aut);
+    aut.delta.remove(1, 'a', 10);
+
+    for (size_t i{ 0 }; i < 10000; ++i) {
+        Nft new_aut{ aut };
+        new_aut.trim();
+    }
+}
+
+//TODO: make this a test for the new version
+TEST_CASE("mata::nft::get_useful_states() for profiling", "[.profiling],[useful_states]")
+{
+    Nft aut{20};
+    FILL_WITH_AUT_A(aut);
+    aut.delta.remove(1, 'a', 10);
+
+    for (size_t i{ 0 }; i < 10000; ++i) {
+        aut.get_useful_states();
+    }
+}
+
+TEST_CASE("mata::nft::trim() trivial") {
+    Nft aut{1};
+    aut.initial.insert(0);
+    aut.final.insert(0);
+    aut.trim();
+}
+
+TEST_CASE("mata::nft::trim()")
+{
+    Nft orig_aut{20};
+    FILL_WITH_AUT_A(orig_aut);
+    orig_aut.delta.remove(1, 'a', 10);
+
+
+    SECTION("Without state map") {
+        Nft aut{orig_aut};
+        aut.trim();
+        CHECK(aut.initial.size() == orig_aut.initial.size());
+        CHECK(aut.final.size() == orig_aut.final.size());
+        CHECK(aut.num_of_states() == 4);
+        for (const Word& word: get_shortest_words(orig_aut))
+        {
+            CHECK(aut.is_in_lang(Run{word,{}}));
+        }
+
+        aut.final.erase(2); // '2' is the new final state in the earlier trimmed automaton.
+        aut.trim();
+        CHECK(aut.delta.empty());
+        CHECK(aut.num_of_states() == 0);
+    }
+
+    SECTION("With state map") {
+        Nft aut{orig_aut};
+        StateRenaming state_map{};
+        aut.trim(&state_map);
+        CHECK(aut.initial.size() == orig_aut.initial.size());
+        CHECK(aut.final.size() == orig_aut.final.size());
+        CHECK(aut.num_of_states() == 4);
+        for (const Word& word: get_shortest_words(orig_aut))
+        {
+            CHECK(aut.is_in_lang(Run{word,{}}));
+        }
+        REQUIRE(state_map.size() == 4);
+        CHECK(state_map.at(1) == 0);
+        CHECK(state_map.at(3) == 1);
+        CHECK(state_map.at(7) == 3);
+        CHECK(state_map.at(5) == 2);
+
+        aut.final.erase(2); // '2' is the new final state in the earlier trimmed automaton.
+        aut.trim(&state_map);
+        CHECK(aut.delta.empty());
+        CHECK(aut.num_of_states() == 0);
+        CHECK(state_map.empty());
+    }
+}
+
+TEST_CASE("mata::nft::Nft::delta.empty()")
+{
+    Nft aut{};
+
+    SECTION("Empty automaton")
+    {
+        CHECK(aut.delta.empty());
+    }
+
+    SECTION("No transitions automaton")
+    {
+        aut.add_state();
+        CHECK(aut.delta.empty());
+    }
+
+    SECTION("Single state automaton with no transitions")
+    {
+        aut.add_state();
+        aut.initial.insert(0);
+        aut.final.insert(0);
+        CHECK(aut.delta.empty());
+    }
+
+    SECTION("Single state automaton with transitions")
+    {
+        aut.add_state();
+        aut.initial.insert(0);
+        aut.final.insert(0);
+        aut.delta.add(0, 'a', 0);
+        CHECK(!aut.delta.empty());
+    }
+
+    SECTION("Single state automaton with transitions")
+    {
+        aut.add_state(1);
+        aut.initial.insert(0);
+        aut.final.insert(1);
+        CHECK(aut.delta.empty());
+    }
+
+    SECTION("Single state automaton with transitions")
+    {
+        aut.add_state(1);
+        aut.initial.insert(0);
+        aut.final.insert(1);
+        aut.delta.add(0, 'a', 1);
+        CHECK(!aut.delta.empty());
+    }
+}
+
+TEST_CASE("mata::nft::delta.operator[]")
+{
+    Nft aut{20};
+    FILL_WITH_AUT_A(aut);
+    REQUIRE(aut.delta.num_of_transitions() == 15);
+    aut.delta[25];
+    REQUIRE(aut.num_of_states() == 20);
+
+    aut.delta.mutable_state_post(25);
+    REQUIRE(aut.num_of_states() == 26);
+    REQUIRE(aut.delta[25].empty());
+
+    aut.delta.mutable_state_post(50);
+    REQUIRE(aut.num_of_states() == 51);
+    REQUIRE(aut.delta[50].empty());
+
+    Nft aut1 = aut;
+    aut1.delta.mutable_state_post(60);
+    REQUIRE(aut1.num_of_states() == 61);
+    REQUIRE(aut1.delta[60].empty());
+
+    const Nft aut2 = aut;
+    aut2.delta[60];
+    REQUIRE(aut2.num_of_states() == 51);
+    REQUIRE(aut2.delta[60].empty());
+}
+
+TEST_CASE("mata::nft::Nft::unify_(initial/final)()") {
+    Nft nft{10};
+
+    SECTION("No initial") {
+        nft.unify_initial();
+        CHECK(nft.num_of_states() == 10);
+        CHECK(nft.initial.empty());
+    }
+
+    SECTION("initial==final unify final") {
+        nft.initial.insert(0);
+        nft.final.insert(0);
+        nft.final.insert(1);
+        nft.unify_final();
+        REQUIRE(nft.num_of_states() == 11);
+        CHECK(nft.final.size() == 1);
+        CHECK(nft.final[10]);
+        CHECK(nft.initial[10]);
+    }
+
+    SECTION("initial==final unify initial") {
+        nft.initial.insert(0);
+        nft.initial.insert(1);
+        nft.final.insert(0);
+        nft.unify_initial();
+        REQUIRE(nft.num_of_states() == 11);
+        CHECK(nft.initial.size() == 1);
+        CHECK(nft.initial[10]);
+        CHECK(nft.final[10]);
+    }
+
+    SECTION("Single initial") {
+        nft.initial.insert(0);
+        nft.unify_initial();
+        CHECK(nft.num_of_states() == 10);
+        CHECK(nft.initial.size() == 1);
+        CHECK(nft.initial[0]);
+    }
+
+    SECTION("Multiple initial") {
+        nft.initial.insert(0);
+        nft.initial.insert(1);
+        nft.unify_initial();
+        CHECK(nft.num_of_states() == 11);
+        CHECK(nft.initial.size() == 1);
+        CHECK(nft.initial[10]);
+    }
+
+    SECTION("With transitions") {
+        nft.initial.insert(0);
+        nft.initial.insert(1);
+        nft.delta.add(0, 'a', 3);
+        nft.delta.add(1, 'b', 0);
+        nft.delta.add(1, 'c', 1);
+        nft.unify_initial();
+        CHECK(nft.num_of_states() == 11);
+        CHECK(nft.initial.size() == 1);
+        CHECK(nft.initial[10]);
+        CHECK(nft.delta.contains(10, 'a', 3));
+        CHECK(nft.delta.contains(10, 'b', 0));
+        CHECK(nft.delta.contains(10, 'c', 1));
+        CHECK(nft.delta.contains(0, 'a', 3));
+        CHECK(nft.delta.contains(1, 'b', 0));
+        CHECK(nft.delta.contains(1, 'c', 1));
+    }
+
+    SECTION("No final") {
+        nft.unify_final();
+        CHECK(nft.num_of_states() == 10);
+        CHECK(nft.final.empty());
+    }
+
+    SECTION("Single final") {
+        nft.final.insert(0);
+        nft.unify_final();
+        CHECK(nft.num_of_states() == 10);
+        CHECK(nft.final.size() == 1);
+        CHECK(nft.final[0]);
+    }
+
+    SECTION("Multiple final") {
+        nft.final.insert(0);
+        nft.final.insert(1);
+        nft.unify_final();
+        CHECK(nft.num_of_states() == 11);
+        CHECK(nft.final.size() == 1);
+        CHECK(nft.final[10]);
+    }
+
+    SECTION("With transitions") {
+        nft.final.insert(0);
+        nft.final.insert(1);
+        nft.delta.add(3, 'a', 0);
+        nft.delta.add(4, 'b', 1);
+        nft.delta.add(1, 'c', 1);
+        nft.unify_final();
+        CHECK(nft.num_of_states() == 11);
+        CHECK(nft.final.size() == 1);
+        CHECK(nft.final[10]);
+        CHECK(nft.delta.contains(3, 'a', 10));
+        CHECK(nft.delta.contains(4, 'b', 10));
+        CHECK(nft.delta.contains(1, 'c', 10));
+        CHECK(nft.delta.contains(3, 'a', 0));
+        CHECK(nft.delta.contains(4, 'b', 1));
+        CHECK(nft.delta.contains(1, 'c', 1));
+    }
+
+    SECTION("Bug: NFT with empty string unifying initial/final repeatedly") {
+        Nft aut;
+        mata::parser::create_nfa(&aut, "a*b*");
+        for (size_t i{ 0 }; i < 8; ++i) {
+            aut.unify_initial();
+            aut.unify_final();
+        }
+        CHECK(true); // Check that the program does not seg fault.
+    }
+}
+
+TEST_CASE("mata::nft::Nft::get_delta.epsilon_symbol_posts()") {
+    Nft aut{20};
+    FILL_WITH_AUT_A(aut);
+    aut.delta.add(0, EPSILON, 3);
+    aut.delta.add(3, EPSILON, 3);
+    aut.delta.add(3, EPSILON, 4);
+
+    auto state_eps_trans{ aut.delta.epsilon_symbol_posts(0) };
+    CHECK(state_eps_trans->symbol == EPSILON);
+    CHECK(state_eps_trans->targets == StateSet{3 });
+    state_eps_trans = aut.delta.epsilon_symbol_posts(3);
+    CHECK(state_eps_trans->symbol == EPSILON);
+    CHECK(state_eps_trans->targets == StateSet{3, 4 });
+
+    aut.delta.add(8, 42, 3);
+    aut.delta.add(8, 42, 4);
+    aut.delta.add(8, 42, 6);
+
+    state_eps_trans = aut.delta.epsilon_symbol_posts(8, 42);
+    CHECK(state_eps_trans->symbol == 42);
+    CHECK(state_eps_trans->targets == StateSet{3, 4, 6 });
+
+    CHECK(aut.delta.epsilon_symbol_posts(1) == aut.delta.state_post(1).end());
+    CHECK(aut.delta.epsilon_symbol_posts(5) == aut.delta.state_post(5).end());
+    CHECK(aut.delta.epsilon_symbol_posts(19) == aut.delta.state_post(19).end());
+
+    StatePost state_post{ aut.delta[0] };
+    state_eps_trans = aut.delta.epsilon_symbol_posts(state_post);
+    CHECK(state_eps_trans->symbol == EPSILON);
+    CHECK(state_eps_trans->targets == StateSet{3 });
+    state_post = aut.delta[3];
+    state_eps_trans = Delta::epsilon_symbol_posts(state_post);
+    CHECK(state_eps_trans->symbol == EPSILON);
+    CHECK(state_eps_trans->targets == StateSet{3, 4 });
+
+    state_post = aut.delta.state_post(1);
+    CHECK(aut.delta.epsilon_symbol_posts(state_post) == state_post.end());
+    state_post = aut.delta.state_post(5);
+    CHECK(aut.delta.epsilon_symbol_posts(state_post) == state_post.end());
+    state_post = aut.delta.state_post(19);
+    CHECK(aut.delta.epsilon_symbol_posts(state_post) == state_post.end());
+}
+
+TEST_CASE("mata::nft::Nft::delta()") {
+    Delta delta(6);
+}
+
+TEST_CASE("mata::nft::make_complement(): A segmentation fault") {
+    Nft r(1);
+    OnTheFlyAlphabet alph{};
+    alph["a"];
+    alph["b"];
+
+    r.initial = {0};
+    r.delta.add(0, 0, 0);
+    REQUIRE(not r.is_complete(&alph));
+    r.make_complete(&alph, 1);
+    REQUIRE(r.is_complete(&alph));
+}
+
+TEST_CASE("mata::nft:: create simple automata") {
+    Nft nft{ builder::create_empty_string_nft(1) };
+    CHECK(nft.is_in_lang(Word{}));
+    CHECK(get_word_lengths(nft) == std::set<std::pair<int, int>>{ std::make_pair(0, 0) });
+
+    OnTheFlyAlphabet alphabet{ { "a", 0 }, { "b", 1 }, { "c", 2 } };
+    nft = builder::create_sigma_star_nft(&alphabet, 1);
+    CHECK(nft.is_in_lang({ {}, {} }));
+    CHECK(nft.is_in_lang({ 0 , {} }));
+    CHECK(nft.is_in_lang({ 1 , {} }));
+    CHECK(nft.is_in_lang({ 2 , {} }));
+    CHECK(nft.is_in_lang({ { 0, 1 }, {} }));
+    CHECK(nft.is_in_lang({ { 1, 0 }, {} }));
+    CHECK(nft.is_in_lang({ { 2, 2, 2 }, {} }));
+    CHECK(nft.is_in_lang({ { 0, 1, 2, 2, 0, 1, 2, 1, 0, 0, 2, 1 }, {} }));
+    CHECK(!nft.is_in_lang({  3 , {} }));
+}
+
+TEST_CASE("mata::nft::print_to_mata()") {
+    Nft aut_big{ Nft::with_levels(2, 9)};
+    aut_big.initial = {1, 2};
+    aut_big.delta.add(1, 'a', 2);
+    aut_big.delta.add(1, 'a', 3);
+    aut_big.delta.add(1, 'b', 4);
+    aut_big.delta.add(2, 'a', 2);
+    aut_big.delta.add(2, 'b', 2);
+    aut_big.delta.add(2, 'a', 3);
+    aut_big.delta.add(2, 'b', 4);
+    aut_big.delta.add(3, 'b', 4);
+    aut_big.delta.add(3, 'c', 7);
+    aut_big.delta.add(3, 'b', 2);
+    aut_big.delta.add(5, 'c', 3);
+    aut_big.delta.add(7, 'a', 8);
+    aut_big.final = {3};
+    CHECK(are_equivalent(aut_big, nft::builder::parse_from_mata(aut_big.print_to_mata())));
+}
+
+TEST_CASE("mata::nft::Nft::trim() bug") {
+	Nft aut(5, {0}, {4});
+	aut.delta.add(0, 122, 1);
+	aut.delta.add(1, 98, 1);
+	aut.delta.add(1, 122, 1);
+	aut.delta.add(1, 97, 2);
+	aut.delta.add(2, 122, 1);
+	aut.delta.add(2, 97, 1);
+	aut.delta.add(1, 97, 4);
+	aut.delta.add(3, 97, 4);
+
+	Nft aut_copy {aut};
+	CHECK(are_equivalent(aut_copy.trim(), aut));
+}
+
+TEST_CASE("mata::nft::get_useful_states_tarjan") {
+	SECTION("Nft 1") {
+		Nft aut(5, {0}, {4});
+		aut.delta.add(0, 122, 1);
+		aut.delta.add(1, 98, 1);
+		aut.delta.add(1, 122, 1);
+		aut.delta.add(1, 97, 2);
+		aut.delta.add(2, 122, 1);
+		aut.delta.add(2, 97, 1);
+		aut.delta.add(1, 97, 4);
+		aut.delta.add(3, 97, 4);
+
+		mata::BoolVector bv = aut.get_useful_states();
+		mata::BoolVector ref({ 1, 1, 1, 0, 1});
+		CHECK(bv == ref);
+	}
+
+	SECTION("Empty NFT") {
+		Nft aut;
+		mata::BoolVector bv = aut.get_useful_states();
+		CHECK(bv == mata::BoolVector({}));
+	}
+
+	SECTION("Single-state NFT") {
+		Nft aut(1, {0}, {});
+		mata::BoolVector bv = aut.get_useful_states();
+		CHECK(bv == mata::BoolVector({ 0}));
+	}
+
+	SECTION("Single-state NFT acc") {
+		Nft aut(1, {0}, {0});
+		mata::BoolVector bv = aut.get_useful_states();
+		CHECK(bv == mata::BoolVector({ 1}));
+	}
+
+	SECTION("Nft 2") {
+		Nft aut(5, {0, 1}, {2});
+		aut.delta.add(0, 122, 2);
+		aut.delta.add(2, 98, 3);
+		aut.delta.add(1, 98, 4);
+		aut.delta.add(4, 97, 3);
+
+		mata::BoolVector bv = aut.get_useful_states();
+		mata::BoolVector ref({ 1, 0, 1, 0, 0});
+		CHECK(bv == ref);
+	}
+
+	SECTION("Nft 3") {
+		Nft aut(2, {0, 1}, {0, 1});
+		aut.delta.add(0, 122, 0);
+		aut.delta.add(1, 98, 1);
+
+		mata::BoolVector bv = aut.get_useful_states();
+		mata::BoolVector ref({ 1, 1});
+		CHECK(bv == ref);
+	}
+
+	SECTION("Nft no final") {
+		Nft aut(5, {0}, {});
+		aut.delta.add(0, 122, 1);
+		aut.delta.add(1, 98, 1);
+		aut.delta.add(1, 122, 1);
+		aut.delta.add(1, 97, 2);
+		aut.delta.add(2, 122, 1);
+		aut.delta.add(2, 97, 1);
+		aut.delta.add(1, 97, 4);
+		aut.delta.add(3, 97, 4);
+
+		mata::BoolVector bv = aut.get_useful_states();
+		mata::BoolVector ref({ 0, 0, 0, 0, 0});
+		CHECK(bv == ref);
+	}
+
+    SECTION("from regex (a+b*a*)") {
+        Nft aut;
+        mata::parser::create_nfa(&aut, "(a+b*a*)", false, EPSILON, false);
+
+        mata::BoolVector bv = aut.get_useful_states();
+        mata::BoolVector ref({ 1, 0, 1, 0, 1, 0, 1, 0, 0});
+        CHECK(bv == ref);
+
+        aut = reduce(aut.trim());
+        bv = aut.get_useful_states();
+        CHECK(bv == mata::BoolVector({ 1, 1, 1, 1}));
+    }
+
+    SECTION("more initials") {
+        Nft aut(4, {0, 1, 2}, {0, 3});
+        aut.delta.add(1, 48, 0);
+        aut.delta.add(2, 53, 3);
+        CHECK(aut.get_useful_states() == mata::BoolVector{ 1, 1, 1, 1});
+    }
+}
+
+TEST_CASE("mata::nft::Nft::get_words") {
+    SECTION("empty") {
+        Nft aut;
+        CHECK(aut.get_words(0) == std::set<mata::Word>());
+        CHECK(aut.get_words(1) == std::set<mata::Word>());
+        CHECK(aut.get_words(5) == std::set<mata::Word>());
+    }
+
+    SECTION("empty word") {
+        Nft aut(1, {0}, {0});
+        CHECK(aut.get_words(0) == std::set<mata::Word>{{}});
+        CHECK(aut.get_words(1) == std::set<mata::Word>{{}});
+        CHECK(aut.get_words(5) == std::set<mata::Word>{{}});
+    }
+
+    SECTION("noodle - one final") {
+        Nft aut(3, {0}, {2});
+        aut.delta.add(0, 0, 1);
+        aut.delta.add(1, 1, 2);
+        CHECK(aut.get_words(0) == std::set<mata::Word>{});
+        CHECK(aut.get_words(1) == std::set<mata::Word>{});
+        CHECK(aut.get_words(2) == std::set<mata::Word>{{0, 1}});
+        CHECK(aut.get_words(3) == std::set<mata::Word>{{0, 1}});
+        CHECK(aut.get_words(5) == std::set<mata::Word>{{0, 1}});
+    }
+
+    SECTION("noodle - two finals") {
+        Nft aut(3, {0}, {1,2});
+        aut.delta.add(0, 0, 1);
+        aut.delta.add(1, 1, 2);
+        CHECK(aut.get_words(0) == std::set<mata::Word>{});
+        CHECK(aut.get_words(1) == std::set<mata::Word>{{0}});
+        CHECK(aut.get_words(2) == std::set<mata::Word>{{0}, {0, 1}});
+        CHECK(aut.get_words(3) == std::set<mata::Word>{{0}, {0, 1}});
+        CHECK(aut.get_words(5) == std::set<mata::Word>{{0}, {0, 1}});
+    }
+
+    SECTION("noodle - three finals") {
+        Nft aut(3, {0}, {0,1,2});
+        aut.delta.add(0, 0, 1);
+        aut.delta.add(1, 1, 2);
+        CHECK(aut.get_words(0) == std::set<mata::Word>{{}});
+        CHECK(aut.get_words(1) == std::set<mata::Word>{{}, {0}});
+        CHECK(aut.get_words(2) == std::set<mata::Word>{{}, {0}, {0, 1}});
+        CHECK(aut.get_words(3) == std::set<mata::Word>{{}, {0}, {0, 1}});
+        CHECK(aut.get_words(5) == std::set<mata::Word>{{}, {0}, {0, 1}});
+    }
+
+    SECTION("more complex") {
+        Nft aut(6, {0,1}, {1,3,4,5});
+        aut.delta.add(0, 0, 3);
+        aut.delta.add(3, 1, 4);
+        aut.delta.add(0, 2, 2);
+        aut.delta.add(3, 3, 2);
+        aut.delta.add(1, 4, 2);
+        aut.delta.add(2, 5, 5);
+        CHECK(aut.get_words(0) == std::set<mata::Word>{{}});
+        CHECK(aut.get_words(1) == std::set<mata::Word>{{}, {0}});
+        CHECK(aut.get_words(2) == std::set<mata::Word>{{}, {0}, {0, 1}, {2,5}, {4,5}});
+        CHECK(aut.get_words(3) == std::set<mata::Word>{{}, {0}, {0, 1}, {2,5}, {4,5}, {0,3,5}});
+        CHECK(aut.get_words(4) == std::set<mata::Word>{{}, {0}, {0, 1}, {2,5}, {4,5}, {0,3,5}});
+        CHECK(aut.get_words(5) == std::set<mata::Word>{{}, {0}, {0, 1}, {2,5}, {4,5}, {0,3,5}});
+    }
+
+    SECTION("cycle") {
+        Nft aut(6, {0,1}, {0,1});
+        aut.delta.add(0, 0, 1);
+        aut.delta.add(1, 1, 0);
+        CHECK(aut.get_words(0) == std::set<mata::Word>{{}});
+        CHECK(aut.get_words(1) == std::set<mata::Word>{{}, {0}, {1}});
+        CHECK(aut.get_words(2) == std::set<mata::Word>{{}, {0}, {1}, {0, 1}, {1, 0}});
+        CHECK(aut.get_words(3) == std::set<mata::Word>{{}, {0}, {1}, {0, 1}, {1, 0}, {0,1,0}, {1,0,1}});
+        CHECK(aut.get_words(4) == std::set<mata::Word>{{}, {0}, {1}, {0, 1}, {1, 0}, {0,1,0}, {1,0,1}, {0,1,0,1}, {1,0,1,0}});
+        CHECK(aut.get_words(5) == std::set<mata::Word>{{}, {0}, {1}, {0, 1}, {1, 0}, {0,1,0}, {1,0,1}, {0,1,0,1}, {1,0,1,0}, {0,1,0,1,0}, {1,0,1,0,1}});
+    }
+}
+
+TEST_CASE("mata::nft::Nft::get_one_level_aut") {
+    #define REPLACE_DONT_CARE(delta, src, trg)\
+                delta.add(src, 0, trg);\
+                delta.add(src, 1, trg);\
+
+    #define SPLIT_TRANSITION(delta, src, symbol, inter, trg)\
+                ((symbol == DONT_CARE) ? (delta.add(src, 0, inter), delta.add(src, 1, inter)) : (delta.add(src, symbol, inter)));\
+                REPLACE_DONT_CARE(delta, inter, trg);\
+
+    SECTION("level_cnt == 1") {
+        Nft aut{ Nft::with_levels(1, 5, {0}, {3, 4}) };
+        aut.delta.add(0, 0, 1);
+        aut.delta.add(0, 1, 2);
+        aut.delta.add(1, 0, 1);
+        aut.delta.add(1, DONT_CARE, 3);
+        aut.delta.add(2, DONT_CARE, 2);
+        aut.delta.add(2, DONT_CARE, 4);
+        aut.delta.add(3, 0, 1);
+        aut.delta.add(3, DONT_CARE, 3);
+        aut.delta.add(4, 1, 2);
+        aut.delta.add(4, DONT_CARE, 4);
+
+        Nft expected{ Nft::with_levels(1, 5, {0}, {3, 4}) };
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(0, 1, 2);
+        expected.delta.add(1, 0, 1);
+        REPLACE_DONT_CARE(expected.delta, 1, 3);
+        REPLACE_DONT_CARE(expected.delta, 2, 2);
+        REPLACE_DONT_CARE(expected.delta, 2, 4);
+        expected.delta.add(3, 0, 1);
+        REPLACE_DONT_CARE(expected.delta, 3, 3);
+        expected.delta.add(4, 1, 2);
+        REPLACE_DONT_CARE(expected.delta, 4, 4);
+
+        CHECK(nfa::are_equivalent(aut.get_one_level_aut({0, 1}, JumpMode::AppendDontCares), expected));
+        CHECK(nfa::are_equivalent(aut.get_one_level_aut({ DONT_CARE }, JumpMode::AppendDontCares).get_one_level_aut({ 0, 1 }, JumpMode::AppendDontCares), expected));
+        CHECK(nft::are_equivalent(aut, expected, JumpMode::AppendDontCares));
+    }
+
+    SECTION("level_cnt == 2") {
+        Nft aut(7, {0}, {5, 6}, {0, 1, 1, 0, 0, 0, 0}, 2);
+        aut.delta.add(0, 0, 1);
+        aut.delta.add(0, 1, 2);
+        aut.delta.add(1, DONT_CARE, 3);
+        aut.delta.add(2, DONT_CARE, 4);
+        aut.delta.add(3, 0, 3);
+        aut.delta.add(3, 0, 5);
+        aut.delta.add(4, DONT_CARE, 4);
+        aut.delta.add(4, DONT_CARE, 6);
+        aut.delta.add(5, DONT_CARE, 5);
+        aut.delta.add(5, 0, 3);
+        aut.delta.add(6, DONT_CARE, 6);
+        aut.delta.add(6, 1, 4);
+
+        Nft expected(15, {0}, {5, 6}, {0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}, 2);
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(0, 1, 2);
+        REPLACE_DONT_CARE(expected.delta, 1, 3);
+        REPLACE_DONT_CARE(expected.delta, 2, 4);
+        SPLIT_TRANSITION(expected.delta, 3, 0, 7, 3);
+        SPLIT_TRANSITION(expected.delta, 3, 0, 8, 5);
+        SPLIT_TRANSITION(expected.delta, 4, DONT_CARE, 10, 4);
+        SPLIT_TRANSITION(expected.delta, 4, DONT_CARE, 12, 6);
+        SPLIT_TRANSITION(expected.delta, 5, DONT_CARE, 13, 5);
+        SPLIT_TRANSITION(expected.delta, 5, 0, 9, 3);
+        SPLIT_TRANSITION(expected.delta, 6, DONT_CARE, 14, 6);
+        SPLIT_TRANSITION(expected.delta, 6, 1, 11, 4);
+
+        CHECK(nfa::are_equivalent(aut.get_one_level_aut({ 0, 1 }, JumpMode::AppendDontCares), expected));
+        CHECK(nfa::are_equivalent(aut.get_one_level_aut({ DONT_CARE }, JumpMode::AppendDontCares).get_one_level_aut({0, 1}, JumpMode::AppendDontCares), expected));
+        CHECK(nft::are_equivalent(aut, expected, JumpMode::AppendDontCares));
+
+    }
+
+    SECTION("level_cnt == 4") {
+        Nft aut(17, {0}, {15, 16}, {0, 1, 1, 3, 3, 0, 0, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0}, 4);
+        aut.delta.add(0, 0, 1);
+        aut.delta.add(0, 1, 2);
+        aut.delta.add(1, 0, 3);
+        aut.delta.add(2, DONT_CARE, 4);
+        aut.delta.add(3, 0, 5);
+        aut.delta.add(4, DONT_CARE, 6);
+        aut.delta.add(5, 0, 5);
+        aut.delta.add(5, 0, 7);
+        aut.delta.add(6, DONT_CARE, 6);
+        aut.delta.add(6, DONT_CARE, 8);
+        aut.delta.add(7, 0, 9);
+        aut.delta.add(8, DONT_CARE, 10);
+        aut.delta.add(9, 0, 11);
+        aut.delta.add(10, DONT_CARE, 12);
+        aut.delta.add(11, 0, 13);
+        aut.delta.add(12, DONT_CARE, 14);
+        aut.delta.add(13, 0, 15);
+        aut.delta.add(14, DONT_CARE, 16);
+
+        Nft expected(31, {0}, {15, 16}, {0, 1, 1, 3, 3, 0, 0, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0, 2, 2, 2, 1, 1, 3, 3, 1, 2, 1, 3, 3, 3, 3}, 4);
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(0, 1, 2);
+        SPLIT_TRANSITION(expected.delta, 1, 0, 17, 3);
+        SPLIT_TRANSITION(expected.delta, 2, DONT_CARE, 18, 4);
+        expected.delta.add(3, 0, 5);
+        REPLACE_DONT_CARE(expected.delta, 4, 6);
+        expected.delta.add(5, 0, 20);
+        REPLACE_DONT_CARE(expected.delta, 20, 19);
+        REPLACE_DONT_CARE(expected.delta, 19, 29);
+        REPLACE_DONT_CARE(expected.delta, 29, 5);
+        SPLIT_TRANSITION(expected.delta, 5, 0, 21, 7);
+        REPLACE_DONT_CARE(expected.delta, 6, 24);
+        REPLACE_DONT_CARE(expected.delta, 24, 25);
+        REPLACE_DONT_CARE(expected.delta, 25, 30);
+        REPLACE_DONT_CARE(expected.delta, 30, 6);
+        SPLIT_TRANSITION(expected.delta, 6, DONT_CARE, 26, 8);
+        SPLIT_TRANSITION(expected.delta, 7, 0, 22, 9);
+        SPLIT_TRANSITION(expected.delta, 8, DONT_CARE, 27, 10);
+        expected.delta.add(9, 0, 11);
+        REPLACE_DONT_CARE(expected.delta, 10, 12);
+        expected.delta.add(11, 0, 13);
+        REPLACE_DONT_CARE(expected.delta, 12, 14);
+        SPLIT_TRANSITION(expected.delta, 13, 0, 23, 15);
+        SPLIT_TRANSITION(expected.delta, 14, DONT_CARE, 28, 16);
+
+        CHECK(nfa::are_equivalent(aut.get_one_level_aut({ 0, 1 }, JumpMode::AppendDontCares), expected));
+        CHECK(nfa::are_equivalent(aut.get_one_level_aut({ DONT_CARE }, JumpMode::AppendDontCares).get_one_level_aut({ 0, 1 }, JumpMode::AppendDontCares), expected));
+        CHECK(nft::are_equivalent(aut, expected, JumpMode::AppendDontCares));
+    }
+
+}
+
+TEST_CASE("mata::nft::Nft::add_state()") {
+    Nft nft{};
+    State state{ nft.add_state() };
+    CHECK(state == 0);
+    CHECK(nft.levels[state] == 0);
+    state = nft.add_state(4);
+    CHECK(state == 4);
+    CHECK(nft.levels[state] == 0);
+    CHECK(nft.num_of_states() == 5);
+    state = nft.add_state_with_level(3);
+    CHECK(state == 5);
+    CHECK(nft.levels[state] == 3);
+    CHECK(nft.num_of_states() == 6);
+    state = nft.add_state_with_level(12, 1);
+    CHECK(state == 12);
+    CHECK(nft.levels[state] == 1);
+    CHECK(nft.num_of_states() == 13);
+}
+
+
+TEST_CASE("mata::nft::project_out(jump_mode == JumpMode::AppendDontCares)") {
+
+    SECTION("LINEAR") {
+        Delta delta;
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+
+        Nft atm(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("project 0") {
+            Nft proj0 = project_out(atm, OrdVector<Level>{ 0 }, JumpMode::AppendDontCares);
+            Nft proj0_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj0_expected.delta.add(0, 1, 1);
+            proj0_expected.delta.add(1, 2, 2);
+            CHECK(are_equivalent(proj0, proj0_expected, JumpMode::AppendDontCares));
+
+        }
+
+        SECTION("project 1") {
+            Nft proj1 = project_out(atm, 1, JumpMode::AppendDontCares);
+            Nft proj1_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj1_expected.delta.add(0, 0, 1);
+            proj1_expected.delta.add(1, 2, 2);
+            CHECK(are_equivalent(proj1, proj1_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 2") {
+            Nft proj2 = project_out(atm, 2, JumpMode::AppendDontCares);
+            Nft proj2_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj2_expected.delta.add(0, 0, 1);
+            proj2_expected.delta.add(1, 1, 2);
+            CHECK(are_equivalent(proj2, proj2_expected, JumpMode::AppendDontCares));
+
+        }
+    }
+
+    SECTION("LOOP") {
+        Delta delta;
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+        delta.add(0, 3, 0);
+        delta.add(3, 4, 3);
+
+        Nft atm_loop(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("project 0") {
+            Nft proj0_loop = project_out(atm_loop, 0, JumpMode::AppendDontCares);
+            Nft proj0_loop_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj0_loop_expected.delta.add(0, DONT_CARE, 0);
+            proj0_loop_expected.delta.add(0, 1, 1);
+            proj0_loop_expected.delta.add(1, 2, 2);
+            proj0_loop_expected.delta.add(2, DONT_CARE, 2);
+            CHECK(are_equivalent(proj0_loop, proj0_loop_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 1") {
+            Nft proj1_loop = project_out(atm_loop, 1, JumpMode::AppendDontCares);
+            Nft proj1_loop_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj1_loop_expected.delta.add(0, 3, 0);
+            proj1_loop_expected.delta.add(0, 0, 1);
+            proj1_loop_expected.delta.add(1, 2, 2);
+            proj1_loop_expected.delta.add(2, 4, 2);
+            CHECK(are_equivalent(proj1_loop, proj1_loop_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 2") {
+            Nft proj2_loop = project_out(atm_loop, 2, JumpMode::AppendDontCares);
+            Nft proj2_loop_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj2_loop_expected.delta.add(0, 3, 0);
+            proj2_loop_expected.delta.add(0, 0, 1);
+            proj2_loop_expected.delta.add(1, 1, 2);
+            proj2_loop_expected.delta.add(2, 4, 2);
+            CHECK(are_equivalent(proj2_loop, proj2_loop_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 0, 1, 2") {
+            Nft atm_empty(delta, { 0 }, {}, { 0, 1, 2, 0 }, 3);
+            Nft proj012_empty = project_out(atm_empty, { 0, 1, 2 }, JumpMode::AppendDontCares);
+            CHECK(are_equivalent(proj012_empty, Nft(1, {}, {}, {}, 0), JumpMode::AppendDontCares));
+        }
+    }
+
+    SECTION("COMPLEX") {
+        Delta delta;
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+        delta.add(0, 3, 3);
+        delta.add(3, 4, 2);
+
+        Nft nft_complex(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("project 0") {
+            Nft proj0_complex = project_out(nft_complex, 0, JumpMode::AppendDontCares);
+            Nft proj0_complex_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj0_complex_expected.delta.add(0, 1, 1);
+            proj0_complex_expected.delta.add(0, DONT_CARE, 2);
+            proj0_complex_expected.delta.add(1, 2, 2);
+            proj0_complex_expected.delta.add(2, DONT_CARE, 1);
+            CHECK(are_equivalent(proj0_complex, proj0_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 1") {
+            Nft proj1_complex = project_out(nft_complex, 1, JumpMode::AppendDontCares);
+            Nft proj1_complex_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj1_complex_expected.delta.add(0, 0, 1);
+            proj1_complex_expected.delta.add(0, 3, 2);
+            proj1_complex_expected.delta.add(1, 2, 2);
+            proj1_complex_expected.delta.add(2, 4, 1);
+            CHECK(are_equivalent(proj1_complex, proj1_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 2") {
+            Nft proj2_complex = project_out(nft_complex, OrdVector<Level>{ 2 }, JumpMode::AppendDontCares);
+            Nft proj2_complex_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj2_complex_expected.delta.add(0, 0, 1);
+            proj2_complex_expected.delta.add(0, 3, 2);
+            proj2_complex_expected.delta.add(1, 1, 2);
+            proj2_complex_expected.delta.add(2, 4, 2);
+            CHECK(are_equivalent(proj2_complex, proj2_complex_expected, JumpMode::AppendDontCares));
+
+            proj2_complex = project_to(nft_complex, OrdVector<Level>{ 2 });
+            proj2_complex_expected = Nft(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            proj2_complex_expected.delta.add(0, 2, 1);
+            proj2_complex_expected.delta.add(0, 3, 1);
+            proj2_complex_expected.delta.add(1, 2, 1);
+            CHECK(are_equivalent(proj2_complex, proj2_complex_expected, JumpMode::AppendDontCares));
+            proj2_complex_expected.delta.add(0, 0, 1);
+            CHECK(!are_equivalent(proj2_complex, proj2_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 0, 1") {
+            Nft proj01_complex = project_out(nft_complex, { 0, 1 }, JumpMode::AppendDontCares);
+            Nft proj01_complex_expected(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            proj01_complex_expected.delta.add(0, 2, 1);
+            proj01_complex_expected.delta.add(0, DONT_CARE, 1);
+            proj01_complex_expected.delta.add(1, 2, 1);
+            CHECK(are_equivalent(proj01_complex, proj01_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 0, 2") {
+            Nft proj02_complex = project_out(nft_complex, { 0, 2 }, JumpMode::AppendDontCares);
+            Nft proj02_complex_expected(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            proj02_complex_expected.delta.add(0, 1, 1);
+            proj02_complex_expected.delta.add(0, DONT_CARE, 1);
+            proj02_complex_expected.delta.add(1, DONT_CARE, 1);
+            CHECK(are_equivalent(proj02_complex, proj02_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 1, 2") {
+            Nft proj12_complex = project_out(nft_complex, { 1, 2 }, JumpMode::AppendDontCares);
+            Nft proj12_complex_expected(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            proj12_complex_expected.delta.add(0, 0, 1);
+            proj12_complex_expected.delta.add(0, 3, 1);
+            proj12_complex_expected.delta.add(1, 4, 1);
+            CHECK(are_equivalent(proj12_complex, proj12_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 0, 1, 2") {
+            Nft proj012_complex = project_out(nft_complex, { 0, 1, 2 }, JumpMode::AppendDontCares);
+            Nft proj012_complex_expected(1, { 0 }, { 0 }, {}, 0);
+            CHECK(are_equivalent(proj012_complex, proj012_complex_expected, JumpMode::AppendDontCares));
+        }
+    }
+
+    SECTION("HARD") {
+        Nft atm_hard(Delta{}, { 0 , 2 }, { 7 }, { 0, 1, 0, 2, 3, 4, 5, 0 }, 6);
+        atm_hard.delta.add(0, 1, 1);
+        atm_hard.delta.add(2, 2, 1);
+        atm_hard.delta.add(2, 3, 2);
+        atm_hard.delta.add(1, 0, 3);
+        atm_hard.delta.add(1, 10, 4);
+        atm_hard.delta.add(3, 4, 4);
+        atm_hard.delta.add(4, 5, 5);
+        atm_hard.delta.add(5, 6, 6);
+        atm_hard.delta.add(6, 7, 0);
+        atm_hard.delta.add(6, 8, 7);
+        atm_hard.delta.add(7, 9, 2);
+
+        Nft proj_hard = project_out(atm_hard, { 0, 3, 4, 5 }, JumpMode::AppendDontCares);
+
+        Nft proj_hard_expected(4, { 0, 1 }, { 3 }, { 0, 0, 1, 0 }, 2);
+        proj_hard_expected.delta.add(0, 0, 2);
+        proj_hard_expected.delta.add(0, 10, 3);
+        proj_hard_expected.delta.add(1, 0, 2);
+        proj_hard_expected.delta.add(1, 10, 3);
+        proj_hard_expected.delta.add(1, DONT_CARE, 1);
+        proj_hard_expected.delta.add(2, 4, 3);
+        proj_hard_expected.delta.add(3, 10, 3);
+        proj_hard_expected.delta.add(3, 0, 2);
+        proj_hard_expected.delta.add(3, DONT_CARE, 1);
+        CHECK(are_equivalent(proj_hard, proj_hard_expected, JumpMode::AppendDontCares));
+    }
+}
+
+TEST_CASE("mata::nft::project_out(jump_mode == JumpMode::RepeatSymbol)") {
+
+    SECTION("LINEAR") {
+        Delta delta;
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+
+        Nft atm(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("project 0") {
+            Nft proj0 = project_out(atm, 0);
+            Nft proj0_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj0_expected.delta.add(0, 1, 1);
+            proj0_expected.delta.add(1, 2, 2);
+            CHECK(are_equivalent(proj0, proj0_expected, JumpMode::AppendDontCares));
+
+        }
+
+        SECTION("project 1") {
+            Nft proj1 = project_out(atm, 1);
+            Nft proj1_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj1_expected.delta.add(0, 0, 1);
+            proj1_expected.delta.add(1, 2, 2);
+            CHECK(are_equivalent(proj1, proj1_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 2") {
+            Nft proj2 = project_out(atm, 2);
+            Nft proj2_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj2_expected.delta.add(0, 0, 1);
+            proj2_expected.delta.add(1, 1, 2);
+            CHECK(are_equivalent(proj2, proj2_expected, JumpMode::AppendDontCares));
+
+        }
+    }
+
+    SECTION("LOOP") {
+        Delta delta;
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+        delta.add(0, 3, 0);
+        delta.add(3, 4, 3);
+
+        Nft atm_loop(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("project 0") {
+            Nft proj0_loop = project_out(atm_loop, 0);
+            Nft proj0_loop_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj0_loop_expected.delta.add(0, 3, 0);
+            proj0_loop_expected.delta.add(0, 1, 1);
+            proj0_loop_expected.delta.add(1, 2, 2);
+            proj0_loop_expected.delta.add(2, 4, 2);
+            CHECK(are_equivalent(proj0_loop, proj0_loop_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 1") {
+            Nft proj1_loop = project_out(atm_loop, 1);
+            Nft proj1_loop_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj1_loop_expected.delta.add(0, 3, 0);
+            proj1_loop_expected.delta.add(0, 0, 1);
+            proj1_loop_expected.delta.add(1, 2, 2);
+            proj1_loop_expected.delta.add(2, 4, 2);
+            CHECK(are_equivalent(proj1_loop, proj1_loop_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 2") {
+            Nft proj2_loop = project_out(atm_loop, 2);
+            Nft proj2_loop_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj2_loop_expected.delta.add(0, 3, 0);
+            proj2_loop_expected.delta.add(0, 0, 1);
+            proj2_loop_expected.delta.add(1, 1, 2);
+            proj2_loop_expected.delta.add(2, 4, 2);
+            CHECK(are_equivalent(proj2_loop, proj2_loop_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 0, 1, 2") {
+            Nft atm_empty(delta, { 0 }, {}, { 0, 1, 2, 0 }, 3);
+            Nft proj012_empty = project_out(atm_empty, { 0, 1, 2 });
+            CHECK(are_equivalent(proj012_empty, Nft(1, {}, {}, {}, 0), JumpMode::AppendDontCares));
+        }
+    }
+
+    SECTION("COMPLEX") {
+        Delta delta;
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+        delta.add(0, 3, 3);
+        delta.add(3, 4, 2);
+
+        Nft atm_complex(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("project 0") {
+            Nft proj0_complex = project_out(atm_complex, 0);
+            Nft proj0_complex_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj0_complex_expected.delta.add(0, 1, 1);
+            proj0_complex_expected.delta.add(0, 3, 2);
+            proj0_complex_expected.delta.add(1, 2, 2);
+            proj0_complex_expected.delta.add(2, 4, 1);
+            CHECK(are_equivalent(proj0_complex, proj0_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 1") {
+            Nft proj1_complex = project_out(atm_complex, 1);
+            Nft proj1_complex_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj1_complex_expected.delta.add(0, 0, 1);
+            proj1_complex_expected.delta.add(0, 3, 2);
+            proj1_complex_expected.delta.add(1, 2, 2);
+            proj1_complex_expected.delta.add(2, 4, 1);
+            CHECK(are_equivalent(proj1_complex, proj1_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 2") {
+            Nft proj2_complex = project_out(atm_complex, 2);
+            Nft proj2_complex_expected(3, { 0 }, { 2 }, { 0, 1, 0 }, 2);
+            proj2_complex_expected.delta.add(0, 0, 1);
+            proj2_complex_expected.delta.add(0, 3, 2);
+            proj2_complex_expected.delta.add(1, 1, 2);
+            proj2_complex_expected.delta.add(2, 4, 2);
+            CHECK(are_equivalent(proj2_complex, proj2_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 0, 1") {
+            Nft proj01_complex = project_out(atm_complex, { 0, 1 });
+            Nft proj01_complex_expected(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            proj01_complex_expected.delta.add(0, 2, 1);
+            proj01_complex_expected.delta.add(0, 3, 1);
+            proj01_complex_expected.delta.add(1, 2, 1);
+            CHECK(are_equivalent(proj01_complex, proj01_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 0, 2") {
+            Nft proj02_complex = project_out(atm_complex, { 0, 2 });
+            Nft proj02_complex_expected(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            proj02_complex_expected.delta.add(0, 1, 1);
+            proj02_complex_expected.delta.add(0, 3, 1);
+            proj02_complex_expected.delta.add(1, 4, 1);
+            CHECK(are_equivalent(proj02_complex, proj02_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 1, 2") {
+            Nft proj12_complex = project_out(atm_complex, { 1, 2 });
+            Nft proj12_complex_expected(2, { 0 }, { 1 }, { 0, 0 }, 1);
+            proj12_complex_expected.delta.add(0, 0, 1);
+            proj12_complex_expected.delta.add(0, 3, 1);
+            proj12_complex_expected.delta.add(1, 4, 1);
+            CHECK(are_equivalent(proj12_complex, proj12_complex_expected, JumpMode::AppendDontCares));
+        }
+
+        SECTION("project 0, 1, 2") {
+            Nft proj012_complex = project_out(atm_complex, { 0, 1, 2 });
+            Nft proj012_complex_expected(1, { 0 }, { 0 }, {}, 0);
+            CHECK(are_equivalent(proj012_complex, proj012_complex_expected, JumpMode::AppendDontCares));
+        }
+    }
+
+    SECTION("HARD") {
+        Nft atm_hard(Delta{}, { 0 , 2 }, { 7 }, { 0, 1, 0, 2, 3, 4, 5, 0 }, 6);
+        atm_hard.delta.add(0, 1, 1);
+        atm_hard.delta.add(2, 2, 1);
+        atm_hard.delta.add(2, 3, 2);
+        atm_hard.delta.add(1, 0, 3);
+        atm_hard.delta.add(1, 10, 4);
+        atm_hard.delta.add(3, 4, 4);
+        atm_hard.delta.add(4, 5, 5);
+        atm_hard.delta.add(5, 6, 6);
+        atm_hard.delta.add(6, 7, 0);
+        atm_hard.delta.add(6, 8, 7);
+        atm_hard.delta.add(7, 9, 2);
+
+        Nft proj_hard = project_out(atm_hard, { 0, 3, 4, 5 }, JumpMode::RepeatSymbol);
+
+        Nft proj_hard_expected(4, { 0, 1 }, { 3 }, { 0, 0, 1, 0 }, 2);
+        proj_hard_expected.delta.add(0, 0, 2);
+        proj_hard_expected.delta.add(0, 10, 3);
+        proj_hard_expected.delta.add(1, 0, 2);
+        proj_hard_expected.delta.add(1, 10, 3);
+        proj_hard_expected.delta.add(1, 3, 1);
+        proj_hard_expected.delta.add(2, 4, 3);
+        proj_hard_expected.delta.add(3, 10, 3);
+        proj_hard_expected.delta.add(3, 0, 2);
+        proj_hard_expected.delta.add(3, 9, 1);
+        CHECK(are_equivalent(proj_hard, proj_hard_expected, JumpMode::AppendDontCares));
+    }
+}
+
+TEST_CASE("mata::nft::project_to()") {
+    Nft nft;
+    Nft projection;
+    Nft expected;
+
+    SECTION("linear") {
+        nft = Nft{ {}, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3 };
+        nft.delta.add(0, 0, 1);
+        nft.delta.add(1, 1, 2);
+        nft.delta.add(2, 2, 3);
+        projection = project_to(nft, 2);
+        expected = Nft{ 2, { 0 }, { 1 }, { 0, 0 }, 1 };
+        expected.delta.add(0, 2, 1);
+        CHECK(nft::are_equivalent(projection, expected));
+    }
+
+    SECTION("linear longer") {
+        nft = Nft{ {}, { 0 }, { 6 }, { 0, 1, 2, 0, 1, 2, 0 }, 3 };
+        nft.delta.add(0, 0, 1);
+        nft.delta.add(1, 1, 2);
+        nft.delta.add(2, 2, 3);
+        nft.delta.add(3, 3, 4);
+        nft.delta.add(4, 4, 5);
+        nft.delta.add(5, 5, 6);
+        projection = project_to(nft, 2);
+        expected = Nft{ {}, { 0 }, { 2 }, { 0, 0, 0 }, 1 };
+        expected.delta.add(0, 2, 1);
+        expected.delta.add(1, 5, 2);
+        CHECK(nft::are_equivalent(projection, expected));
+    }
+
+    SECTION("linear longer symbol long jump") {
+        nft = Nft{ {}, { 0 }, { 6 }, { 0, 1, 2, 0, 1, 2, 0 }, 3 };
+        nft.delta.add(0, 0, 1);
+        nft.delta.add(1, 1, 2);
+        nft.delta.add(2, 2, 3);
+        nft.delta.add(3, 3, 4);
+        nft.delta.add(4, 4, 5);
+        nft.delta.add(5, 5, 6);
+        nft.delta.add(0, 'j', 6);
+        projection = project_to(nft, 2);
+        expected = Nft{ {}, { 0 }, { 2 }, { 0, 0, 0 }, 1 };
+        expected.delta.add(0, 2, 1);
+        expected.delta.add(1, 5, 2);
+        expected.delta.add(0, 'j', 2);
+        CHECK(nft::are_equivalent(projection, expected));
+        expected.delta.add(0, 'b', 2);
+        CHECK(!nft::are_equivalent(projection, expected));
+    }
+
+    SECTION("cycle longer") {
+        nft = Nft{ {}, { 0 }, { 6 }, { 0, 1, 2, 0, 1, 2, 0, 1, 2, 1, 2 }, 3 };
+        nft.delta.add(0, 0, 1);
+        nft.delta.add(1, 1, 2);
+        nft.delta.add(2, 2, 3);
+        nft.delta.add(3, 3, 4);
+        nft.delta.add(4, 4, 5);
+        nft.delta.add(5, 5, 6);
+        nft.delta.add(3, 6, 7);
+        nft.delta.add(7, 7, 8);
+        nft.delta.add(8, 8, 0);
+        nft.delta.add(6, 9, 9);
+        nft.delta.add(9, 9, 10);
+        nft.delta.add(10, 10, 0);
+        projection = project_to(nft, 2);
+        expected = Nft{ {}, { 0 }, { 2 }, { 0, 0, 0 }, 1 };
+        expected.delta.add(0, 2, 1);
+        expected.delta.add(1, 8, 0);
+        expected.delta.add(1, 5, 2);
+        expected.delta.add(2, 10, 0);
+        CHECK(nft::are_equivalent(projection, expected));
+    }
+
+    SECTION("cycle longer project to { 0, 2 }") {
+        nft = Nft{ {}, { 0 }, { 6 }, { 0, 1, 2, 0, 1, 2, 0, 1, 2, 1, 2 }, 3 };
+        nft.delta.add(0, 0, 1);
+        nft.delta.add(1, 1, 2);
+        nft.delta.add(2, 2, 3);
+        nft.delta.add(3, 3, 4);
+        nft.delta.add(4, 4, 5);
+        nft.delta.add(5, 5, 6);
+        nft.delta.add(3, 6, 7);
+        nft.delta.add(7, 7, 8);
+        nft.delta.add(8, 8, 0);
+        nft.delta.add(6, 9, 9);
+        nft.delta.add(9, 9, 10);
+        nft.delta.add(10, 10, 0);
+        projection = project_to(nft, { 0, 2 });
+        expected = Nft{ {}, { 0 }, { 6 }, { 0, 1, 0, 1, 0, 1, 0 }, 2 };
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(1, 2, 3);
+        expected.delta.add(3, 6, 4);
+        expected.delta.add(3, 3, 5);
+        expected.delta.add(4, 8, 0);
+        expected.delta.add(5, 5, 6);
+        expected.delta.add(6, 9, 7);
+        expected.delta.add(7, 10, 0);
+        CHECK(nft::are_equivalent(projection, expected));
+    }
+
+    SECTION("cycle longer project to { 0, 2 } with epsilon and dont care symbols") {
+        nft = Nft{ {}, { 0 }, { 6 }, { 0, 1, 2, 0, 1, 2, 0, 1, 2, 1, 2 }, 3 };
+        nft.delta.add(0, EPSILON, 2);
+        nft.delta.add(2, 2, 3);
+        nft.delta.add(3, 3, 4);
+        nft.delta.add(4, 4, 5);
+        nft.delta.add(5, 5, 6);
+        nft.delta.add(3, EPSILON, 7);
+        nft.delta.add(7, 7, 8);
+        nft.delta.add(8, DONT_CARE, 0);
+        nft.delta.add(6, 9, 9);
+        nft.delta.add(9, 10, 10);
+        nft.delta.add(10, 11, 0);
+        projection = project_to(nft, { 0, 2 });
+        expected = Nft{ {}, { 0 }, { 6 }, { 0, 1, 0, 0, 1, 1, 0, 1 }, 2 };
+        expected.delta.add(0, EPSILON, 1);
+        expected.delta.add(1, 2, 3);
+        expected.delta.add(3, EPSILON, 4);
+        expected.delta.add(3, 3, 5);
+        expected.delta.add(4, DONT_CARE, 0);
+        expected.delta.add(5, 5, 6);
+        expected.delta.add(6, 9, 7);
+        expected.delta.add(7, 11, 0);
+        CHECK(nft::are_equivalent(projection, expected));
+    }
+}
+
+TEST_CASE("mata::nft::insert_level() and mata::nft::insert_levels()") {
+    Delta delta;
+    Nft input_nft, output_nft, expected_nft;
+
+    SECTION("Linear - jump_mode == JumpMode::AppendDontCares") {
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+
+        input_nft = Nft(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("add level 0") {
+            output_nft = insert_level(input_nft, 0, JumpMode::AppendDontCares);
+            expected_nft = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected_nft.delta.add(0, DONT_CARE, 1);
+            expected_nft.delta.add(1, 0, 2);
+            expected_nft.delta.add(2, 1, 3);
+            expected_nft.delta.add(3, 2, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 1") {
+            output_nft = insert_level(input_nft, 1, JumpMode::AppendDontCares);
+            expected_nft = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, DONT_CARE, 2);
+            expected_nft.delta.add(2, 1, 3);
+            expected_nft.delta.add(3, 2, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 2") {
+            output_nft = insert_level(input_nft, 2, JumpMode::AppendDontCares);
+            expected_nft = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, DONT_CARE, 3);
+            expected_nft.delta.add(3, 2, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 3") {
+            output_nft = insert_level(input_nft, 3, JumpMode::AppendDontCares);
+            expected_nft = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, 2, 3);
+            expected_nft.delta.add(3, DONT_CARE, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 4") {
+            output_nft = insert_level(input_nft, 4, JumpMode::AppendDontCares);
+            expected_nft = Nft(6, { 0 }, { 5 }, { 0, 1, 2, 3, 4, 0 }, 5);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, 2, 3);
+            expected_nft.delta.add(3, DONT_CARE, 4);
+            expected_nft.delta.add(4, DONT_CARE, 5);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add levels according to the mask 100011") {
+            output_nft = insert_levels(input_nft, { 1, 0, 0, 0, 1, 1 }, JumpMode::AppendDontCares);
+            expected_nft = Nft(7, { 0 }, { 6 }, { 0, 1, 2, 3, 4, 5, 0 }, 6);
+            expected_nft.delta.add(0, DONT_CARE, 1);
+            expected_nft.delta.add(1, 0, 2);
+            expected_nft.delta.add(2, 1, 3);
+            expected_nft.delta.add(3, 2, 4);
+            expected_nft.delta.add(4, DONT_CARE, 5);
+            expected_nft.delta.add(5, DONT_CARE, 6);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+    }
+
+    SECTION("Linear - default_symbol = DONT_CARE, jump_mode == JumpMode::RepeatSymbol") {
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+
+        input_nft = Nft(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("add level 0") {
+            output_nft = insert_level(input_nft, 0);
+            expected_nft = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected_nft.delta.add(0, DONT_CARE, 1);
+            expected_nft.delta.add(1, 0, 2);
+            expected_nft.delta.add(2, 1, 3);
+            expected_nft.delta.add(3, 2, 4);
+
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::RepeatSymbol));
+        }
+
+        SECTION("add level 1") {
+            output_nft = insert_level(input_nft, 1);
+            expected_nft = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, DONT_CARE, 2);
+            expected_nft.delta.add(2, 1, 3);
+            expected_nft.delta.add(3, 2, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::RepeatSymbol));
+        }
+
+        SECTION("add level 2") {
+            output_nft = insert_level(input_nft, 2);
+            expected_nft = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, DONT_CARE, 3);
+            expected_nft.delta.add(3, 2, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::RepeatSymbol));
+        }
+
+        SECTION("add level 3") {
+            output_nft = insert_level(input_nft, 3);
+            expected_nft = Nft(5, { 0 }, { 4 }, { 0, 1, 2, 3, 0 }, 4);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, 2, 3);
+            expected_nft.delta.add(3, DONT_CARE, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::RepeatSymbol));
+        }
+
+        SECTION("add level 4") {
+            output_nft = insert_level(input_nft, 4);
+            expected_nft = Nft(6, { 0 }, { 5 }, { 0, 1, 2, 3, 4, 0 }, 5);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, 2, 3);
+            expected_nft.delta.add(3, DONT_CARE, 4);
+            expected_nft.delta.add(4, DONT_CARE, 5);
+
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::RepeatSymbol));
+        }
+
+        SECTION("add levels according to the mask 100011") {
+            output_nft = insert_levels(input_nft, { 1, 0, 0, 0, 1, 1 });
+            expected_nft = Nft(7, { 0 }, { 6 }, { 0, 1, 2, 3, 4, 5, 0 }, 6);
+            expected_nft.delta.add(0, DONT_CARE, 1);
+            expected_nft.delta.add(1, 0, 2);
+            expected_nft.delta.add(2, 1, 3);
+            expected_nft.delta.add(3, 2, 4);
+            expected_nft.delta.add(4, DONT_CARE, 5);
+            expected_nft.delta.add(5, DONT_CARE, 6);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::RepeatSymbol));
+        }
+    }
+
+    SECTION("loop - default_symbol = DONT_CARE, jump_mode == JumpMode::AppendDontCares") {
+        delta.clear();
+        delta.add(0, 4, 0);
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 2);
+        delta.add(2, 2, 3);
+        delta.add(3, 5, 3);
+
+        input_nft = Nft(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("add level 0") {
+            output_nft = insert_level(input_nft, 0, JumpMode::AppendDontCares);
+            expected_nft = Nft(7, { 0 }, { 4 }, { 0, 1, 2, 3, 0, 1, 1 }, 4);
+            expected_nft.delta.add(0, DONT_CARE, 5);
+            expected_nft.delta.add(5, 4, 0);
+            expected_nft.delta.add(0, DONT_CARE, 1);
+            expected_nft.delta.add(1, 0, 2);
+            expected_nft.delta.add(2, 1, 3);
+            expected_nft.delta.add(3, 2, 4);
+            expected_nft.delta.add(4, DONT_CARE, 6);
+            expected_nft.delta.add(6, 5, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 1") {
+            output_nft = insert_level(input_nft, 1, JumpMode::AppendDontCares);
+            expected_nft = Nft(11, { 0 }, { 4 }, { 0, 1, 2, 3, 0, 1, 1, 2, 3, 2, 3}, 4);
+            expected_nft.delta.add(0, 4, 5);
+            expected_nft.delta.add(5, DONT_CARE, 7);
+            expected_nft.delta.add(7, DONT_CARE, 8);
+            expected_nft.delta.add(8, DONT_CARE, 0);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, DONT_CARE, 2);
+            expected_nft.delta.add(2, 1, 3);
+            expected_nft.delta.add(3, 2, 4);
+            expected_nft.delta.add(4, 5, 6);
+            expected_nft.delta.add(6, DONT_CARE, 9);
+            expected_nft.delta.add(9, DONT_CARE, 10);
+            expected_nft.delta.add(10, DONT_CARE, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 2") {
+            output_nft = insert_level(input_nft, 2, JumpMode::AppendDontCares);
+            expected_nft = Nft(9, { 0 }, { 4 }, { 0, 1, 2, 3, 0, 2, 3, 2, 3 }, 4);
+            expected_nft.delta.add(0, 4, 7);
+            expected_nft.delta.add(7, DONT_CARE, 8);
+            expected_nft.delta.add(8, DONT_CARE, 0);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, DONT_CARE, 3);
+            expected_nft.delta.add(3, 2, 4);
+            expected_nft.delta.add(4, 5, 5);
+            expected_nft.delta.add(5, DONT_CARE, 6);
+            expected_nft.delta.add(6, DONT_CARE, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 3") {
+            output_nft = insert_level(input_nft, 3, JumpMode::AppendDontCares);
+            expected_nft = Nft(7, { 0 }, { 4 }, { 0, 1, 2, 3, 0, 3, 3 }, 4);
+            expected_nft.delta.add(0, 4, 5);
+            expected_nft.delta.add(5, DONT_CARE, 0);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, 2, 3);
+            expected_nft.delta.add(3, DONT_CARE, 4);
+            expected_nft.delta.add(4, 5, 6);
+            expected_nft.delta.add(6, DONT_CARE, 4);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add levels according to the mask 1010011") {
+            output_nft = insert_levels(input_nft, { 1, 0, 1, 0, 0, 1, 1 }, JumpMode::AppendDontCares);
+            expected_nft = Nft(20, { 0 }, { 7 }, { 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6 }, 7);
+            expected_nft.delta.add(0, DONT_CARE, 8);
+            expected_nft.delta.add(8, 4, 9);
+            expected_nft.delta.add(9, DONT_CARE, 10);
+            expected_nft.delta.add(10, DONT_CARE, 11);
+            expected_nft.delta.add(11, DONT_CARE, 12);
+            expected_nft.delta.add(12, DONT_CARE, 13);
+            expected_nft.delta.add(13, DONT_CARE, 0);
+            expected_nft.delta.add(0, DONT_CARE, 1);
+            expected_nft.delta.add(1, 0, 2);
+            expected_nft.delta.add(2, DONT_CARE, 3);
+            expected_nft.delta.add(3, 1, 4);
+            expected_nft.delta.add(4, 2, 5);
+            expected_nft.delta.add(5, DONT_CARE, 6);
+            expected_nft.delta.add(6, DONT_CARE, 7);
+            expected_nft.delta.add(7, DONT_CARE, 14);
+            expected_nft.delta.add(14, 5, 15);
+            expected_nft.delta.add(15, DONT_CARE, 16);
+            expected_nft.delta.add(16, DONT_CARE, 17);
+            expected_nft.delta.add(17, DONT_CARE, 18);
+            expected_nft.delta.add(18, DONT_CARE, 19);
+            expected_nft.delta.add(19, DONT_CARE, 7);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+    }
+
+    SECTION("complex - default_symbol = DONT_CARE, jump_mode == JumpMode::AppendDontCares") {
+        delta.clear();
+        delta.add(0, 0, 1);
+        delta.add(0, 4, 2);
+        delta.add(1, 1, 2);
+        delta.add(1, 5, 3);
+        delta.add(2, 2, 3);
+        delta.add(3, 3, 0);
+
+        input_nft = Nft(delta, { 0 }, { 3 }, { 0, 1, 2, 0 }, 3);
+
+        SECTION("add level 0") {
+            output_nft = insert_level(input_nft, 0, JumpMode::AppendDontCares);
+            expected_nft = Nft(7, { 0 }, { 3 }, { 0, 2, 3, 0, 1, 1, 1 }, 4);
+            expected_nft.delta.add(0, DONT_CARE, 4);
+            expected_nft.delta.add(0, DONT_CARE, 5);
+            expected_nft.delta.add(4, 0, 1);
+            expected_nft.delta.add(5, 4, 2);
+            expected_nft.delta.add(1, 5, 3);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(2, 2, 3);
+            expected_nft.delta.add(3, DONT_CARE, 6);
+            expected_nft.delta.add(6, 3, 0);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 1") {
+            output_nft = insert_level(input_nft, 1, JumpMode::AppendDontCares);
+            expected_nft = Nft(8, { 0 }, { 3 }, { 0, 1, 3, 0, 2, 2, 2, 2 }, 4);
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(0, 4, 6);
+            expected_nft.delta.add(1, DONT_CARE, 4);
+            expected_nft.delta.add(1, DONT_CARE, 5);
+            expected_nft.delta.add(4, 1, 2);
+            expected_nft.delta.add(5, 5, 3);
+            expected_nft.delta.add(6, DONT_CARE, 2);
+            expected_nft.delta.add(2, 2, 3);
+            expected_nft.delta.add(3, 3, 7);
+            expected_nft.delta.add(7, DONT_CARE, 0);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 2") {
+            output_nft = insert_level(input_nft, 2, JumpMode::AppendDontCares);
+            expected_nft = Nft(7, { 0 }, { 3 }, {0, 1, 2, 0, 2, 3, 2 }, 4 );
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(0, 4, 2);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(1, 5, 4);
+            expected_nft.delta.add(2, DONT_CARE, 5);
+            expected_nft.delta.add(5, 2, 3);
+            expected_nft.delta.add(4, DONT_CARE, 3);
+            expected_nft.delta.add(3, 3, 6);
+            expected_nft.delta.add(6, DONT_CARE, 0);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add level 3") {
+            output_nft = insert_level(input_nft, 3, JumpMode::AppendDontCares);
+            expected_nft = Nft(7, { 0 }, { 3 }, {0, 1, 2, 0, 3, 3, 3 }, 4 );
+            expected_nft.delta.add(0, 0, 1);
+            expected_nft.delta.add(0, 4, 2);
+            expected_nft.delta.add(1, 1, 2);
+            expected_nft.delta.add(1, 5, 4);
+            expected_nft.delta.add(2, 2, 5);
+            expected_nft.delta.add(5, DONT_CARE, 3);
+            expected_nft.delta.add(4, DONT_CARE, 3);
+            expected_nft.delta.add(3, 3, 6);
+            expected_nft.delta.add(6, DONT_CARE, 0);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+
+        SECTION("add levels according to the mask 1010011") {
+            output_nft = insert_levels(input_nft, { 1, 0, 1, 0, 0, 1, 1 }, JumpMode::AppendDontCares);
+            expected_nft = Nft(21, { 0 }, { 3 }, { 0, 2, 4, 0, 1, 1, 3, 3, 3, 5, 5, 6, 6, 1, 5, 6, 3, 2, 4, 2, 4 }, 7);
+            expected_nft.delta.add(0, DONT_CARE, 5);
+            expected_nft.delta.add(5, 0, 1);
+            expected_nft.delta.add(0, DONT_CARE, 4);
+            expected_nft.delta.add(4, 4, 17);
+            expected_nft.delta.add(17, DONT_CARE, 8);
+            expected_nft.delta.add(8, DONT_CARE, 2);
+            expected_nft.delta.add(1, DONT_CARE, 6);
+            expected_nft.delta.add(1, DONT_CARE, 7);
+            expected_nft.delta.add(6, 5, 18);
+            expected_nft.delta.add(18, DONT_CARE, 9);
+            expected_nft.delta.add(9, DONT_CARE, 11);
+            expected_nft.delta.add(11, DONT_CARE, 3);
+            expected_nft.delta.add(7, 1, 2);
+            expected_nft.delta.add(2, 2, 10);
+            expected_nft.delta.add(10, DONT_CARE, 12);
+            expected_nft.delta.add(12, DONT_CARE, 3);
+            expected_nft.delta.add(3, DONT_CARE, 13);
+            expected_nft.delta.add(13, 3, 19);
+            expected_nft.delta.add(19, DONT_CARE, 16);
+            expected_nft.delta.add(16, DONT_CARE, 20);
+            expected_nft.delta.add(20, DONT_CARE, 14);
+            expected_nft.delta.add(14, DONT_CARE, 15);
+            expected_nft.delta.add(15, DONT_CARE, 0);
+            CHECK(are_equivalent(output_nft, expected_nft, JumpMode::AppendDontCares));
+        }
+    }
+
+    SECTION("Testing inner state reuse with JumpMode::RepeatSymbol.") {
+        delta.clear();
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 3);
+        delta.add(1, 2, 3);
+        delta.add(1, 3, 2);
+        delta.add(2, 4, 3);
+
+        input_nft = Nft(delta, { 0 }, { 3 }, { 0, 1, 3, 0 }, 4);
+
+        output_nft = insert_levels(input_nft, { 0, 1, 1, 0, 0, 1, 0 }, JumpMode::RepeatSymbol);
+        CHECK(output_nft.num_of_states() == 13);
+    }
+
+    SECTION("Testing inner state reuse with JumpMode::AppendDontCares.") {
+        delta.clear();
+        delta.add(0, 0, 1);
+        delta.add(1, 1, 3);
+        delta.add(1, 2, 3);
+        delta.add(1, 3, 2);
+        delta.add(2, 4, 3);
+
+        input_nft = Nft(delta, { 0 }, { 3 }, { 0, 1, 3, 0 }, 4);
+
+        output_nft = insert_levels(input_nft, { 0, 1, 1, 0, 0, 1, 0 }, JumpMode::AppendDontCares);
+        CHECK(output_nft.num_of_states() == 9);
+    }
+}
+TEST_CASE("mata::nft::Nft::insert_word()") {
+    Delta delta;
+    delta.add(0, 0, 1);
+    delta.add(0, 4, 0);
+    delta.add(1, 1, 2);
+    delta.add(1, 5, 1);
+    delta.add(2, 2, 3);
+    delta.add(2, 6, 2);
+    delta.add(3, 3, 4);
+    delta.add(3, 7, 3);
+
+    Nft nft, expected;
+
+    SECTION("Insert 'a'") {
+        SECTION("num_of_levels == 1") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 1);
+            State new_tgt = nft.insert_word(1, {'a'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 1);
+            expected.delta.add(1, 'a', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("num_of_levels == 3") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 3);
+            State new_tgt = nft.insert_word(1, {'a'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 3);
+            expected.delta.add(1, 'a', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("self-loop, num_of_levels == 1") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 1);
+            State new_tgt = nft.insert_word(3, {'a'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 1);
+            expected.delta.add(3, 'a', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("self-loop, num_of_levels == 3") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 3);
+            State new_tgt = nft.insert_word(3, {'a'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 3);
+            expected.delta.add(3, 'a', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("tgt is not specified") {
+            nft = Nft(delta, { 0 }, {}, { 0, 0, 0, 0, 0 }, 1);
+            State new_tgt = nft.insert_word(1, {'a'});
+            nft.final.insert(new_tgt);
+
+            expected = Nft(3, { 0 }, { 2 }, { 0, 0, 0, 0}, 1);
+            expected.delta.add(0, 0, 1);
+            expected.delta.add(0, 4, 0);
+            expected.delta.add(1, 5, 1);
+            expected.delta.add(1, 'a', 2);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+    }
+
+    SECTION("Insert 'ab'") {
+        SECTION("num_of_levels == 1") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 1);
+            State new_tgt = nft.insert_word(1, {'a', 'b'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0, 0 }, 1);
+            expected.delta.add(1, 'a', 5);
+            expected.delta.add(5, 'b', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("num_of_levels == 3") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 3);
+            State new_tgt = nft.insert_word(1, {'a', 'b'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0, 1 }, 3);
+            expected.delta.add(1, 'a', 5);
+            expected.delta.add(5, 'b', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("self-loop, num_of_levels == 1") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 1);
+            State new_tgt = nft.insert_word(3, {'a', 'b'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0, 0 }, 1);
+            expected.delta.add(3, 'a', 5);
+            expected.delta.add(5, 'b', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("self-loop, num_of_levels == 3") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0 }, 3);
+            State new_tgt = nft.insert_word(3, {'a', 'b'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0, 1 }, 3);
+            expected.delta.add(3, 'a', 5);
+            expected.delta.add(5, 'b', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("tgt is not specified") {
+            nft = Nft(delta, { 0 }, {}, { 0, 0, 0, 0, 0 }, 1);
+            State new_tgt = nft.insert_word(1, {'a', 'b'});
+            nft.final.insert(new_tgt);
+
+            expected = Nft(4, { 0 }, { 3 }, { 0, 0, 0, 0, 0 }, 1);
+            expected.delta.add(0, 0, 1);
+            expected.delta.add(0, 4, 0);
+            expected.delta.add(1, 5, 1);
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'b', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+    }
+
+    SECTION("Insert 'abcd'") {
+        SECTION("num_of_levels == 1") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0}, 1);
+            State new_tgt = nft.insert_word(1, {'a', 'b', 'c', 'd'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0, 0, 0, 0}, 1);
+            expected.delta.add(1, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("num_of_levels == 3") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0}, 3);
+            State new_tgt = nft.insert_word(1, {'a', 'b', 'c', 'd'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0, 1, 2, 0}, 3);
+            expected.delta.add(1, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("self-loop, num_of_levels == 1") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0}, 1);
+            State new_tgt = nft.insert_word(3, {'a', 'b', 'c', 'd'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0, 0, 0, 0}, 1);
+            expected.delta.add(3, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("self-loop, num_of_levels == 3") {
+            nft = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0}, 3);
+            State new_tgt = nft.insert_word(3, {'a', 'b', 'c', 'd'}, 3);
+            CHECK(new_tgt == 3);
+
+            expected = Nft(delta, { 0 }, { 4 }, { 0, 0, 0, 0, 0, 1, 2, 0}, 3);
+            expected.delta.add(3, 'a', 5);
+            expected.delta.add(5, 'b', 6);
+            expected.delta.add(6, 'c', 7);
+            expected.delta.add(7, 'd', 3);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("tgt is not specified") {
+            nft = Nft(delta, { 0 }, {}, { 0, 0, 0, 0, 0 }, 1);
+            State new_tgt = nft.insert_word(1, {'a', 'b', 'c', 'd'});
+            nft.final.insert(new_tgt);
+
+            expected = Nft(6, { 0 }, { 5 }, { 0, 0, 0, 0, 0, 0, 0 }, 1);
+            expected.delta.add(0, 0, 1);
+            expected.delta.add(0, 4, 0);
+            expected.delta.add(1, 5, 1);
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'b', 3);
+            expected.delta.add(3, 'c', 4);
+            expected.delta.add(4, 'd', 5);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+    }
+
+    SECTION("add_transition()") {
+        nft = Nft::with_levels(3);
+        nft.add_transition(0, { 'a', 'b', 'c' }, 1);
+        nft.add_transition(1, { 'd', 'e', 'f' }, 2);
+
+        expected = Nft::with_levels(3, {}, { 0, 0, 0, 1, 2, 1, 2 });
+        expected.delta.add(0, 'a', 3);
+        expected.delta.add(3, 'b', 4);
+        expected.delta.add(4, 'c', 1);
+        expected.delta.add(1, 'd', 5);
+        expected.delta.add(5, 'e', 6);
+        expected.delta.add(6, 'f', 2);
+
+        CHECK(are_equivalent(nft, expected));
+    }
+}
+
+TEST_CASE("mata::nft::Nft::insert_identity()") {
+    Nft nft, expected;
+    SECTION("Creating an identity on two states (both initial and final) with empty delta.") {
+        Delta delta{};
+        SECTION("num_of_levels == 1") {
+            nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 1);
+            nft.insert_identity(0, 'a');
+            nft.insert_identity(1, 'b');
+
+            expected = Nft(2, { 0, 1 }, { 0, 1 }, { 0, 0 }, 1);
+            expected.delta.add(0, 'a', 0);
+            expected.delta.add(1, 'b', 1);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("num_of_levels == 2") {
+            nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 2);
+            nft.insert_identity(0, 'a');
+            nft.insert_identity(1, 'b');
+
+            expected = Nft(4, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 1 }, 2);
+            expected.delta.add(0, 'a', 2);
+            expected.delta.add(2, 'a', 0);
+            expected.delta.add(1, 'b', 3);
+            expected.delta.add(3, 'b', 1);
+
+            CHECK(are_equivalent(nft, expected, JumpMode::RepeatSymbol));
+        }
+
+        SECTION("num_of_levels == 4") {
+            nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 4);
+            nft.insert_identity(0, 'a', JumpMode::AppendDontCares);
+            nft.insert_identity(1, 'b', JumpMode::AppendDontCares);
+
+            expected = Nft(8, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 1, 2, 2, 3, 3 }, 4);
+            expected.delta.add(0, 'a', 2);
+            expected.delta.add(2, 'a', 4);
+            expected.delta.add(4, 'a', 6);
+            expected.delta.add(6, 'a', 0);
+            expected.delta.add(1, 'b', 3);
+            expected.delta.add(3, 'b', 5);
+            expected.delta.add(5, 'b', 7);
+            expected.delta.add(7, 'b', 1);
+
+            CHECK(are_equivalent(nft, expected, JumpMode::AppendDontCares));
+        }
+    }
+
+    SECTION("Creating an identity on a state with incoming and outcoming transitions.") {
+        Delta delta{};
+        delta.add(0, 'a', 1);
+        delta.add(1, 'b', 2);
+
+        SECTION("num_of_levels == 1") {
+
+            SECTION("symbols cnt == 1") {
+                nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 1);
+                nft.insert_identity(1, 'c', JumpMode::AppendDontCares);
+
+                expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 1);
+                expected.delta.add(1, 'c', 1);
+
+                CHECK(are_equivalent(nft, expected, JumpMode::AppendDontCares));
+            }
+
+            SECTION("symbols cnt == 2") {
+                nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 1);
+                nft.insert_identity(1, {'c', 'd'});
+
+                expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0, 1 }, 1);
+                expected.insert_identity(1, 'c');
+                expected.insert_identity(1, 'd');
+
+                CHECK(are_equivalent(nft, expected, JumpMode::RepeatSymbol));
+            }
+        }
+
+        SECTION("num_of_levels == 2") {
+
+            SECTION("symbols cnt == 1") {
+                nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 2);
+                nft.insert_identity(1, 'c');
+
+                expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0, 1 }, 2);
+                expected.delta.add(1, 'c', 3);
+                expected.delta.add(3, 'c', 1);
+
+                CHECK(are_equivalent(nft, expected, JumpMode::RepeatSymbol));
+            }
+
+            SECTION("symbols cnt == 2") {
+                nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 2);
+                nft.insert_identity(1, {'c', 'd'}, JumpMode::AppendDontCares);
+
+                expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0, 1 }, 2);
+                expected.insert_identity(1, 'c', JumpMode::AppendDontCares);
+                expected.insert_identity(1, 'd', JumpMode::AppendDontCares);
+
+                CHECK(are_equivalent(nft, expected, JumpMode::AppendDontCares));
+            }
+        }
+
+        SECTION("num_of_levels == 4") {
+
+            SECTION("symbols cnt == 1") {
+                nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 4);
+                nft.insert_identity(1, 'c');
+
+                expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0, 1, 2, 3 }, 4);
+                expected.delta.add(1, 'c', 3);
+                expected.delta.add(3, 'c', 4);
+                expected.delta.add(4, 'c', 5);
+                expected.delta.add(5, 'c', 1);
+
+                CHECK(are_equivalent(nft, expected, JumpMode::RepeatSymbol));
+            }
+
+            SECTION("symbols cnt == 4") {
+                nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 4);
+                nft.insert_identity(1, {'c', 'd', 'e', 'f'});
+
+                expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 4);
+                expected.insert_identity(1, 'c');
+                expected.insert_identity(1, 'd');
+                expected.insert_identity(1, 'e');
+                expected.insert_identity(1, 'f');
+
+                CHECK(are_equivalent(nft, expected, JumpMode::RepeatSymbol));
+
+            }
+        }
+    }
+
+    SECTION("Creating an identity on a state with only incoming transitions.") {
+        Delta delta{};
+        delta.add(0, 'a', 1);
+        delta.add(1, 'b', 2);
+
+        SECTION("num_of_levels == 1") {
+            nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 1);
+            nft.insert_identity(2, 'c');
+
+            expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 1);
+            expected.delta.add(2, 'c', 2);
+
+            CHECK(are_equivalent(nft, expected, JumpMode::RepeatSymbol));
+        }
+
+        SECTION("num_of_levels == 2") {
+            nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 2);
+            nft.insert_identity(2, 'c');
+
+            expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0, 1 }, 2);
+            expected.delta.add(2, 'c', 3);
+            expected.delta.add(3, 'c', 2);
+
+            CHECK(are_equivalent(nft, expected, JumpMode::RepeatSymbol));
+        }
+
+        SECTION("num_of_levels == 4") {
+            nft = Nft(delta, { 0 }, { 2 }, { 0, 0, 0 }, 4);
+            nft.insert_identity(2, 'c');
+
+            expected = Nft(delta, { 0 }, { 2 }, { 0, 0, 0, 1, 2, 3 }, 4);
+            expected.delta.add(2, 'c', 3);
+            expected.delta.add(3, 'c', 4);
+            expected.delta.add(4, 'c', 5);
+            expected.delta.add(5, 'c', 2);
+
+            CHECK(are_equivalent(nft, expected, JumpMode::RepeatSymbol));
+        }
+    }
+}
+
+TEST_CASE("mata::nft::Nft::insert_word_by_parts()") {
+    Nft nft, expected;
+
+    SECTION("Inserting a word between two states (both initial and final) with empty delta.") {
+
+        Delta delta{};
+
+        SECTION("num_of_levels == 1 && word_part.size() == 1") {
+            nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 1);
+            State new_tgt = nft.insert_word_by_parts(0, { {'a'} } , 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 1);
+            expected.delta.add(0, 'a', 1);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("num_of_levels == 1 && word_part.size() == 2") {
+            nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 1);
+            State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b'} } , 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 0 }, 1);
+            expected.delta.add(0, 'a', 2);
+            expected.delta.add(2, 'b', 1);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("num_of_levels == 1 && word_part.size() == 4") {
+            nft = Nft(delta, { 0, 1 }, { 0, 1  }, { 0, 0, 0, 0, 0 }, 1);
+            State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b', 'c', 'd'} }, 1);
+            CHECK(new_tgt == 1);
+
+            expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 0, 0, 0 }, 1);
+            expected.delta.add(0, 'a', 2);
+            expected.delta.add(2, 'b', 3);
+            expected.delta.add(3, 'c', 4);
+            expected.delta.add(4, 'd', 1);
+
+            CHECK(are_equivalent(nft, expected));
+        }
+    }
+
+    SECTION("The source sate and target state are in delta.") {
+
+        Delta delta;
+        delta.add(0, 'w', 0);
+        delta.add(0, 'y', 1);
+        delta.add(1, 'x', 1);
+        delta.add(1, 'z', 0);
+        SECTION("num_of_levels == 2") {
+
+            SECTION("word_part.size() == 1") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a'}, {'b'} } , 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1 }, 2);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'b', 1);
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 2") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b'}, {'c', 'd'} } , 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 0, 1 }, 2);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'c', 3);
+                expected.delta.add(3, 'b', 4);
+                expected.delta.add(4, 'd', 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 4") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b', 'c', 'd'}, {'e', 'f', 'g', 'h'} }, 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 0, 1, 0, 1, 0, 1 }, 2);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'e', 3);
+                expected.delta.add(3, 'b', 4);
+                expected.delta.add(4, 'f', 5);
+                expected.delta.add(5, 'c', 6);
+                expected.delta.add(6, 'g', 7);
+                expected.delta.add(7, 'd', 8);
+                expected.delta.add(8, 'h', 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+        }
+
+        SECTION("num_of_levels == 4") {
+
+            SECTION("word_part.size() == 1") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a'}, {'b'}, {'c'}, {'d'} }, 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 2, 3 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'b', 3);
+                expected.delta.add(3, 'c', 4);
+                expected.delta.add(4, 'd', 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 2") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b'}, {'c', 'd'}, {'e', 'f'}, {'g', 'h'} }, 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 2, 3, 0, 1, 2, 3 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'c', 3);
+                expected.delta.add(3, 'e', 4);
+                expected.delta.add(4, 'g', 5);
+                expected.delta.add(5, 'b', 6);
+                expected.delta.add(6, 'd', 7);
+                expected.delta.add(7, 'f', 8);
+                expected.delta.add(8, 'h', 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 4") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b', 'c', 'd'}, {'e', 'f', 'g', 'h'}, {'i', 'j', 'k', 'l'}, {'m', 'n', 'o', 'p'} }, 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'e', 3);
+                expected.delta.add(3, 'i', 4);
+                expected.delta.add(4, 'm', 5);
+                expected.delta.add(5, 'b', 6);
+                expected.delta.add(6, 'f', 7);
+                expected.delta.add(7, 'j', 8);
+                expected.delta.add(8, 'n', 9);
+                expected.delta.add(9, 'c', 10);
+                expected.delta.add(10, 'g', 11);
+                expected.delta.add(11, 'k', 12);
+                expected.delta.add(12, 'o', 13);
+                expected.delta.add(13, 'd', 14);
+                expected.delta.add(14, 'h', 15);
+                expected.delta.add(15, 'l', 16);
+                expected.delta.add(16, 'p', 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+        }
+    }
+
+    SECTION("The target state is not specified.") {
+        Delta delta;
+        delta.add(0, 'w', 0);
+        delta.add(0, 'y', 1);
+        delta.add(1, 'x', 1);
+        delta.add(1, 'z', 0);
+
+        SECTION("num_of_levels == 2") {
+            SECTION("word_part.size() == 1") {
+                nft = Nft(delta, { 0, 1 }, {}, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a'}, {'b'} });
+                nft.final.insert(new_tgt);
+
+                expected = Nft(delta, { 0, 1 }, { 3 }, { 0, 0, 1, 0 }, 2);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'b', 3);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 2") {
+                nft = Nft(delta, { 0, 1 }, {}, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b'}, {'c', 'd'} });
+                nft.final.insert(new_tgt);
+
+                expected = Nft(delta, { 0, 1 }, { 5 }, { 0, 0, 1, 0, 1, 0 }, 2);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'c', 3);
+                expected.delta.add(3, 'b', 4);
+                expected.delta.add(4, 'd', 5);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 4") {
+                nft = Nft(delta, { 0, 1 }, {}, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b', 'c', 'd'}, {'e', 'f', 'g', 'h'} });
+                nft.final.insert(new_tgt);
+
+                expected = Nft(delta, { 0, 1 }, { 9 }, { 0, 0, 1, 0, 1, 0, 1, 0, 1, 0 }, 2);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'e', 3);
+                expected.delta.add(3, 'b', 4);
+                expected.delta.add(4, 'f', 5);
+                expected.delta.add(5, 'c', 6);
+                expected.delta.add(6, 'g', 7);
+                expected.delta.add(7, 'd', 8);
+                expected.delta.add(8, 'h', 9);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+        }
+
+        SECTION("num_of_levels == 4") {
+            SECTION("word_part.size() == 1") {
+                nft = Nft(delta, { 0, 1 }, {}, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a'}, {'b'}, {'c'}, {'d'} });
+                nft.final.insert(new_tgt);
+
+                expected = Nft(delta, { 0, 1 }, { 5 }, { 0, 0, 1, 2, 3, 0 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'b', 3);
+                expected.delta.add(3, 'c', 4);
+                expected.delta.add(4, 'd', 5);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 2") {
+                nft = Nft(delta, { 0, 1 }, {}, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b'}, {'c', 'd'}, {'e', 'f'}, {'g', 'h'} });
+                nft.final.insert(new_tgt);
+
+                expected = Nft(delta, { 0, 1 }, { 9 }, { 0, 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'c', 3);
+                expected.delta.add(3, 'e', 4);
+                expected.delta.add(4, 'g', 5);
+                expected.delta.add(5, 'b', 6);
+                expected.delta.add(6, 'd', 7);
+                expected.delta.add(7, 'f', 8);
+                expected.delta.add(8, 'h', 9);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 4") {
+                nft = Nft(delta, { 0, 1 }, {}, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b', 'c', 'd'}, {'e', 'f', 'g', 'h'}, {'i', 'j', 'k', 'l'}, {'m', 'n', 'o', 'p'} });
+                nft.final.insert(new_tgt);
+
+                expected = Nft(delta, { 0, 1 }, { 17 }, { 0, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'e', 3);
+                expected.delta.add(3, 'i', 4);
+                expected.delta.add(4, 'm', 5);
+                expected.delta.add(5, 'b', 6);
+                expected.delta.add(6, 'f', 7);
+                expected.delta.add(7, 'j', 8);
+                expected.delta.add(8, 'n', 9);
+                expected.delta.add(9, 'c', 10);
+                expected.delta.add(10, 'g', 11);
+                expected.delta.add(11, 'k', 12);
+                expected.delta.add(12, 'o', 13);
+                expected.delta.add(13, 'd', 14);
+                expected.delta.add(14, 'h', 15);
+                expected.delta.add(15, 'l', 16);
+                expected.delta.add(16, 'p', 17);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("The lengths of word pats dont have the same length.") {
+                nft = Nft(delta, { 0, 1 }, {}, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b'}, {'e', 'f', 'g', 'h'}, {'i', 'j', 'k', 'l'}, {} });
+                nft.final.insert(new_tgt);
+
+                expected = Nft(delta, { 0, 1 }, { 17 }, { 0, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'e', 3);
+                expected.delta.add(3, 'i', 4);
+                expected.delta.add(4, EPSILON, 5);
+                expected.delta.add(5, 'b', 6);
+                expected.delta.add(6, 'f', 7);
+                expected.delta.add(7, 'j', 8);
+                expected.delta.add(8, EPSILON, 9);
+                expected.delta.add(9, EPSILON, 10);
+                expected.delta.add(10, 'g', 11);
+                expected.delta.add(11, 'k', 12);
+                expected.delta.add(12, EPSILON, 13);
+                expected.delta.add(13, EPSILON, 14);
+                expected.delta.add(14, 'h', 15);
+                expected.delta.add(15, 'l', 16);
+                expected.delta.add(16, EPSILON, 17);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+        }
+    }
+
+    SECTION("The lengths of word parts dont have to match.") {
+        Delta delta;
+        delta.add(0, 'w', 0);
+        delta.add(0, 'y', 1);
+        delta.add(1, 'x', 1);
+        delta.add(1, 'z', 0);
+
+        SECTION("num_of_levels == 2") {
+            SECTION("word_part.size() == 1") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {}, {'b'} } , 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1 }, 2);
+                expected.delta.add(0, EPSILON, 2);
+                expected.delta.add(2, 'b', 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 2") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b'}, {'c'} } , 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 0, 1 }, 2);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'c', 3);
+                expected.delta.add(3, 'b', 4);
+                expected.delta.add(4, EPSILON, 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 4") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 2);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a', 'b', 'c', 'd'}, {'e'} }, 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 0, 1, 0, 1, 0, 1 }, 2);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'e', 3);
+                expected.delta.add(3, 'b', 4);
+                expected.delta.add(4, EPSILON, 5);
+                expected.delta.add(5, 'c', 6);
+                expected.delta.add(6, EPSILON, 7);
+                expected.delta.add(7, 'd', 8);
+                expected.delta.add(8, EPSILON, 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+        }
+
+        SECTION("num_of_levels == 4") {
+            SECTION("word_part.size() == 1") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a'}, {}, {'c'}, {} }, 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 2, 3 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, EPSILON, 3);
+                expected.delta.add(3, 'c', 4);
+                expected.delta.add(4, EPSILON, 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 2") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {'a'}, {'c', 'd'}, {}, {'g'} }, 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 2, 3, 0, 1, 2, 3 }, 4);
+                expected.delta.add(0, 'a', 2);
+                expected.delta.add(2, 'c', 3);
+                expected.delta.add(3, EPSILON, 4);
+                expected.delta.add(4, 'g', 5);
+                expected.delta.add(5, EPSILON, 6);
+                expected.delta.add(6, 'd', 7);
+                expected.delta.add(7, EPSILON, 8);
+                expected.delta.add(8, EPSILON, 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+
+            SECTION("word_part.size() == 4") {
+                nft = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0 }, 4);
+                State new_tgt = nft.insert_word_by_parts(0, { {}, {'e', 'f'}, {'i', 'j', 'k', 'l'}, {'m', 'n', 'o', 'p'} }, 1);
+                CHECK(new_tgt == 1);
+
+                expected = Nft(delta, { 0, 1 }, { 0, 1 }, { 0, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3 }, 4);
+                expected.delta.add(0, EPSILON, 2);
+                expected.delta.add(2, 'e', 3);
+                expected.delta.add(3, 'i', 4);
+                expected.delta.add(4, 'm', 5);
+                expected.delta.add(5, EPSILON, 6);
+                expected.delta.add(6, 'f', 7);
+                expected.delta.add(7, 'j', 8);
+                expected.delta.add(8, 'n', 9);
+                expected.delta.add(9, EPSILON, 10);
+                expected.delta.add(10, EPSILON, 11);
+                expected.delta.add(11, 'k', 12);
+                expected.delta.add(12, 'o', 13);
+                expected.delta.add(13, EPSILON, 14);
+                expected.delta.add(14, EPSILON, 15);
+                expected.delta.add(15, 'l', 16);
+                expected.delta.add(16, 'p', 1);
+
+                CHECK(are_equivalent(nft, expected));
+            }
+        }
+    }
+
+    SECTION("begin insertion from levels != 0") {
+        SECTION("from level 1") {
+            nft = Nft{};
+            nft.num_of_levels = 3;
+            const State state_lvl1{ nft.add_state_with_level(1) };
+            const State target{ nft.insert_word_by_parts(state_lvl1, { { 'a', 'b', 'c' }, { 'd', 'e' }, { 'f' } }) };
+            CHECK(nft.levels[target] == 1);
+            expected = Nft{};
+            expected.num_of_levels = 3;
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'd', 3);
+            expected.delta.add(3, 'f', 4);
+            expected.delta.add(4, 'b', 5);
+            expected.delta.add(5, 'e', 6);
+            expected.delta.add(6, EPSILON, 7);
+            expected.delta.add(7, 'c', 8);
+            expected.delta.add(8, EPSILON, 9);
+            expected.delta.add(9, EPSILON, 10);
+            expected.levels = { 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1 };
+            CHECK(are_equivalent(nft, expected));
+        }
+
+        SECTION("from level 2") {
+            nft = Nft{};
+            nft.num_of_levels = 3;
+            const State state_lvl2{ nft.add_state_with_level(2) };
+            const State target{ nft.insert_word_by_parts(state_lvl2, { { 'a', 'b', 'c' }, { 'd', 'e' }, { 'f' } }) };
+            CHECK(nft.levels[target] == 2);
+            expected = Nft{};
+            expected.num_of_levels = 3;
+            expected.delta.add(1, 'a', 2);
+            expected.delta.add(2, 'd', 3);
+            expected.delta.add(3, 'f', 4);
+            expected.delta.add(4, 'b', 5);
+            expected.delta.add(5, 'e', 6);
+            expected.delta.add(6, EPSILON, 7);
+            expected.delta.add(7, 'c', 8);
+            expected.delta.add(8, EPSILON, 9);
+            expected.delta.add(9, EPSILON, 10);
+            expected.levels = { 0, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2 };
+            CHECK(are_equivalent(nft, expected));
+        }
+    }
+}
+
+TEST_CASE("mata::nft::Nft::apply()") {
+    SECTION("replace reluctant regex NFT") {
+        Nfa nfa{};
+        parser::create_nfa(&nfa, "da+b+ce");
+        mata::EnumAlphabet alphabet{ 'a', 'b', 'c', 'd', 'e', 'f' };
+        Nft nft{ nft::strings::replace_reluctant_regex("a+b+c", { 'f' }, &alphabet) };
+        Nft nft_applied_nfa{ nft.apply(nfa, 0) };
+        Nfa result{ project_to(nft_applied_nfa, 1).to_nfa_move() };
+        result.remove_epsilon();
+        result.trim();
+        Nfa expected{};
+        expected.initial.insert(0);
+        expected.delta.add(0, 'd', 1);
+        expected.delta.add(1, 'f', 2);
+        expected.delta.add(2, 'e', 3);
+        expected.final.insert(3);
+        CHECK(nfa::are_equivalent(result, expected));
+    }
+
+    SECTION("replace reluctant literal NFT") {
+        Nfa nfa{};
+        parser::create_nfa(&nfa, "dabce");
+        mata::EnumAlphabet alphabet{ 'a', 'b', 'c', 'd', 'e', 'f' };
+        Nft nft{ nft::strings::replace_reluctant_literal({'a', 'b', 'c' }, { 'f' }, &alphabet) };
+        Nft nft_applied_nfa{ nft.apply(nfa, 0) };
+        Nfa result{ project_to(nft_applied_nfa, 1).to_nfa_move() };
+        result.remove_epsilon();
+        result.trim();
+        Nfa expected{};
+        expected.initial.insert(0);
+        expected.delta.add(0, 'd', 1);
+        expected.delta.add(1, 'f', 2);
+        expected.delta.add(2, 'e', 3);
+        expected.final.insert(3);
+        CHECK(nfa::are_equivalent(result, expected));
+    }
+}
diff --git a/tests/nft/strings.cc b/tests/nft/strings.cc
new file mode 100644
index 000000000..a26ab77c1
--- /dev/null
+++ b/tests/nft/strings.cc
@@ -0,0 +1,951 @@
+// TODO: some header
+
+#include <vector>
+#include <fstream>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_string.hpp>
+
+#include "mata/nfa/builder.hh"
+#include "mata/parser/re2parser.hh"
+#include "mata/parser/parser.hh"
+#include "mata/nft/nft.hh"
+#include "mata/nft/builder.hh"
+#include "mata/nft/strings.hh"
+
+using namespace mata;
+using namespace mata::nft;
+using namespace mata::nft::strings;
+using IntAlphabet = mata::IntAlphabet;
+using OnTheFlyAlphabet = mata::OnTheFlyAlphabet;
+using mata::EnumAlphabet;
+
+class ReluctantReplaceSUT: public nft::strings::ReluctantReplace {
+    using super = nft::strings::ReluctantReplace;
+public:
+    using super::reluctant_nfa_with_marker, super::replace_literal_nft, super::generic_marker_dfa, super::end_marker_dfa,
+          super::marker_nft, super::reluctant_leftmost_nft, super::begin_marker_nfa, super::begin_marker_nft,
+          super::end_marker_dft;
+};
+
+TEST_CASE("nft::create_identity()") {
+    Nft nft{};
+    nft.initial = { 0 };
+    nft.final = { 0 };
+    SECTION("small identity nft") {
+        EnumAlphabet alphabet{ 0, 1, 2, 3 };
+        nft.alphabet = &alphabet;
+        nft.delta.add(0, 0, 1);
+        nft.delta.add(1, 0, 2);
+        nft.delta.add(2, 0, 0);
+        nft.delta.add(0, 1, 3);
+        nft.delta.add(3, 1, 4);
+        nft.delta.add(4, 1, 0);
+        nft.delta.add(0, 2, 5);
+        nft.delta.add(5, 2, 6);
+        nft.delta.add(6, 2, 0);
+        nft.delta.add(0, 3, 7);
+        nft.delta.add(7, 3, 8);
+        nft.delta.add(8, 3, 0);
+        nft.num_of_levels = 3;
+        nft.levels.resize(nft.num_of_levels * (alphabet.get_number_of_symbols() - 1));
+        nft.levels[0] = 0;
+        nft.levels[1] = 1;
+        nft.levels[2] = 2;
+        nft.levels[3] = 1;
+        nft.levels[4] = 2;
+        nft.levels[5] = 1;
+        nft.levels[6] = 2;
+        nft.levels[7] = 1;
+        nft.levels[8] = 2;
+        Nft nft_identity{ create_identity(&alphabet, 3) };
+        CHECK(nft::are_equivalent(nft_identity, nft));
+    }
+
+    SECTION("identity nft no symbols") {
+        EnumAlphabet alphabet{};
+        nft.alphabet = &alphabet;
+        nft.num_of_levels = 3;
+        nft.levels.resize(1);
+        nft.levels[0] = 0;
+        Nft nft_identity{ create_identity(&alphabet, 3) };
+        CHECK(nft::are_equivalent(nft_identity, nft));
+    }
+
+    SECTION("identity nft one symbol") {
+        EnumAlphabet alphabet{ 0 };
+        nft.alphabet = &alphabet;
+        nft.num_of_levels = 2;
+        nft.levels.resize(2);
+        nft.levels[0] = 0;
+        nft.levels[1] = 1;
+        nft.delta.add(0, 0, 1);
+        nft.delta.add(1, 0, 0);
+        Nft nft_identity{ create_identity(&alphabet, 2) };
+        CHECK(nft::are_equivalent(nft_identity, nft));
+        nft_identity = create_identity(&alphabet);
+        CHECK(nft::are_equivalent(nft_identity, nft));
+    }
+
+    SECTION("small identity nft one level") {
+        EnumAlphabet alphabet{ 0, 1, 2, 3 };
+        nft.alphabet = &alphabet;
+        nft.delta.add(0, 0, 0);
+        nft.delta.add(0, 1, 0);
+        nft.delta.add(0, 2, 0);
+        nft.delta.add(0, 3, 0);
+        nft.num_of_levels = 1;
+        nft.levels.resize(1);
+        nft.levels[0] = 0;
+        Nft nft_identity{ create_identity(&alphabet, 1) };
+        CHECK(nft::are_equivalent(nft_identity, nft));
+    }
+}
+
+TEST_CASE("nft::create_identity_with_single_symbol_replace()") {
+    Nft expected{};
+    expected.initial = { 0 };
+    expected.final = { 0 };
+    SECTION("small identity nft") {
+        EnumAlphabet alphabet{ 0, 1, 2, 3 };
+        expected.alphabet = &alphabet;
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(1, 0, 0);
+        expected.delta.add(0, 1, 2);
+        expected.delta.add(2, 3, 0);
+        expected.delta.add(0, 2, 3);
+        expected.delta.add(3, 2, 0);
+        expected.delta.add(0, 3, 4);
+        expected.delta.add(4, 3, 0);
+        expected.num_of_levels = 2;
+        expected.levels.resize(5);
+        expected.levels[0] = 0;
+        expected.levels[1] = 1;
+        expected.levels[2] = 1;
+        expected.levels[3] = 1;
+        expected.levels[4] = 1;
+        Nft nft_identity_with_replace{ create_identity_with_single_symbol_replace(&alphabet, 1, 3) };
+        CHECK(nft::are_equivalent(nft_identity_with_replace, expected));
+    }
+
+    SECTION("identity nft no symbols") {
+        EnumAlphabet alphabet{};
+        CHECK_THROWS(create_identity_with_single_symbol_replace(&alphabet, 1, 2));
+    }
+
+    SECTION("identity nft one symbol") {
+        EnumAlphabet alphabet{ 0 };
+        expected.alphabet = &alphabet;
+        expected.num_of_levels = 2;
+        expected.levels.resize(2);
+        expected.levels[0] = 0;
+        expected.levels[1] = 1;
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(1, 1, 0);
+        Nft nft_identity{ create_identity_with_single_symbol_replace(&alphabet, 0, 1) };
+        CHECK(nft::are_equivalent(nft_identity, expected));
+    }
+
+    SECTION("small identity expected longer replace") {
+        EnumAlphabet alphabet{ 0, 1, 2, 3 };
+        expected.alphabet = &alphabet;
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(1, 0, 0);
+        expected.delta.add(0, 1, 2);
+        expected.delta.add(2, 5, 5);
+        expected.delta.add(5, EPSILON, 6);
+        expected.delta.add(6, 6, 7);
+        expected.delta.add(7, EPSILON, 8);
+        expected.delta.add(8, 7, 0);
+        expected.delta.add(0, 2, 3);
+        expected.delta.add(3, 2, 0);
+        expected.delta.add(0, 3, 4);
+        expected.delta.add(4, 3, 0);
+        expected.num_of_levels = 2;
+        expected.levels.resize(9);
+        expected.levels[0] = 0;
+        expected.levels[1] = 1;
+        expected.levels[2] = 1;
+        expected.levels[3] = 1;
+        expected.levels[4] = 1;
+        expected.levels[5] = 0;
+        expected.levels[6] = 1;
+        expected.levels[7] = 0;
+        expected.levels[8] = 1;
+        Nft nft_identity_with_replace{ create_identity_with_single_symbol_replace(&alphabet, 1, Word{ 5, 6, 7 }) };
+        CHECK(nft::are_equivalent(nft_identity_with_replace, expected));
+    }
+
+    SECTION("small identity expected replace symbol with empty string") {
+        EnumAlphabet alphabet{ 0, 1, 2, 3 };
+        expected.alphabet = &alphabet;
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(1, 0, 0);
+        expected.delta.add(0, 1, 2);
+        expected.delta.add(2, EPSILON, 0);
+        expected.delta.add(0, 2, 3);
+        expected.delta.add(3, 2, 0);
+        expected.delta.add(0, 3, 4);
+        expected.delta.add(4, 3, 0);
+        expected.num_of_levels = 2;
+        expected.levels.resize(5);
+        expected.levels[0] = 0;
+        expected.levels[1] = 1;
+        expected.levels[2] = 1;
+        expected.levels[3] = 1;
+        expected.levels[4] = 1;
+        Nft nft_identity_with_replace{ create_identity_with_single_symbol_replace(&alphabet, 1, Word{}) };
+        CHECK(nft::are_equivalent(nft_identity_with_replace, expected));
+    }
+
+    SECTION("identity expected one symbol with word replace") {
+        EnumAlphabet alphabet{ 0 };
+        expected.alphabet = &alphabet;
+        expected.num_of_levels = 2;
+        expected.levels.resize(2);
+        expected.levels[0] = 0;
+        expected.levels[1] = 1;
+        expected.delta.add(0, 0, 1);
+        expected.delta.add(1, 0, 0);
+        Nft nft_identity{ create_identity_with_single_symbol_replace(&alphabet, 0, Word{ 0 }) };
+        CHECK(nft::are_equivalent(nft_identity, expected));
+    }
+
+    SECTION("small identity expected longer replace single replacement") {
+        EnumAlphabet alphabet{ 0, 1, 2, 3 };
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q0 q9\n%Levels q0:0 q1:1 q2:1 q3:1 q4:1 q5:0 q6:1 q7:0 q8:1 q9:0 q10:1 q11:1 q12:1 q13:1\n%LevelsNum 2\nq0 0 q1\nq0 1 q2\nq0 2 q3\nq0 3 q4\nq1 0 q0\nq2 5 q5\nq3 2 q0\nq4 3 q0\nq5 4294967295 q6\nq6 6 q7\nq7 4294967295 q8\nq8 7 q9\nq9 0 q10\nq9 1 q11\nq9 2 q12\nq9 3 q13\nq10 0 q9\nq11 1 q9\nq12 2 q9\nq13 3 q9\n"
+        ));
+        Nft nft_identity_with_replace{ create_identity_with_single_symbol_replace(&alphabet, 1, Word{ 5, 6, 7 }, ReplaceMode::Single) };
+        CHECK(nft::are_equivalent(nft_identity_with_replace, expected));
+    }
+
+    SECTION("small identity expected replace symbol with empty string single replace") {
+        EnumAlphabet alphabet{ 0, 1, 2, 3 };
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q0 q5\n%Levels q0:0 q1:1 q2:1 q3:1 q4:1 q5:0 q6:1 q7:1 q8:1 q9:1\n%LevelsNum 2\nq0 0 q1\nq0 1 q2\nq0 2 q3\nq0 3 q4\nq1 0 q0\nq2 4294967295 q5\nq3 2 q0\nq4 3 q0\nq5 0 q6\nq5 1 q7\nq5 2 q8\nq5 3 q9\nq6 0 q5\nq7 1 q5\nq8 2 q5\nq9 3 q5\n"
+        ));
+        Nft nft_identity_with_replace{ create_identity_with_single_symbol_replace(&alphabet, 1, Word{}, ReplaceMode::Single) };
+        CHECK(nft::are_equivalent(nft_identity_with_replace, expected));
+    }
+
+    SECTION("identity expected one symbol with word replace single replace") {
+        EnumAlphabet alphabet{ 0 };
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q0 q2\n%Levels q0:0 q1:1 q2:0 q3:1\n%LevelsNum 2\nq0 0 q1\nq1 0 q2\nq2 0 q3\nq3 0 q2\n"
+        ));
+        Nft nft_identity{ create_identity_with_single_symbol_replace(&alphabet, 0, Word{ 0 }, ReplaceMode::Single) };
+        CHECK(nft::are_equivalent(nft_identity, expected));
+    }
+}
+
+TEST_CASE("nft::reluctant_replacement()") {
+    Nft nft{};
+    nfa::Nfa regex{};
+    EnumAlphabet alphabet{ 'a', 'b', 'c' };
+    ReluctantReplaceSUT reluctant_replace{};
+    SECTION("nft::end_marker_dfa()") {
+        parser::create_nfa(&regex, "cb+a+");
+        nfa::Nfa dfa_end_marker{ reluctant_replace.end_marker_dfa(regex) };
+        nfa::Nfa dfa_expected_end_marker{};
+        dfa_expected_end_marker.initial = { 0 };
+        dfa_expected_end_marker.final = { 4 };
+        dfa_expected_end_marker.delta.add(0, 'c', 1);
+        dfa_expected_end_marker.delta.add(1, 'b', 2);
+        dfa_expected_end_marker.delta.add(2, 'b', 2);
+        dfa_expected_end_marker.delta.add(2, 'a', 3);
+        dfa_expected_end_marker.delta.add(3, EPSILON, 4);
+        dfa_expected_end_marker.delta.add(4, 'a', 3);
+        CHECK(dfa_end_marker.is_deterministic());
+        CHECK(nfa::are_equivalent(dfa_end_marker, dfa_expected_end_marker));
+        Nft dft_end_marker{ reluctant_replace.end_marker_dft(dfa_end_marker, END_MARKER) };
+        Nft dft_expected_end_marker{};
+        dft_expected_end_marker.num_of_levels = 2;
+        dft_expected_end_marker.levels = { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1 };
+        dft_expected_end_marker.initial = { 0 };
+        dft_expected_end_marker.final = { 9 };
+        dft_expected_end_marker.delta.add(0, 'c', 1);
+        dft_expected_end_marker.delta.add(1, 'c', 2);
+        dft_expected_end_marker.delta.add(2, 'b', 3);
+        dft_expected_end_marker.delta.add(3, 'b', 4);
+        dft_expected_end_marker.delta.add(4, 'b', 5);
+        dft_expected_end_marker.delta.add(5, 'b', 4);
+        dft_expected_end_marker.delta.add(4, 'a', 6);
+        dft_expected_end_marker.delta.add(6, 'a', 7);
+        dft_expected_end_marker.delta.add(7, EPSILON, 8);
+        dft_expected_end_marker.delta.add(8, END_MARKER, 9);
+        dft_expected_end_marker.delta.add(9, 'a', 10);
+        dft_expected_end_marker.delta.add(10, 'a', 7);
+        CHECK(dft_end_marker.is_deterministic());
+        CHECK(nft::are_equivalent(dft_end_marker, dft_expected_end_marker));
+    }
+
+    SECTION("nft::generic_end_marker_dft() regex cb+a+") {
+        nfa::Nfa dfa_generic_end_marker{ reluctant_replace.generic_marker_dfa("cb+a+", &alphabet) };
+        nfa::Nfa dfa_expected{ nfa::Delta{}, { 0 }, { 0, 1, 2, 4 }};
+        dfa_expected.delta.add(0, 'a', 0);
+        dfa_expected.delta.add(0, 'b', 0);
+        dfa_expected.delta.add(0, 'c', 1);
+        dfa_expected.delta.add(1, 'a', 0);
+        dfa_expected.delta.add(1, 'b', 2);
+        dfa_expected.delta.add(1, 'c', 1);
+        dfa_expected.delta.add(2, 'a', 3);
+        dfa_expected.delta.add(2, 'b', 2);
+        dfa_expected.delta.add(2, 'c', 1);
+        dfa_expected.delta.add(3, EPSILON, 4);
+        dfa_expected.delta.add(4, 'a', 3);
+        dfa_expected.delta.add(4, 'b', 0);
+        dfa_expected.delta.add(4, 'c', 1);
+        CHECK(nfa::are_equivalent(dfa_generic_end_marker, dfa_expected));
+
+        Nft dft_generic_end_marker{ reluctant_replace.end_marker_dft(dfa_generic_end_marker, END_MARKER) };
+        Nft dft_expected{};
+        dft_expected.initial.insert(0);
+        dft_expected.final = { 0, 4, 7, 14 };
+        dft_expected.num_of_levels = 2;
+        dft_expected.delta.add(0, 'a', 1);
+        dft_expected.delta.add(1, 'a', 0);
+        dft_expected.delta.add(0, 'b', 2);
+        dft_expected.delta.add(2, 'b', 0);
+        dft_expected.delta.add(0, 'c', 3);
+        dft_expected.delta.add(3, 'c', 4);
+        dft_expected.delta.add(4, 'a', 5);
+        dft_expected.delta.add(5, 'a', 0);
+        dft_expected.delta.add(4, 'b', 6);
+        dft_expected.delta.add(6, 'b', 7);
+        dft_expected.delta.add(4, 'c', 8);
+        dft_expected.delta.add(8, 'c', 4);
+        dft_expected.delta.add(7, 'a', 9);
+        dft_expected.delta.add(9, 'a', 10);
+        dft_expected.delta.add(7, 'b', 11);
+        dft_expected.delta.add(11, 'b', 7);
+        dft_expected.delta.add(7, 'c', 12);
+        dft_expected.delta.add(12, 'c', 4);
+        dft_expected.delta.add(10, EPSILON, 13);
+        dft_expected.delta.add(13, END_MARKER, 14);
+        dft_expected.delta.add(14, 'a', 15);
+        dft_expected.delta.add(15, 'a', 10);
+        dft_expected.delta.add(14, 'b', 16);
+        dft_expected.delta.add(16, 'b', 0);
+        dft_expected.delta.add(14, 'c', 17);
+        dft_expected.delta.add(17, 'c', 4);
+        dft_expected.levels.resize(18);
+        dft_expected.levels[0] = 0;
+        dft_expected.levels[1] = 1;
+        dft_expected.levels[2] = 1;
+        dft_expected.levels[3] = 1;
+        dft_expected.levels[4] = 0;
+        dft_expected.levels[5] = 1;
+        dft_expected.levels[6] = 1;
+        dft_expected.levels[7] = 0;
+        dft_expected.levels[8] = 1;
+        dft_expected.levels[9] = 1;
+        dft_expected.levels[10] = 0;
+        dft_expected.levels[11] = 1;
+        dft_expected.levels[12] = 1;
+        dft_expected.levels[13] = 1;
+        dft_expected.levels[14] = 0;
+        dft_expected.levels[15] = 1;
+        dft_expected.levels[16] = 1;
+        dft_expected.levels[17] = 1;
+        CHECK(nft::are_equivalent(dft_generic_end_marker, dft_expected));
+    }
+
+    SECTION("nft::generic_end_marker_dft() regex ab+a+") {
+        nfa::Nfa dfa_generic_end_marker{ reluctant_replace.generic_marker_dfa("ab+a+", &alphabet) };
+        nfa::Nfa dfa_expected{ nfa::Delta{}, { 0 }, { 0, 1, 2, 4 }};
+        dfa_expected.delta.add(0, 'a', 1);
+        dfa_expected.delta.add(0, 'b', 0);
+        dfa_expected.delta.add(0, 'c', 0);
+        dfa_expected.delta.add(1, 'a', 1);
+        dfa_expected.delta.add(1, 'b', 2);
+        dfa_expected.delta.add(1, 'c', 0);
+        dfa_expected.delta.add(2, 'a', 3);
+        dfa_expected.delta.add(2, 'b', 2);
+        dfa_expected.delta.add(2, 'c', 0);
+        dfa_expected.delta.add(3, EPSILON, 4);
+        dfa_expected.delta.add(4, 'a', 3);
+        dfa_expected.delta.add(4, 'b', 2);
+        dfa_expected.delta.add(4, 'c', 0);
+        CHECK(nfa::are_equivalent(dfa_generic_end_marker, dfa_expected));
+
+        Nft dft_generic_end_marker{ reluctant_replace.end_marker_dft(dfa_generic_end_marker, END_MARKER) };
+        Nft dft_expected{};
+        dft_expected.initial.insert(0);
+        dft_expected.final = { 0, 2, 7, 14 };
+        dft_expected.num_of_levels = 2;
+        dft_expected.delta.add(0, 'a', 1);
+        dft_expected.delta.add(1, 'a', 2);
+        dft_expected.delta.add(0, 'b', 3);
+        dft_expected.delta.add(3, 'b', 0);
+        dft_expected.delta.add(0, 'c', 4);
+        dft_expected.delta.add(4, 'c', 0);
+        dft_expected.delta.add(2, 'a', 5);
+        dft_expected.delta.add(5, 'a', 2);
+        dft_expected.delta.add(2, 'b', 6);
+        dft_expected.delta.add(6, 'b', 7);
+        dft_expected.delta.add(2, 'c', 8);
+        dft_expected.delta.add(8, 'c', 0);
+        dft_expected.delta.add(7, 'a', 9);
+        dft_expected.delta.add(9, 'a', 10);
+        dft_expected.delta.add(7, 'b', 11);
+        dft_expected.delta.add(11, 'b', 7);
+        dft_expected.delta.add(7, 'c', 12);
+        dft_expected.delta.add(12, 'c', 0);
+        dft_expected.delta.add(10, EPSILON, 13);
+        dft_expected.delta.add(13, END_MARKER, 14);
+        dft_expected.delta.add(14, 'a', 15);
+        dft_expected.delta.add(15, 'a', 10);
+        dft_expected.delta.add(14, 'b', 16);
+        dft_expected.delta.add(16, 'b', 7);
+        dft_expected.delta.add(14, 'c', 17);
+        dft_expected.delta.add(17, 'c', 0);
+        dft_expected.levels.resize(18);
+        dft_expected.levels[0] = 0;
+        dft_expected.levels[1] = 1;
+        dft_expected.levels[2] = 0;
+        dft_expected.levels[3] = 1;
+        dft_expected.levels[4] = 1;
+        dft_expected.levels[5] = 1;
+        dft_expected.levels[6] = 1;
+        dft_expected.levels[7] = 0;
+        dft_expected.levels[8] = 1;
+        dft_expected.levels[9] = 1;
+        dft_expected.levels[10] = 0;
+        dft_expected.levels[11] = 1;
+        dft_expected.levels[12] = 1;
+        dft_expected.levels[13] = 1;
+        dft_expected.levels[14] = 0;
+        dft_expected.levels[15] = 1;
+        dft_expected.levels[16] = 1;
+        dft_expected.levels[17] = 1;
+        CHECK(nft::are_equivalent(dft_generic_end_marker, dft_expected));
+    }
+
+    SECTION("nft::begin_marker_nft() regex a+b+c") {
+        nfa::Nfa nfa_begin_marker{ reluctant_replace.begin_marker_nfa("a+b+c", &alphabet) };
+        nfa::Nfa nfa_expected{ nfa::Delta{}, { 0 }, { 0, 1, 2, 4 }};
+        nfa_expected.delta.add(0, 'a', 0);
+        nfa_expected.delta.add(0, 'b', 0);
+        nfa_expected.delta.add(1, 'c', 0);
+        nfa_expected.delta.add(0, 'a', 1);
+        nfa_expected.delta.add(2, 'b', 1);
+        nfa_expected.delta.add(1, 'c', 1);
+        nfa_expected.delta.add(3, 'a', 2);
+        nfa_expected.delta.add(2, 'b', 2);
+        nfa_expected.delta.add(1, 'c', 2);
+        nfa_expected.delta.add(4, EPSILON, 3);
+        nfa_expected.delta.add(3, 'a', 4);
+        nfa_expected.delta.add(0, 'b', 4);
+        nfa_expected.delta.add(1, 'c', 4);
+        CHECK(nfa::are_equivalent(nfa_begin_marker, nfa_expected));
+
+        Nft nft_begin_marker{ reluctant_replace.begin_marker_nft(nfa_begin_marker, BEGIN_MARKER) };
+        Nft nft_expected{};
+        nft_expected.initial.insert(0);
+        nft_expected.final.insert(1);
+        nft_expected.num_of_levels = 2;
+        nft_expected.delta.add(0, EPSILON, 1);
+        nft_expected.delta.add(0, EPSILON, 2);
+        nft_expected.delta.add(0, EPSILON, 3);
+        nft_expected.delta.add(0, EPSILON, 5);
+        nft_expected.delta.add(1, 'a', 6);
+        nft_expected.delta.add(6, 'a', 1);
+        nft_expected.delta.add(6, 'a', 2);
+        nft_expected.delta.add(1, 'b', 7);
+        nft_expected.delta.add(7, 'b', 1);
+        nft_expected.delta.add(7, 'b', 5);
+        nft_expected.delta.add(2, 'c', 8);
+        nft_expected.delta.add(8, 'c', 2);
+        nft_expected.delta.add(8, 'c', 1);
+        nft_expected.delta.add(8, 'c', 3);
+        nft_expected.delta.add(8, 'c', 5);
+        nft_expected.delta.add(3, 'b', 9);
+        nft_expected.delta.add(9, 'b', 3);
+        nft_expected.delta.add(9, 'b', 2);
+        nft_expected.delta.add(4, 'a', 10);
+        nft_expected.delta.add(10, 'a', 3);
+        nft_expected.delta.add(10, 'a', 5);
+        nft_expected.delta.add(5, EPSILON, 11);
+        nft_expected.delta.add(11, BEGIN_MARKER, 4);
+        nft_expected.levels.resize(12);
+        nft_expected.levels[0] = 0;
+        nft_expected.levels[1] = 0;
+        nft_expected.levels[2] = 0;
+        nft_expected.levels[3] = 0;
+        nft_expected.levels[4] = 0;
+        nft_expected.levels[5] = 0;
+        nft_expected.levels[6] = 1;
+        nft_expected.levels[7] = 1;
+        nft_expected.levels[8] = 1;
+        nft_expected.levels[9] = 1;
+        nft_expected.levels[10] = 1;
+        nft_expected.levels[11] = 1;
+        CHECK(nft::are_equivalent(nft_begin_marker, nft_expected));
+        CHECK(nft_begin_marker.is_tuple_in_lang({ Word{ 'a', 'b', 'c' }, Word{ BEGIN_MARKER, 'a', 'b', 'c' } }));
+        CHECK(nft_begin_marker.is_tuple_in_lang({ Word{ 'a', 'b', 'b', 'c', 'c', 'c' }, Word{ BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'c' } }));
+        CHECK(nft_begin_marker.is_tuple_in_lang({ Word{ 'a', 'a', 'b', 'c' }, Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'c' } }));
+        CHECK(nft_begin_marker.is_tuple_in_lang({ Word{ 'b', 'c' }, Word{ 'b', 'c' } }));
+        CHECK(nft_begin_marker.is_tuple_in_lang({ Word{ 'a', 'a', 'b', 'b', 'b', 'a', 'b', 'c' }, Word{ 'a', 'a', 'b', 'b', 'b', BEGIN_MARKER, 'a', 'b', 'c' } }));
+        CHECK(nft_begin_marker.is_tuple_in_lang({ Word{ 'a', 'a', 'b', 'b', 'b', 'c', 'a', 'b', 'c' }, Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'b', 'c', BEGIN_MARKER, 'a', 'b', 'c' } }));
+    }
+
+    SECTION("nft::begin_marker_nft() regex ab+a+") {
+        nfa::Nfa nfa_begin_marker{ reluctant_replace.begin_marker_nfa("ab+a+", &alphabet) };
+        nfa::Nfa nfa_expected{ nfa::Delta{}, { 0 }, { 0, 1, 2, 4 }};
+        nfa_expected.delta.add(1, 'a', 0);
+        nfa_expected.delta.add(0, 'b', 0);
+        nfa_expected.delta.add(0, 'c', 0);
+        nfa_expected.delta.add(1, 'a', 1);
+        nfa_expected.delta.add(1, 'a', 4);
+        nfa_expected.delta.add(2, 'b', 1);
+        nfa_expected.delta.add(0, 'c', 1);
+        nfa_expected.delta.add(3, 'a', 2);
+        nfa_expected.delta.add(2, 'b', 2);
+        nfa_expected.delta.add(0, 'c', 2);
+        nfa_expected.delta.add(4, EPSILON, 3);
+        nfa_expected.delta.add(2, 'b', 4);
+        nfa_expected.delta.add(0, 'c', 4);
+        CHECK(nfa::are_equivalent(nfa_begin_marker, nfa_expected));
+
+        Nft nft_begin_marker{ reluctant_replace.begin_marker_nft(nfa_begin_marker, BEGIN_MARKER) };
+        Nft nft_expected{ nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q11\n%Final q0\n%Levels q0:0 q1:1 q2:1 q3:0 q4:0 q5:0 q6:1 q7:1 q8:0 q9:1 q10:1 q11:0\n%LevelsNum 2\nq0 98 q1\nq0 99 q2\nq1 98 q0\nq2 99 q0\nq2 99 q3\nq2 99 q4\nq2 99 q5\nq3 97 q6\nq4 98 q7\nq5 4294967295 q10\nq6 97 q0\nq6 97 q3\nq6 97 q5\nq7 98 q3\nq7 98 q4\nq7 98 q5\nq8 97 q9\nq9 97 q4\nq10 4294967195 q8\nq11 4294967295 q0\nq11 4294967295 q3\nq11 4294967295 q4\nq11 4294967295 q5\n"
+        )) };
+        CHECK(nft::are_equivalent(nft_begin_marker, nft_expected));
+    }
+}
+
+TEST_CASE("mata::nft::strings::reluctant_nfa_with_marker()") {
+    Nft nft{};
+    nfa::Nfa regex{};
+    EnumAlphabet alphabet{ 'a', 'b', 'c' };
+    ReluctantReplaceSUT reluctant_replace{};
+
+    SECTION("regex cb+a+") {
+        nfa::Nfa nfa{ [&]() {
+            nfa::Nfa nfa{};
+            mata::parser::create_nfa(&nfa, "cb+a+");
+            return reluctant_replace.reluctant_nfa_with_marker(nfa, BEGIN_MARKER, &alphabet);
+        }() };
+        nfa::Nfa expected{ nfa::builder::parse_from_mata(std::string(
+            "@NFA-explicit\n%Alphabet-auto\n%Initial q0\n%Final q3\nq0 99 q1\nq0 4294967195 q0\nq1 98 q2\nq1 4294967195 q1\nq2 97 q3\nq2 98 q2\nq2 4294967195 q2\n")) };
+        CHECK(nfa::are_equivalent(nfa, expected));
+    }
+}
+
+TEST_CASE("mata::nft::strings::reluctant_leftmost_nft()") {
+    Nft nft{};
+    Nft expected{};
+    EnumAlphabet alphabet{ 'a', 'b', 'c' };
+    ReluctantReplaceSUT reluctant_replace{};
+
+    SECTION("all 'cb+a+' replaced with 'ddd'") {
+        nft = reluctant_replace.reluctant_leftmost_nft("cb+a+", &alphabet, BEGIN_MARKER, Word{ 'd', 'd', 'd' }, ReplaceMode::All);
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q13\n%Final q13\n%Levels q0:0 q1:1 q2:0 q3:1 q4:1 q5:0 q6:1 q7:1 q8:0 q9:1 q10:1 q11:0 q12:1 q13:0 q14:1 q15:1 q16:1 q17:1 q18:1 q19:0 q20:1 q21:0 q22:1 q23:0 q24:1 q25:0\n%LevelsNum 2\nq0 99 q1\nq0 4294967195 q3\nq1 4294967295 q2\nq2 98 q4\nq2 4294967195 q6\nq3 4294967295 q0\nq4 4294967295 q5\nq5 97 q7\nq5 98 q9\nq5 4294967195 q10\nq6 4294967295 q2\nq7 4294967295 q8\nq8 4294967295 q18\nq9 4294967295 q5\nq10 4294967295 q5\nq11 4294967195 q12\nq12 4294967295 q11\nq13 97 q14\nq13 98 q15\nq13 99 q16\nq13 4294967195 q17\nq14 97 q13\nq15 98 q13\nq16 99 q13\nq17 4294967295 q0\nq18 100 q19\nq19 4294967295 q20\nq20 100 q21\nq21 4294967295 q22\nq22 100 q23\nq23 4294967295 q24\nq24 4294967295 q25\nq25 4294967295 q13\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("single 'a+b+c' replaced with '' (empty string)") {
+        nft = reluctant_replace.reluctant_leftmost_nft("a+b+c", &alphabet, BEGIN_MARKER, Word{}, ReplaceMode::Single);
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q14\n%Final q22 q14\n%Levels q0:0 q1:1 q2:0 q3:1 q4:1 q5:1 q6:0 q7:1 q8:1 q9:1 q10:0 q11:1 q12:0 q13:1 q14:0 q15:1 q16:1 q17:1 q18:1 q19:1 q20:1 q21:0 q22:0 q23:1 q24:1 q25:1 q26:1\n%LevelsNum 2\nq0 97 q1\nq0 4294967195 q3\nq1 4294967295 q2\nq2 97 q4\nq2 98 q5\nq2 4294967195 q7\nq3 4294967295 q0\nq4 4294967295 q2\nq5 4294967295 q6\nq6 98 q8\nq6 99 q9\nq6 4294967195 q11\nq7 4294967295 q2\nq8 4294967295 q6\nq9 4294967295 q10\nq10 4294967295 q19\nq11 4294967295 q6\nq12 4294967195 q13\nq13 4294967295 q12\nq14 97 q15\nq14 98 q16\nq14 99 q17\nq14 4294967195 q18\nq15 97 q14\nq16 98 q14\nq17 99 q14\nq18 4294967295 q0\nq19 4294967295 q21\nq20 4294967295 q22\nq21 4294967295 q20\nq22 97 q23\nq22 98 q24\nq22 99 q25\nq22 4294967195 q26\nq23 97 q22\nq24 98 q22\nq25 99 q22\nq26 4294967295 q22\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+        CHECK(nft.is_tuple_in_lang({ Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'c' }, Word{ 'c', 'b', 'a', 'c' } }));
+        CHECK(!nft.is_tuple_in_lang({ Word{ 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'c' }, Word{ 'c', 'b', 'a', 'c' } }));
+        CHECK(!nft.is_tuple_in_lang({ Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'c' }, Word{ 'b', 'a', 'c' } }));
+        CHECK(!nft.is_tuple_in_lang({ Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'c' }, Word{ 'c', 'c', 'b', 'a', 'c' } }));
+        CHECK(nft.is_tuple_in_lang({ Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'b', BEGIN_MARKER, 'a', 'b', 'c', BEGIN_MARKER, 'a', 'b', 'c', 'b' }, Word{ 'c', 'b', 'a', 'b', 'a', 'b', 'c', 'a', 'b', 'c', 'b' } }));
+    }
+
+    SECTION("All 'a+b+c' replaced with 'd'") {
+        nft = reluctant_replace.reluctant_leftmost_nft("a+b+c", &alphabet, BEGIN_MARKER, Word{ 'd' }, ReplaceMode::All);
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q14\n%Final q14\n%Levels q0:0 q1:1 q2:0 q3:1 q4:1 q5:1 q6:0 q7:1 q8:1 q9:1 q10:0 q11:1 q12:0 q13:1 q14:0 q15:1 q16:1 q17:1 q18:1 q19:1 q20:0 q21:1 q22:0\n%LevelsNum 2\nq0 97 q1\nq0 4294967195 q3\nq1 4294967295 q2\nq2 97 q4\nq2 98 q5\nq2 4294967195 q7\nq3 4294967295 q0\nq4 4294967295 q2\nq5 4294967295 q6\nq6 98 q8\nq6 99 q9\nq6 4294967195 q11\nq7 4294967295 q2\nq8 4294967295 q6\nq9 4294967295 q10\nq10 4294967295 q19\nq11 4294967295 q6\nq12 4294967195 q13\nq13 4294967295 q12\nq14 97 q15\nq14 98 q16\nq14 99 q17\nq14 4294967195 q18\nq15 97 q14\nq16 98 q14\nq17 99 q14\nq18 4294967295 q0\nq19 100 q20\nq20 4294967295 q21\nq21 4294967295 q22\nq22 4294967295 q14\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+        CHECK(nft.is_tuple_in_lang({ Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'c' }, Word{ 'd', 'c', 'b', 'a', 'c' } }));
+        CHECK(!nft.is_tuple_in_lang({ Word{ 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'c' }, Word{ 'd', 'c', 'b', 'a', 'c' } }));
+        CHECK(!nft.is_tuple_in_lang({ Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'c' }, Word{ 'c', 'b', 'a', 'c' } }));
+        CHECK(!nft.is_tuple_in_lang({ Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'c' }, Word{ 'c', 'c', 'b', 'a', 'c' } }));
+        CHECK(nft.is_tuple_in_lang({ Word{ BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', BEGIN_MARKER, 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'b', BEGIN_MARKER, 'a', 'b', 'c', BEGIN_MARKER, 'a', 'b', 'c', 'b' }, Word{ 'd', 'c', 'b', 'a', 'b', 'd', 'd', 'b' } }));
+    }
+}
+
+TEST_CASE("mata::nft::strings::literal_replace_nft()") {
+    Nft nft{};
+    Nft expected{};
+    EnumAlphabet alphabet{ 'a', 'b', 'c' };
+    ReluctantReplaceSUT reluctant_replace{};
+
+    SECTION("'abcc' replace with 'a' replace all") {
+        nft = reluctant_replace.replace_literal_nft(Word{ 'a', 'b', 'c', 'c' }, Word{ 'a' },  &alphabet, END_MARKER, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'c', 'c', 'a', 'a', 'b', 'c', 'c', 'a', END_MARKER },
+                                     { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'a', 'a', 'a' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q35\n%Levels q0:0 q1:0 q2:0 q3:0 q4:0 q5:1 q6:1 q7:1 q8:1 q9:1 q10:1 q11:0 q12:1 q13:1 q14:0 q15:1 q16:1 q17:0 q18:1 q19:0 q20:1 q21:1 q22:1 q23:0 q24:1 q25:0 q26:1 q27:1 q28:0 q29:1 q30:0 q31:1 q32:0 q33:1 q34:1 q35:0 q36:1 q37:1 q38:1 q39:1 q40:0 q41:1 q42:1 q43:0 q44:1 q45:0 q46:1\n%LevelsNum 2\nq0 97 q5\nq0 98 q6\nq0 99 q7\nq0 4294967196 q37\nq1 97 q8\nq1 98 q9\nq1 99 q10\nq1 4294967196 q38\nq2 97 q13\nq2 98 q16\nq2 99 q21\nq2 4294967196 q39\nq3 97 q22\nq3 98 q27\nq3 99 q34\nq3 4294967196 q42\nq4 4294967295 q36\nq5 4294967295 q1\nq6 98 q0\nq7 99 q0\nq8 97 q1\nq9 4294967295 q2\nq10 97 q11\nq11 4294967295 q12\nq12 99 q0\nq13 97 q14\nq14 4294967295 q15\nq15 98 q1\nq16 97 q17\nq17 4294967295 q18\nq18 98 q19\nq19 4294967295 q20\nq20 98 q0\nq21 4294967295 q3\nq22 97 q23\nq23 4294967295 q24\nq24 98 q25\nq25 4294967295 q26\nq26 99 q1\nq27 97 q28\nq28 4294967295 q29\nq29 98 q30\nq30 4294967295 q31\nq31 99 q32\nq32 4294967295 q33\nq33 98 q0\nq34 4294967295 q4\nq36 97 q0\nq37 4294967295 q35\nq38 97 q35\nq39 97 q40\nq40 4294967295 q41\nq41 98 q35\nq42 97 q43\nq43 4294967295 q44\nq44 98 q45\nq45 4294967295 q46\nq46 99 q35\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("'abcc' replace with 'bbb' replace single") {
+        nft = reluctant_replace.replace_literal_nft(Word{ 'a', 'b', 'c', 'c' }, Word{ 'b', 'b', 'b' },  &alphabet, END_MARKER, ReplaceMode::Single);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'c', 'c', 'a', 'a', 'b', 'c', 'c', END_MARKER },
+                                     { 'a', 'a', 'a', 'b', 'a', 'a', 'b', 'b', 'b', 'a', 'a', 'b', 'c', 'c' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q45 q46\n%Levels q0:0 q1:0 q2:0 q3:0 q4:0 q5:1 q6:1 q7:1 q8:1 q9:1 q10:1 q11:0 q12:1 q13:1 q14:0 q15:1 q16:1 q17:0 q18:1 q19:0 q20:1 q21:1 q22:1 q23:0 q24:1 q25:0 q26:1 q27:1 q28:0 q29:1 q30:0 q31:1 q32:0 q33:1 q34:1 q35:1 q36:0 q37:1 q38:0 q39:1 q40:0 q41:1 q42:1 q43:1 q44:1 q45:0 q46:0 q47:1 q48:1 q49:1 q50:0 q51:1 q52:1 q53:0 q54:1 q55:0 q56:1\n%LevelsNum 2\nq0 97 q5\nq0 98 q6\nq0 99 q7\nq0 4294967196 q47\nq1 97 q8\nq1 98 q9\nq1 99 q10\nq1 4294967196 q48\nq2 97 q13\nq2 98 q16\nq2 99 q21\nq2 4294967196 q49\nq3 97 q22\nq3 98 q27\nq3 99 q34\nq3 4294967196 q52\nq4 4294967295 q35\nq5 4294967295 q1\nq6 98 q0\nq7 99 q0\nq8 97 q1\nq9 4294967295 q2\nq10 97 q11\nq11 4294967295 q12\nq12 99 q0\nq13 97 q14\nq14 4294967295 q15\nq15 98 q1\nq16 97 q17\nq17 4294967295 q18\nq18 98 q19\nq19 4294967295 q20\nq20 98 q0\nq21 4294967295 q3\nq22 97 q23\nq23 4294967295 q24\nq24 98 q25\nq25 4294967295 q26\nq26 99 q1\nq27 97 q28\nq28 4294967295 q29\nq29 98 q30\nq30 4294967295 q31\nq31 99 q32\nq32 4294967295 q33\nq33 98 q0\nq34 4294967295 q4\nq35 98 q36\nq36 4294967295 q37\nq37 98 q38\nq38 4294967295 q39\nq39 98 q40\nq40 97 q41\nq40 98 q42\nq40 99 q43\nq40 4294967196 q44\nq41 97 q40\nq42 98 q40\nq43 99 q40\nq44 4294967295 q45\nq47 4294967295 q46\nq48 97 q46\nq49 97 q50\nq50 4294967295 q51\nq51 98 q46\nq52 97 q53\nq53 4294967295 q54\nq54 98 q55\nq55 4294967295 q56\nq56 99 q46\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("'aabac' replace with 'd' replace all") {
+        nft = reluctant_replace.replace_literal_nft(Word{ 'a', 'a', 'b', 'a', 'c' }, Word{ 'd' }, &alphabet, END_MARKER,
+                                  ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a', END_MARKER },
+                                     { 'a', 'a', 'a', 'b', 'a', 'd', 'a' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q54\n%Levels q0:0 q1:0 q2:0 q3:0 q4:0 q5:0 q6:1 q7:1 q8:1 q9:1 q10:1 q11:0 q12:1 q13:1 q14:0 q15:1 q16:1 q17:1 q18:1 q19:0 q20:1 q21:0 q22:1 q23:1 q24:1 q25:0 q26:1 q27:0 q28:1 q29:0 q30:1 q31:1 q32:0 q33:1 q34:0 q35:1 q36:0 q37:1 q38:1 q39:0 q40:1 q41:0 q42:1 q43:1 q44:0 q45:1 q46:0 q47:1 q48:0 q49:1 q50:0 q51:1 q52:1 q53:1 q54:0 q55:1 q56:1 q57:1 q58:0 q59:1 q60:1 q61:0 q62:1 q63:0 q64:1 q65:1 q66:0 q67:1 q68:0 q69:1 q70:0 q71:1\n%LevelsNum 2\nq0 97 q6\nq0 98 q7\nq0 99 q8\nq0 4294967196 q55\nq1 97 q9\nq1 98 q10\nq1 99 q13\nq1 4294967196 q56\nq2 97 q16\nq2 98 q17\nq2 99 q18\nq2 4294967196 q57\nq3 97 q23\nq3 98 q24\nq3 99 q31\nq3 4294967196 q60\nq4 97 q38\nq4 98 q43\nq4 99 q52\nq4 4294967196 q65\nq5 4294967295 q53\nq6 4294967295 q1\nq7 98 q0\nq8 99 q0\nq9 4294967295 q2\nq10 97 q11\nq11 4294967295 q12\nq12 98 q0\nq13 97 q14\nq14 4294967295 q15\nq15 99 q0\nq16 97 q2\nq17 4294967295 q3\nq18 97 q19\nq19 4294967295 q20\nq20 97 q21\nq21 4294967295 q22\nq22 99 q0\nq23 4294967295 q4\nq24 97 q25\nq25 4294967295 q26\nq26 97 q27\nq27 4294967295 q28\nq28 98 q29\nq29 4294967295 q30\nq30 98 q0\nq31 97 q32\nq32 4294967295 q33\nq33 97 q34\nq34 4294967295 q35\nq35 98 q36\nq36 4294967295 q37\nq37 99 q0\nq38 97 q39\nq39 4294967295 q40\nq40 97 q41\nq41 4294967295 q42\nq42 98 q2\nq43 97 q44\nq44 4294967295 q45\nq45 97 q46\nq46 4294967295 q47\nq47 98 q48\nq48 4294967295 q49\nq49 97 q50\nq50 4294967295 q51\nq51 98 q0\nq52 4294967295 q5\nq53 100 q0\nq55 4294967295 q54\nq56 97 q54\nq57 97 q58\nq58 4294967295 q59\nq59 97 q54\nq60 97 q61\nq61 4294967295 q62\nq62 97 q63\nq63 4294967295 q64\nq64 98 q54\nq65 97 q66\nq66 4294967295 q67\nq67 97 q68\nq68 4294967295 q69\nq69 98 q70\nq70 4294967295 q71\nq71 97 q54\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+}
+
+TEST_CASE("mata::nft::strings::replace_reluctant_literal()") {
+    Nft nft{};
+    Nft expected{};
+    EnumAlphabet alphabet{ 'a', 'b', 'c' };
+
+    SECTION("'abcc' replace with 'a' replace all") {
+        nft = nft::strings::replace_reluctant_literal(Word{ 'a', 'b', 'c', 'c' }, Word{ 'a' },  &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'b', 'c', 'c' },
+                                   { 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'b', 'c', 'c' },
+                                   { 'a', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'c', 'a', 'b', 'c', 'c' },
+                                   { 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'c', 'a', 'b', 'c', 'c', 'a' },
+                                   { 'c', 'a', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'c', 'a', 'b', 'c', 'c', 'a', 'a', 'b', 'c', 'c' },
+                                   { 'c', 'a', 'a', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'c', 'a', 'b', 'c', 'c', 'a', 'a', 'b', 'c', 'c' },
+                                   { 'a', 'c', 'a', 'a', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'c', 'c', 'a', 'a', 'b', 'c', 'c', 'a' },
+                                   { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'a', 'a', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { }, { } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'b', 'c' },
+                                   { 'a', 'b', 'c' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'b', 'c' },
+                                   { 'a', 'b', 'c' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'c', 'c', 'c' },
+                                   { 'c', 'c', 'c' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q35\n%Levels q0:0 q1:0 q2:0 q3:0 q4:0 q5:1 q6:1 q7:1 q8:1 q9:1 q10:1 q11:0 q12:1 q13:1 q14:0 q15:1 q16:1 q17:0 q18:1 q19:0 q20:1 q21:1 q22:1 q23:0 q24:1 q25:0 q26:1 q27:1 q28:0 q29:1 q30:0 q31:1 q32:0 q33:1 q34:1 q35:0 q36:1 q37:1 q38:1 q39:1 q40:0 q41:1 q42:1 q43:0 q44:1 q45:0 q46:1\n%LevelsNum 2\nq0 97 q5\nq0 98 q6\nq0 99 q7\nq0 4294967295 q37\nq1 97 q8\nq1 98 q9\nq1 99 q10\nq1 4294967295 q38\nq2 97 q13\nq2 98 q16\nq2 99 q21\nq2 4294967295 q39\nq3 97 q22\nq3 98 q27\nq3 99 q34\nq3 4294967295 q42\nq4 4294967295 q36\nq5 4294967295 q1\nq6 98 q0\nq7 99 q0\nq8 97 q1\nq9 4294967295 q2\nq10 97 q11\nq11 4294967295 q12\nq12 99 q0\nq13 97 q14\nq14 4294967295 q15\nq15 98 q1\nq16 97 q17\nq17 4294967295 q18\nq18 98 q19\nq19 4294967295 q20\nq20 98 q0\nq21 4294967295 q3\nq22 97 q23\nq23 4294967295 q24\nq24 98 q25\nq25 4294967295 q26\nq26 99 q1\nq27 97 q28\nq28 4294967295 q29\nq29 98 q30\nq30 4294967295 q31\nq31 99 q32\nq32 4294967295 q33\nq33 98 q0\nq34 4294967295 q4\nq36 97 q0\nq37 4294967295 q35\nq38 97 q35\nq39 97 q40\nq40 4294967295 q41\nq41 98 q35\nq42 97 q43\nq43 4294967295 q44\nq44 98 q45\nq45 4294967295 q46\nq46 99 q35\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+   SECTION("'abcc' replace with 'dd' replace single") {
+       nft = nft::strings::replace_reluctant_literal(Word{ 'a', 'b', 'c', 'c' }, Word{ 'd', 'd' },  &alphabet, ReplaceMode::Single);
+       CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'c', 'c', 'a', 'a', 'b', 'c', 'c', 'a' },
+                                  { 'a', 'a', 'a', 'b', 'a', 'a', 'd', 'd', 'a', 'a', 'b', 'c', 'c', 'a' } }));
+       CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'c' },
+                                  { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'c' } }));
+       expected = nft::builder::parse_from_mata(std::string(
+         "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q5\n%Levels q0:0 q1:1 q2:1 q3:1 q4:1 q5:0 q6:0 q7:1 q8:1 q9:1 q10:1 q11:0 q12:1 q13:0 q14:1 q15:1 q16:1 q17:1 q18:0 q19:1 q20:0 q21:1 q22:1 q23:1 q24:1 q25:0 q26:1 q27:0 q28:1 q29:0 q30:1 q31:0 q32:1 q33:0 q34:1 q35:1 q36:1 q37:1 q38:0 q39:1 q40:0 q41:1 q42:0 q43:1 q44:0 q45:1 q46:0 q47:1 q48:0 q49:1 q50:0 q51:1 q52:0 q53:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq0 4294967295 q4\nq1 4294967295 q6\nq2 98 q0\nq3 99 q0\nq4 4294967295 q5\nq6 97 q7\nq6 98 q8\nq6 99 q9\nq6 4294967295 q10\nq7 97 q6\nq8 4294967295 q13\nq9 97 q11\nq10 97 q5\nq11 4294967295 q12\nq12 99 q0\nq13 97 q14\nq13 98 q15\nq13 99 q16\nq13 4294967295 q17\nq14 97 q52\nq15 97 q48\nq16 4294967295 q20\nq17 97 q18\nq18 4294967295 q19\nq19 98 q5\nq20 97 q21\nq20 98 q22\nq20 99 q23\nq20 4294967295 q24\nq21 97 q44\nq22 97 q38\nq23 4294967295 q29\nq24 97 q25\nq25 4294967295 q26\nq26 98 q27\nq27 4294967295 q28\nq28 99 q5\nq29 4294967295 q30\nq30 100 q31\nq31 4294967295 q32\nq32 100 q33\nq33 97 q34\nq33 98 q35\nq33 99 q36\nq33 4294967295 q37\nq34 97 q33\nq35 98 q33\nq36 99 q33\nq37 4294967295 q5\nq38 4294967295 q39\nq39 98 q40\nq40 4294967295 q41\nq41 99 q42\nq42 4294967295 q43\nq43 98 q0\nq44 4294967295 q45\nq45 98 q46\nq46 4294967295 q47\nq47 99 q6\nq48 4294967295 q49\nq49 98 q50\nq50 4294967295 q51\nq51 98 q0\nq52 4294967295 q53\nq53 98 q6\n"
+       ));
+       CHECK(nft::are_equivalent(nft, expected));
+   }
+
+   SECTION("'aabac' replace with 'd' replace all") {
+       nft = nft::strings::replace_reluctant_literal(Word{ 'a', 'a', 'b', 'a', 'c' }, Word{ 'd' }, &alphabet,
+                                                   ReplaceMode::All);
+       CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                  { 'a', 'a', 'a', 'b', 'a', 'd', 'a' } }));
+       CHECK(nft.is_tuple_in_lang({ { }, { } }));
+       CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' },
+                                  { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' } }));
+       expected = nft::builder::parse_from_mata(std::string(
+          "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q5\n%Levels q0:0 q1:1 q2:1 q3:1 q4:1 q5:0 q6:0 q7:1 q8:1 q9:1 q10:1 q11:0 q12:1 q13:0 q14:1 q15:0 q16:1 q17:1 q18:1 q19:1 q20:0 q21:1 q22:0 q23:1 q24:0 q25:1 q26:0 q27:1 q28:1 q29:1 q30:1 q31:0 q32:1 q33:0 q34:1 q35:0 q36:1 q37:0 q38:1 q39:0 q40:1 q41:0 q42:1 q43:0 q44:1 q45:0 q46:1 q47:0 q48:1 q49:1 q50:1 q51:1 q52:0 q53:1 q54:0 q55:1 q56:0 q57:1 q58:0 q59:1 q60:0 q61:1 q62:0 q63:1 q64:0 q65:1 q66:0 q67:1 q68:0 q69:1 q70:0 q71:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq0 4294967295 q4\nq1 4294967295 q6\nq2 98 q0\nq3 99 q0\nq4 4294967295 q5\nq6 97 q7\nq6 98 q8\nq6 99 q9\nq6 4294967295 q10\nq7 4294967295 q15\nq8 97 q13\nq9 97 q11\nq10 97 q5\nq11 4294967295 q12\nq12 99 q0\nq13 4294967295 q14\nq14 98 q0\nq15 97 q16\nq15 98 q17\nq15 99 q18\nq15 4294967295 q19\nq16 97 q15\nq17 4294967295 q26\nq18 97 q22\nq19 97 q20\nq20 4294967295 q21\nq21 97 q5\nq22 4294967295 q23\nq23 97 q24\nq24 4294967295 q25\nq25 99 q0\nq26 97 q27\nq26 98 q28\nq26 99 q29\nq26 4294967295 q30\nq27 4294967295 q47\nq28 97 q41\nq29 97 q35\nq30 97 q31\nq31 4294967295 q32\nq32 97 q33\nq33 4294967295 q34\nq34 98 q5\nq35 4294967295 q36\nq36 97 q37\nq37 4294967295 q38\nq38 98 q39\nq39 4294967295 q40\nq40 99 q0\nq41 4294967295 q42\nq42 97 q43\nq43 4294967295 q44\nq44 98 q45\nq45 4294967295 q46\nq46 98 q0\nq47 97 q48\nq47 98 q49\nq47 99 q50\nq47 4294967295 q51\nq48 97 q68\nq49 97 q60\nq50 4294967295 q58\nq51 97 q52\nq52 4294967295 q53\nq53 97 q54\nq54 4294967295 q55\nq55 98 q56\nq56 4294967295 q57\nq57 97 q5\nq58 4294967295 q59\nq59 100 q0\nq60 4294967295 q61\nq61 97 q62\nq62 4294967295 q63\nq63 98 q64\nq64 4294967295 q65\nq65 97 q66\nq66 4294967295 q67\nq67 98 q0\nq68 4294967295 q69\nq69 97 q70\nq70 4294967295 q71\nq71 98 q15\n"
+       ));
+       CHECK(nft::are_equivalent(nft, expected));
+   }
+
+    SECTION("drop all 'aabac'") {
+        nft = nft::strings::replace_reluctant_literal(Word{ 'a', 'a', 'b', 'a', 'c' }, Word{}, &alphabet,
+                                                      ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'a', 'a', 'a', 'b', 'a', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a', 'a', 'b', 'a', 'c' },
+                                     { 'a', 'a', 'a', 'b', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { }, { } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' },
+                                     { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q5\n%Levels q0:0 q1:1 q2:1 q3:1 q4:1 q5:0 q6:0 q7:1 q8:1 q9:1 q10:1 q11:0 q12:1 q13:0 q14:1 q15:0 q16:1 q17:1 q18:1 q19:1 q20:0 q21:1 q22:0 q23:1 q24:0 q25:1 q26:0 q27:1 q28:1 q29:1 q30:1 q31:0 q32:1 q33:0 q34:1 q35:0 q36:1 q37:0 q38:1 q39:0 q40:1 q41:0 q42:1 q43:0 q44:1 q45:0 q46:1 q47:0 q48:1 q49:1 q50:1 q51:1 q52:0 q53:1 q54:0 q55:1 q56:0 q57:1 q58:0 q59:1 q60:0 q61:1 q62:0 q63:1 q64:0 q65:1 q66:0 q67:1 q68:0 q69:1 q70:0 q71:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq0 4294967295 q4\nq1 4294967295 q6\nq2 98 q0\nq3 99 q0\nq4 4294967295 q5\nq6 97 q7\nq6 98 q8\nq6 99 q9\nq6 4294967295 q10\nq7 4294967295 q15\nq8 97 q13\nq9 97 q11\nq10 97 q5\nq11 4294967295 q12\nq12 99 q0\nq13 4294967295 q14\nq14 98 q0\nq15 97 q16\nq15 98 q17\nq15 99 q18\nq15 4294967295 q19\nq16 97 q15\nq17 4294967295 q26\nq18 97 q22\nq19 97 q20\nq20 4294967295 q21\nq21 97 q5\nq22 4294967295 q23\nq23 97 q24\nq24 4294967295 q25\nq25 99 q0\nq26 97 q27\nq26 98 q28\nq26 99 q29\nq26 4294967295 q30\nq27 4294967295 q47\nq28 97 q41\nq29 97 q35\nq30 97 q31\nq31 4294967295 q32\nq32 97 q33\nq33 4294967295 q34\nq34 98 q5\nq35 4294967295 q36\nq36 97 q37\nq37 4294967295 q38\nq38 98 q39\nq39 4294967295 q40\nq40 99 q0\nq41 4294967295 q42\nq42 97 q43\nq43 4294967295 q44\nq44 98 q45\nq45 4294967295 q46\nq46 98 q0\nq47 97 q48\nq47 98 q49\nq47 99 q50\nq47 4294967295 q51\nq48 97 q68\nq49 97 q60\nq50 4294967295 q58\nq51 97 q52\nq52 4294967295 q53\nq53 97 q54\nq54 4294967295 q55\nq55 98 q56\nq56 4294967295 q57\nq57 97 q5\nq58 4294967295 q59\nq59 4294967295 q0\nq60 4294967295 q61\nq61 97 q62\nq62 4294967295 q63\nq63 98 q64\nq64 4294967295 q65\nq65 97 q66\nq66 4294967295 q67\nq67 98 q0\nq68 4294967295 q69\nq69 97 q70\nq70 4294967295 q71\nq71 98 q15\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("drop single 'aabac'") {
+        nft = nft::strings::replace_reluctant_literal(Word{ 'a', 'a', 'b', 'a', 'c' }, Word{}, &alphabet,
+                                                      ReplaceMode::Single);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a', 'a', 'b', 'a', 'c' },
+                                     { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c'} }));
+        CHECK(nft.is_tuple_in_lang({ { }, { } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' },
+                                     { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q5\n%Levels q0:0 q1:1 q2:1 q3:1 q4:1 q5:0 q6:0 q7:1 q8:1 q9:1 q10:1 q11:0 q12:1 q13:0 q14:1 q15:0 q16:1 q17:1 q18:1 q19:1 q20:0 q21:1 q22:0 q23:1 q24:0 q25:1 q26:0 q27:1 q28:1 q29:1 q30:1 q31:0 q32:1 q33:0 q34:1 q35:0 q36:1 q37:0 q38:1 q39:0 q40:1 q41:0 q42:1 q43:0 q44:1 q45:0 q46:1 q47:0 q48:1 q49:1 q50:1 q51:1 q52:0 q53:1 q54:0 q55:1 q56:0 q57:1 q58:0 q59:1 q60:0 q61:1 q62:1 q63:1 q64:1 q65:0 q66:1 q67:0 q68:1 q69:0 q70:1 q71:0 q72:1 q73:0 q74:1 q75:0 q76:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq0 4294967295 q4\nq1 4294967295 q6\nq2 98 q0\nq3 99 q0\nq4 4294967295 q5\nq6 97 q7\nq6 98 q8\nq6 99 q9\nq6 4294967295 q10\nq7 4294967295 q15\nq8 97 q13\nq9 97 q11\nq10 97 q5\nq11 4294967295 q12\nq12 99 q0\nq13 4294967295 q14\nq14 98 q0\nq15 97 q16\nq15 98 q17\nq15 99 q18\nq15 4294967295 q19\nq16 97 q15\nq17 4294967295 q26\nq18 97 q22\nq19 97 q20\nq20 4294967295 q21\nq21 97 q5\nq22 4294967295 q23\nq23 97 q24\nq24 4294967295 q25\nq25 99 q0\nq26 97 q27\nq26 98 q28\nq26 99 q29\nq26 4294967295 q30\nq27 4294967295 q47\nq28 97 q41\nq29 97 q35\nq30 97 q31\nq31 4294967295 q32\nq32 97 q33\nq33 4294967295 q34\nq34 98 q5\nq35 4294967295 q36\nq36 97 q37\nq37 4294967295 q38\nq38 98 q39\nq39 4294967295 q40\nq40 99 q0\nq41 4294967295 q42\nq42 97 q43\nq43 4294967295 q44\nq44 98 q45\nq45 4294967295 q46\nq46 98 q0\nq47 97 q48\nq47 98 q49\nq47 99 q50\nq47 4294967295 q51\nq48 97 q73\nq49 97 q65\nq50 4294967295 q58\nq51 97 q52\nq52 4294967295 q53\nq53 97 q54\nq54 4294967295 q55\nq55 98 q56\nq56 4294967295 q57\nq57 97 q5\nq58 4294967295 q59\nq59 4294967295 q60\nq60 97 q61\nq60 98 q62\nq60 99 q63\nq60 4294967295 q64\nq61 97 q60\nq62 98 q60\nq63 99 q60\nq64 4294967295 q5\nq65 4294967295 q66\nq66 97 q67\nq67 4294967295 q68\nq68 98 q69\nq69 4294967295 q70\nq70 97 q71\nq71 4294967295 q72\nq72 98 q0\nq73 4294967295 q74\nq74 97 q75\nq75 4294967295 q76\nq76 98 q15\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+}
+
+TEST_CASE("mata::nft::strings::replace_reluctant_symbol()") {
+    Nft nft{};
+    Nft expected{};
+    EnumAlphabet alphabet{ 'a', 'b', 'c' };
+
+    SECTION("'a' replace with 'b' replace all") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_single_symbol('a', 'd', &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'd', 'd', 'd', 'b', 'd', 'd', 'd', 'b', 'd', 'c', 'd' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' },
+                                     { 'd', 'd', 'd', 'b', 'd', 'd', 'd', 'b', 'd', 'd' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q0\n%Levels q0:0 q1:1 q2:1 q3:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq1 100 q0\nq2 98 q0\nq3 99 q0\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+
+        // Use replace symbol with literal containing a single symbol.
+        nft = nft::strings::replace_reluctant_single_symbol('a', Word{ 'd' }, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'd', 'd', 'd', 'b', 'd', 'd', 'd', 'b', 'd', 'c', 'd' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' },
+                                     { 'd', 'd', 'd', 'b', 'd', 'd', 'd', 'b', 'd', 'd' } }));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("'a' replace with 'b' replace single") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_single_symbol('a', 'd', &alphabet, ReplaceMode::Single);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'd', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' },
+                                     { 'd', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q0 q4\n%Levels q0:0 q1:1 q2:1 q3:1 q4:0 q5:1 q6:1 q7:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq1 100 q4\nq2 98 q0\nq3 99 q0\nq4 97 q5\nq4 98 q6\nq4 99 q7\nq5 97 q4\nq6 98 q4\nq7 99 q4\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+
+        // Use replace symbol with literal containing a single symbol.
+        nft = nft::strings::replace_reluctant_single_symbol('a', Word{ 'd' }, &alphabet, ReplaceMode::Single);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'd', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' },
+                                     { 'd', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'b', 'b', 'b', 'b', 'a', 'a', 'a', 'b', 'a', 'a' },
+                                     { 'b', 'b', 'b', 'b', 'd', 'a', 'a', 'b', 'a', 'a' } }));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("'a' replace with 'bb' replace all") {
+        nft = nft::strings::replace_reluctant_single_symbol('a', Word{ 'b', 'b' }, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'c', 'b', 'b' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+        CHECK(nft.is_tuple_in_lang({ { 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b' },
+                                     { 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q0\n%Levels q0:0 q1:1 q2:1 q3:1 q4:0 q5:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq1 98 q4\nq2 98 q0\nq3 99 q0\nq4 4294967295 q5\nq5 98 q0\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("drop all 'a'") {
+        nft = nft::strings::replace_reluctant_single_symbol('a', Word{}, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'b', 'b', 'c' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+        CHECK(nft.is_tuple_in_lang({ { 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b' },
+                                     { 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q0\n%Levels q0:0 q1:1 q2:1 q3:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq1 4294967295 q0\nq2 98 q0\nq3 99 q0\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("drop single 'a'") {
+        nft = nft::strings::replace_reluctant_single_symbol('a', Word{}, &alphabet, ReplaceMode::Single);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+        CHECK(nft.is_tuple_in_lang({ { 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b' },
+                                     { 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b' } }));
+        expected = nft::builder::parse_from_mata(std::string(
+            "@NFT-explicit\n%Alphabet-auto\n%Initial q0\n%Final q0 q4\n%Levels q0:0 q1:1 q2:1 q3:1 q4:0 q5:1 q6:1 q7:1\n%LevelsNum 2\nq0 97 q1\nq0 98 q2\nq0 99 q3\nq1 4294967295 q4\nq2 98 q0\nq3 99 q0\nq4 97 q5\nq4 98 q6\nq4 99 q7\nq5 97 q4\nq6 98 q4\nq7 99 q4\n"
+        ));
+        CHECK(nft::are_equivalent(nft, expected));
+    }
+}
+
+TEST_CASE("mata::nft::strings::replace_reluctant_regex()") {
+    Nft nft{};
+    Nft expected{};
+    EnumAlphabet alphabet{ 'a', 'b', 'c' };
+
+    SECTION("'a+b+c' replace with 'dd' replace all") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a+b+c", Word{ 'd', 'd' }, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'b', 'c', 'c', 'a', 'a' },
+                                     { 'a', 'a', 'a', 'b', 'd', 'd', 'c', 'a', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'c', 'a', 'a', 'a', 'b', 'b', 'a', 'a', 'b', 'c' },
+                                     { 'd', 'd', 'a', 'a', 'a', 'b', 'b', 'd', 'd' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'c', 'a', 'a', 'a', 'b', 'b', 'a', 'a', 'b', 'c', 'c', 'a', 'b', 'a', 'c' },
+                                     { 'd', 'd', 'a', 'a', 'a', 'b', 'b', 'd', 'd', 'c', 'a', 'b', 'a', 'c' } }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("'a' replace with 'd' replace all") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a", Word{ 'd' }, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'd', 'd', 'd', 'b', 'd', 'd', 'd', 'b', 'd', 'c', 'd' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("drop 'a' replace all") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a", Word{}, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'b', 'b', 'c' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("drop 'a' replace single") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a", Word{}, &alphabet, ReplaceMode::Single);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("drop 'a+b' replace single") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a+b", Word{}, &alphabet, ReplaceMode::Single);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'a', 'a', 'a', 'b', 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("drop 'a+b' replace all") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a+b", Word{}, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("replace 'a+b' with 'dd' replace all") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a+b", Word{ 'd', 'd' }, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'd', 'd', 'd', 'd', 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("replace 'a+b' with 'dd' replace single") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a+b", Word{ 'd', 'd' }, &alphabet, ReplaceMode::Single);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'd', 'd', 'a', 'a', 'a', 'b', 'a', 'c', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("replace 'a*b*c' with 'dd' replace all") {
+        // Use replace symbol with symbol.
+        nft = nft::strings::replace_reluctant_regex("a*b*c", Word{ 'd', 'd' }, &alphabet, ReplaceMode::All);
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'a', 'c', 'a' },
+                                     { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'd', 'd', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'a', 'a', 'a', 'b', 'c', 'a' },
+                                     { 'a', 'a', 'a', 'b', 'd', 'd', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ { 'a', 'a', 'a', 'b', 'c', 'a', 'a', 'a', 'b', 'c', 'a' },
+                                     { 'd', 'd', 'd', 'd', 'a' } }));
+        CHECK(nft.is_tuple_in_lang({ {},
+                                     {} }));
+//        expected = nft::builder::parse_from_mata(std::string(
+//        ));
+//        CHECK(nft::are_equivalent(nft, expected));
+    }
+
+    SECTION("bugfix: replace from Noodler") {
+        nfa::Nfa regex = nfa::builder::parse_from_mata(std::string(
+            "@NFA-explicit\n%Alphabet-auto\n%Initial q0\n%Final q7\nq0 60 q1\nq1 83 q2\nq2 67 q3\nq3 82 q4\nq4 73 q5\nq5 80 q6\nq6 84 q7\nq7 48 q7\nq7 49 q7\nq7 50 q7\nq7 51 q7\nq7 52 q7\nq7 53 q7\nq7 54 q7\nq7 55 q7\nq7 56 q7\nq7 57 q7\nq7 60 q7\nq7 66 q7\nq7 67 q7\nq7 68 q7\nq7 69 q7\nq7 70 q7\nq7 71 q7\nq7 72 q7\nq7 73 q7\nq7 74 q7\nq7 75 q7\nq7 76 q7\nq7 78 q7\nq7 79 q7\nq7 80 q7\nq7 82 q7\nq7 83 q7\nq7 84 q7\nq7 86 q7\nq7 87 q7\nq7 92 q7\nq7 97 q7\nq7 98 q7\nq7 99 q7\nq7 100 q7\nq7 101 q7\nq7 102 q7\nq7 103 q7\nq7 104 q7\nq7 105 q7\nq7 106 q7\nq7 107 q7\nq7 108 q7\nq7 109 q7\nq7 110 q7\nq7 112 q7\nq7 114 q7\nq7 115 q7\nq7 116 q7\nq7 119 q7\nq7 120 q7\nq7 121 q7\nq7 196608 q7\n"
+        ));
+        mata::Word replacement{ 66, 76, 79, 67, 75, 69, 68 };
+        alphabet = mata::EnumAlphabet{ 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 60, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 78, 79, 80, 82, 83, 84, 86, 87, 92, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 112, 114, 115, 116, 119, 120, 121, 196608 };
+        nft = nft::strings::replace_reluctant_regex(regex, replacement, &alphabet, ReplaceMode::All);
+
+        CHECK(nft.is_tuple_in_lang({ { '<', 'S', 'C', 'R', 'I', 'P', 'T', '<', '<', '<', '<' },
+                                     { 'B', 'L', 'O', 'C', 'K', 'E', 'D', '<', '<', '<', '<' } }));
+        CHECK(nft.is_tuple_in_lang({ { '<', '<', 'S', 'C', 'R', 'I', 'P', 'T', '<', '<', '<', '<' },
+                                     { '<', 'B', 'L', 'O', 'C', 'K', 'E', 'D', '<', '<', '<', '<' } }));
+        CHECK(nft.is_tuple_in_lang({ { '<', 'S', '<', 'S', 'C', 'R', 'I', 'P', 'T', '<', '<', '<', '<' },
+                                     { '<', 'S', 'B', 'L', 'O', 'C', 'K', 'E', 'D', '<', '<', '<', '<' } }));
+        CHECK(nft.is_tuple_in_lang({ { '<', 'S', '<', 'S', 'C', 'R', 'I', 'P', 'T', 'T', 'T', '<', '<' },
+                                     { '<', 'S', 'B', 'L', 'O', 'C', 'K', 'E', 'D', 'T', 'T', '<', '<' } }));
+        CHECK(!nft.is_tuple_in_lang({ { '<', 'S', '<', 'S', 'C', 'R', 'I', 'P', 'T', 'T', 'T', '<', '<' },
+                                     { '<', 'S', 'B', 'L', 'O', 'C', 'K', 'E', 'D', 'T', '<', '<' } }));
+        CHECK(!nft.is_tuple_in_lang({ { '<', 's', '<', 's', 'c', 'r', 'i', 'p', 't', 't', 't', '<', '<' },
+                                     { '<', 's', 'B', 'L', 'O', 'C', 'K', 'E', 'D', 't', 't', '<', '<' } }));
+    }
+}
diff --git a/tests/nft/utils.hh b/tests/nft/utils.hh
new file mode 100644
index 000000000..8ee2cf007
--- /dev/null
+++ b/tests/nft/utils.hh
@@ -0,0 +1,102 @@
+#ifndef UTILS_HH
+#define UTILS_HH
+
+// Automaton A
+#define FILL_WITH_AUT_A(x) \
+    x.initial = {1, 3}; \
+	x.final = {5}; \
+	x.delta.add(1, 'a', 3); \
+	x.delta.add(1, 'a', 10); \
+	x.delta.add(1, 'b', 7); \
+	x.delta.add(3, 'a', 7); \
+	x.delta.add(3, 'b', 9); \
+	x.delta.add(9, 'a', 9); \
+	x.delta.add(7, 'b', 1); \
+	x.delta.add(7, 'a', 3); \
+	x.delta.add(7, 'c', 3); \
+	x.delta.add(10, 'a', 7); \
+	x.delta.add(10, 'b', 7); \
+	x.delta.add(10, 'c', 7); \
+	x.delta.add(7, 'a', 5); \
+	x.delta.add(5, 'a', 5); \
+	x.delta.add(5, 'c', 9); \
+
+// Automaton B
+#define FILL_WITH_AUT_B(x) \
+	x.initial = {4}; \
+	x.final = {2, 12}; \
+	x.delta.add(4, 'c', 8); \
+	x.delta.add(4, 'a', 8); \
+	x.delta.add(8, 'b', 4); \
+	x.delta.add(4, 'a', 6); \
+	x.delta.add(4, 'b', 6); \
+	x.delta.add(6, 'a', 2); \
+	x.delta.add(2, 'b', 2); \
+	x.delta.add(2, 'a', 0); \
+	x.delta.add(0, 'a', 2); \
+	x.delta.add(2, 'c', 12); \
+	x.delta.add(12, 'a', 14); \
+	x.delta.add(14, 'b', 12); \
+
+// Automaton C
+// the same as B, but with small symbols
+#define FILL_WITH_AUT_C(x) \
+	x.initial = {4}; \
+	x.final = {2, 12}; \
+	x.delta.add(4, 3, 8); \
+	x.delta.add(4, 1, 8); \
+	x.delta.add(8, 2, 4); \
+	x.delta.add(4, 1, 6); \
+	x.delta.add(4, 2, 6); \
+	x.delta.add(6, 1, 2); \
+	x.delta.add(2, 2, 2); \
+	x.delta.add(2, 1, 0); \
+	x.delta.add(0, 1, 2); \
+	x.delta.add(2, 3, 12); \
+	x.delta.add(12, 1, 14); \
+	x.delta.add(14, 2, 12);   \
+
+// Automaton D // shomewhat larger
+#define FILL_WITH_AUT_D(x) \
+    x.initial = {0}; \
+    x.final = {3}; \
+    x.delta.add(0, 46, 0); \
+    x.delta.add(0, 47, 0); \
+    x.delta.add(0, 58, 0); \
+    x.delta.add(0, 58, 1); \
+    x.delta.add(0, 64, 1); \
+    x.delta.add(0, 64, 1); \
+    x.delta.add(0, 82, 2); \
+    x.delta.add(0, 92, 2); \
+    x.delta.add(0, 98, 2); \
+    x.delta.add(0, 100, 1);\
+    x.delta.add(0, 103, 0);\
+    x.delta.add(0, 109, 0);\
+    x.delta.add(0, 110, 1);\
+    x.delta.add(0, 111, 2);\
+    x.delta.add(0, 114, 0);\
+    x.delta.add(1, 47, 2); \
+    x.delta.add(2, 47, 3); \
+    x.delta.add(3, 46, 2); \
+    x.delta.add(3, 47, 0); \
+    x.delta.add(3, 58, 2); \
+    x.delta.add(3, 64, 3); \
+    x.delta.add(3, 82, 2); \
+    x.delta.add(3, 92, 0); \
+    x.delta.add(3, 98, 2); \
+    x.delta.add(3, 100, 2);\
+    x.delta.add(3, 103, 3);\
+    x.delta.add(3, 109, 2);\
+    x.delta.add(3, 110, 3);\
+    x.delta.add(3, 111, 2);\
+    x.delta.add(3, 114, 3);\
+
+// Automaton E
+#define FILL_WITH_AUT_E(x) \
+	x.initial = {1, 3}; \
+	x.final = {4}; \
+	x.delta.add(1, 'b', 2); \
+	x.delta.add(2, 'a', 4); \
+	x.delta.add(1, 'a', 3); \
+
+#endif // UTILS_HH