rewrite ast

The main motivation is to use AST nodes as vertices of networkx.DiGraph.
Using integers as vertices and maintaining the map between the two is
cumbersome and fragile. It is also slower.

The benefit of using networkx comes from easy AST manipulation, like
attaching a subtree, or making in-place node substitutions.
So if more AST-related functionality is needed in the future,
manipulating the AST will be easier and lexical workarounds less
attractive.

By default, user-defined classes (as all the AST node types are) are
hashable, with __hash__ == __id__. So they can be used as nodes, w/o
modification. This allows removing the "id" attribute. However, the
"graph" attribute needs to remain, because otherwise the node classes
cannot access their neighbors. Using the __hash__ but leaving a "graph"
attribute that ties a node to some specific tree leads to a
contradiction.

So the "graph" attribute must be removed, if the AST nodes are to be
used as immutable. Deciding to remove "graph" necessitates rewriting the
classes to extract the tree flattening algorithm from them.
reduce running time in naive loop

lessons learned:

- flattening is not tail-recursive, so we need to maintain state using Q, S: very expensive in time
- getEffectiveLevel and isEnabledFor can double running time
rewrite AST flattening using recursion

to avoid maintaining state, and visit each node once only. After all the call stack will the of the same size as the queue Q. This also substantially simplifies function "flatten".

lessons learned:

- use visitor pattern (only for issuing the right call) because it turns out to be faster that getattr

- avoid try/except in favor of hasattr, when the attribute will most frequently be absent

- "context" too general and expensive in time

- do not use custom __hash__: too slow

- order of cases in selection of function "flatten" is important to reduce running time: checking for emptiness is cheaper than checking len(s) == 1.

- avoid next(iter(..)) for unpacking singletons

- the u.op == 'X' check for context is performed by the "flatten" outside of nodes. This is faster than using a "context" method. It is less general. However, taking into account that this is a parser specifically for LTL and that flattening is a very frequent operation, it is worth optimizing it.

  After all the flattening algorithm is so short that it can be copied and customized for each language, and still the result will be briefer and more readable than previously.

- store some string representations, e.g., Bool.str, because calling __str__, that calls __repr__, that calls str(self.val) can consume more time.

- line_profiler proved really helpful when it comes down to detail:
    https://github.com/rkern/line_profiler