Repository: qntm/greenery (language: python)

Issue:
Rename the `lego` module to something more sensible and less trademarked
`lego` is a silly name. At the moment I am thinking either `re` (it wouldn't clash with [Python 3's `re`](https://docs.python.org/3.1/library/re.html) because this one is in the `greenery` namespace) or `rx` for novelty. Anybody want to jump in here and offer an opinion?

Error when parse  lego.parse('[\S]') or similar regex
When I parse regex like [\S], it will give error like
``` python
>>> lego.parse('[\S\s]')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/yyy/.pyenv/versions/xx/lib/python3.6/site-packages/greenery/lego.py", line 68, in parse
    return pattern.parse(string)
  File "/Users/yy/.pyenv/versions/xx/lib/python3.6/site-packages/greenery/lego.py", line 244, in parse
    raise Exception("Could not parse '" + string + "' beyond index " + str(i))
Exception: Could not parse '[\S\s]' beyond index 0
```



Discussion / hints:
How about `greenery.xp`?

Yeah, negated shorthands `\D`, `\W` and `\S` are not recognised inside a charclass. This would have been fiddly but maybe I'll implement it sometime. Fortunately there is a trivial workaround, instead of writing `[\S]` just write `[^\s]` or `\S`, and instead of writing `[\S\s]` just write `.`. If you have something more complex like `[\Wdef]`, render it as `\W|[def]` and then do what `greenery` is designed to do:

```
>>> from greenery import lego
>>> str(lego.parse('\\W|[def]').reduce())
'[^0-9A-Z_abcg-z]'
```

Fix patch:
diff --git a/.github/workflows/workflow-1.yml b/.github/workflows/workflow-1.yml
index e29333c..089fd7c 100644
--- a/.github/workflows/workflow-1.yml
+++ b/.github/workflows/workflow-1.yml
@@ -28,8 +28,8 @@ jobs:
       run: |
         # stop the build if there are Python syntax errors or undefined names
         flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
-        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
-        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+        # exit-zero treats all errors as warnings
+        flake8 . --count --exit-zero --max-line-length=80 --statistics
     - name: Test with pytest
       run: |
         pytest
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..82c49fa
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# CHANGELOG
+
+## 4.0.0
+
+https://github.com/qntm/greenery/pull/67
+
+## 3.0
+
+https://github.com/qntm/greenery/commit/347760c730232b2f0c243917f34bdf596288984a
+
+## 2.0
+
+https://github.com/qntm/greenery/pull/10
+
+## 1.0
+
+Initial release.
diff --git a/GNUmakefile b/GNUmakefile
deleted file mode 100644
index b88e139..0000000
--- a/GNUmakefile
+++ /dev/null
@@ -1,65 +0,0 @@
-# GNU 'make' file for greenery
-
-SHELL	= bash
-PY2	= python
-PY2_6	= python2.6
-PY2_7	= python2.7
-PY3	= python3
-PY3_3	= python3.3
-PYTHONS	= $(PY2) $(PY2_6) $(PY3) $(PY3_3)
-PYTEST	= -m pytest -v --capture=no
-
-all: help
-
-help:
-	@echo "Manage testing and installation across all supported/available Pythons"
-	@echo
-	@echo "help     This help"
-	@echo "test     Test greenery on all supported/available Pythons"
-	@echo "clean    Remove generated files"
-	@echo "install  Install on all supported/available Pythons"
-	@echo "upload   Upload new version to python.org (must log in)"
-
-# Test on all supported/available Pythons, using py.test or direct testing
-TESTS	= greenery/lego_test.py						\
-	  greenery/fsm_test.py						\
-	  greenery/v1_test.py
-test: clean
-	@for py in $(PYTHONS); do					\
-	    if ! $$py -V 2>/dev/null; then				\
-		echo "$$py not available; not testing";			\
-		continue;						\
-	    fi;								\
-	    if $$py -c 'import pytest' >/dev/null 2>&1; then		\
-		if ! $$py $(PYTEST); then				\
-		    (( failed++ ));					\
-		fi;							\
-	    else							\
-		for t in $(TESTS); do 					\
-		    echo -n $$t;					\
-		    if $$py $$t; then					\
-			echo " PASSED";					\
-		    else						\
-			echo " FAILED";					\
-			(( failed++ ));					\
-		    fi;							\
-		done;							\
-	    fi;								\
-	done; exit $${failed:-0}
-
-clean:
-	@rm -rf dist build *.egg-info */__pycache__ *.pyc */*.pyc
-
-install:
-	@for py in $(PYTHONS); do					\
-	    if ! $$py -V 2>/dev/null; then				\
-	        echo "$$py not available; not installing";		\
-	    else							\
-		if ! make clean || ! $$py setup.py install; then	\
-		    echo "$$py installation failed";			\
-		fi;							\
-	    fi;								\
-	done
-
-upload:
-	python setup.py sdist upload
\ No newline at end of file
diff --git a/README.md b/README.md
index c1e2310..c9a15ee 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
 # greenery
 
-Tools for parsing and manipulating regular expressions (`greenery.lego`), for producing finite-state machines (`greenery.fsm`), and for freely converting between the two. Python 3 only.
+Tools for parsing and manipulating regular expressions. Note that this is a very different concept from that of simply *creating and using* those regular expressions, functionality which is present in basically every programming language in the world, [Python included](http://docs.python.org/library/re.html).
 
-This project was undertaken because I wanted to be able to **compute the intersection between two regular expressions**. The "intersection" is the set of strings which both regexes will accept, represented as a third regular expression.
+This project was undertaken because I wanted to be able to **compute the intersection between two regular expressions**. The "intersection" is the set of strings which both regular expressions will accept, represented as a third regular expression.
 
 ## Installation
 
@@ -12,188 +12,37 @@ pip install greenery
 
 ## Example
 
-    >>> from greenery.lego import parse
-    >>> print(parse("abc...") & parse("...def"))
-    abcdef
-    >>> print(parse("\d{4}-\d{2}-\d{2}") & parse("19.*"))
-    19\d{2}-\d{2}-\d{2}
-    >>> print(parse("\W*") & parse("[a-g0-8$%\^]+") & parse("[^d]{2,8}"))
-    [$%\^]{2,8}
-    >>> print(parse("[bc]*[ab]*") & parse("[ab]*[bc]*"))
-    ([ab]*a|[bc]*c)?b*
-    >>> print(parse("a*") & parse("b*"))
+```python
+from greenery import parse
 
-    >>> print(parse("a") & parse("b"))
-    []
+print(parse("abc...") & parse("...def"))
+# "abcdef"
 
-In the penultimate example, the empty string is returned, because only the empty string is in both of the regular languages `a*` and `b*`. In the final example, an empty character class has been returned. An empty character class can never match anything, which means that this is the smallest representation of a regular expression which matches no strings at all. (Note that this is different from only matching the empty string.)
+print(parse("\d{4}-\d{2}-\d{2}") & parse("19.*"))
+# "19\d{2}-\d{2}-\d{2}"
 
-`greenery` works by converting both regexes to finite state machines, computing the intersection of the two FSMs as a third FSM, and converting the third FSM back to a regex.
+print(parse("\W*") & parse("[a-g0-8$%\^]+") & parse("[^d]{2,8}"))
+# "[$%\^]{2,8}"
 
-As such, `greenery` is divided into two libraries:
+print(parse("[bc]*[ab]*") & parse("[ab]*[bc]*"))
+# "([ab]*a|[bc]*c)?b*"
 
-## greenery.fsm
+print(parse("a*") & parse("b*"))
+# ""
 
-This module provides for the creation and manipulation of **deterministic** finite state machines.
-
-### Example
-
-*To do: a slightly more impressive example.*
-
-    >>> from greenery import fsm
-    >>> a = fsm.fsm(
-    ...     alphabet = {"a", "b"},
-    ...     states   = {0, 1},
-    ...     initial  = 0,
-    ...     finals   = {1},
-    ...     map      = {
-    ...             0 : {"a" : 1},
-    ...     },
-    ... )
-    >>> print(a)
-      name final? a b
-    ------------------
-    * 0    False  1
-      1    True
-    >>> a.accepts([])
-    False
-    >>> a.accepts(["a"])
-    True
-    >>> a.accepts(["b"])
-    False
-    >>> print(a.accepts(["c"]))
-    Traceback (most recent call last):
-      File "<stdin>", line 1, in <module>
-      File "fsm.py", line 68, in accepts
-        state = self.map[state][symbol]
-    KeyError: 'c'
-
-### Functions in this module
-
-#### `fsm(alphabet, states, initial, finals, map)`
-
-Constructor for an `fsm` object, as demonstrated above. `fsm` objects are intended to be immutable.
-
-`map` may be sparse. If a transition is missing from `map`, then it is assumed that this transition leads to an undocumented "oblivion state" which is not final. This oblivion state does not appear when the FSM is printed out.
-
-Ordinarily, you may only feed known alphabet symbols into the FSM. Any other symbol will result in an exception, as seen above. However, if you add the special symbol `fsm.anything_else` to your alphabet, then any unrecognised symbol will be automatically converted into `fsm.anything_else` before following whatever transition you have specified for this symbol.
-
-#### `crawl(alphabet, initial, final, follow)`
-
-Crawl what is assumed to be an FSM and return a new `fsm` object representing it. Starts at state `initial`. At any given state, `crawl` calls `final(state)` to determine whether it is final. Then, for each symbol in `alphabet`, it calls `follow(state, symbol)` to try to discover new states. Obviously this procedure could go on for ever if your implementation of `follow` is faulty. `follow` may also throw an `OblivionError` to indicate that you have reached an inescapable, non-final "oblivion state"; in this case, the transition will be omitted from the resulting FSM.
-
-#### `null(alphabet)`
-
-Returns an FSM over the supplied alphabet which accepts no strings at all.
-
-#### `epsilon(alphabet)`
-
-Returns an FSM over the supplied alphabet which accepts only the empty string, `""`.
-
-### Methods on class `fsm`
-
-An FSM accepts a possibly-infinite set of strings. With this in mind, `fsm` implements numerous [methods like those on `frozenset`](https://docs.python.org/3.5/library/stdtypes.html#frozenset), as well as many FSM-specific methods. FSMs are immutable.
-
-Method | Behaviour
----|---
-`fsm1.accepts("a")` <br/> `"a" in fsm1` | Returns `True` or `False` or throws an exception if the string contains a symbol which is not in the FSM's alphabet. The string should be an iterable of symbols.
-`fsm1.strings()` <br/> `for string in fsm1` | Returns a generator of all the strings that this FSM accepts.
-`fsm1.empty()` | Returns `True` if this FSM accepts no strings, otherwise `False`.
-`fsm1.cardinality()` <br/> `len(fsm1)` | Returns the number of strings which the FSM accepts. Throws an `OverflowError` if this number is infinite.
-`fsm1.equivalent(fsm2)` <br/> `fsm1 == fsm2` | Returns `True` if the two FSMs accept exactly the same strings, otherwise `False`.
-`fsm1.different(fsm2)` <br/> `fsm1 != fsm2` | Returns `True` if the FSMs accept different strings, otherwise `False`.
-`fsm1.issubset(fsm2)` <br/> `fsm1 <= fsm2` | Returns `True` if the set of strings accepted by `fsm1` is a subset of those accepted by `fsm2`, otherwise `False`.
-`fsm1.ispropersubset(fsm2)` <br/> `fsm1 < fsm2` | Returns `True` if the set of strings accepted by `fsm1` is a proper subset of those accepted by `fsm2`, otherwise `False`.
-`fsm1.issuperset(fsm2)` <br/> `fsm1 >= fsm2` | Returns `True` if the set of strings accepted by `fsm1` is a superset of those accepted by `fsm2`, otherwise `False`.
-`fsm1.ispropersuperset(fsm2)` <br/> `fsm1 > fsm2` | Returns `True` if the set of strings accepted by `fsm1` is a proper superset of those accepted by `fsm2`, otherwise `False`.
-`fsm1.isdisjoint(fsm2)` | Returns `True` if the set of strings accepted by `fsm1` is disjoint from those accepted by `fsm2`, otherwise `False`.
-`fsm1.copy()` | Returns a copy of `fsm1`.
-`fsm1.reduce()` | Returns an FSM which accepts exactly the same strings as `fsm1` but has a minimal number of states.
-`fsm1.star()` | Returns a new FSM which is the *[Kleene star closure](https://en.wikipedia.org/wiki/Kleene_star)* of the original. For example, if `fsm1` accepts only `"asdf"`, `fsm1.star()` accepts `""`, `"asdf"`, `"asdfasdf"`, `"asdfasdfasdf"`, and so on.
-`fsm1.everythingbut()` | Returns an FSM which accepts every string not accepted by the original. `x.everythingbut().everythingbut()` accepts the same strings as `x` for all `fsm` objects `x`, but is not necessarily mechanically identical.
-`fsm1.reversed()` <br/> `reversed(fsm1)` | Returns a reversed FSM. For each string that `fsm1` accepted, `reversed(fsm1)` will accept the reversed string. `reversed(reversed(x))` accepts the same strings as `x` for all `fsm` objects `x`, but is not necessarily mechanically identical.
-`fsm1.times(7)` <br/> `fsm1 * 7` | Essentially, this is repeated self-concatenation. If `fsm1` only accepts `"z"`, `fsm2` only accepts `"zzzzzzz"`.
-`fsm1.concatenate(fsm2, ...)` <br/> `fsm1 + fsm2 + ...` | Returns the concatenation of the FSMs. If `fsm1` accepts all strings in *A* and `fsm2` accepts all strings in *B*, then `fsm1 + fsm2` accepts all strings of the form *a·b* where *a* is in *A* and *b* is in *B*.
-`fsm1.union(fsm2, ...)` <br/> `fsm1 \| fsm2 \| ...` | Returns an FSM accepting any string accepted by any input FSM. This is also called *alternation*.
-`fsm1.intersection(fsm2, ...)` <br/> `fsm1 & fsm2 & ...` | Returns an FSM accepting any string accepted by all input FSMs.
-`fsm1.difference(fsm2, ...)` <br/> `fsm1 - fsm2 - ...` | Subtract the set of strings accepted by `fsm2` onwards from those accepted by `fsm1` and return the resulting new FSM.
-`fsm1.symmetric_difference(fsm2, ...)` <br/> `fsm1 ^ fsm2 ^ ...` | Returns an FSM accepting any string accepted by `fsm1` or `fsm2` but not both.
-`fsm1.derive("a")` | Return the [Brzozowski derivative](https://en.wikipedia.org/wiki/Brzozowski_derivative) of the original FSM with respect to the input string. E.g. if `fsm1` only accepts `"ab"` or `"ac+"`, returns an FSM only accepting `"b"` or `"c+"`.
-
-## greenery.lego
-
-This module provides methods for parsing a regular expression (i.e. a string) into a manipulable nested data structure, and for manipulating that data structure.
-
-Note that this is an entirely different concept from that of simply creating and using those regexes, functionality which is present in basically every programming language in the world, [Python included](http://docs.python.org/library/re.html).
-
-This module requires `greenery.fsm` in order to carry out many of its most important functions. (`greenery.fsm`, in comparison, is completely standalone.)
-
-### Classes in this module
-
-#### `lego.bound`
-
-A non-negative integer, or `inf`, plus a bunch of arithmetic methods which make it possible to compare, add and multiply them.
-
-##### Special bounds
-
-* `inf`
-
-#### `lego.multiplier`
-
-A combination of a finite lower `bound` and a possibly-infinite upper `bound`, plus a bunch of methods which make it possible to compare, add and multiply them.
-
-##### Special multipliers
-
-* `zero` (`multiplier(bound(0), bound(0)`) (has some occasional uses internally)
-* `qm` (`multiplier(bound(0), bound(1))`)
-* `star` (`multiplier(bound(0), inf)`)
-* `one` (`multiplier(bound(1), bound(1))`)
-* `plus` (`multiplier(bound(1), inf)`)
-
-#### `lego.lego`
-
-Parent class for `charclass`, `mult`, `conc` and `pattern`. In general, this represents a regular expression object.
-
-#### `lego.charclass`
-
-Represents a character class, e.g `a`, `[abc]`, `[^xyz]`, `\d`.
-
-##### Special character classes
-
-* `w` (`charclass("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz")`)
-* `d` (`charclass("0123456789")`)
-* `s` (`charclass("\t\n\v\f\r ")`)
-* `W` (any character except those matched by `w`)
-* `D` (any character except those matched by `d`)
-* `S` (any character except those matched by `s`)
-* `dot` (any character)
-* `nothing` (empty character class, no matches possible)
-
-#### `lego.mult`
-
-Represents a `charclass` or `pattern` combined with a `multiplier`, e.g. `[abc]*` or `(a|bc)*`.
-
-#### `lego.conc`
-
-Represents a sequence of zero or more `mult`s, e.g. `ab`, `[abc]*d`.
-
-##### Special concatenations
-
-* `emptystring`, the regular expression which only matches the empty string (`conc()`)
-
-#### `lego.pattern`
-
-Represents an alternation between one or more `conc`s, e.g. `[abc]*d|e`.
+print(parse("a") & parse("b"))
+# "[]"
+```
 
-### Methods in this module
+In the penultimate example, the empty string is returned, because only the empty string is in both of the regular languages `a*` and `b*`. In the final example, an empty character class has been returned. An empty character class can never match anything, which means `greenery` can use this to represent a regular expression which matches no strings at all. Note that this is different from only matching the empty string.
 
-#### `lego.from_fsm()`
+Internally, `greenery` works by converting regular expressions to finite state machines, computing the intersection of the two FSMs as a third FSM, and using the Brzozowski algebraic method (*q.v.*) to convert the third FSM back to a regular expression.
 
-Uses the Brzozowski algebraic method to convert a `greenery.fsm` object into a `lego` object, which is a regular expression.
+## API
 
-#### `lego.parse(string)`
+### parse(string)
 
-Returns a `lego` object representing the regular expression in the string.
+This function takes a regular expression (_i.e._ a string) as input and returns a `Pattern` object (see below) representing that regular expression.
 
 The following metacharacters and formations have their usual meanings: `.`, `*`, `+`, `?`, `{m}`, `{m,}`, `{m,n}`, `()`, `|`, `[]`, `^` within `[]` character ranges only, `-` within `[]` character ranges only, and `\` to escape any of the preceding characters or itself.
 
@@ -201,56 +50,61 @@ These character escapes are possible: `\t`, `\r`, `\n`, `\f`, `\v`.
 
 These predefined character sets also have their usual meanings: `\w`, `\d`, `\s` and their negations `\W`, `\D`, `\S`. `.` matches any character, including new line characters and carriage returns.
 
-An empty charclass `[]` is legal and matches no characters: when used in a regex, the regex may match no strings.
+An empty charclass `[]` is legal and matches no characters: when used in a regular expression, the regular expression may match no strings.
 
-##### Unsupported constructs
+#### Unsupported constructs
 
 * This method is intentionally rigorously simple, and tolerates no ambiguity. For example, a hyphen must be escaped in a character class even if it appears first or last. `[-abc]` is a syntax error, write `[\-abc]`. Escaping something which doesn't need it is a syntax error too: `[\ab]` resolves to neither `[\\ab]` nor `[ab]`.
 
 * The `^` and `$` metacharacters are not supported. By default, `greenery` assumes that all regexes are anchored at the start and end of any input string. Carets and dollar signs will be parsed as themselves. If you want to *not* anchor at the start or end of the string, put `.*` at the start or end of your regex respectively.
 
- This is because computing the intersection between `.*a.*` and `.*b.*` (1) is largely pointless and (2) usually results in gibberish coming out of the program.
+  This is because computing the intersection between `.*a.*` and `.*b.*` (1) is largely pointless and (2) usually results in gibberish coming out of the program.
 
 * The greedy operators `*?`, `+?`, `??` and `{m,n}?` are not supported, since they do not alter the regular language.
 
 * Parentheses are used to alternate between multiple possibilities e.g. `(a|bc)` only, not for capture grouping. Here's why:
 
-        >>> print(parse("(ab)c") & parse("a(bc)"))
-        abc
+        ```python
+        print(parse("(ab)c") & parse("a(bc)"))
+        # "abc"
+        ```
 
- The `(?:...)` syntax for non-capturing groups is permitted, but does nothing.
+* The `(?:...)` syntax for non-capturing groups is permitted, but does nothing.
 
 * Other `(?...)` constructs are not supported (and most are not [regular in the computer science sense](http://en.wikipedia.org/wiki/Regular_language)).
 
 *  Back-references, such as `([aeiou])\1`, are not regular.
 
-### Methods on the `lego` class
+### Pattern
+
+A `Pattern` represents a regular expression and exposes various methods for manipulating it and combining it with other regular expressions. `Pattern`s are immutable.
 
-All objects of class `lego` (`charclass`, `mult`, `conc` and `pattern`) share these methods.
+A regular language is a possibly-infinite set of strings. With this in mind, `Pattern` implements numerous [methods like those on `frozenset`](https://docs.python.org/3/library/stdtypes.html#frozenset), as well as many regular expression-specific methods.
+
+It's not intended that you construct new `Pattern` instances directly; use `parse(string)`, above.
 
 Method | Behaviour
 ---|---
-`lego1.to_fsm()` | Returns an `fsm` object, a finite state machine which recognises exactly the strings that the original regular expression can match. The majority of the other methods employ this one.
-`lego1.matches("a")` <br/> `"a" in lego1` | Returns `True` if the regular expression matches the string or `False` if not.
-`lego1.strings()` <br/> `for string in lego1` | Returns a generator of all the strings that this regular expression matches.
-`lego1.empty()` | Returns `True` if this regular expression matches no strings, otherwise `False`.
-`lego1.cardinality()` <br/> `len(lego1)` | Returns the number of strings which the regular expression matches. Throws an `OverflowError` if this number is infinite.
-`lego1.equivalent(lego2)` | Returns `True` if the two regular expressions match exactly the same strings, otherwise `False`.
-`lego1.copy()` | Returns a copy of `lego1`.
-`lego1.everythingbut()` | Returns a regular expression which matches every string not matched by the original. `x.everythingbut().everythingbut()` matches the same strings as `x` for all `lego` objects `x`, but is not necessarily identical.
-`lego1.reversed()` <br/> `reversed(lego1)` | Returns a reversed regular expression. For each string that `lego1` matched, `reversed(lego1)` will match the reversed string. `reversed(reversed(x))` matches the same strings as `x` for all `lego` objects `x`, but is not necessarily identical.
-`lego1.times(star)` <br/> `lego1 * star` | Returns the input regular expression multiplied  by any `multiplier`.
-`lego1.concatenate(lego2, ...)` <br/> `lego1 + lego2 + ...` | Returns the concatenation of the regular expressions.
-`lego1.union(lego2, ...)` <br/> `lego1 \| lego2 \| ...` | Returns the alternation of the two regular expressions.
-`lego1.intersection(lego2, ...)` <br/> `lego1 & lego2 & ...` | Returns a regular expression matching any string matched by all input regular expressions. The successful implementation of this method was the ultimate goal of this entire project.
-`lego1.difference(lego2, ...)` <br/> `lego1 - lego2 - ...` | Subtract the set of strings matched by `lego2` onwards from those matched by `lego1` and return the resulting regular expression.
-`lego1.symmetric_difference(lego2, ...)` <br/> `lego1 ^ lego2 ^ ...` | Returns a regular expression matching any string accepted by `lego1` or `lego2` but not both.
-`lego1.reduce()` | Returns a regular expression which matches exactly the same strings as `lego1` but is simplified as far as possible. See dedicated section below.
-`lego1.derive("a")` | Return the [Brzozowski derivative](https://en.wikipedia.org/wiki/Brzozowski_derivative) of the input regular expression with respect to "a".
-
-#### `reduce()`
-
-Call this method to try to simplify the regular expression object, according to the following patterns:
+`pattern.matches("a")` <br/> `"a" in pattern` | Returns `True` if the regular expression matches the string or `False` if not.
+`pattern.strings()` <br/> `for string in pattern` | Returns a generator of all the strings that this regular expression matches.
+`pattern.empty()` | Returns `True` if this regular expression matches no strings, otherwise `False`.
+`pattern.cardinality()` <br/> `len(pattern)` | Returns the number of strings which the regular expression matches. Throws an `OverflowError` if this number is infinite.
+`pattern1.equivalent(pattern2)` | Returns `True` if the two regular expressions match exactly the same strings, otherwise `False`.
+`pattern.copy()` | Returns a shallow copy of `pattern`.
+`pattern.everythingbut()` | Returns a regular expression which matches every string not matched by the original. `pattern.everythingbut().everythingbut()` matches the same strings as `pattern`, but is not necessarily identical in structure.
+`pattern.reversed()` <br/> `reversed(pattern)` | Returns a reversed regular expression. For each string that `pattern` matched, `reversed(pattern)` will match the reversed string. `reversed(reversed(pattern))` matches the same strings as `pattern`, but is not necessarily identical.
+`pattern.times(star)` <br/> `pattern * star` | Returns the input regular expression multiplied by any `Multiplier` (see below).
+`pattern1.concatenate(pattern2, ...)` <br/> `pattern1 + pattern2 + ...` | Returns a regular expression which matches any string of the form *a·b·...* where *a* is a string matched by `pattern1`, *b* is a string matched by `pattern2` and so on.
+`pattern1.union(pattern2, ...)` <br/> `pattern1 \| pattern2 \| ...` | Returns a regular expression matching any string matched by any of the input regular expressions. This is also called *alternation*.
+`pattern1.intersection(pattern2, ...)` <br/> `pattern1 & pattern2 & ...` | Returns a regular expression matching any string matched by all input regular expressions. The successful implementation of this method was the ultimate goal of this entire project.
+`pattern1.difference(pattern2, ...)` <br/> `pattern1 - pattern2 - ...` | Subtract the set of strings matched by `pattern2` onwards from those matched by `pattern1` and return the resulting regular expression.
+`pattern1.symmetric_difference(pattern2, ...)` <br/> `pattern1 ^ pattern2 ^ ...` | Returns a regular expression matching any string accepted by `pattern1` or `pattern2` but not both.
+`pattern.derive("a")` | Return the [Brzozowski derivative](https://en.wikipedia.org/wiki/Brzozowski_derivative) of the input regular expression with respect to "a".
+`pattern.reduce()` | Returns a regular expression which is equivalent to `pattern` (*i.e.* matches exactly the same strings) but is simplified as far as possible. See dedicated section below.
+
+#### pattern.reduce()
+
+Call this method to try to simplify the regular expression object. The follow simplification heuristics are supported:
 
 * `(ab|cd|ef|)g` to `(ab|cd|ef)?g`
 * `([ab])*` to `[ab]*`
@@ -262,17 +116,51 @@ Call this method to try to simplify the regular expression object, according to
 * `abc()def` to `abcdef`
 * `a{1,2}|a{3,4}` to `a{1,4}`
 
-The various `reduce()` methods are extensible.
+The value returned is a new `Pattern` object.
 
 Note that in a few cases this did *not* result in a shorter regular expression.
 
-### Name
+### Multiplier(Bound, Bound)
+
+A combination of a finite lower `Bound` (see below) and a possibly-infinite upper `Bound`.
 
-I spent a long time trying to find an appropriate metaphor for what I was trying to do: "I need an X such that lots of Xs go together to make a Y, but lots of Ys go together to make an X". Unfortunately the real world doesn't seem to be recursive in this way so I plumped for "lego" as a basic catchall term for the various components that go together to make up a data structure.
+```python
+from greenery import parse, Bound, INF, Multiplier
 
-This was a dumb idea in retrospect and it will be changed to `greenery.re` or `greenery.rx` in the near future. Vote now if you have an opinion.
+print(parse("a") * Multiplier(Bound(3), INF)) # "a{3,}"
+```
+
+### STAR
+
+Special `Multiplier`, equal to `Multiplier(Bound(0), INF)`. When it appears in a regular expression, this is `{0,}` or the [Kleene star](https://en.wikipedia.org/wiki/Kleene_star) `*`.
+
+### QM
+
+Special `Multiplier`, equal to `Multiplier(Bound(0), Bound(1))`. When it appears in a regular expression, this is `{0,1}` or `?`.
+
+### PLUS
+
+Special `Multiplier`, equal to `Multiplier(Bound(1), INF)`. When it appears in a regular expression, this is `{1,}` or `+`.
+
+### Bound(number)
+
+Represents a non-negative integer or infinity.
+
+### INF
+
+Special `Bound` representing no limit. Can be used as an upper bound only.
+
+## Development
+
+### Running tests
+
+```sh
+flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+flake8 . --count --exit-zero --max-complexity=10 --max-line-length=80 --statistics
+pytest
+```
 
-### Development
+### Building and publishing new versions
 
 * Update the version in `./setup.py`
 * Trash `./dist`
diff --git a/greenery/__init__.py b/greenery/__init__.py
index 77e014e..157bc26 100644
--- a/greenery/__init__.py
+++ b/greenery/__init__.py
@@ -1,3 +1,15 @@
 # -*- coding: utf-8 -*-
 
-__all__ = ["fsm", "lego"]
+# In theory this should just pull in the `parse` function, but it turns out
+# that the act of performing any relative import suddenly makes EVERY MODULE
+# AVAILABLE IN SCOPE?
+# Ignore errors because we are in fact importing these values to re-export them
+from .parse import parse  # noqa: F401
+from .bound import Bound, INF  # noqa: F401
+from .multiplier import Multiplier, QM, STAR, PLUS  # noqa: F401
+
+# WHY ARE THESE MODULE OBJECTS IN SCOPE SUDDENLY? I DIDN'T ASK FOR THEM
+# print(bound, fsm, charclass, rxelems)
+
+# This, however, is the *function*, *not* its containing module object?
+# print(parse)
diff --git a/greenery/bound.py b/greenery/bound.py
new file mode 100644
index 0000000..f795d48
--- /dev/null
+++ b/greenery/bound.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass(frozen=True)
+class Bound:
+    '''An integer but sometimes also possibly infinite (None)'''
+    v: Optional[int]
+
+    def __post_init__(self):
+        if self.v is not None and self.v < 0:
+            raise Exception(f"Invalid bound: {repr(self.v)}")
+
+    def __repr__(self):
+        return f"Bound({repr(self.v)})"
+
+    def __str__(self):
+        if self == INF:
+            # This only happens for an unlimited upper bound
+            return ""
+        return str(self.v)
+
+    def __eq__(self, other):
+        return self.v == other.v
+
+    def __hash__(self):
+        return hash(self.v)
+
+    def __lt__(self, other):
+        if self == INF:
+            return False
+        if other == INF:
+            return True
+        return self.v < other.v
+
+    def __ge__(self, other):
+        return not self < other
+
+    def __mul__(self, other):
+        '''Multiply this bound by another'''
+        if self == Bound(0) or other == Bound(0):
+            return Bound(0)
+        if self == INF or other == INF:
+            return INF
+        return Bound(self.v * other.v)
+
+    def __add__(self, other):
+        '''Add this bound to another'''
+        if self == INF or other == INF:
+            return INF
+        return Bound(self.v + other.v)
+
+    def __sub__(self, other):
+        '''
+            Subtract another bound from this one.
+            Caution: this operation is not meaningful for all bounds.
+        '''
+        if other == INF:
+            if self != INF:
+                raise Exception(
+                    f"Can't subtract {repr(other)} from {repr(self)}"
+                )
+
+            # Infinity minus infinity is zero. This has to be true so that
+            # we can for example subtract Multiplier(Bound(0), INF) from
+            # Multiplier(Bound(1), INF) to get Multiplier(Bound(1), Bound(1))
+            return Bound(0)
+        if self == INF:
+            return self
+        return Bound(self.v - other.v)
+
+    def copy(self):
+        return Bound(self.v)
+
+
+# Use this for cases where no upper bound is needed
+INF = Bound(None)
diff --git a/greenery/charclass.py b/greenery/charclass.py
new file mode 100644
index 0000000..7157e15
--- /dev/null
+++ b/greenery/charclass.py
@@ -0,0 +1,272 @@
+# -*- coding: utf-8 -*-
+
+from dataclasses import dataclass
+from typing import Union, FrozenSet
+
+from .fsm import Fsm, ANYTHING_ELSE
+
+
+@dataclass(frozen=True)
+class Charclass:
+    '''
+        A `Charclass` is basically a `frozenset` of symbols.
+        A `Charclass` with the `negated` flag set is assumed
+        to contain every symbol that is in the alphabet of all symbols but not
+        explicitly listed inside the frozenset. e.g. [^a]. This is very handy
+        if the full alphabet is extremely large, but also requires dedicated
+        combination functions.
+    '''
+    chars: Union[FrozenSet[str], str]
+    negated: bool = False
+
+    def __post_init__(self):
+        object.__setattr__(self, "chars", frozenset(self.chars))
+        # chars should consist only of chars
+        if ANYTHING_ELSE in self.chars:
+            raise Exception(
+                f"Can't put {repr(ANYTHING_ELSE)} in a `Charclass`"
+            )
+
+    def __eq__(self, other):
+        return isinstance(other, Charclass) \
+               and self.chars == other.chars \
+               and self.negated == other.negated
+
+    def __hash__(self):
+        return hash((self.chars, self.negated))
+
+    # These are the characters carrying special meanings when they appear
+    # "outdoors" within a regular expression. To be interpreted literally, they
+    # must be escaped with a backslash.
+    allSpecial = set("\\[]|().?*+{}")
+
+    # These are the characters carrying special meanings when they appear
+    # INSIDE a character class (delimited by square brackets) within a regular
+    # expression. To be interpreted literally, they must be escaped with a
+    # backslash. Notice how much smaller this class is than the one above; note
+    # also that the hyphen and caret do NOT appear above.
+    classSpecial = set("\\[]^-")
+
+    # Shorthand codes for use inside `Charclass`es e.g. [abc\d]
+    w = frozenset(
+        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
+    )
+    d = frozenset("0123456789")
+    s = frozenset("\t\n\v\f\r ")
+
+    shorthand = {
+        w: "\\w",
+        d: "\\d",
+        s: "\\s",
+    }
+
+    negated_shorthand = {
+        w: "\\W",
+        d: "\\D",
+        s: "\\S",
+    }
+
+    def __str__(self):
+        # e.g. \w
+        if self in shorthand.keys():
+            return shorthand[self]
+
+        # e.g. [^a]
+        if self.negated:
+            return "[^" + self.escape() + "]"
+
+        # single character, not contained inside square brackets.
+        if len(self.chars) == 1:
+            # Python lacks the Axiom of Choice
+            char = "".join(self.chars)
+
+            # e.g. if char is "\t", return "\\t"
+            if char in escapes.keys():
+                return escapes[char]
+
+            if char in Charclass.allSpecial:
+                return "\\" + char
+
+            # If char is an ASCII control character, don't print it directly,
+            # return a hex escape sequence e.g. "\\x00". Note that this
+            # includes tab and other characters already handled above
+            if 0 <= ord(char) <= 0x1F or ord(char) == 0x7f:
+                return "\\x" + "{0:02x}".format(ord(char))
+
+            return char
+
+        # multiple characters (or possibly 0 characters)
+        return "[" + self.escape() + "]"
+
+    def escape(self):
+        def escapeChar(char):
+            if char in Charclass.classSpecial:
+                return "\\" + char
+            if char in escapes.keys():
+                return escapes[char]
+
+            # If char is an ASCII control character, don't print it directly,
+            # return a hex escape sequence e.g. "\\x00". Note that this
+            # includes tab and other characters already handled above
+            if 0 <= ord(char) <= 0x1F or ord(char) == 0x7f:
+                return "\\x" + "{0:02x}".format(ord(char))
+
+            return char
+
+        def recordRange():
+            # there's no point in putting a range when the whole thing is
+            # 3 characters or fewer. "abc" -> "abc" but "abcd" -> "a-d"
+            strs = [
+                # "ab" or "abc" or "abcd"
+                "".join(escapeChar(char) for char in currentRange),
+                # "a-b" or "a-c" or "a-d"
+                (
+                    escapeChar(currentRange[0]) +
+                    "-" +
+                    escapeChar(currentRange[-1])
+                ),
+            ]
+            return sorted(strs, key=lambda str: len(str))[0]
+
+        output = ""
+
+        # use shorthand for known character ranges
+        # note the nested processing order. DO NOT process \d before processing
+        # \w. if more character class constants arise which do not nest nicely,
+        # a problem will arise because there is no clear ordering to use...
+
+        # look for ranges
+        currentRange = ""
+        for char in sorted(self.chars, key=ord):
+
+            # range is not empty: new char must fit after previous one
+            if len(currentRange) > 0:
+
+                i = ord(char)
+
+                # char doesn't fit old range: restart
+                if i != ord(currentRange[-1]) + 1:
+                    output += recordRange()
+                    currentRange = ""
+
+            currentRange += char
+
+        if len(currentRange) > 0:
+            output += recordRange()
+
+        return output
+
+    def to_fsm(self, alphabet=None):
+        if alphabet is None:
+            alphabet = self.alphabet()
+
+        # 0 is initial, 1 is final
+
+        # If negated, make a singular FSM accepting any other characters
+        if self.negated:
+            map = {
+                0: dict([(symbol, 1) for symbol in alphabet - self.chars]),
+            }
+
+        # If normal, make a singular FSM accepting only these characters
+        else:
+            map = {
+                0: dict([(symbol, 1) for symbol in self.chars]),
+            }
+
+        return Fsm(
+            alphabet=alphabet,
+            states={0, 1},
+            initial=0,
+            finals={1},
+            map=map,
+        )
+
+    def __repr__(self):
+        string = ""
+        if self.negated is True:
+            string += "~"
+        string += "Charclass("
+        string += repr("".join(
+            str(char) for char in sorted(self.chars, key=str)
+        ))
+        string += ")"
+        return string
+
+    def reduce(self):
+        # `Charclass`es cannot be reduced.
+        return self
+
+    def alphabet(self):
+        return {ANYTHING_ELSE} | self.chars
+
+    def empty(self):
+        return len(self.chars) == 0 and not self.negated
+
+    # set operations
+    def negate(self):
+        '''
+            Negate the current `Charclass`. e.g. [ab] becomes [^ab]. Call
+            using "charclass2 = ~charclass1"
+        '''
+        return Charclass(self.chars, negated=not self.negated)
+
+    def __invert__(self):
+        return self.negate()
+
+    def reversed(self):
+        return self
+
+    def __or__(self, other):
+        # ¬A OR ¬B = ¬(A AND B)
+        # ¬A OR B = ¬(A - B)
+        # A OR ¬B = ¬(B - A)
+        # A OR B
+        if self.negated:
+            if other.negated:
+                return ~Charclass(self.chars & other.chars)
+            else:
+                return ~Charclass(self.chars - other.chars)
+        else:
+            if other.negated:
+                return ~Charclass(other.chars - self.chars)
+            else:
+                return Charclass(self.chars | other.chars)
+
+
+# Standard character classes
+WORDCHAR = Charclass(
+    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
+)
+DIGIT = Charclass("0123456789")
+SPACECHAR = Charclass("\t\n\v\f\r ")
+
+# This `Charclass` expresses "no possibilities at all"
+# and can never match anything.
+NULLCHARCLASS = Charclass("")
+
+NONWORDCHAR = ~WORDCHAR
+NONDIGITCHAR = ~DIGIT
+NONSPACECHAR = ~SPACECHAR
+DOT = ~NULLCHARCLASS
+
+# Textual representations of standard character classes
+shorthand = {
+    WORDCHAR: "\\w",
+    DIGIT: "\\d",
+    SPACECHAR: "\\s",
+    NONWORDCHAR: "\\W",
+    NONDIGITCHAR: "\\D",
+    NONSPACECHAR: "\\S",
+    DOT: ".",
+}
+
+# Characters which users may escape in a regex instead of inserting them
+# literally. In ASCII order:
+escapes = {
+    "\t": "\\t",  # tab
+    "\n": "\\n",  # line feed
+    "\v": "\\v",  # vertical tab
+    "\f": "\\f",  # form feed
+    "\r": "\\r",  # carriage return
+}
diff --git a/greenery/fsm.py b/greenery/fsm.py
index 9e6d84f..6e3b5dc 100644
--- a/greenery/fsm.py
+++ b/greenery/fsm.py
@@ -1,36 +1,41 @@
 # -*- coding: utf-8 -*-
 
 '''
-    Finite state machine library.
+    Finite state machine library, intended to be used by `greenery` only
 '''
 
 from typing import Optional, Union, Set, Dict
 from dataclasses import dataclass
 
-anything_else = '9bd74361-04f9-4742-9d3a-1d14a6f0044c'
+ANYTHING_ELSE = '9bd74361-04f9-4742-9d3a-1d14a6f0044c'
+
+
+def alphabet_key(symbol):
+    '''Ensure `ANYTHING_ELSE` always sorts last'''
+    return (symbol is ANYTHING_ELSE, symbol)
 
-def key(symbol):
-    '''Ensure `fsm.anything_else` always sorts last'''
-    return (symbol is anything_else, symbol)
 
 class OblivionError(Exception):
     '''
-        This exception is thrown while `crawl()`ing an FSM if we transition to the
-        oblivion state. For example while crawling two FSMs in parallel we may
-        transition to the oblivion state of both FSMs at once. This warrants an
-        out-of-bound signal which will reduce the complexity of the new FSM's map.
+        This exception is thrown while `crawl()`ing an FSM if we transition to
+        the oblivion state. For example while crawling two FSMs in parallel we
+        may transition to the oblivion state of both FSMs at once. This
+        warrants an out-of-band signal which will reduce the complexity of the
+        new FSM's map.
     '''
     pass
 
+
 state_type = Optional[Union[int, str]]
 
+
 @dataclass(frozen=True)
-class fsm:
+class Fsm:
     '''
-        A Finite State Machine or FSM has an alphabet and a set of states. At any
-        given moment, the FSM is in one state. When passed a symbol from the
-        alphabet, the FSM jumps to another state (or possibly the same state).
-        A map (Python dictionary) indicates where to jump.
+        A Finite State Machine or FSM has an alphabet and a set of states. At
+        any given moment, the FSM is in one state. When passed a symbol from
+        the alphabet, the FSM jumps to another state (or possibly the same
+        state). A map (Python dictionary) indicates where to jump.
         One state is nominated as a starting state. Zero or more states are
         nominated as final states. If, after consuming a string of symbols,
         the FSM is in a final state, then it is said to "accept" the string.
@@ -51,19 +56,31 @@ def __post_init__(self):
             `states` is the set of states for the FSM
             `initial` is the initial state
             `finals` is the set of accepting states
-            `map` may be sparse (i.e. it may omit transitions). In the case of omitted
-            transitions, a non-final "oblivion" state is simulated.
+            `map` may be sparse (i.e. it may omit transitions). In the case of
+            omitted transitions, a non-final "oblivion" state is simulated.
         '''
 
-        # Validation. Thanks to immutability, this only needs to be carried out once.
-        if not self.initial in self.states:
-            raise Exception("Initial state " + repr(self.initial) + " must be one of " + repr(self.states))
+        # Validation. Thanks to immutability, this only needs to be carried out
+        # once.
+        if self.initial not in self.states:
+            raise Exception(
+                f"Initial state {repr(self.initial)} "
+                f"must be one of {repr(self.states)}"
+            )
         if not self.finals.issubset(self.states):
-            raise Exception("Final states " + repr(self.finals) + " must be a subset of " + repr(self.states))
+            raise Exception(
+                f"Final states {repr(self.finals)} "
+                f"must be a subset of {repr(self.states)}"
+            )
         for state, symbol in self.map.items():
             for symbol in self.map[state]:
                 if not self.map[state][symbol] in self.states:
-                    raise Exception("Transition for state " + repr(state) + " and symbol " + repr(symbol) + " leads to " + repr(self.map[state][symbol]) + ", which is not a state")
+                    raise Exception(
+                        f"Transition for state {repr(state)} "
+                        f"and symbol {repr(symbol)} "
+                        f"leads to {repr(self.map[state][symbol])}, "
+                        "which is not a state"
+                    )
 
         # Initialise the hard way due to immutability.
         object.__setattr__(self, "alphabet", set(self.alphabet))
@@ -72,17 +89,17 @@ def __post_init__(self):
 
     def accepts(self, input):
         '''
-            Test whether the present FSM accepts the supplied string (iterable of
-            symbols). Equivalently, consider `self` as a possibly-infinite set of
-            strings and test whether `string` is a member of it.
-            This is actually mainly used for unit testing purposes.
-            If `fsm.anything_else` is in your alphabet, then any symbol not in your
-            alphabet will be converted to `fsm.anything_else`.
+            Test whether the present FSM accepts the supplied string (iterable
+            of symbols). Equivalently, consider `self` as a possibly-infinite
+            set of strings and test whether `string` is a member of it. This is
+            actually mainly used for unit testing purposes. If `ANYTHING_ELSE`
+            is in your alphabet, then any symbol not in your alphabet will be
+            converted to `ANYTHING_ELSE`.
         '''
         state = self.initial
         for symbol in input:
-            if anything_else in self.alphabet and not symbol in self.alphabet:
-                symbol = anything_else
+            if ANYTHING_ELSE in self.alphabet and symbol not in self.alphabet:
+                symbol = ANYTHING_ELSE
 
             # Missing transition = transition to dead state
             if not (state in self.map and symbol in self.map[state]):
@@ -93,38 +110,38 @@ def accepts(self, input):
 
     def __contains__(self, string):
         '''
-            This lets you use the syntax `"a" in fsm1` to see whether the string "a"
-            is in the set of strings accepted by `fsm1`.
+            This lets you use the syntax `"a" in fsm1` to see whether the
+            string "a" is in the set of strings accepted by `fsm1`.
         '''
         return self.accepts(string)
 
     def reduce(self):
         '''
-            A result by Brzozowski (1963) shows that a minimal finite state machine
-            equivalent to the original can be obtained by reversing the original
-            twice.
+            A result by Brzozowski (1963) shows that a minimal finite state
+            machine equivalent to the original can be obtained by reversing the
+            original twice.
         '''
         return reversed(reversed(self))
 
     def __repr__(self):
-        string = "fsm("
-        string += "alphabet = " + repr(self.alphabet)
-        string += ", states = " + repr(self.states)
-        string += ", initial = " + repr(self.initial)
-        string += ", finals = " + repr(self.finals)
-        string += ", map = " + repr(self.map)
-        string += ")"
-        return string
+        args = ", ".join([
+            f"alphabet={repr(self.alphabet)}",
+            f"states={repr(self.states)}",
+            f"initial={repr(self.initial)}",
+            f"finals={repr(self.finals)}",
+            f"map={repr(self.map)}",
+        ])
+        return f"Fsm({args})"
 
     def __str__(self):
         rows = []
 
-        sorted_alphabet = sorted(self.alphabet, key=key)
+        sorted_alphabet = sorted(self.alphabet, key=alphabet_key)
 
         # top row
         row = ["", "name", "final?"]
         row.extend(
-          'anything_else' if symbol is anything_else else str(symbol)
+          'ANYTHING_ELSE' if symbol is ANYTHING_ELSE else str(symbol)
           for symbol in sorted_alphabet
         )
         rows.append(row)
@@ -151,7 +168,9 @@ def __str__(self):
         # column widths
         colwidths = []
         for x in range(len(rows[0])):
-            colwidths.append(max(len(str(rows[y][x])) for y in range(len(rows))) + 1)
+            colwidths.append(max(
+                len(str(rows[y][x])) for y in range(len(rows))
+            ) + 1)
 
         # apply padding
         for y in range(len(rows)):
@@ -163,7 +182,7 @@ def __str__(self):
 
         return "".join("".join(row) + "\n" for row in rows)
 
-    def concatenate(*fsms: 'fsm'):
+    def concatenate(*fsms):
         '''
             Concatenate arbitrarily many finite state machines together.
         '''
@@ -171,9 +190,10 @@ def concatenate(*fsms: 'fsm'):
 
         def connect_all(i, substate):
             '''
-                Take a state in the numbered FSM and return a set containing it, plus
-                (if it's final) the first state from the next FSM, plus (if that's
-                final) the first state from the next but one FSM, plus...
+                Take a state in the numbered FSM and return a set containing
+                it, plus (if it's final) the first state from the next FSM,
+                plus (if that's final) the first state from the next but one
+                FSM, plus...
             '''
             result = {(i, substate)}
             while i < len(fsms) - 1 and substate in fsms[i].finals:
@@ -184,7 +204,8 @@ def connect_all(i, substate):
 
         # Use a superset containing states from all FSMs at once.
         # We start at the start of the first FSM. If this state is final in the
-        # first FSM, then we are also at the start of the second FSM. And so on.
+        # first FSM, then we are also at the start of the second FSM. And so
+        # on.
         initial = set()
         if len(fsms) > 0:
             initial.update(connect_all(0, fsms[0].initial))
@@ -199,9 +220,10 @@ def final(state):
 
         def follow(current, symbol):
             '''
-                Follow the collection of states through all FSMs at once, jumping to the
-                next FSM if we reach the end of the current one
-                TODO: improve all follow() implementations to allow for dead metastates?
+                Follow the collection of states through all FSMs at once,
+                jumping to the next FSM if we reach the end of the current one
+                TODO: improve all follow() implementations to allow for dead
+                metastates?
             '''
             next = set()
             for (i, substate) in current:
@@ -209,8 +231,14 @@ def follow(current, symbol):
                 if substate in fsm.map:
                     if symbol in fsm.map[substate]:
                         next.update(connect_all(i, fsm.map[substate][symbol]))
-                    elif anything_else in fsm.map[substate] and symbol not in fsm.alphabet:
-                        next.update(connect_all(i, fsm.map[substate][anything_else]))
+                    elif (
+                        ANYTHING_ELSE in fsm.map[substate]
+                        and symbol not in fsm.alphabet
+                    ):
+                        next.update(connect_all(
+                            i,
+                            fsm.map[substate][ANYTHING_ELSE]
+                        ))
             if len(next) == 0:
                 raise OblivionError
             return frozenset(next)
@@ -229,9 +257,9 @@ def __add__(self, other):
 
     def star(self):
         '''
-            If the present FSM accepts X, returns an FSM accepting X* (i.e. 0 or
-            more Xes). This is NOT as simple as naively connecting the final states
-            back to the initial state: see (b*ab)* for example.
+            If the present FSM accepts X, returns an FSM accepting X* (i.e. 0
+            or more Xes). This is NOT as simple as naively connecting the final
+            states back to the initial state: see (b*ab)* for example.
         '''
         alphabet = self.alphabet
 
@@ -246,8 +274,8 @@ def follow(state, symbol):
                 # If one of our substates is final, then we can also consider
                 # transitions from the initial state of the original FSM.
                 if substate in self.finals \
-                and self.initial in self.map \
-                and symbol in self.map[self.initial]:
+                   and self.initial in self.map \
+                   and symbol in self.map[self.initial]:
                     next.add(self.map[self.initial][symbol])
 
             if len(next) == 0:
@@ -265,7 +293,7 @@ def times(self, multiplier):
             Given an FSM and a multiplier, return the multiplied FSM.
         '''
         if multiplier < 0:
-            raise Exception("Can't multiply an FSM by " + repr(multiplier))
+            raise Exception(f"Can't multiply an FSM by {repr(multiplier)}")
 
         alphabet = self.alphabet
 
@@ -273,10 +301,16 @@ def times(self, multiplier):
         initial = {(self.initial, 0)}
 
         def final(state):
-            '''If the initial state is final then multiplying doesn't alter that'''
+            '''
+                If the initial state is final then multiplying doesn't alter
+                that
+            '''
             for (substate, iteration) in state:
                 if substate == self.initial \
-                and (self.initial in self.finals or iteration == multiplier):
+                  and (
+                    self.initial in self.finals or
+                    iteration == multiplier
+                  ):
                     return True
             return False
 
@@ -284,8 +318,8 @@ def follow(current, symbol):
             next = []
             for (substate, iteration) in current:
                 if iteration < multiplier \
-                and substate in self.map \
-                and symbol in self.map[substate]:
+                   and substate in self.map \
+                   and symbol in self.map[substate]:
                     next.append((self.map[substate][symbol], iteration))
                     # final of self? merge with initial on next iteration
                     if self.map[substate][symbol] in self.finals:
@@ -304,9 +338,9 @@ def __mul__(self, multiplier):
 
     def union(*fsms):
         '''
-            Treat `fsms` as a collection of arbitrary FSMs and return the union FSM.
-            Can be used as `fsm1.union(fsm2, ...)` or `fsm.union(fsm1, ...)`. `fsms`
-            may be empty.
+            Treat `fsms` as a collection of arbitrary FSMs and return the union
+            FSM. Can be used as `fsm1.union(fsm2, ...)` or
+            `fsm.union(fsm1, ...)`. `fsms` may be empty.
         '''
         return parallel(fsms, any)
 
@@ -314,8 +348,8 @@ def __or__(self, other):
         '''
             Alternation.
             Return a finite state machine which accepts any sequence of symbols
-            that is accepted by either self or other. Note that the set of strings
-            recognised by the two FSMs undergoes a set union.
+            that is accepted by either self or other. Note that the set of
+            strings recognised by the two FSMs undergoes a set union.
             Call using "fsm3 = fsm1 | fsm2"
         '''
         return self.union(other)
@@ -324,33 +358,33 @@ def intersection(*fsms):
         '''
             Intersection.
             Take FSMs and AND them together. That is, return an FSM which
-            accepts any sequence of symbols that is accepted by both of the original
-            FSMs. Note that the set of strings recognised by the two FSMs undergoes
-            a set intersection operation.
+            accepts any sequence of symbols that is accepted by both of the
+            original FSMs. Note that the set of strings recognised by the two
+            FSMs undergoes a set intersection operation.
             Call using "fsm3 = fsm1 & fsm2"
         '''
         return parallel(fsms, all)
 
     def __and__(self, other):
         '''
-            Treat the FSMs as sets of strings and return the intersection of those
-            sets in the form of a new FSM. `fsm1.intersection(fsm2, ...)` or
-            `fsm.intersection(fsm1, ...)` are acceptable.
+            Treat the FSMs as sets of strings and return the intersection of
+            those sets in the form of a new FSM.
         '''
         return self.intersection(other)
 
     def symmetric_difference(*fsms):
         '''
-            Treat `fsms` as a collection of sets of strings and compute the symmetric
-            difference of them all. The python set method only allows two sets to be
-            operated on at once, but we go the extra mile since it's not too hard.
+            Treat `fsms` as a collection of sets of strings and compute the
+            symmetric difference of them all. The python set method only allows
+            two sets to be operated on at once, but we go the extra mile since
+            it's not too hard.
         '''
         return parallel(fsms, lambda accepts: (accepts.count(True) % 2) == 1)
 
     def __xor__(self, other):
         '''
-            Symmetric difference. Returns an FSM which recognises only the strings
-            recognised by `self` or `other` but not both.
+            Symmetric difference. Returns an FSM which recognises only the
+            strings recognised by `self` or `other` but not both.
         '''
         return self.symmetric_difference(other)
 
@@ -358,16 +392,18 @@ def everythingbut(self):
         '''
             Return a finite state machine which will accept any string NOT
             accepted by self, and will not accept any string accepted by self.
-            This is more complicated if there are missing transitions, because the
-            missing "dead" state must now be reified.
+            This is more complicated if there are missing transitions, because
+            the missing "dead" state must now be reified.
         '''
         alphabet = self.alphabet
 
-        initial = {0 : self.initial}
+        initial = {0: self.initial}
 
         def follow(current, symbol):
             next = {}
-            if 0 in current and current[0] in self.map and symbol in self.map[current[0]]:
+            if 0 in current \
+               and current[0] in self.map \
+               and symbol in self.map[current[0]]:
                 next[0] = self.map[current[0]][symbol]
             return next
 
@@ -435,22 +471,24 @@ def islive(self, state):
 
     def empty(self):
         '''
-            An FSM is empty if it recognises no strings. An FSM may be arbitrarily
-            complicated and have arbitrarily many final states while still recognising
-            no strings because those final states may all be inaccessible from the
-            initial state. Equally, an FSM may be non-empty despite having an empty
-            alphabet if the initial state is final.
+            An FSM is empty if it recognises no strings. An FSM may be
+            arbitrarily complicated and have arbitrarily many final states
+            while still recognising no strings because those final states may
+            all be inaccessible from the initial state. Equally, an FSM may be
+            non-empty despite having an empty alphabet if the initial state is
+            final.
         '''
         return not self.islive(self.initial)
 
     def strings(self):
         '''
-            Generate strings (lists of symbols) that this FSM accepts. Since there may
-            be infinitely many of these we use a generator instead of constructing a
-            static list. Strings will be sorted in order of length and then lexically.
-            This procedure uses arbitrary amounts of memory but is very fast. There
-            may be more efficient ways to do this, that I haven't investigated yet.
-            You can use this in list comprehensions.
+            Generate strings (lists of symbols) that this FSM accepts. Since
+            there may be infinitely many of these we use a generator instead of
+            constructing a static list. Strings will be sorted in order of
+            length and then lexically. This procedure uses arbitrary amounts of
+            memory but is very fast. There may be more efficient ways to do
+            this, that I haven't investigated yet. You can use this in list
+            comprehensions.
         '''
 
         # Many FSMs have "dead states". Once you reach a dead state, you can no
@@ -458,10 +496,10 @@ def strings(self):
         # advantageous to constrain our search to live states only.
         livestates = set(state for state in self.states if self.islive(state))
 
-        # We store a list of tuples. Each tuple consists of an input string and the
-        # state that this input string leads to. This means we don't have to run the
-        # state machine from the very beginning every time we want to check a new
-        # string.
+        # We store a list of tuples. Each tuple consists of an input string and
+        # the state that this input string leads to. This means we don't have
+        # to run the state machine from the very beginning every time we want
+        # to check a new string.
         strings = []
 
         # Initial entry (or possibly not, in which case this is a short one)
@@ -477,7 +515,7 @@ def strings(self):
         while i < len(strings):
             (cstring, cstate) = strings[i]
             if cstate in self.map:
-                for symbol in sorted(self.map[cstate], key=key):
+                for symbol in sorted(self.map[cstate], key=alphabet_key):
                     nstate = self.map[cstate][symbol]
                     nstring = cstring + [symbol]
                     if nstate in livestates:
@@ -494,30 +532,30 @@ def __iter__(self):
 
     def equivalent(self, other):
         '''
-            Two FSMs are considered equivalent if they recognise the same strings.
-            Or, to put it another way, if their symmetric difference recognises no
-            strings.
+            Two FSMs are considered equivalent if they recognise the same
+            strings. Or, to put it another way, if their symmetric difference
+            recognises no strings.
         '''
         return (self ^ other).empty()
 
     def __eq__(self, other):
         '''
-            You can use `fsm1 == fsm2` to determine whether two FSMs recognise the
-            same strings.
+            You can use `fsm1 == fsm2` to determine whether two FSMs recognise
+            the same strings.
         '''
         return self.equivalent(other)
 
     def different(self, other):
         '''
-            Two FSMs are considered different if they have a non-empty symmetric
-            difference.
+            Two FSMs are considered different if they have a non-empty
+            symmetric difference.
         '''
         return not (self ^ other).empty()
 
     def __ne__(self, other):
         '''
-            Use `fsm1 != fsm2` to determine whether two FSMs recognise different
-            strings.
+            Use `fsm1 != fsm2` to determine whether two FSMs recognise
+            different strings.
         '''
         return self.different(other)
 
@@ -526,26 +564,31 @@ def difference(*fsms):
             Difference. Returns an FSM which recognises only the strings
             recognised by the first FSM in the list, but none of the others.
         '''
-        return parallel(fsms, lambda accepts: accepts[0] and not any(accepts[1:]))
+        return parallel(
+            fsms,
+            lambda accepts: accepts[0] and not any(accepts[1:])
+        )
 
     def __sub__(self, other):
         return self.difference(other)
 
     def cardinality(self):
         '''
-            Consider the FSM as a set of strings and return the cardinality of that
-            set, or raise an OverflowError if there are infinitely many
+            Consider the FSM as a set of strings and return the cardinality of
+            that set, or raise an OverflowError if there are infinitely many
         '''
         num_strings = {}
+
         def get_num_strings(state):
             # Many FSMs have at least one oblivion state
             if self.islive(state):
                 if state in num_strings:
-                    if num_strings[state] is None: # "computing..."
-                        # Recursion! There are infinitely many strings recognised
+                    if num_strings[state] is None:  # "computing..."
+                        # Recursion! There are infinitely many strings
+                        # recognised
                         raise OverflowError(state)
                     return num_strings[state]
-                num_strings[state] = None # i.e. "computing..."
+                num_strings[state] = None  # i.e. "computing..."
 
                 n = 0
                 if state in self.finals:
@@ -565,42 +608,45 @@ def get_num_strings(state):
 
     def __len__(self):
         '''
-            Consider the FSM as a set of strings and return the cardinality of that
-            set, or raise an OverflowError if there are infinitely many
+            Consider the FSM as a set of strings and return the cardinality of
+            that set, or raise an OverflowError if there are infinitely many
         '''
         return self.cardinality()
 
     def isdisjoint(self, other):
         '''
-            Treat `self` and `other` as sets of strings and see if they are disjoint
+            Treat `self` and `other` as sets of strings and see if they are
+            disjoint
         '''
         return (self & other).empty()
 
     def issubset(self, other):
         '''
-            Treat `self` and `other` as sets of strings and see if `self` is a subset
-            of `other`... `self` recognises no strings which `other` doesn't.
+            Treat `self` and `other` as sets of strings and see if `self` is a
+            subset of `other`... `self` recognises no strings which `other`
+            doesn't.
         '''
         return (self - other).empty()
 
     def __le__(self, other):
         '''
-            Treat `self` and `other` as sets of strings and see if `self` is a subset
-            of `other`... `self` recognises no strings which `other` doesn't.
+            Treat `self` and `other` as sets of strings and see if `self` is a
+            subset of `other`... `self` recognises no strings which `other`
+            doesn't.
         '''
         return self.issubset(other)
 
     def ispropersubset(self, other):
         '''
-            Treat `self` and `other` as sets of strings and see if `self` is a proper
-            subset of `other`.
+            Treat `self` and `other` as sets of strings and see if `self` is a
+            proper subset of `other`.
         '''
         return self <= other and self != other
 
     def __lt__(self, other):
         '''
-            Treat `self` and `other` as sets of strings and see if `self` is a strict
-            subset of `other`.
+            Treat `self` and `other` as sets of strings and see if `self` is a
+            strict subset of `other`.
         '''
         return self.ispropersubset(other)
 
@@ -620,8 +666,8 @@ def __ge__(self, other):
 
     def ispropersuperset(self, other):
         '''
-            Treat `self` and `other` as sets of strings and see if `self` is a proper
-            superset of `other`.
+            Treat `self` and `other` as sets of strings and see if `self` is a
+            proper superset of `other`.
         '''
         return self >= other and self != other
 
@@ -634,33 +680,34 @@ def __gt__(self, other):
 
     def copy(self):
         '''
-            For completeness only, since `set.copy()` also exists. FSM objects are
-            immutable, so I can see only very odd reasons to need this.
+            For completeness only, since `set.copy()` also exists. FSM objects
+            are immutable, so I can see only very odd reasons to need this.
         '''
-        return fsm(
-            alphabet = self.alphabet,
-            states   = self.states,
-            initial  = self.initial,
-            finals   = self.finals,
-            map      = self.map,
+        return Fsm(
+            alphabet=self.alphabet,
+            states=self.states,
+            initial=self.initial,
+            finals=self.finals,
+            map=self.map,
         )
 
     def derive(self, input):
         '''
-            Compute the Brzozowski derivative of this FSM with respect to the input
-            string of symbols. <https://en.wikipedia.org/wiki/Brzozowski_derivative>
-            If any of the symbols are not members of the alphabet, that's a KeyError.
-            If you fall into oblivion, then the derivative is an FSM accepting no
-            strings.
+            Compute the Brzozowski derivative of this FSM with respect to the
+            input string of symbols.
+            <https://en.wikipedia.org/wiki/Brzozowski_derivative>
+            If any of the symbols are not members of the alphabet, that's a
+            `KeyError`. If you fall into oblivion, then the derivative is an
+            FSM accepting no strings.
         '''
         try:
             # Consume the input string.
             state = self.initial
             for symbol in input:
-                if not symbol in self.alphabet:
-                    if not anything_else in self.alphabet:
+                if symbol not in self.alphabet:
+                    if ANYTHING_ELSE not in self.alphabet:
                         raise KeyError(symbol)
-                    symbol = anything_else
+                    symbol = ANYTHING_ELSE
 
                 # Missing transition = transition to dead state
                 if not (state in self.map and symbol in self.map[state]):
@@ -668,54 +715,57 @@ def derive(self, input):
 
                 state = self.map[state][symbol]
 
-            # OK so now we have consumed that string, use the new location as the
-            # starting point.
-            return fsm(
-                alphabet = self.alphabet,
-                states   = self.states,
-                initial  = state,
-                finals   = self.finals,
-                map      = self.map,
+            # OK so now we have consumed that string, use the new location as
+            # the starting point.
+            return Fsm(
+                alphabet=self.alphabet,
+                states=self.states,
+                initial=state,
+                finals=self.finals,
+                map=self.map,
             )
 
         except OblivionError:
             # Fell out of the FSM. The derivative of this FSM is the empty FSM.
             return null(self.alphabet)
 
+
 def null(alphabet):
     '''
         An FSM accepting nothing (not even the empty string). This is
         demonstrates that this is possible, and is also extremely useful
         in some situations
     '''
-    return fsm(
-        alphabet = alphabet,
-        states   = {0},
-        initial  = 0,
-        finals   = set(),
-        map      = {
+    return Fsm(
+        alphabet=alphabet,
+        states={0},
+        initial=0,
+        finals=set(),
+        map={
             0: dict([(symbol, 0) for symbol in alphabet]),
         },
     )
 
+
 def epsilon(alphabet):
     '''
         Return an FSM matching an empty string, "", only.
         This is very useful in many situations
     '''
-    return fsm(
-        alphabet = alphabet,
-        states   = {0},
-        initial  = 0,
-        finals   = {0},
-        map      = {},
+    return Fsm(
+        alphabet=alphabet,
+        states={0},
+        initial=0,
+        finals={0},
+        map={},
     )
 
+
 def parallel(fsms, test):
     '''
-        Crawl several FSMs in parallel, mapping the states of a larger meta-FSM.
-        To determine whether a state in the larger FSM is final, pass all of the
-        finality statuses (e.g. [True, False, False] to `test`.
+        Crawl several FSMs in parallel, mapping the states of a larger
+        meta-FSM. To determine whether a state in the larger FSM is final, pass
+        all of the finality statuses (e.g. [True, False, False] to `test`.
     '''
     alphabet = set().union(*[fsm.alphabet for fsm in fsms])
 
@@ -726,13 +776,14 @@ def parallel(fsms, test):
     def follow(current, symbol):
         next = {}
         for i in range(len(fsms)):
-            if symbol not in fsms[i].alphabet and anything_else in fsms[i].alphabet:
-                actual_symbol = anything_else
+            if symbol not in fsms[i].alphabet \
+               and ANYTHING_ELSE in fsms[i].alphabet:
+                actual_symbol = ANYTHING_ELSE
             else:
                 actual_symbol = symbol
             if i in current \
-            and current[i] in fsms[i].map \
-            and actual_symbol in fsms[i].map[current[i]]:
+               and current[i] in fsms[i].map \
+               and actual_symbol in fsms[i].map[current[i]]:
                 next[i] = fsms[i].map[current[i]][actual_symbol]
         if len(next.keys()) == 0:
             raise OblivionError
@@ -741,11 +792,15 @@ def follow(current, symbol):
     # Determine the "is final?" condition of each substate, then pass it to the
     # test to determine finality of the overall FSM.
     def final(state):
-        accepts = [i in state and state[i] in fsm.finals for (i, fsm) in enumerate(fsms)]
+        accepts = [
+            i in state and state[i] in fsm.finals
+            for (i, fsm) in enumerate(fsms)
+        ]
         return test(accepts)
 
     return crawl(alphabet, initial, final, follow).reduce()
 
+
 def crawl(alphabet, initial, final, follow):
     '''
         Given the above conditions and instructions, crawl a new unknown FSM,
@@ -769,7 +824,7 @@ def crawl(alphabet, initial, final, follow):
 
         # compute map for this state
         map[i] = {}
-        for symbol in sorted(alphabet, key=key):
+        for symbol in sorted(alphabet, key=alphabet_key):
             try:
                 next = follow(state, symbol)
 
@@ -787,10 +842,10 @@ def crawl(alphabet, initial, final, follow):
 
         i += 1
 
-    return fsm(
-        alphabet = alphabet,
-        states   = set(range(len(states))),
-        initial  = 0,
-        finals   = finals,
-        map      = map,
+    return Fsm(
+        alphabet=alphabet,
+        states=set(range(len(states))),
+        initial=0,
+        finals=finals,
+        map=map,
     )
diff --git a/greenery/lego.py b/greenery/lego.py
deleted file mode 100644
index 9100c53..0000000
--- a/greenery/lego.py
+++ /dev/null
@@ -1,1967 +0,0 @@
-# -*- coding: utf-8 -*-
-
-'''
-    LEGO:
-    Classes and methods for the creation and manipulation of regular expression
-    objects and components.
-
-    * A regular expression is a "pattern" object.
-    * Each pattern alternates (with a pipe, "|") between zero or more "conc"
-    (concatenation) objects.
-    * Each conc is a concatenation of zero or more "mult" (multiplication)
-    objects.
-    * Each mult consists of a multiplicand and a multiplier. A multiplier consists
-    of a minimum and a maximum, e.g. min = 0, max = 1 indicates the "?"
-    multiplier. The multiplicand is either a nested pattern object, or a
-    charclass object.
-    * A charclass is a set of chars, such as "a", "[a-z]", "\\d", ".", with a
-    possible "negated" flag as in "[^a]".
-    * Since these can be combined together freely they are, in the absence of a
-    better metaphor, collectively referred to as lego pieces.
-
-    We also include methods for parsing a string into a pattern object,
-    serialising a pattern object out as a string (or "regular expression", if you
-    will), and for concatenating or alternating between arbitrary "pieces of
-    lego", using overloaded operators.
-
-    If the FSM module is available, call lego.to_fsm() on any lego piece to return
-    a finite state machine capable of accepting strings described by that piece.
-
-    Most important are the reduce() methods present in charclass, mult, conc and
-    pattern. While there is no such thing as a canonical form for a given regex
-    pattern, these procedures can drastically simplify a regex structure for
-    readability. They're also pretty extensible.
-'''
-from typing import Optional, Union, Tuple, FrozenSet
-from greenery import fsm
-from dataclasses import dataclass, field
-
-class nomatch(Exception):
-    '''Thrown when parsing fails. Almost always caught and almost never fatal'''
-    pass
-
-def reduce_after(method):
-    '''reduce() the result of this method call (unless you already reduced it).'''
-    def new_method(self, *args, **kwargs):
-        result = method(self, *args, **kwargs)
-        if result == self:
-            return result
-        return result.reduce()
-    return new_method
-
-def call_fsm(method):
-    '''
-        Take a method which acts on 0 or more regular expression objects... return a
-        new method which simply converts them all to FSMs, calls the FSM method
-        on them instead, then converts the result back to a regular expression.
-        We do this for several of the more annoying operations.
-    '''
-    fsm_method = getattr(fsm.fsm, method.__name__)
-    def new_method(*legos):
-        alphabet = set().union(*[lego.alphabet() for lego in legos])
-        return from_fsm(fsm_method(*[lego.to_fsm(alphabet) for lego in legos]))
-    return new_method
-
-def parse(string: str):
-    '''
-        Parse a full string and return a lego piece. Fail if the whole string
-        wasn't parsed
-    '''
-    return pattern.parse(string)
-
-def from_fsm(f: fsm.fsm):
-    '''
-        Turn the supplied finite state machine into a `lego` object. This is
-        accomplished using the Brzozowski algebraic method.
-    '''
-    # Make sure the supplied alphabet is kosher. It must contain only single-
-    # character strings or `fsm.anything_else`.
-    for symbol in f.alphabet:
-        if symbol == fsm.anything_else:
-            continue
-        if isinstance(symbol, str) and len(symbol) == 1:
-            continue
-        raise Exception("Symbol " + repr(symbol) + " cannot be used in a regular expression")
-
-    # We need a new state not already used
-    outside = object()
-
-    # The set of strings that would be accepted by this FSM if you started
-    # at state i is represented by the regex R_i.
-    # If state i has a sole transition "a" to state j, then we know R_i = a R_j.
-    # If state i is final, then the empty string is also accepted by this regex.
-    # And so on...
-
-    # From this we can build a set of simultaneous equations in len(f.states)
-    # variables. This system is easily solved for all variables, but we only
-    # need one: R_a, where a is the starting state.
-
-    # The first thing we need to do is organise the states into order of depth,
-    # so that when we perform our back-substitutions, we can start with the
-    # last (deepest) state and therefore finish with R_a.
-    states = [f.initial]
-    i = 0
-    while i < len(states):
-        current = states[i]
-        if current in f.map:
-            for symbol in sorted(f.map[current], key=fsm.key):
-                next = f.map[current][symbol]
-                if next not in states:
-                    states.append(next)
-        i += 1
-
-    # Our system of equations is represented like so:
-    brz = {}
-    for a in f.states:
-        brz[a] = {}
-        for b in f.states:
-            brz[a][b] = nothing
-        brz[a][outside] = emptystring if a in f.finals else nothing
-
-    # Populate it with some initial data.
-    for a in f.map:
-        for symbol in f.map[a]:
-            b = f.map[a][symbol]
-            if symbol == fsm.anything_else:
-                brz[a][b] |= ~charclass(f.alphabet - {fsm.anything_else})
-            else:
-                brz[a][b] |= charclass({symbol})
-
-    # Now perform our back-substitution
-    for i in reversed(range(len(states))):
-        a = states[i]
-
-        # Before the equation for R_a can be substituted into the other
-        # equations, we need to resolve the self-transition (if any).
-        # e.g.    R_a = 0 R_a |   1 R_b |   2 R_c
-        # becomes R_a =         0*1 R_b | 0*2 R_c
-        loop = brz[a][a] * star # i.e. "0*"
-        del brz[a][a]
-
-        for right in brz[a]:
-            brz[a][right] = (loop + brz[a][right]).reduce()
-
-        # Note: even if we're down to our final equation, the above step still
-        # needs to be performed before anything is returned.
-
-        # Now we can substitute this equation into all of the previous ones.
-        for j in range(i):
-            b = states[j]
-
-            # e.g. substituting R_a =  0*1 R_b |      0*2 R_c
-            # into              R_b =    3 R_a |        4 R_c | 5 R_d
-            # yields            R_b = 30*1 R_b | (30*2|4) R_c | 5 R_d
-            univ = brz[b][a] # i.e. "3"
-            del brz[b][a]
-
-            for right in brz[a]:
-                brz[b][right] = (brz[b][right] | (univ + brz[a][right])).reduce()
-
-    return brz[f.initial][outside].reduce()
-
-def static(string, i, static):
-    j = i + len(static)
-    if string[i:j] == static:
-        return j
-    raise nomatch
-
-def select_static(string, i, *statics):
-    for st in statics:
-        j = i+len(st)
-        if string[i:j] == st:
-            return j, st
-    raise nomatch
-
-def read_until(string: str, i: int, stop_char: str) -> Tuple[int, str]:
-    start = i
-    while True:
-        if i >= len(string):
-            raise nomatch
-        if string[i] == stop_char:
-            break
-        i += 1
-    return i + 1, string[start:i]
-
-class lego:
-    '''
-        Parent class for all lego pieces.
-        All lego pieces have some things in common. This parent class mainly
-        hosts documentation though.
-    '''
-
-    def __setattr__(self, name, value):
-        '''
-            Lego pieces are immutable. It caused some pretty serious problems when
-            I didn't have this.
-        '''
-        raise Exception("This object is immutable.")
-
-    def to_fsm(self, alphabet=None):
-        '''
-            Return the present lego piece in the form of a finite state machine,
-            as imported from the fsm module.
-            If no alphabet is explicitly supplied, which seems quite probable,
-            we use the lego.alphabet() method (later) to list all the characters
-            mentioned in self. However, if we intend to connect this FSM to another
-            one which uses different characters, we may need to supply an alphabet
-            which is a superset of both sets.
-        '''
-        raise NotImplementedError()
-
-    def __repr__(self):
-        '''
-            Return a string approximating the instantiation line
-            for the present lego piece.
-        '''
-        raise NotImplementedError()
-
-    def __str__(self):
-        '''
-            Render the present lego piece in the form of a regular expression.
-            Some lego pieces may be created which cannot be rendered in this way.
-            In particular: a pattern containing no concs; a multiplier of zero.
-        '''
-        raise NotImplementedError()
-
-    @classmethod
-    def match(cls, string: str, i = 0):
-        '''
-            Start at index i in the supplied string and try to match one of the
-            present class. Elementary recursive descent parsing with very little
-            need for flair. The opposite of __str__(), above. (In most cases.)
-            Throws a nomatch in the event of failure.
-        '''
-        raise NotImplementedError()
-
-    @classmethod
-    def parse(cls, string: str):
-        '''
-            Parse the entire supplied string as an instance of the present class.
-            Mainly for internal use in unit tests because it drops through to match()
-            in a convenient way.
-        '''
-        obj, i = cls.match(string, 0)
-        if i != len(string):
-            raise Exception("Could not parse '" + string + "' beyond index " + str(i))
-        return obj
-
-    @reduce_after
-    def reduce(self):
-        '''
-            The most important and algorithmically complex method. Takes the current
-            lego piece and simplifies it in every way possible, returning a simpler
-            lego piece which is quite probably not of the same class as the original.
-            Approaches vary by the class of the present lego piece.
-
-            It is critically important to (1) always call reduce() on whatever you're
-            returning before you return it and therefore (2) always return something
-            STRICTLY SIMPLER than the current object. Otherwise, infinite loops become
-            possible in reduce() calls.
-        '''
-        raise NotImplementedError()
-
-    @call_fsm
-    def concatenate(*legos):
-        '''
-            Concatenate a sequence of lego pieces, regardless of differing classes.
-            Call using "a = b + c"
-        '''
-        pass
-
-    def __add__(self, other):
-        return self.concatenate(other)
-
-    @call_fsm
-    def times(self, multiplier):
-        '''
-            Equivalent to repeated concatenation. Multiplier consists of a minimum
-            and a maximum; maximum may be infinite (for Kleene star closure).
-            Call using "a = b * qm"
-        '''
-        raise NotImplementedError()
-
-    def __mul__(self, multiplier):
-        return self.times(multiplier)
-
-    @call_fsm
-    def union(*legos):
-        '''
-            Alternate between any two lego pieces, regardless of differing classes.
-            Call using "a = b | c".
-            This method MUST NOT call the to_fsm() method, because this method is used
-            in turn when converting an FSM back to a regex.
-        '''
-        pass
-
-    def __or__(self, other):
-        return self.union(other)
-
-    @call_fsm
-    def intersection(self, other):
-        '''
-            Intersection function. Return a lego piece that can match any string
-            that both self and other can match. Fairly elementary results relating
-            to regular languages and finite state machines show that this is
-            possible, but implementation is a BEAST in many cases. Here, we convert
-            both lego pieces to FSMs (see to_fsm(), above) for the intersection, then
-            back to lego afterwards.
-            Call using "a = b & c"
-        '''
-        pass
-
-    def __and__(self, other):
-        return self.intersection(other)
-
-    @call_fsm
-    def difference(*legos):
-        '''
-            Return a regular expression which matches any string which `self` matches
-            but none of the strings which `other` matches.
-        '''
-        pass
-
-    def __sub__(self, other):
-        return self.difference(other)
-
-    @call_fsm
-    def symmetric_difference(*legos):
-        '''
-            Return a regular expression matching only the strings recognised by
-            `self` or `other` but not both.
-        '''
-        pass
-
-    def __xor__(self, other):
-        return self.symmetric_difference(other)
-
-    def equivalent(self, other):
-        '''
-            Two lego objects are equivalent if they recognise the same strings. Note
-            that in the general case this is actually quite an intensive calculation,
-            but far from unsolvable, as we demonstrate here:
-        '''
-        return self.to_fsm().equivalent(other.to_fsm())
-
-    def alphabet(self):
-        '''
-            Return a set of all unique characters used in this lego piece.
-            In theory this could be a static property, self.alphabet, not
-            a function, self.alphabet(), but in the vast majority of cases
-            this will never be queried so it's a waste of computation to
-            calculate it every time a lego piece is instantiated.
-            By convention, fsm.anything_else is always included in this result.
-        '''
-        raise NotImplementedError()
-
-    @call_fsm
-    def everythingbut(self):
-        '''
-            Return a lego object which will match any string not matched by self,
-            and which will not match any string matched by self.
-            Another task which is very difficult in general (and typically returns
-            utter garbage when actually printed), but becomes trivial to code
-            thanks to FSM routines.
-        '''
-        pass
-
-    def reversed(self):
-        '''
-            Return a lego object which will match any string which, when reversed,
-            self would match. E.g. if self matches "beer" then reversed(self) will
-            match "reeb".
-        '''
-        raise NotImplementedError()
-
-    def __reversed__(self):
-        return self.reversed()
-
-    def empty(self):
-        '''
-            Return False if there exists a string which the present lego piece
-            can match. Return True if no such string exists. Examples of empty
-            lego pieces are charclass() and pattern()
-        '''
-        raise NotImplementedError()
-
-    def matches(self, string):
-        return self.to_fsm().accepts(string)
-
-    def __contains__(self, string):
-        '''
-            This lets you use the syntax `"a" in pattern1` to see whether the string
-            "a" is in the set of strings matched by `pattern1`.
-        '''
-        return self.matches(string)
-
-    def strings(self, otherchar=None):
-        '''
-            Each time next() is called on this iterator, a new string is returned
-            which will the present lego piece can match. StopIteration is raised once
-            all such strings have been returned, although a regex with a * in may
-            match infinitely many strings.
-        '''
-
-        # In the case of a regex like "[^abc]", there are infinitely many (well, a
-        # very large finite number of) single characters which will match. It's not
-        # productive to iterate over all of these giving every single example.
-        # You must supply your own "otherchar" to stand in for all of these
-        # possibilities.
-        for string in self.to_fsm().strings():
-
-            # Have to represent `fsm.anything_else` somehow.
-            if fsm.anything_else in string:
-                if otherchar == None:
-                    raise Exception("Please choose an 'otherchar'")
-                string = [
-                    otherchar if char == fsm.anything_else else char
-                    for char in string
-                ]
-
-            yield "".join(string)
-
-    def __iter__(self):
-        '''
-            This allows you to do `for string in pattern1` as a list comprehension!
-        '''
-        return self.strings()
-
-    def cardinality(self):
-        '''
-            Consider the regular expression as a set of strings and return the
-            cardinality of that set, or raise an OverflowError if there are infinitely
-            many.
-        '''
-        # There is no way to do this other than converting to an FSM, because the
-        # pattern may allow duplicate routes, such as "a|a".
-        return self.to_fsm().cardinality()
-
-    def __len__(self):
-        return self.cardinality()
-
-    @call_fsm
-    def isdisjoint(self, other):
-        '''
-            Treat `self` and `other` as sets of strings and see if they are disjoint
-        '''
-        pass
-
-    def copy(self):
-        '''
-            For completeness only, since `set.copy()` also exists. Regular expression
-            objects are immutable, so I can see only very odd reasons to need this.
-        '''
-        raise NotImplementedError()
-
-    def __hash__(self):
-        '''For dictionaries'''
-        raise NotImplementedError()
-
-    def derive(self, string):
-        return from_fsm(self.to_fsm().derive(string))
-
-@dataclass(frozen=True)
-class charclass(lego):
-    '''
-        A charclass is basically a frozenset of symbols. The reason for the
-        charclass object instead of using frozenset directly is to allow us to
-        set a "negated" flag. A charclass with the negation flag set is assumed
-        to contain every symbol that is in the alphabet of all symbols but not
-        explicitly listed inside the frozenset. e.g. [^a]. This is very handy
-        if the full alphabet is extremely large, but also requires dedicated
-        combination functions.
-    '''
-    chars: Union[FrozenSet[str], str]
-    negated: bool = False
-
-    def __post_init__(self):
-        object.__setattr__(self, "chars", frozenset(self.chars))
-        # chars should consist only of chars
-        if fsm.anything_else in self.chars:
-            raise Exception("Can't put " + repr(fsm.anything_else) + " in a charclass")
-
-    def __eq__(self, other):
-        try:
-            return self.chars == other.chars and self.negated == other.negated
-        except AttributeError:
-            return False
-
-    def __hash__(self):
-        return hash((self.chars, self.negated))
-
-    def times(self, multiplier):
-        # e.g. "a" * {0,1} = "a?"
-        if multiplier == one:
-            return self
-        return mult(self, multiplier)
-
-    # These are the characters carrying special meanings when they appear "outdoors"
-    # within a regular expression. To be interpreted literally, they must be
-    # escaped with a backslash.
-    allSpecial = set("\\[]|().?*+{}")
-
-    # These are the characters carrying special meanings when they appear INSIDE a
-    # character class (delimited by square brackets) within a regular expression.
-    # To be interpreted literally, they must be escaped with a backslash.
-    # Notice how much smaller this class is than the one above; note also that the
-    # hyphen and caret do NOT appear above.
-    classSpecial = set("\\[]^-")
-
-    # Shorthand codes for use inside charclasses e.g. [abc\d]
-    w = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
-    d = "0123456789"
-    s = "\t\n\v\f\r "
-    shorthand = {
-        w : "\\w",
-        d : "\\d",
-        s : "\\s",
-    }
-
-    def __str__(self):
-        # e.g. \w
-        if self in shorthand.keys():
-            return shorthand[self]
-
-        # e.g. [^a]
-        if self.negated:
-            return "[^" + self.escape() + "]"
-
-        # single character, not contained inside square brackets.
-        if len(self.chars) == 1:
-            # Python lacks the Axiom of Choice
-            char = "".join(self.chars)
-
-            # e.g. if char is "\t", return "\\t"
-            if char in escapes.keys():
-                return escapes[char]
-
-            if char in charclass.allSpecial:
-                return "\\" + char
-
-            # If char is an ASCII control character, don't print it directly,
-            # return a hex escape sequence e.g. "\\x00". Note that this includes
-            # tab and other characters already handled above
-            if 0 <= ord(char) <= 0x1F or ord(char) == 0x7f:
-                return "\\x" + "{0:02x}".format(ord(char))
-
-            return char
-
-        # multiple characters (or possibly 0 characters)
-        return "[" + self.escape() + "]"
-
-    def escape(self):
-
-        def escapeChar(char):
-            if char in charclass.classSpecial:
-                return "\\" + char
-            if char in escapes.keys():
-                return escapes[char]
-
-            # If char is an ASCII control character, don't print it directly,
-            # return a hex escape sequence e.g. "\\x00". Note that this includes
-            # tab and other characters already handled above
-            if 0 <= ord(char) <= 0x1F or ord(char) == 0x7f:
-                return "\\x" + "{0:02x}".format(ord(char))
-
-            return char
-
-        def recordRange():
-            # there's no point in putting a range when the whole thing is
-            # 3 characters or fewer. "abc" -> "abc" but "abcd" -> "a-d"
-            strs = [
-                # "ab" or "abc" or "abcd"
-                "".join(escapeChar(char) for char in currentRange),
-                # "a-b" or "a-c" or "a-d"
-                escapeChar(currentRange[0]) + "-" + escapeChar(currentRange[-1]),
-            ]
-            return sorted(strs, key=lambda str: len(str))[0]
-
-        output = ""
-
-        # use shorthand for known character ranges
-        # note the nested processing order. DO NOT process \d before processing
-        # \w. if more character class constants arise which do not nest nicely,
-        # a problem will arise because there is no clear ordering to use...
-
-        # look for ranges
-        currentRange = ""
-        for char in sorted(self.chars, key=ord):
-
-            # range is not empty: new char must fit after previous one
-            if len(currentRange) > 0:
-
-                i = ord(char)
-
-                # char doesn't fit old range: restart
-                if i != ord(currentRange[-1]) + 1:
-                    output += recordRange()
-                    currentRange = ""
-
-            currentRange += char
-
-        if len(currentRange) > 0:
-            output += recordRange()
-
-        return output
-
-    def to_fsm(self, alphabet=None):
-        if alphabet is None:
-            alphabet = self.alphabet()
-
-        # 0 is initial, 1 is final
-
-        # If negated, make a singular FSM accepting any other characters
-        if self.negated:
-            map = {
-                0: dict([(symbol, 1) for symbol in alphabet - self.chars]),
-            }
-
-        # If normal, make a singular FSM accepting only these characters
-        else:
-            map = {
-                0: dict([(symbol, 1) for symbol in self.chars]),
-            }
-
-        return fsm.fsm(
-            alphabet = alphabet,
-            states   = {0, 1},
-            initial  = 0,
-            finals   = {1},
-            map      = map,
-        )
-
-    def __repr__(self):
-        string = ""
-        if self.negated is True:
-            string += "~"
-        string += "charclass("
-        if len(self.chars) > 0:
-            string += repr("".join(str(char) for char in sorted(self.chars, key=str)))
-        string += ")"
-        return string
-
-    @reduce_after
-    def reduce(self):
-        # Charclasses cannot be reduced().
-        return self
-
-    def concatenate(self, other):
-        return mult(self, one) + other
-
-    def alphabet(self):
-        return {fsm.anything_else} | self.chars
-
-    def empty(self):
-        return len(self.chars) == 0 and self.negated == False
-
-    @classmethod
-    def match(cls, string, i = 0):
-        if i >= len(string):
-            raise nomatch
-
-        # Turn e.g. "\\x40" into "@". Exactly two hex digits
-        def unescapeHex(string, i):
-            hex_digits = "0123456789AaBbCcDdEeFf"
-
-            j = static(string, i, "\\x")
-
-            hex1 = string[j] # e.g. "4"
-            if not hex1 in hex_digits:
-                raise nomatch
-            j += len(hex1)
-
-            hex2 = string[j] # e.g. "0"
-            if not hex2 in hex_digits:
-                raise nomatch
-            j += len(hex2)
-
-            codepoint = int(hex1 + hex2, 16) # e.g. 64
-            char = chr(codepoint) # "@"
-            return char, j
-
-        def matchInternalChar(string, i):
-
-            # e.g. if we see "\\t", return "\t"
-            for key in escapes.keys():
-                try:
-                    return key, static(string, i, escapes[key])
-                except nomatch:
-                    pass
-
-            # special chars e.g. "\\-" returns "-"
-            for char in charclass.classSpecial:
-                try:
-                    return char, static(string, i, "\\" + char)
-                except nomatch:
-                    pass
-
-            # hex escape e.g. "\\x40" returns "@"
-            try:
-                return unescapeHex(string, i)
-            except nomatch:
-                pass
-
-            # single non-special character, not contained
-            # inside square brackets
-            char, j = string[i], i+1
-            if char in charclass.classSpecial:
-                raise nomatch
-
-            return char, j
-
-        def matchClassInterior1(string, i):
-
-            # Attempt 1: shorthand e.g. "\w"
-            for key in charclass.shorthand:
-                try:
-                    return key, static(string, i, charclass.shorthand[key])
-                except nomatch:
-                    pass
-
-            # Attempt 2: a range e.g. "d-h"
-            try:
-                first, j = matchInternalChar(string, i) # `first` is "d"
-                k = static(string, j, "-")
-                last, k = matchInternalChar(string, k) # `last` is "h"
-
-                firstIndex = ord(first) # 100
-                lastIndex = ord(last) # 104
-
-                # Be strict here, "d-d" is not allowed
-                if firstIndex >= lastIndex:
-                    raise nomatch("Range '" + first + "' to '" + last + "' not allowed")
-
-                chars = "".join([
-                    chr(i) for i in range(firstIndex, lastIndex + 1)
-                ])
-                return chars, k
-            except nomatch:
-                pass
-
-            # Attempt 3: just a character on its own
-            return matchInternalChar(string, i)
-
-        def matchClassInterior(string, i):
-            internals = ""
-            try:
-                while True:
-                    internal, i = matchClassInterior1(string, i)
-                    internals += internal
-            except nomatch:
-                pass
-            return internals, i
-
-        # wildcard ".", "\\w", "\\d", etc.
-        for key in shorthand.keys():
-            try:
-                return key, static(string, i, shorthand[key])
-            except nomatch:
-                pass
-
-        # "[^dsgsdg]"
-        try:
-            j = static(string, i, "[^")
-            chars, j = matchClassInterior(string, j)
-            j = static(string, j, "]")
-            return ~charclass(chars), j
-        except nomatch:
-            pass
-
-        # "[sdfsf]"
-        try:
-            j = static(string, i, "[")
-            chars, j = matchClassInterior(string, j)
-            j = static(string, j, "]")
-            return charclass(chars), j
-        except nomatch:
-            pass
-
-        # e.g. if seeing "\\t", return "\t"
-        for key in escapes.keys():
-            try:
-                return charclass(key), static(string, i, escapes[key])
-            except nomatch:
-                pass
-
-        # e.g. if seeing "\\{", return "{"
-        for char in charclass.allSpecial:
-            try:
-                return charclass(char), static(string, i, "\\" + char)
-            except nomatch:
-                pass
-
-        # e.g. if seeing "\\x40", return "@"
-        try:
-            char, j = unescapeHex(string, i)
-            return charclass(char), j
-        except nomatch:
-            pass
-
-        # single non-special character, not contained inside square brackets
-        char, i = string[i], i+1
-        if char in charclass.allSpecial:
-            raise nomatch
-
-        return charclass(char), i
-
-    # set operations
-    def negate(self):
-        '''
-            Negate the current charclass. e.g. [ab] becomes [^ab]. Call
-            using "charclass2 = ~charclass1"
-        '''
-        return charclass(self.chars, negated=not self.negated)
-
-    def __invert__(self):
-        return self.negate()
-
-    def union(self, other):
-        try:
-            # ¬A OR ¬B = ¬(A AND B)
-            # ¬A OR B = ¬(A - B)
-            # A OR ¬B = ¬(B - A)
-            # A OR B
-            if self.negated:
-                if other.negated:
-                    return ~charclass(self.chars & other.chars)
-                return ~charclass(self.chars - other.chars)
-            if other.negated:
-                return ~charclass(other.chars - self.chars)
-            return charclass(self.chars | other.chars)
-
-        # "other" lacks attribute "negated" or "chars"
-        # "other" is not a charclass
-        # Never mind!
-        except AttributeError:
-            return mult(self, one) | other
-
-    def intersection(self, other):
-        try:
-            # ¬A AND ¬B = ¬(A OR B)
-            # ¬A AND B = B - A
-            # A AND ¬B = A - B
-            # A AND B
-            if self.negated:
-                if other.negated:
-                    return ~charclass(self.chars | other.chars)
-                return charclass(other.chars - self.chars)
-            if other.negated:
-                return charclass(self.chars - other.chars)
-            return charclass(self.chars & other.chars)
-
-        # "other" lacks attribute "negated" or "chars"
-        # "other" is not a charclass
-        # Never mind!
-        except AttributeError:
-            return mult(self, one) & other
-
-    def reversed(self):
-        return self
-
-    def copy(self):
-        return charclass(self.chars.copy(), negated=self.negated)
-
-@dataclass(frozen=True)
-class bound:
-    '''An integer but sometimes also possibly infinite (None)'''
-    v: Optional[int]
-
-    def __post_init__(self):
-        if not self.v is None and self.v < 0:
-            raise Exception("Invalid bound: " + repr(self.v))
-
-    def __repr__(self):
-        return "bound(" + repr(self.v) + ")"
-
-    def __str__(self):
-        if self == inf:
-            # This only happens for an unlimited upper bound
-            return ""
-        return str(self.v)
-
-    @classmethod
-    def match(cls, string: str, i = 0):
-        def matchAnyOf(string, i, collection):
-            for char in collection:
-                try:
-                    return char, static(string, i, char)
-                except nomatch:
-                    pass
-            raise nomatch
-
-        # "0"
-        try:
-            return bound(0), static(string, i, "0")
-        except nomatch:
-            pass
-
-        # "1", etc.
-        try:
-            digit, j = matchAnyOf(string, i, "123456789")
-            integer = int(digit)
-            try:
-                while True:
-                    digit, j = matchAnyOf(string, j, "0123456789")
-                    integer *= 10
-                    integer += int(digit)
-            except nomatch:
-                return bound(integer), j
-        except nomatch:
-            pass
-
-        # "" empty string = infinite bound as in "{4,}"
-        return inf, i
-
-    def __eq__(self, other):
-        try:
-            return self.v == other.v
-        except AttributeError:
-            return False
-
-    def __hash__(self):
-        return hash(self.v)
-
-    def __lt__(self, other):
-        if self == inf:
-            return False
-        if other == inf:
-            return True
-        return self.v < other.v
-
-    def __ge__(self, other):
-        return not self < other
-
-    def __mul__(self, other):
-        '''Multiply this bound by another'''
-        if self == bound(0) or other == bound(0):
-            return bound(0)
-        if self == inf or other == inf:
-            return inf
-        return bound(self.v * other.v)
-
-    def __add__(self, other):
-        '''Add this bound to another'''
-        if self == inf or other == inf:
-            return inf
-        return bound(self.v + other.v)
-
-    def __sub__(self, other):
-        '''
-            Subtract another bound from this one.
-            Caution: this operation is not meaningful for all bounds.
-        '''
-        if other == inf:
-            if self != inf:
-                raise Exception("Can't subtract " + repr(other) + " from " + repr(self))
-
-            # Infinity minus infinity is zero. This has to be true so that
-            # we can for example subtract multiplier(bound(0), inf) from
-            # multiplier(bound(1), inf) to get multiplier(bound(1), bound(1))
-            return bound(0)
-        if self == inf:
-            return self
-        return bound(self.v - other.v)
-
-    def copy(self):
-        return bound(self.v)
-
-@dataclass(frozen=True)
-class multiplier:
-    '''
-        A min and a max. The vast majority of characters in regular
-        expressions occur without a specific multiplier, which is implicitly
-        equivalent to a min of 1 and a max of 1, but many more have explicit
-        multipliers like "*" (min = 0, max = inf) and so on.
-        Although it seems odd and can lead to some confusing edge cases, we do
-        also permit a max of 0 (iff min is 0 too). This allows the multiplier
-        "zero" to exist, which actually are quite useful in their own special way.
-    '''
-    min: bound
-    max: bound
-    mandatory: bound = field(init=False)
-    optional: bound = field(init=False)
-
-    def __post_init__(self):
-        if self.min == inf:
-            raise Exception("Minimum bound of a multiplier can't be " + repr(inf))
-        if self.min > self.max:
-            raise Exception("Invalid multiplier bounds: " + repr(self.min) + " and " + repr(self.max))
-
-        # More useful than "min" and "max" in many situations
-        # are "mandatory" and "optional".
-        object.__setattr__(self, "mandatory", self.min)
-        object.__setattr__(self, "optional", self.max - self.min)
-
-    def __eq__(self, other):
-        try:
-            return self.min == other.min and self.max == other.max
-        except AttributeError:
-            return False
-
-    def __hash__(self):
-        return hash((self.min, self.max))
-
-    def __repr__(self):
-        return "multiplier(" + repr(self.min) + ", " + repr(self.max) + ")"
-
-    def __str__(self):
-        if self.max == bound(0):
-            raise Exception("Can't serialise a multiplier with bound " + repr(self.max))
-        if self in symbolic.keys():
-            return symbolic[self]
-        if self.min == self.max:
-            return "{" + str(self.min) + "}"
-        return "{" + str(self.min) + "," + str(self.max) + "}"
-
-    @classmethod
-    def match(cls, string, i = 0):
-
-        # {2,3} or {2,}
-        try:
-            j = static(string, i, "{")
-            min, j = bound.match(string, j)
-            j = static(string, j, ",")
-            max, j = bound.match(string, j)
-            j = static(string, j, "}")
-            return multiplier(min, max), j
-        except nomatch:
-            pass
-
-        # {2}
-        try:
-            j = static(string, i, "{")
-            min, j = bound.match(string, j)
-            j = static(string, j, "}")
-            return multiplier(min, min), j
-        except nomatch:
-            pass
-
-        # "?"/"*"/"+"/""
-        # we do these in reverse order of symbol length, because
-        # that forces "" to be done last
-        for key in sorted(symbolic, key=lambda key: -len(symbolic[key])):
-            try:
-                return key, static(string, i, symbolic[key])
-            except nomatch:
-                pass
-
-        raise nomatch
-
-    @classmethod
-    def parse(cls, string):
-        '''
-            Parse the entire supplied string as an instance of the present class.
-            Mainly for internal use in unit tests because it drops through to match()
-            in a convenient way.
-        '''
-        obj, i = cls.match(string, 0)
-        if i != len(string):
-            raise Exception("Could not parse '" + string + "' beyond index " + str(i))
-        return obj
-
-    def canmultiplyby(self, other):
-        '''
-            Multiplication is not well-defined for all pairs of multipliers because
-            the resulting possibilities do not necessarily form a continuous range.
-            For example:
-                {0,x} * {0,y} = {0,x*y}
-                {2} * {3} = {6}
-                {2} * {1,2} = ERROR
-
-            The proof isn't simple but suffice it to say that {p,p+q} * {r,r+s} is
-            equal to {pr, (p+q)(r+s)} only if s=0 or qr+1 >= p. If not, then at least
-            one gap appears in the range. The first inaccessible number is (p+q)r + 1.
-        '''
-        return other.optional == bound(0) or \
-        self.optional * other.mandatory + bound(1) >= self.mandatory
-
-    def __mul__(self, other):
-        '''Multiply this multiplier by another'''
-        if not self.canmultiplyby(other):
-            raise Exception("Can't multiply " + repr(self) + " by " + repr(other))
-        return multiplier(self.min * other.min, self.max * other.max)
-
-    def __add__(self, other):
-        '''Add two multipliers together'''
-        return multiplier(self.min + other.min, self.max + other.max)
-
-    def __sub__(self, other):
-        '''
-            Subtract another multiplier from this one.
-            Caution: multipliers are not totally ordered.
-            This operation is not meaningful for all pairs of multipliers.
-        '''
-        mandatory = self.mandatory - other.mandatory
-        optional = self.optional - other.optional
-        return multiplier(mandatory, mandatory + optional)
-
-    def canintersect(self, other):
-        '''
-            Intersection is not well-defined for all pairs of multipliers.
-            For example:
-                {2,3} & {3,4} = {3}
-                {2,} & {1,7} = {2,7}
-                {2} & {5} = ERROR
-        '''
-        return not (self.max < other.min or other.max < self.min)
-
-    def __and__(self, other):
-        '''
-            Find the intersection of two multipliers: that is, a third multiplier
-            expressing the range covered by both of the originals. This is not
-            defined for all multipliers since they may not overlap.
-        '''
-        if not self.canintersect(other):
-            raise Exception("Can't compute intersection of " + repr(self) + " and " + repr(other))
-        a = max(self.min, other.min)
-        b = min(self.max, other.max)
-        return multiplier(a, b)
-
-    def canunion(self, other):
-        '''Union is not defined for all pairs of multipliers. e.g. {0,1} | {3,4}'''
-        return not (self.max + bound(1) < other.min or other.max + bound(1) < self.min)
-
-    def __or__(self, other):
-        '''
-            Find the union of two multipliers: that is, a third multiplier expressing
-            the range covered by either of the originals. This is not defined for
-            all multipliers since they may not intersect.
-        '''
-        if not self.canunion(other):
-            raise Exception("Can't compute the union of " + repr(self) + " and " + repr(other))
-        a = min(self.min, other.min)
-        b = max(self.max, other.max)
-        return multiplier(a, b)
-
-    def common(self, other):
-        '''
-            Find the shared part of two multipliers. This is the largest multiplier
-            which can be safely subtracted from both the originals. This may
-            return the "zero" multiplier.
-        '''
-        mandatory = min(self.mandatory, other.mandatory)
-        optional = min(self.optional, other.optional)
-        return multiplier(mandatory, mandatory + optional)
-
-    def copy(self):
-        return multiplier(self.min.copy(), self.max.copy())
-
-class mult(lego):
-    '''
-        A mult is a combination of a multiplicand with
-        a multiplier (a min and a max). The vast majority of characters in regular
-        expressions occur without a specific multiplier, which is implicitly
-        equivalent to a min of 1 and a max of 1, but many more have explicit
-        multipliers like "*" (min = 0, max = inf) and so on.
-
-        e.g. a, b{2}, c?, d*, [efg]{2,5}, f{2,}, (anysubpattern)+, .*, and so on
-    '''
-
-    def __init__(self, multiplicand, multiplier):
-        self.__dict__["multiplicand"] = multiplicand
-        self.__dict__["multiplier"]   = multiplier
-
-    def __eq__(self, other):
-        try:
-            return self.multiplicand == other.multiplicand \
-            and self.multiplier == other.multiplier
-        except AttributeError:
-            return False
-
-    def __hash__(self):
-        return hash((self.multiplicand, self.multiplier))
-
-    def __repr__(self):
-        string = "mult("
-        string += repr(self.multiplicand)
-        string += ", " + repr(self.multiplier)
-        string += ")"
-        return string
-
-    def times(self, multiplier):
-        if multiplier == one:
-            return self
-        if self.multiplier.canmultiplyby(multiplier):
-            return mult(self.multiplicand, self.multiplier * multiplier)
-        return mult(pattern(conc(self)), multiplier)
-
-    def concatenate(self, other):
-        return conc(self) + other
-
-    def union(self, other):
-        return conc(self) | other
-
-    def dock(self, other):
-        '''
-            "Dock" another mult from this one (i.e. remove part of the tail) and
-            return the result. The reverse of concatenation. This is a lot trickier.
-            e.g. a{4,5} - a{3} = a{1,2}
-        '''
-        if other.multiplicand != self.multiplicand:
-            raise Exception("Can't subtract " + repr(other) + " from " + repr(self))
-        return mult(self.multiplicand, self.multiplier - other.multiplier)
-
-    def common(self, other):
-        '''
-            Return the common part of these two mults. This is the largest mult
-            which can be safely subtracted from both the originals. The multiplier
-            on this mult could be zero: this is the case if, for example, the
-            multiplicands disagree.
-        '''
-        if self.multiplicand == other.multiplicand:
-            return mult(self.multiplicand, self.multiplier.common(other.multiplier))
-
-        # Multiplicands disagree, no common part at all.
-        return mult(nothing, zero)
-
-    def intersection(self, other):
-        if hasattr(other, "chars"):
-            other = mult(other, one)
-
-        # If two mults are given which have a common multiplicand, the shortcut
-        # is just to take the intersection of the two multiplicands.
-        try:
-            if self.multiplicand == other.multiplicand \
-            and self.canintersect(other):
-                return mult(self.multiplicand, self.multiplier & other.multiplier)
-        except AttributeError:
-            # "other" isn't a mult; lacks either a multiplicand or a multiplier.
-            # Never mind!
-            pass
-
-        # This situation is substantially more complicated if the multiplicand is,
-        # for example, a pattern. It's difficult to reason sensibly about this
-        # kind of thing.
-        return conc(self) & other
-
-    def alphabet(self):
-        return {fsm.anything_else} | self.multiplicand.alphabet()
-
-    def empty(self):
-        return self.multiplicand.empty() and self.multiplier.min > bound(0)
-
-    @reduce_after
-    def reduce(self):
-        # Can't match anything: reduce to nothing
-        if self.empty():
-            return nothing
-
-        # If our multiplicand is a pattern containing an empty conc()
-        # we can pull that "optional" bit out into our own multiplier
-        # instead.
-        # e.g. (A|B|C|)D -> (A|B|C)?D
-        # e.g. (A|B|C|){2} -> (A|B|C){0,2}
-        try:
-            if emptystring in self.multiplicand.concs \
-            and self.multiplier.canmultiplyby(qm):
-                return mult(
-                    pattern(
-                        *self.multiplicand.concs.difference({emptystring})
-                    ),
-                    self.multiplier * qm,
-                )
-        except AttributeError:
-            # self.multiplicand has no attribute "concs"; isn't a pattern; never mind
-            pass
-
-        # If we have an empty multiplicand, we can only match it
-        # zero times
-        if self.multiplicand.empty() \
-        and self.multiplier.min == bound(0):
-            return emptystring
-
-        # Failing that, we have a positive multiplicand which we
-        # intend to match zero times. In this case the only possible
-        # match is the empty string.
-        if self.multiplier == zero:
-            return emptystring
-
-        # no point multiplying in the singular
-        if self.multiplier == one:
-            return self.multiplicand
-
-        # Try recursively reducing our internal.
-        reduced = self.multiplicand.reduce()
-        # "bulk up" smaller lego pieces to pattern if need be
-        if hasattr(reduced, "multiplicand"):
-            reduced = conc(reduced)
-        if hasattr(reduced, "mults"):
-            reduced = pattern(reduced)
-        if reduced != self.multiplicand:
-            return mult(reduced, self.multiplier)
-
-        # If our multiplicand is a pattern containing a single conc
-        # containing a single mult, we can separate that out a lot
-        # e.g. ([ab])* -> [ab]*
-        try:
-            if len(self.multiplicand.concs) == 1:
-                (singleton,) = self.multiplicand.concs
-                if len(singleton.mults) == 1:
-                    singlemult = singleton.mults[0]
-                    if singlemult.multiplier.canmultiplyby(self.multiplier):
-                        return mult(
-                            singlemult.multiplicand,
-                            singlemult.multiplier * self.multiplier
-                        )
-        except AttributeError:
-            # self.multiplicand has no attribute "concs"; isn't a pattern; never mind
-            pass
-
-        return self
-
-    def __str__(self):
-        # recurse into subpattern
-        if hasattr(self.multiplicand, "concs"):
-            output = "(" + str(self.multiplicand) + ")"
-
-        else:
-            output = str(self.multiplicand)
-
-        suffix = str(self.multiplier)
-
-        return output + suffix
-
-    def to_fsm(self, alphabet=None):
-        if alphabet is None:
-            alphabet = self.alphabet()
-
-        # worked example: (min, max) = (5, 7) or (5, inf)
-        # (mandatory, optional) = (5, 2) or (5, inf)
-
-        unit = self.multiplicand.to_fsm(alphabet)
-        # accepts e.g. "ab"
-
-        # accepts "ababababab"
-        mandatory = unit * self.multiplier.mandatory.v
-
-        # unlimited additional copies
-        if self.multiplier.optional == inf:
-            optional = unit.star()
-            # accepts "(ab)*"
-
-        else:
-            optional = fsm.epsilon(alphabet) | unit
-            # accepts "(ab)?"
-
-            optional *= self.multiplier.optional.v
-            # accepts "(ab)?(ab)?"
-
-        return mandatory + optional
-
-    @classmethod
-    def match(cls, string, i = 0):
-
-        def matchMultiplicand(string, i):
-            # explicitly non-capturing "(?:...)" syntax. No special significance
-            try:
-                j = static(string, i, "(?")
-                j, st = select_static(string, j, ':', 'P<')
-                if st == 'P<':
-                    j, group_name = read_until(string, j, '>')
-                multiplicand, j = pattern.match(string, j)
-                j = static(string, j, ")")
-                return multiplicand, j
-            except nomatch:
-                pass
-
-            # normal "(...)" syntax
-            try:
-                j = static(string, i, "(")
-                multiplicand, j = pattern.match(string, j)
-                j = static(string, j, ")")
-                return multiplicand, j
-            except nomatch:
-                pass
-
-            # Just a charclass on its own
-            return charclass.match(string, i)
-
-        multiplicand, j = matchMultiplicand(string, i)
-        multiplier_, j = multiplier.match(string, j)
-        return mult(multiplicand, multiplier_), j
-
-    def reversed(self):
-        return mult(reversed(self.multiplicand), self.multiplier)
-
-    def copy(self):
-        return mult(self.multiplicand.copy(), self.multiplier.copy())
-
-class conc(lego):
-    '''
-        A conc (short for "concatenation") is a tuple of mults i.e. an unbroken
-        string of mults occurring one after the other.
-        e.g. abcde[^fg]*h{4}[a-z]+(subpattern)(subpattern2)
-        To express the empty string, use an empty conc, conc().
-    '''
-
-    def __init__(self, *mults):
-        self.__dict__["mults"] = tuple(mults)
-
-    def __eq__(self, other):
-        try:
-            return self.mults == other.mults
-        except AttributeError:
-            return False
-
-    def __hash__(self):
-        return hash(self.mults)
-
-    def __repr__(self):
-        string = "conc("
-        string += ", ".join(repr(m) for m in self.mults)
-        string += ")"
-        return string
-
-    def times(self, multiplier):
-        if multiplier == one:
-            return self
-        # Have to replace self with a pattern unfortunately
-        return pattern(self) * multiplier
-
-    def concatenate(self, other):
-        # other must be a conc too
-        if hasattr(other, "chars") or hasattr(other, "concs"):
-            other = mult(other, one)
-        if hasattr(other, "multiplicand"):
-            other = conc(other)
-
-        return conc(*(self.mults + other.mults))
-
-    def union(self, other):
-        return pattern(self) | other
-
-    def intersection(self, other):
-        return pattern(self) & other
-
-    @reduce_after
-    def reduce(self):
-        # Can't match anything
-        if self.empty():
-            return nothing
-
-        # no point concatenating one thing (note: concatenating 0 things is
-        # entirely valid)
-        if len(self.mults) == 1:
-            return self.mults[0]
-
-        # Try recursively reducing our internals
-        reduced = [m.reduce() for m in self.mults]
-        # "bulk up" smaller lego pieces to concs if need be
-        reduced = [
-            pattern(x) if hasattr(x, "mults") else x
-            for x in reduced
-        ]
-        reduced = [
-            mult(x, one) if hasattr(x, "chars") or hasattr(x, "concs") else x
-            for x in reduced
-        ]
-        reduced = tuple(reduced)
-        if reduced != self.mults:
-            return conc(*reduced)
-
-        # Conc contains "()" (i.e. a mult containing only a pattern containing the
-        # empty string)? That can be removed e.g. "a()b" -> "ab"
-        for i in range(len(self.mults)):
-            if self.mults[i].multiplicand == pattern(emptystring):
-                new = self.mults[:i] + self.mults[i + 1:]
-                return conc(*new)
-
-        # We might be able to combine some mults together or at least simplify the multiplier on
-        # one of them.
-        if len(self.mults) > 1:
-            for i in range(len(self.mults) - 1):
-                r = self.mults[i]
-                s = self.mults[i + 1]
-
-                # If R = S, then we can squish the multipliers together
-                # e.g. ab?b?c -> ab{0,2}c
-                if r.multiplicand == s.multiplicand:
-                    squished = mult(
-                        r.multiplicand,
-                        r.multiplier + s.multiplier
-                    )
-                    new = self.mults[:i] + (squished,) + self.mults[i + 2:]
-                    return conc(*new)
-
-                # If R's language is a subset of S's, then R{a,b}S{c,} reduces to R{a}S{c,}...
-                # e.g. \d+\w+ -> \d\w+
-                # Do the cheapest checks first
-                if r.multiplier.min < r.multiplier.max \
-                and s.multiplier.max == inf \
-                and (r.multiplicand & s.multiplicand).equivalent(r.multiplicand):
-                    trimmed = mult(
-                        r.multiplicand,
-                        multiplier(r.multiplier.min, r.multiplier.min)
-                    )
-                    new = self.mults[:i] + (trimmed, s) + self.mults[i + 2:]
-                    return conc(*new)
-
-                # Conversely, if R is superset of S, then R{c,}S{a,b} reduces to R{c,}S{a}.
-                # e.g. [ab]+a? -> [ab]+
-                # Do the cheapest checks first
-                if r.multiplier.max == inf \
-                and s.multiplier.min < s.multiplier.max \
-                and (r.multiplicand & s.multiplicand).equivalent(s.multiplicand):
-                    trimmed = mult(
-                        s.multiplicand,
-                        multiplier(s.multiplier.min, s.multiplier.min)
-                    )
-                    new = self.mults[:i] + (r, trimmed) + self.mults[i + 2:]
-                    return conc(*new)
-
-        # Conc contains (among other things) a *singleton* mult containing a pattern
-        # with only one internal conc? Flatten out.
-        # e.g. "a(d(ab|a*c))" -> "ad(ab|a*c)"
-        # BUT NOT "a(d(ab|a*c)){2,}"
-        # AND NOT "a(d(ab|a*c)|y)"
-        for i in range(len(self.mults)):
-            m = self.mults[i]
-            try:
-                if m.multiplier == one and len(m.multiplicand.concs) == 1:
-                    (single,) = m.multiplicand.concs
-                    new = self.mults[:i] + single.mults + self.mults[i+1:]
-                    return conc(*new)
-            except AttributeError:
-                # m.multiplicand has no attribute "concs"; isn't a pattern; never mind
-                pass
-
-        return self
-
-    def to_fsm(self, alphabet=None):
-        if alphabet is None:
-            alphabet = self.alphabet()
-
-        # start with a component accepting only the empty string
-        fsm1 = fsm.epsilon(alphabet)
-        for m in self.mults:
-            fsm1 += m.to_fsm(alphabet)
-        return fsm1
-
-    def alphabet(self):
-        return {fsm.anything_else}.union(*[m.alphabet() for m in self.mults])
-
-    def empty(self):
-        for m in self.mults:
-            if m.empty():
-                return True
-        return False
-
-    def __str__(self):
-        return "".join(str(m) for m in self.mults)
-
-    @classmethod
-    def match(cls, string, i = 0):
-        mults = list()
-        try:
-            while True:
-                m, i = mult.match(string, i)
-                mults.append(m)
-        except nomatch:
-            pass
-        return conc(*mults), i
-
-    def common(self, other, suffix=False):
-        '''
-            Return the common prefix of these two concs; that is, the largest conc
-            which can be safely beheaded() from the front of both.
-            The result could be emptystring.
-            "ZYAA, ZYBB" -> "ZY"
-            "CZ, CZ" -> "CZ"
-            "YC, ZC" -> ""
-
-            With the "suffix" flag set, works from the end. E.g.:
-            "AAZY, BBZY" -> "ZY"
-            "CZ, CZ" -> "CZ"
-            "CY, CZ" -> ""
-        '''
-        mults = []
-
-        indices = range(min(len(self.mults), len(other.mults))) # e.g. [0, 1, 2, 3]
-
-        # Work backwards from the end of both concs instead.
-        if suffix:
-            indices = [-i - 1 for i in indices] # e.g. [-1, -2, -3, -4]
-
-        for i in indices:
-
-            common = self.mults[i].common(other.mults[i])
-
-            # Happens when multiplicands disagree (e.g. "A.common(B)") or if
-            # the multiplicand is shared but the common multiplier is zero
-            # (e.g. "ABZ*.common(CZ)".)
-            if common.multiplier == zero:
-                break
-
-            mults.append(common)
-
-            # If we did not remove the entirety of both mults, we cannot continue.
-            if common != self.mults[i] or common != other.mults[i]:
-                break
-
-        if suffix:
-            mults = reversed(mults)
-
-        return conc(*mults)
-
-    def dock(self, other):
-        '''
-            Subtract another conc from this one.
-            This is the opposite of concatenation. For example, if ABC + DEF = ABCDEF,
-            then logically ABCDEF - DEF = ABC.
-        '''
-
-        # e.g. self has mults at indices [0, 1, 2, 3, 4, 5, 6] len=7
-        # e.g. other has mults at indices [0, 1, 2] len=3
-        new = list(self.mults)
-        for i in reversed(range(len(other.mults))): # [2, 1, 0]
-            # e.g. i = 1, j = 7 - 3 + 1 = 5
-            j = len(self.mults) - len(other.mults) + i
-            new[j] = new[j].dock(other.mults[i])
-
-            if new[j].multiplier == zero:
-                # omit that mult entirely since it has been factored out
-                del new[j]
-
-            # If the subtraction is incomplete but there is more to
-            # other.mults, then we have a problem. For example, "ABC{2} - BC"
-            # subtracts the C successfully but leaves something behind,
-            # then tries to subtract the B too, which isn't possible
-            else:
-                if i != 0:
-                    raise Exception("Can't subtract " + repr(other) + " from " + repr(self))
-
-        return conc(*new)
-
-    def behead(self, other):
-        '''
-            As with dock() but the other way around. For example, if
-            ABC + DEF = ABCDEF, then ABCDEF.behead(AB) = CDEF.
-        '''
-        # Observe that FEDCBA - BA = FEDC.
-        return reversed(reversed(self).dock(reversed(other)))
-
-    def reversed(self):
-        return conc(*reversed([reversed(m) for m in self.mults]))
-
-    def copy(self):
-        return conc(*[m.copy() for m in self.mults])
-
-class pattern(lego):
-    '''
-        A pattern (also known as an "alt", short for "alternation") is a
-        set of concs. A pattern expresses multiple alternate possibilities.
-        When written out as a regex, these would separated by pipes. A pattern
-        containing no possibilities is possible and represents a regular expression
-        matching no strings whatsoever (there is no conventional string form for
-        this).
-
-        e.g. "abc|def(ghi|jkl)" is an alt containing two concs: "abc" and
-        "def(ghi|jkl)". The latter is a conc containing four mults: "d", "e", "f"
-        and "(ghi|jkl)". The latter in turn is a mult consisting of an upper bound
-        1, a lower bound 1, and a multiplicand which is a new subpattern, "ghi|jkl".
-        This new subpattern again consists of two concs: "ghi" and "jkl".
-    '''
-    def __init__(self, *concs):
-        self.__dict__["concs"] = frozenset(concs)
-
-    def __eq__(self, other):
-        try:
-            return self.concs == other.concs
-        except AttributeError:
-            return False
-
-    def __hash__(self):
-        return hash(self.concs)
-
-    def __repr__(self):
-        string = "pattern("
-        string += ", ".join(repr(c) for c in self.concs)
-        string += ")"
-        return string
-
-    def times(self, multiplier):
-        if multiplier == one:
-            return self
-        return mult(self, multiplier)
-
-    def concatenate(self, other):
-        return mult(self, one) + other
-
-    def alphabet(self):
-        return {fsm.anything_else}.union(*[c.alphabet() for c in self.concs])
-
-    def empty(self):
-        for c in self.concs:
-            if not c.empty():
-                return False
-        return True
-
-    def intersection(self, other):
-        # A deceptively simple method for an astoundingly difficult operation
-        alphabet = self.alphabet() | other.alphabet()
-
-        # Which means that we can build finite state machines sharing that alphabet
-        combined = self.to_fsm(alphabet) & other.to_fsm(alphabet)
-        return from_fsm(combined)
-
-    def union(self, other):
-        # other must be a pattern too
-        if hasattr(other, "chars"):
-            other = mult(other, one)
-        if hasattr(other, "multiplicand"):
-            other = conc(other)
-        if hasattr(other, "mults"):
-            other = pattern(other)
-
-        return pattern(*(self.concs | other.concs))
-
-    def __str__(self):
-        if len(self.concs) == 0:
-            raise Exception("Can't serialise " + repr(self))
-
-        # take the alternation of the input collection of regular expressions.
-        # i.e. jam "|" between each element
-
-        # 1+ elements.
-        return "|".join(sorted(str(c) for c in self.concs))
-
-    @reduce_after
-    def reduce(self):
-        # emptiness
-        if self.empty():
-            return nothing
-
-        # If one of our internal concs is empty, remove it
-        for c in self.concs:
-            if c.empty():
-                new = self.concs - {c}
-                return pattern(*new)
-
-        # no point alternating among one possibility
-        if len(self.concs) == 1:
-            return list(self.concs)[0]
-
-        # Try recursively reducing our internals first.
-        reduced = [c.reduce() for c in self.concs]
-        # "bulk up" smaller lego pieces to concs if need be
-        reduced = [
-            mult(x, one) if hasattr(x, "chars") or hasattr(x, "concs") else x
-            for x in reduced
-        ]
-        reduced = [
-            conc(x) if hasattr(x, "multiplicand") else x
-            for x in reduced
-        ]
-        reduced = frozenset(reduced)
-        if reduced != self.concs:
-            return pattern(*reduced)
-
-        # If this pattern contains several concs each containing just 1 mult and
-        # their multiplicands agree, we may be able to merge the multipliers
-        # e.g. "a{1,2}|a{3,4}|bc" -> "a{1,4}|bc"
-        oldconcs = list(self.concs) # so we can index the things
-        for i in range(len(oldconcs)):
-            conc1 = oldconcs[i]
-            if len(conc1.mults) != 1:
-                continue
-            multiplicand1 = conc1.mults[0].multiplicand
-            for j in range(i + 1, len(oldconcs)):
-                conc2 = oldconcs[j]
-                if len(conc2.mults) != 1:
-                    continue
-                multiplicand2 = conc2.mults[0].multiplicand
-                if multiplicand2 != multiplicand1:
-                    continue
-                multiplicand = multiplicand1
-                multiplier1 = conc1.mults[0].multiplier
-                multiplier2 = conc2.mults[0].multiplier
-                if not multiplier1.canunion(multiplier2):
-                    continue
-                multiplier = multiplier1 | multiplier2
-                newconcs = \
-                    oldconcs[:i] + \
-                    oldconcs[i + 1:j] + \
-                    oldconcs[j + 1:] + \
-                    [conc(mult(multiplicand, multiplier))]
-                return pattern(*newconcs)
-
-        # If this pattern contains several concs each containing just 1 mult
-        # each containing just a charclass, with a multiplier of 1,
-        # then we can merge those branches together.
-        # e.g. "0|[1-9]|ab" -> "[0-9]|ab"
-        changed = False
-        merger = None
-        rest = []
-        for c in self.concs:
-            if len(c.mults) == 1 \
-            and c.mults[0].multiplier == one \
-            and hasattr(c.mults[0].multiplicand, "chars"):
-                if merger is None:
-                    merger = c.mults[0].multiplicand
-                else:
-                    merger |= c.mults[0].multiplicand
-                    changed = True
-            else:
-                rest.append(c)
-        if changed:
-            rest.append(conc(mult(merger, one)))
-            return pattern(*rest)
-
-        # If one of the present pattern's concs is the empty string, and
-        # there is another conc with a single mult whose lower bound is 0, we
-        # can omit the empty string.
-        # E.g. "|(ab)*|def" => "(ab)*|def".
-        # If there is another conc with a single mult whose lower bound is 1,
-        # we can merge the empty string into that.
-        # E.g. "|(ab)+|def" => "(ab)*|def".
-        if conc() in self.concs:
-            for c in self.concs:
-                if len(c.mults) != 1:
-                    continue
-                m = c.mults[0]
-                if m.multiplier.min == bound(0):
-                    rest = self.concs - {conc()}
-                    return pattern(*rest)
-                if m.multiplier.min == bound(1):
-                    rest = self.concs - {conc(), c} | {m * qm}
-                    return pattern(*rest)
-
-        # If the present pattern's concs all have a common prefix, split
-        # that out. This increases the depth of the object
-        # but it is still arguably simpler/ripe for further reduction
-        # e.g. "abc|ade" -> a(bc|de)"
-        prefix = self._commonconc()
-        if prefix != emptystring:
-            leftovers = self.behead(prefix)
-            mults = prefix.mults + (mult(leftovers, one),)
-            return conc(*mults)
-
-        # Same but for suffixes.
-        # e.g. "xyz|stz -> (xy|st)z"
-        suffix = self._commonconc(suffix=True)
-        if suffix != emptystring:
-            leftovers = self.dock(suffix)
-            mults = (mult(leftovers, one),) + suffix.mults
-            return conc(*mults)
-
-        return self
-
-    @classmethod
-    def match(cls, string: str, i = 0):
-        concs = list()
-
-        # first one
-        c, i = conc.match(string, i)
-        concs.append(c)
-
-        # the rest
-        while True:
-            try:
-                i = static(string, i, "|")
-                c, i = conc.match(string, i)
-                concs.append(c)
-            except nomatch:
-                return pattern(*concs), i
-
-    def dock(self, other):
-        '''
-            The opposite of concatenation. Remove a common suffix from the present
-            pattern; that is, from each of its constituent concs.
-            AYZ|BYZ|CYZ - YZ = A|B|C.
-        '''
-        return pattern(*[c.dock(other) for c in self.concs])
-
-    def behead(self, other):
-        '''
-            Like dock() but the other way around. Remove a common prefix from the
-            present pattern; that is, from each of its constituent concs.
-            ZA|ZB|ZC.behead(Z) = A|B|C
-        '''
-        return pattern(*[c.behead(other) for c in self.concs])
-
-    def _commonconc(self, suffix=False):
-        '''
-            Find the longest conc which acts as prefix to every conc in this pattern.
-            This could be the empty string. Return the common prefix along with all
-            the leftovers after truncating that common prefix from each conc.
-            "ZA|ZB|ZC" -> "Z", "(A|B|C)"
-            "ZA|ZB|ZC|Z" -> "Z", "(A|B|C|)"
-            "CZ|CZ" -> "CZ", "()"
-
-            If "suffix" is True, the same result but for suffixes.
-        '''
-        if len(self.concs) == 0:
-            raise Exception("Can't call _commonconc on " + repr(self))
-
-        from functools import reduce
-        return reduce(
-            lambda x, y: x.common(y, suffix=suffix),
-            self.concs
-        )
-
-    def to_fsm(self, alphabet=None):
-        if alphabet is None:
-            alphabet = self.alphabet()
-
-        fsm1 = fsm.null(alphabet)
-        for c in self.concs:
-            fsm1 |= c.to_fsm(alphabet)
-        return fsm1
-
-    def reversed(self):
-        return pattern(*(reversed(c) for c in self.concs))
-
-    def copy(self):
-        return pattern(*(c.copy() for c in self.concs))
-
-# Special and useful values go here.
-
-# Standard character classes
-w = charclass("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz")
-d = charclass("0123456789")
-s = charclass("\t\n\v\f\r ")
-W = ~w
-D = ~d
-S = ~s
-dot = ~charclass("")
-
-# This charclasses expresses "no possibilities at all"
-# and can never match anything.
-nothing = charclass("")
-
-# Textual representations of standard character classes
-shorthand = {
-    w : "\\w", d : "\\d", s : "\\s",
-    W : "\\W", D : "\\D", S : "\\S",
-    dot : ".",
-}
-
-# Characters which users may escape in a regex instead of inserting them
-# literally. In ASCII order:
-escapes = {
-    "\t" : "\\t", # tab
-    "\n" : "\\n", # line feed
-    "\v" : "\\v", # vertical tab
-    "\f" : "\\f", # form feed
-    "\r" : "\\r", # carriage return
-}
-
-# Use this for cases where no upper bound is needed
-inf = bound(None)
-
-# Preset multipliers. These get used ALL THE TIME in unit tests
-zero = multiplier(bound(0), bound(0)) # has some occasional uses
-qm   = multiplier(bound(0), bound(1))
-one  = multiplier(bound(1), bound(1))
-star = multiplier(bound(0), inf)
-plus = multiplier(bound(1), inf)
-
-# Symbol lookup table for preset multipliers.
-symbolic = {
-    qm   : "?",
-    one  : "" ,
-    star : "*",
-    plus : "+",
-}
-
-# A very special conc expressing the empty string, ""
-emptystring = conc()
diff --git a/greenery/multiplier.py b/greenery/multiplier.py
new file mode 100644
index 0000000..c3012f2
--- /dev/null
+++ b/greenery/multiplier.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+
+from dataclasses import dataclass, field
+
+from .bound import Bound, INF
+
+
+@dataclass(frozen=True)
+class Multiplier:
+    '''
+        A min and a max. The vast majority of characters in regular expressions
+        occur without a specific multiplier, which is implicitly equivalent to
+        a min of 1 and a max of 1, but many more have explicit multipliers like
+        "*" (min = 0, max = inf) and so on.
+
+        Although it seems odd and can lead to some confusing edge cases, we do
+        also permit a max of 0 (iff min is 0 too). This allows the multiplier
+        `ZERO` to exist, which actually are quite useful in their own special
+        way.
+    '''
+    min: Bound
+    max: Bound
+    mandatory: Bound = field(init=False)
+    optional: Bound = field(init=False)
+
+    def __post_init__(self):
+        if self.min == INF:
+            raise Exception(
+                f"Minimum bound of a multiplier can't be {repr(INF)}"
+            )
+        if self.min > self.max:
+            raise Exception(
+                "Invalid multiplier bounds: "
+                f"{repr(self.min)} and {repr(self.max)}"
+            )
+
+        # More useful than "min" and "max" in many situations
+        # are "mandatory" and "optional".
+        object.__setattr__(self, "mandatory", self.min)
+        object.__setattr__(self, "optional", self.max - self.min)
+
+    def __eq__(self, other):
+        return self.min == other.min and self.max == other.max
+
+    def __hash__(self):
+        return hash((self.min, self.max))
+
+    def __repr__(self):
+        return f"Multiplier({repr(self.min)}, {repr(self.max)})"
+
+    def __str__(self):
+        if self.max == Bound(0):
+            raise Exception(
+                f"Can't serialise a multiplier with bound {repr(self.max)}"
+            )
+        if self in symbolic.keys():
+            return symbolic[self]
+        if self.min == self.max:
+            return "{" + str(self.min) + "}"
+        return "{" + str(self.min) + "," + str(self.max) + "}"
+
+    def canmultiplyby(self, other):
+        '''
+            Multiplication is not well-defined for all pairs of multipliers
+            because the resulting possibilities do not necessarily form a
+            continuous range.
+
+            For example:
+                {0,x} * {0,y} = {0,x*y}
+                {2} * {3} = {6}
+                {2} * {1,2} = ERROR
+
+            The proof isn't simple but suffice it to say that {p,p+q} * {r,r+s}
+            is equal to {pr, (p+q)(r+s)} only if s=0 or qr+1 >= p. If not, then
+            at least one gap appears in the range. The first inaccessible
+            number is (p+q)r+1. And no, multiplication is not commutative
+        '''
+        return other.optional == Bound(0) or \
+            self.optional * other.mandatory + Bound(1) >= self.mandatory
+
+    def __mul__(self, other):
+        '''Multiply this multiplier by another'''
+        if not self.canmultiplyby(other):
+            raise Exception(
+                f"Can't multiply {repr(self)} by {repr(other)}"
+            )
+        return Multiplier(self.min * other.min, self.max * other.max)
+
+    def __add__(self, other):
+        '''Add two multipliers together'''
+        return Multiplier(self.min + other.min, self.max + other.max)
+
+    def __sub__(self, other):
+        '''
+            Subtract another multiplier from this one.
+            Caution: multipliers are not totally ordered.
+            This operation is not meaningful for all pairs of multipliers.
+        '''
+        mandatory = self.mandatory - other.mandatory
+        optional = self.optional - other.optional
+        return Multiplier(mandatory, mandatory + optional)
+
+    def canintersect(self, other):
+        '''
+            Intersection is not well-defined for all pairs of multipliers.
+            For example:
+                {2,3} & {3,4} = {3}
+                {2,} & {1,7} = {2,7}
+                {2} & {5} = ERROR
+        '''
+        return not (self.max < other.min or other.max < self.min)
+
+    def __and__(self, other):
+        '''
+            Find the intersection of two multipliers: that is, a third
+            multiplier expressing the range covered by both of the originals.
+            This is not defined for all multipliers since they may not overlap.
+        '''
+        if not self.canintersect(other):
+            raise Exception(
+                f"Can't compute intersection of {repr(self)} and {repr(other)}"
+            )
+        a = max(self.min, other.min)
+        b = min(self.max, other.max)
+        return Multiplier(a, b)
+
+    def canunion(self, other):
+        '''
+            Union is not defined for all pairs of multipliers.
+            E.g. {0,1} | {3,4} -> nope
+        '''
+        return not (
+            self.max + Bound(1) < other.min or
+            other.max + Bound(1) < self.min
+        )
+
+    def __or__(self, other):
+        '''
+            Find the union of two multipliers: that is, a third multiplier
+            expressing the range covered by either of the originals. This is
+            not defined for all multipliers since they may not intersect.
+        '''
+        if not self.canunion(other):
+            raise Exception(
+                f"Can't compute the union of {repr(self)} and {repr(other)}"
+            )
+        a = min(self.min, other.min)
+        b = max(self.max, other.max)
+        return Multiplier(a, b)
+
+    def common(self, other):
+        '''
+            Find the shared part of two multipliers. This is the largest
+            multiplier which can be safely subtracted from both the originals.
+            This may return the `ZERO` multiplier.
+        '''
+        mandatory = min(self.mandatory, other.mandatory)
+        optional = min(self.optional, other.optional)
+        return Multiplier(mandatory, mandatory + optional)
+
+    def copy(self):
+        return Multiplier(self.min.copy(), self.max.copy())
+
+
+# Preset multipliers. These get used ALL THE TIME in unit tests
+ZERO = Multiplier(Bound(0), Bound(0))  # has some occasional uses
+QM = Multiplier(Bound(0), Bound(1))
+ONE = Multiplier(Bound(1), Bound(1))
+STAR = Multiplier(Bound(0), INF)
+PLUS = Multiplier(Bound(1), INF)
+
+# Symbol lookup table for preset multipliers.
+symbolic = {
+    QM: "?",
+    ONE: "",
+    STAR: "*",
+    PLUS: "+",
+}
diff --git a/greenery/parse.py b/greenery/parse.py
new file mode 100644
index 0000000..6165bd4
--- /dev/null
+++ b/greenery/parse.py
@@ -0,0 +1,355 @@
+# -*- coding: utf-8 -*-
+
+from typing import Tuple
+
+from .bound import Bound, INF
+from .charclass import Charclass, shorthand, escapes
+from .multiplier import Multiplier, symbolic
+from .rxelems import Pattern, Conc, Mult
+
+
+class NoMatch(Exception):
+    '''
+        Thrown when parsing fails.
+        Almost always caught and almost never fatal
+    '''
+    pass
+
+
+def read_until(string: str, i: int, stop_char: str) -> Tuple[int, str]:
+    start = i
+    while True:
+        if i >= len(string):
+            raise NoMatch
+        if string[i] == stop_char:
+            break
+        i += 1
+    return i + 1, string[start:i]
+
+
+def static(string, i, static):
+    j = i + len(static)
+    if string[i:j] == static:
+        return j
+    raise NoMatch
+
+
+def select_static(string, i, *statics):
+    for st in statics:
+        j = i + len(st)
+        if string[i:j] == st:
+            return j, st
+    raise NoMatch
+
+
+def unescape_hex(string, i):
+    '''Turn e.g. "\\x40" into "@". Exactly two hex digits'''
+    hex_digits = "0123456789AaBbCcDdEeFf"
+
+    j = static(string, i, "\\x")
+
+    hex1 = string[j]  # e.g. "4"
+    if hex1 not in hex_digits:
+        raise NoMatch
+    j += len(hex1)
+
+    hex2 = string[j]  # e.g. "0"
+    if hex2 not in hex_digits:
+        raise NoMatch
+    j += len(hex2)
+
+    codepoint = int(hex1 + hex2, 16)  # e.g. 64
+    char = chr(codepoint)  # "@"
+    return char, j
+
+
+def match_internal_char(string, i):
+    # e.g. if we see "\\t", return "\t"
+    for key in escapes.keys():
+        try:
+            return key, static(string, i, escapes[key])
+        except NoMatch:
+            pass
+
+    # special chars e.g. "\\-" returns "-"
+    for char in Charclass.classSpecial:
+        try:
+            return char, static(string, i, "\\" + char)
+        except NoMatch:
+            pass
+
+    # hex escape e.g. "\\x40" returns "@"
+    try:
+        return unescape_hex(string, i)
+    except NoMatch:
+        pass
+
+    # single non-special character, not contained
+    # inside square brackets
+    char, j = string[i], i+1
+    if char in Charclass.classSpecial:
+        raise NoMatch
+
+    return char, j
+
+
+def match_class_interior_1(string, i):
+    # Attempt 1: shorthand e.g. "\w"
+    for frozenset, cc_shorthand in Charclass.shorthand.items():
+        try:
+            return frozenset, False, static(string, i, cc_shorthand)
+        except NoMatch:
+            pass
+
+    # Attempt 1B: shorthand e.g. "\W"
+    for frozenset, cc_shorthand in Charclass.negated_shorthand.items():
+        try:
+            return frozenset, True, static(string, i, cc_shorthand)
+        except NoMatch:
+            pass
+
+    # Attempt 2: a range e.g. "d-h"
+    try:
+        first, j = match_internal_char(string, i)  # `first` is "d"
+        k = static(string, j, "-")
+        last, k = match_internal_char(string, k)  # `last` is "h"
+
+        firstIndex = ord(first)  # 100
+        lastIndex = ord(last)  # 104
+
+        # Be strict here, "d-d" is not allowed
+        if firstIndex >= lastIndex:
+            raise NoMatch(f"Range '{first}' to '{last}' not allowed")
+
+        chars = set([
+            chr(i) for i in range(firstIndex, lastIndex + 1)
+        ])
+        return chars, False, k
+    except NoMatch:
+        pass
+
+    # Attempt 3: just a character on its own
+    (char, j) = match_internal_char(string, i)
+    return set(char), False, j
+
+
+def match_class_interior(string, i):
+    internals = set()
+    internals_negated = False
+    try:
+        while True:
+            internal, internal_negated, i = match_class_interior_1(string, i)
+            if internal_negated:
+                if internals_negated:
+                    # E.g. [a1\D\W]
+                    internals |= internal
+                else:
+                    internals_negated = True
+                    internals = internal - internals
+            else:
+                if internals_negated:
+                    internals = internals - internal
+                else:
+                    internals |= internal
+    except NoMatch:
+        pass
+    return internals, internals_negated, i
+
+
+def match_charclass(string: str, i):
+    if i >= len(string):
+        raise NoMatch
+
+    # wildcard ".", "\\w", "\\d", etc.
+    for key in shorthand.keys():
+        try:
+            return key, static(string, i, shorthand[key])
+        except NoMatch:
+            pass
+
+    # "[^dsgsdg]"
+    try:
+        j = static(string, i, "[^")
+        chars, negated, j = match_class_interior(string, j)
+        j = static(string, j, "]")
+        return Charclass(chars, not negated), j
+    except NoMatch:
+        pass
+
+    # "[sdfsf]"
+    try:
+        j = static(string, i, "[")
+        chars, negated, j = match_class_interior(string, j)
+        j = static(string, j, "]")
+        return Charclass(chars, negated), j
+    except NoMatch:
+        pass
+
+    # e.g. if seeing "\\t", return "\t"
+    for key in escapes.keys():
+        try:
+            return Charclass(key), static(string, i, escapes[key])
+        except NoMatch:
+            pass
+
+    # e.g. if seeing "\\{", return "{"
+    for char in Charclass.allSpecial:
+        try:
+            return Charclass(char), static(string, i, "\\" + char)
+        except NoMatch:
+            pass
+
+    # e.g. if seeing "\\x40", return "@"
+    try:
+        char, j = unescape_hex(string, i)
+        return Charclass(char), j
+    except NoMatch:
+        pass
+
+    # single non-special character, not contained inside square brackets
+    char, i = string[i], i+1
+    if char in Charclass.allSpecial:
+        raise NoMatch
+
+    return Charclass(char), i
+
+
+def match_multiplicand(string, i):
+    # explicitly non-capturing "(?:...)" syntax. No special significance
+    try:
+        j = static(string, i, "(?")
+        j, st = select_static(string, j, ':', 'P<')
+        if st == 'P<':
+            j, group_name = read_until(string, j, '>')
+        pattern, j = match_pattern(string, j)
+        j = static(string, j, ")")
+        return pattern, j
+    except NoMatch:
+        pass
+
+    # normal "(...)" syntax
+    try:
+        j = static(string, i, "(")
+        pattern, j = match_pattern(string, j)
+        j = static(string, j, ")")
+        return pattern, j
+    except NoMatch:
+        pass
+
+    # Just a `Charclass` on its own
+    charclass, j = match_charclass(string, i)
+    return charclass, j
+
+
+def match_any_of(string, i, collection):
+    for char in collection:
+        try:
+            return char, static(string, i, char)
+        except NoMatch:
+            pass
+    raise NoMatch
+
+
+def match_bound(string: str, i):
+    # "0"
+    try:
+        return Bound(0), static(string, i, "0")
+    except NoMatch:
+        pass
+
+    # "1", etc.
+    try:
+        digit, j = match_any_of(string, i, "123456789")
+        integer = int(digit)
+        try:
+            while True:
+                digit, j = match_any_of(string, j, "0123456789")
+                integer *= 10
+                integer += int(digit)
+        except NoMatch:
+            return Bound(integer), j
+    except NoMatch:
+        pass
+
+    # "" empty string = infinite bound as in "{4,}"
+    return INF, i
+
+
+def match_multiplier(string: str, i):
+    # {2,3} or {2,}
+    try:
+        j = static(string, i, "{")
+        min, j = match_bound(string, j)
+        j = static(string, j, ",")
+        max, j = match_bound(string, j)
+        j = static(string, j, "}")
+        return Multiplier(min, max), j
+    except NoMatch:
+        pass
+
+    # {2}
+    try:
+        j = static(string, i, "{")
+        min, j = match_bound(string, j)
+        j = static(string, j, "}")
+        return Multiplier(min, min), j
+    except NoMatch:
+        pass
+
+    # "?"/"*"/"+"/""
+    # we do these in reverse order of symbol length, because
+    # that forces "" to be done last
+    for key in sorted(symbolic, key=lambda key: -len(symbolic[key])):
+        try:
+            return key, static(string, i, symbolic[key])
+        except NoMatch:
+            pass
+
+    raise NoMatch
+
+
+def match_mult(string: str, i):
+    multiplicand, j = match_multiplicand(string, i)
+    multiplier, j = match_multiplier(string, j)
+    return Mult(multiplicand, multiplier), j
+
+
+def match_conc(string: str, i):
+    mults = list()
+    try:
+        while True:
+            m, i = match_mult(string, i)
+            mults.append(m)
+    except NoMatch:
+        pass
+    return Conc(*mults), i
+
+
+def match_pattern(string: str, i):
+    concs = list()
+
+    # first one
+    c, i = match_conc(string, i)
+    concs.append(c)
+
+    # the rest
+    while True:
+        try:
+            i = static(string, i, "|")
+            c, i = match_conc(string, i)
+            concs.append(c)
+        except NoMatch:
+            return Pattern(*concs), i
+
+
+def parse(string: str):
+    '''
+        Parse a full string and return a `Pattern` object. Fail if
+        the whole string wasn't parsed
+    '''
+    obj, i = match_pattern(string, 0)
+    if i != len(string):
+        raise Exception(
+            f"Could not parse '{string}' beyond index {str(i)}"
+        )
+    return obj
diff --git a/greenery/rxelems.py b/greenery/rxelems.py
new file mode 100644
index 0000000..a501792
--- /dev/null
+++ b/greenery/rxelems.py
@@ -0,0 +1,911 @@
+# -*- coding: utf-8 -*-
+
+'''
+    Because of the circularity between `Pattern`, `Conc` and `Mult`, all three
+    need to be in the same source file?
+'''
+
+from dataclasses import dataclass
+from typing import Union
+
+from .fsm import Fsm, ANYTHING_ELSE, null, epsilon, alphabet_key
+from .multiplier import Multiplier, ZERO, QM, ONE, STAR
+from .charclass import Charclass, NULLCHARCLASS
+from .bound import Bound, INF
+
+
+@dataclass(frozen=True)
+class Conc():
+    '''
+        A `Conc` (short for "concatenation") is a tuple of `Mult`s i.e. an
+        unbroken string of mults occurring one after the other.
+        e.g. abcde[^fg]*h{4}[a-z]+(subpattern)(subpattern2)
+        To express the empty string, use an empty `Conc`, Conc().
+    '''
+    def __init__(self, *mults):
+        object.__setattr__(self, "mults", tuple(mults))
+
+    def __eq__(self, other):
+        return self.mults == other.mults
+
+    def __hash__(self):
+        return hash(self.mults)
+
+    def __repr__(self):
+        args = ", ".join(repr(mult) for mult in self.mults)
+        return f"Conc({args})"
+
+    def reduce(self):
+        if self == NULLCONC:
+            return self
+
+        if self.empty():
+            return NULLCONC
+
+        # Try recursively reducing our mults
+        reduced = tuple(mult.reduce() for mult in self.mults)
+        if reduced != self.mults:
+            return Conc(*reduced).reduce()
+
+        # strip out mults which can only match the empty string
+        for i, mult in enumerate(self.mults):
+            if (
+                # Conc contains "()" (i.e. a `Mult` containing only a `Pattern`
+                # containing the empty string)? That can be removed
+                # e.g. "a()b" -> "ab"
+                (
+                    mult.multiplicand == Pattern(EMPTYSTRING)
+                ) \
+
+                # If a `Mult` has an empty multiplicand, we can only match it
+                # zero times => empty string => remove it entirely
+                # e.g. "a[]{0,3}b" -> "ab"
+                or (
+                    mult.multiplicand.empty()
+                    and mult.multiplier.min == Bound(0)
+                ) \
+
+                # Failing that, we have a positive multiplicand which we
+                # intend to match zero times. In this case the only possible
+                # match is the empty string => remove it
+                # e.g. "a[XYZ]{0}b" -> "ab"
+                or mult.multiplier == ZERO
+            ):
+                new = self.mults[:i] + self.mults[i + 1:]
+                return Conc(*new).reduce()
+
+        # We might be able to combine some mults together or at least simplify
+        # the multiplier on one of them.
+        if len(self.mults) > 1:
+            for i in range(len(self.mults) - 1):
+                r = self.mults[i]
+                s = self.mults[i + 1]
+
+                def to_pattern(multiplicand):
+                    if isinstance(multiplicand, Pattern):
+                        return multiplicand
+                    return Pattern(Conc(Mult(multiplicand, ONE)))
+
+                # so we can do intersection
+                rm_pattern = to_pattern(r.multiplicand)
+                sm_pattern = to_pattern(s.multiplicand)
+                rm_sm_intersection = None
+
+                # If R = S, then we can squish the multipliers together
+                # e.g. ab?b?c -> ab{0,2}c
+                if rm_pattern == sm_pattern:
+                    squished = Mult(
+                        rm_pattern,
+                        r.multiplier + s.multiplier
+                    )
+                    new = self.mults[:i] + (squished,) + self.mults[i + 2:]
+                    return Conc(*new).reduce()
+
+                # If R's language is a subset of S's, then R{a,b}S{c,} reduces
+                # to R{a}S{c,}...
+                # e.g. \d+\w+ -> \d\w+
+                # Do the cheapest checks first
+                if r.multiplier.min < r.multiplier.max \
+                   and s.multiplier.max == INF:
+                    rm_sm_intersection = rm_pattern & sm_pattern
+                    if rm_sm_intersection.equivalent(rm_pattern):
+                        trimmed = Mult(
+                            rm_pattern,
+                            Multiplier(r.multiplier.min, r.multiplier.min)
+                        )
+                        new = self.mults[:i] + \
+                            (trimmed, s) + \
+                            self.mults[i + 2:]
+                        return Conc(*new).reduce()
+
+                # Conversely, if R is superset of S, then R{c,}S{a,b} reduces
+                # to R{c,}S{a}.
+                # e.g. [ab]+a? -> [ab]+
+                # Do the cheapest checks first
+                if r.multiplier.max == INF \
+                   and s.multiplier.min < s.multiplier.max:
+                    if rm_sm_intersection is None:
+                        rm_sm_intersection = rm_pattern & sm_pattern
+                    if rm_sm_intersection.equivalent(sm_pattern):
+                        trimmed = Mult(
+                            sm_pattern,
+                            Multiplier(s.multiplier.min, s.multiplier.min)
+                        )
+                        new = self.mults[:i] + \
+                            (r, trimmed) + \
+                            self.mults[i + 2:]
+                        return Conc(*new).reduce()
+
+        # Conc contains (among other things) a *singleton* `Mult` containing
+        # `Pattern` with only one internal `Conc`? Flatten out.
+        # e.g. "a(d(ab|a*c))" -> "ad(ab|a*c)"
+        # BUT NOT "a(d(ab|a*c)){2,}"
+        # AND NOT "a(d(ab|a*c)|y)"
+        for i, mult in enumerate(self.mults):
+            if mult.multiplier == ONE \
+               and isinstance(mult.multiplicand, Pattern) \
+               and len(mult.multiplicand.concs) == 1:
+                (conc,) = mult.multiplicand.concs
+                new = self.mults[:i] + conc.mults + self.mults[i + 1:]
+                return Conc(*new).reduce()
+
+        return self
+
+    def to_fsm(self, alphabet=None):
+        if alphabet is None:
+            alphabet = self.alphabet()
+
+        # start with a component accepting only the empty string
+        fsm1 = epsilon(alphabet)
+        for mult in self.mults:
+            fsm1 += mult.to_fsm(alphabet)
+        return fsm1
+
+    def alphabet(self):
+        return {ANYTHING_ELSE}.union(*[mult.alphabet() for mult in self.mults])
+
+    def empty(self):
+        return any(mult.empty() for mult in self.mults)
+
+    def __str__(self):
+        return "".join(str(m) for m in self.mults)
+
+    def common(self, other, suffix=False):
+        '''
+            Return the common prefix of these two `Conc`s; that is, the largest
+            `Conc` which can be safely beheaded() from the front of both. The
+            result could be `EMPTYSTRING`.
+            "ZYAA, ZYBB" -> "ZY"
+            "CZ, CZ" -> "CZ"
+            "YC, ZC" -> ""
+
+            With the "suffix" flag set, works from the end. E.g.:
+            "AAZY, BBZY" -> "ZY"
+            "CZ, CZ" -> "CZ"
+            "CY, CZ" -> ""
+        '''
+        mults = []
+
+        indices = range(min(len(self.mults), len(other.mults)))
+        # e.g. [0, 1, 2, 3]
+
+        # Work backwards from the end of both `Conc`s instead.
+        if suffix:
+            indices = [-i - 1 for i in indices]  # e.g. [-1, -2, -3, -4]
+
+        for i in indices:
+            x = self.mults[i]
+            y = other.mults[i]
+            common = x.common(y)
+
+            # Happens when multiplicands disagree (e.g. "A.common(B)") or if
+            # the multiplicand is shared but the common multiplier is `ZERO`
+            # (e.g. "ABZ*.common(CZ)".)
+            if common.multiplier == ZERO:
+                break
+
+            mults.append(common)
+
+            # If we did not remove the entirety of both mults, we cannot
+            # continue.
+            if common != x or common != y:
+                break
+
+        if suffix:
+            mults = reversed(mults)
+
+        return Conc(*mults)
+
+    def dock(self, other):
+        '''
+            Subtract another `Conc` from this one.
+            This is the opposite of concatenation.
+            For example, if ABC + DEF = ABCDEF,
+            then logically ABCDEF - DEF = ABC.
+        '''
+
+        # e.g. self has mults at indices [0, 1, 2, 3, 4, 5, 6] len=7
+        # e.g. other has mults at indices [0, 1, 2] len=3
+        new = list(self.mults)
+        for i in reversed(range(len(other.mults))):  # [2, 1, 0]
+            # e.g. i = 1, j = 7 - 3 + 1 = 5
+            j = len(self.mults) - len(other.mults) + i
+            new[j] = new[j].dock(other.mults[i])
+
+            if new[j].multiplier == ZERO:
+                # omit that `Mult` entirely since it has been factored out
+                del new[j]
+
+            # If the subtraction is incomplete but there is more to
+            # other.mults, then we have a problem. For example, "ABC{2} - BC"
+            # subtracts the C successfully but leaves something behind,
+            # then tries to subtract the B too, which isn't possible
+            else:
+                if i != 0:
+                    raise Exception(
+                        f"Can't subtract {repr(other)} from {repr(self)}"
+                    )
+
+        return Conc(*new)
+
+    def behead(self, other):
+        '''
+            As with dock() but the other way around. For example, if
+            ABC + DEF = ABCDEF, then ABCDEF.behead(AB) = CDEF.
+        '''
+        # Observe that FEDCBA - BA = FEDC.
+        return self.reversed().dock(other.reversed()).reversed()
+
+    def reversed(self):
+        return Conc(*reversed([mult.reversed() for mult in self.mults]))
+
+
+def from_fsm(f: Fsm):
+    '''
+        Turn the supplied finite state machine into a `Pattern`. This is
+        accomplished using the Brzozowski algebraic method.
+    '''
+    # Make sure the supplied alphabet is kosher. It must contain only single-
+    # character strings or `ANYTHING_ELSE`.
+    for symbol in f.alphabet:
+        if symbol == ANYTHING_ELSE:
+            continue
+        if isinstance(symbol, str) and len(symbol) == 1:
+            continue
+        raise Exception(
+            f"Symbol {repr(symbol)} cannot be used in a regular expression"
+        )
+
+    # We need a new state not already used
+    outside = object()
+
+    # The set of strings that would be accepted by this FSM if you started
+    # at state i is represented by the regex R_i.
+    # If state i has a sole transition "a" to state j, then we know
+    # R_i = a R_j.
+    # If state i is final, then the empty string is also accepted by this
+    # regex.
+    # And so on...
+
+    # From this we can build a set of simultaneous equations in len(f.states)
+    # variables. This system is easily solved for all variables, but we only
+    # need one: R_a, where a is the starting state.
+
+    # The first thing we need to do is organise the states into order of depth,
+    # so that when we perform our back-substitutions, we can start with the
+    # last (deepest) state and therefore finish with R_a.
+    states = [f.initial]
+    i = 0
+    while i < len(states):
+        current = states[i]
+        if current in f.map:
+            for symbol in sorted(f.map[current], key=alphabet_key):
+                next = f.map[current][symbol]
+                if next not in states:
+                    states.append(next)
+        i += 1
+
+    # Our system of equations is represented like so:
+    brz = {}
+    for a in f.states:
+        brz[a] = {}
+        for b in f.states:
+            brz[a][b] = NULLPATTERN
+
+        if a in f.finals:
+            brz[a][outside] = Pattern(EMPTYSTRING)
+        else:
+            brz[a][outside] = NULLPATTERN
+
+    # Populate it with some initial data.
+    for a in f.map:
+        for symbol in f.map[a]:
+            b = f.map[a][symbol]
+            if symbol == ANYTHING_ELSE:
+                charclass = ~Charclass(f.alphabet - {ANYTHING_ELSE})
+            else:
+                charclass = Charclass({symbol})
+
+            brz[a][b] = Pattern(
+                *brz[a][b].concs,
+                Conc(Mult(charclass, ONE))
+            ).reduce()
+
+    # Now perform our back-substitution
+    for i in reversed(range(len(states))):
+        a = states[i]
+
+        # Before the equation for R_a can be substituted into the other
+        # equations, we need to resolve the self-transition (if any).
+        # e.g.    R_a = 0 R_a |   1 R_b |   2 R_c
+        # becomes R_a =         0*1 R_b | 0*2 R_c
+        loop = Mult(brz[a][a], STAR)  # i.e. "0*"
+        del brz[a][a]
+
+        for right in brz[a]:
+            brz[a][right] = Pattern(
+                Conc(
+                    loop,
+                    Mult(brz[a][right], ONE)
+                )
+            ).reduce()
+
+        # Note: even if we're down to our final equation, the above step still
+        # needs to be performed before anything is returned.
+
+        # Now we can substitute this equation into all of the previous ones.
+        for j in range(i):
+            b = states[j]
+
+            # e.g. substituting R_a =  0*1 R_b |      0*2 R_c
+            # into              R_b =    3 R_a |        4 R_c | 5 R_d
+            # yields            R_b = 30*1 R_b | (30*2|4) R_c | 5 R_d
+            univ = brz[b][a]  # i.e. "3"
+            del brz[b][a]
+
+            for right in brz[a]:
+                brz[b][right] = Pattern(
+                    *brz[b][right].concs,
+                    Conc(
+                        Mult(univ, ONE),
+                        Mult(brz[a][right], ONE)
+                    )
+                ).reduce()
+
+    return brz[f.initial][outside].reduce()
+
+
+def call_fsm(method):
+    '''
+        Take a method which acts on 0 or more regular expression objects...
+        return a new method which simply converts them all to FSMs, calls the
+        FSM method on them instead, then converts the result back to a regular
+        expression. We do this for several of the more annoying operations.
+    '''
+    fsm_method = getattr(Fsm, method.__name__)
+
+    def new_method(*elems):
+        alphabet = set().union(*[elem.alphabet() for elem in elems])
+        return from_fsm(fsm_method(*[elem.to_fsm(alphabet) for elem in elems]))
+    return new_method
+
+
+@dataclass(frozen=True)
+class Pattern:
+    '''
+        A `Pattern` (also known as an "alt", short for "alternation") is a
+        set of `Conc`s. A `Pattern` expresses multiple alternate possibilities.
+        When written out as a regex, these would separated by pipes. A
+        `Pattern` containing no possibilities is possible and represents a
+        regular expression matching no strings whatsoever (there is no
+        conventional string form for this).
+
+        e.g. "abc|def(ghi|jkl)" is an alt containing two `Conc`s: "abc" and
+        "def(ghi|jkl)". The latter is a `Conc` containing four `Mult`s: "d",
+        "e", "f" and "(ghi|jkl)". The latter in turn is a `Mult` consisting of
+        an upper bound 1, a lower bound 1, and a multiplicand which is a new
+        subpattern, "ghi|jkl". This new subpattern again consists of two
+        `Conc`s: "ghi" and "jkl".
+    '''
+    def __init__(self, *concs):
+        object.__setattr__(self, "concs", frozenset(concs))
+
+    def __eq__(self, other):
+        return isinstance(other, Pattern) and self.concs == other.concs
+
+    def __hash__(self):
+        return hash(self.concs)
+
+    def __repr__(self):
+        args = ", ".join(repr(conc) for conc in self.concs)
+        return f"Pattern({args})"
+
+    def alphabet(self):
+        return {ANYTHING_ELSE}.union(*[c.alphabet() for c in self.concs])
+
+    def empty(self):
+        return all(conc.empty() for conc in self.concs)
+
+    def intersection(self, other):
+        # A deceptively simple method for an astoundingly difficult operation
+        alphabet = self.alphabet() | other.alphabet()
+
+        # Which means that we can build finite state machines sharing that
+        # alphabet
+        combined = self.to_fsm(alphabet) & other.to_fsm(alphabet)
+        return from_fsm(combined)
+
+    def __and__(self, other):
+        return self.intersection(other)
+
+    @call_fsm
+    def difference(*elems):
+        '''
+            Return a regular expression which matches any string which `self`
+            matches but none of the strings which `other` matches.
+        '''
+        pass
+
+    def __sub__(self, other):
+        return self.difference(other)
+
+    def union(self, other):
+        return Pattern(*(self.concs | other.concs))
+
+    def __or__(self, other):
+        return self.union(other)
+
+    def __str__(self):
+        if len(self.concs) == 0:
+            raise Exception(f"Can't serialise {repr(self)}")
+        return "|".join(sorted(str(conc) for conc in self.concs))
+
+    def reduce(self):
+        if self == NULLPATTERN:
+            return self
+
+        if self.empty():
+            return NULLPATTERN
+
+        # Try recursively reducing our internal `Conc`s.
+        reduced = frozenset(c.reduce() for c in self.concs)
+        if reduced != self.concs:
+            return Pattern(*reduced).reduce()
+
+        # If one of our internal concs is empty, remove it
+        for conc in self.concs:
+            if conc.empty():
+                new = self.concs - {conc}
+                return Pattern(*new).reduce()
+
+        # If we have just one `Conc` with just one `Mult` with a multiplier of
+        # 1, and the multiplicand is a `Pattern`, pull that up
+        if len(self.concs) == 1:
+            (conc,) = self.concs
+            if len(conc.mults) == 1 \
+               and conc.mults[0].multiplier == ONE \
+               and isinstance(conc.mults[0].multiplicand, Pattern):
+                return conc.mults[0].multiplicand.reduce()
+
+        # If this `Pattern` contains several `Conc`s each containing just 1
+        # `Mult` and their multiplicands agree, we may be able to merge the
+        # multipliers.
+        # e.g. "a{1,2}|a{3,4}|bc" -> "a{1,4}|bc"
+        oldconcs = list(self.concs)  # so we can index the things
+        for i in range(len(oldconcs)):
+            conc1 = oldconcs[i]
+            if len(conc1.mults) != 1:
+                continue
+            multiplicand1 = conc1.mults[0].multiplicand
+            for j in range(i + 1, len(oldconcs)):
+                conc2 = oldconcs[j]
+                if len(conc2.mults) != 1:
+                    continue
+                multiplicand2 = conc2.mults[0].multiplicand
+                if multiplicand2 != multiplicand1:
+                    continue
+                multiplicand = multiplicand1
+                multiplier1 = conc1.mults[0].multiplier
+                multiplier2 = conc2.mults[0].multiplier
+                if not multiplier1.canunion(multiplier2):
+                    continue
+                multiplier = multiplier1 | multiplier2
+                newconcs = \
+                    oldconcs[:i] + \
+                    oldconcs[i + 1:j] + \
+                    oldconcs[j + 1:] + \
+                    [Conc(Mult(multiplicand, multiplier))]
+                return Pattern(*newconcs).reduce()
+
+        # If this `Pattern` contains several `Conc`s each containing just 1
+        # `Mult` each containing just a `Charclass`, with a multiplier of 1,
+        # then we can merge those `Charclass`es together.
+        # e.g. "0|[1-9]|ab" -> "[0-9]|ab"
+        merged_charclass = NULLCHARCLASS
+        num_merged = 0
+        rest = []
+        for conc in self.concs:
+            if len(conc.mults) == 1 \
+               and conc.mults[0].multiplier == ONE \
+               and isinstance(conc.mults[0].multiplicand, Charclass):
+                merged_charclass |= conc.mults[0].multiplicand
+                num_merged += 1
+            else:
+                rest.append(conc)
+        if num_merged >= 2:
+            rest.append(Conc(Mult(merged_charclass, ONE)))
+            return Pattern(*rest).reduce()
+
+        # If one of the present `Pattern`'s `Conc`s is the empty string...
+        if EMPTYSTRING in self.concs:
+            for conc in self.concs:
+                # ...and there is another `Conc`
+                # with a single `Mult` whose lower bound is 0...
+                if len(conc.mults) == 1 \
+                   and conc.mults[0].multiplier.min == Bound(0):
+                    # Then we can omit the empty string.
+                    # E.g. "|(ab)*|def" => "(ab)*|def".
+                    rest = self.concs - {EMPTYSTRING}
+                    return Pattern(*rest).reduce()
+
+            for conc in self.concs:
+                # ...and there is another `Conc`
+                # with a single `Mult` whose lower bound is 1...
+                if len(conc.mults) == 1 \
+                   and conc.mults[0].multiplier.min == Bound(1):
+                    # Then we can merge the empty string into that.
+                    # E.g. "|(ab)+|def" => "(ab)*|def".
+                    merged_conc = Conc(
+                        Mult(
+                            conc.mults[0].multiplicand,
+                            conc.mults[0].multiplier * QM
+                        )
+                    )
+                    rest = self.concs - {EMPTYSTRING, conc} | {merged_conc}
+                    return Pattern(*rest).reduce()
+
+        # If the present `Pattern`'s `Conc`s all have a common prefix, split
+        # that out. This increases the depth of the object
+        # but it is still arguably simpler/ripe for further reduction
+        # e.g. "abc|ade" -> a(bc|de)"
+        if len(self.concs) > 1:
+            prefix = self._commonconc()
+            if prefix != EMPTYSTRING:
+                leftovers = self.behead(prefix)
+                mults = prefix.mults + (Mult(leftovers, ONE),)
+                return Pattern(Conc(*mults)).reduce()
+
+            # Same but for suffixes.
+            # e.g. "xyz|stz -> (xy|st)z"
+            suffix = self._commonconc(suffix=True)
+            if suffix != EMPTYSTRING:
+                leftovers = self.dock(suffix)
+                mults = (Mult(leftovers, ONE),) + suffix.mults
+                return Pattern(Conc(*mults)).reduce()
+
+        return self
+
+    @call_fsm
+    def symmetric_difference(*elems):
+        '''
+            Return a regular expression matching only the strings recognised by
+            `self` or `other` but not both.
+        '''
+        pass
+
+    def __xor__(self, other):
+        return self.symmetric_difference(other)
+
+    def dock(self, other):
+        '''
+            The opposite of concatenation. Remove a common suffix from the
+            present `Pattern`; that is, from each of its constituent concs.
+
+            AYZ|BYZ|CYZ - YZ -> A|B|C.
+        '''
+        return Pattern(*[conc.dock(other) for conc in self.concs])
+
+    def behead(self, other):
+        '''
+            Like dock() but the other way around. Remove a common prefix from
+            the present `Pattern`; that is, from each of its constituent concs.
+
+            ZA|ZB|ZC.behead(Z) -> A|B|C
+        '''
+        return Pattern(*[conc.behead(other) for conc in self.concs])
+
+    def _commonconc(self, suffix=False):
+        '''
+            Find the longest `Conc` which acts as prefix to every `Conc` in
+            this `Pattern`. This could be `EMPTYSTRING`. Return the common
+            prefix along with all the leftovers after truncating that common
+            prefix from each `Conc`.
+
+            "ZA|ZB|ZC" -> "Z", "(A|B|C)"
+            "ZA|ZB|ZC|Z" -> "Z", "(A|B|C|)"
+            "CZ|CZ" -> "CZ", "()"
+
+            If "suffix" is True, the same result but for suffixes.
+        '''
+        if len(self.concs) == 0:
+            raise Exception(f"Can't call _commonconc on {repr(self)}")
+
+        from functools import reduce
+        return reduce(
+            lambda x, y: x.common(y, suffix=suffix),
+            self.concs
+        )
+
+    def to_fsm(self, alphabet=None):
+        if alphabet is None:
+            alphabet = self.alphabet()
+
+        fsm1 = null(alphabet)
+        for conc in self.concs:
+            fsm1 |= conc.to_fsm(alphabet)
+        return fsm1
+
+    def reversed(self):
+        return Pattern(*(c.reversed() for c in self.concs))
+
+    def copy(self):
+        '''
+            For completeness only, since `set.copy()` also exists. `Pattern`s
+            are immutable, so I can see only very odd reasons to need this
+        '''
+        return Pattern(*self.concs)
+
+    def equivalent(self, other):
+        '''
+            Two `Pattern`s are equivalent if they recognise the same strings.
+            Note that in the general case this is actually quite an intensive
+            calculation, but far from unsolvable, as we demonstrate here:
+        '''
+        return self.to_fsm().equivalent(other.to_fsm())
+
+    def times(self, multiplier):
+        '''
+            Equivalent to repeated concatenation. Multiplier consists of a
+            minimum and a maximum; maximum may be infinite (for Kleene star
+            closure). Call using "a = b * qm"
+        '''
+        return Pattern(Conc(Mult(self, multiplier)))
+
+    def __mul__(self, multiplier):
+        return self.times(multiplier)
+
+    @call_fsm
+    def everythingbut(self):
+        '''
+            Return a `Pattern` which will match any string not matched by
+            `self`, and which will not match any string matched by `self`.
+            Another task which is very difficult in general (and typically
+            returns utter garbage when actually printed), but becomes trivial
+            to code thanks to FSM routines.
+        '''
+        pass
+
+    def derive(self, string):
+        return from_fsm(self.to_fsm().derive(string))
+
+    @call_fsm
+    def isdisjoint(self, other):
+        '''
+            Treat `self` and `other` as sets of strings and see if they are
+            disjoint
+        '''
+        pass
+
+    def matches(self, string):
+        return self.to_fsm().accepts(string)
+
+    def __contains__(self, string):
+        '''
+            This lets you use the syntax `"a" in pattern` to see whether the
+            string "a" is in the set of strings matched by `pattern`.
+        '''
+        return self.matches(string)
+
+    def __reversed__(self):
+        return self.reversed()
+
+    def cardinality(self):
+        '''
+            Consider the regular expression as a set of strings and return the
+            cardinality of that set, or raise an OverflowError if there are
+            infinitely many.
+        '''
+        # There is no way to do this other than converting to an FSM, because
+        # the `Pattern` may allow duplicate routes, such as "a|a".
+        return self.to_fsm().cardinality()
+
+    def __len__(self):
+        return self.cardinality()
+
+    def strings(self, otherchar=None):
+        '''
+            Each time next() is called on this iterator, a new string is
+            returned which this `Pattern` can match. `StopIteration`
+            is raised once all such strings have been returned, although a
+            regex with a * in may match infinitely many strings.
+        '''
+
+        # In the case of a regex like "[^abc]", there are infinitely many
+        # (well, a very large finite number of) single characters which will
+        # match. It's not productive to iterate over all of these giving every
+        # single example. You must supply your own "otherchar" to stand in for
+        # all of these possibilities.
+        for string in self.to_fsm().strings():
+
+            # Have to represent `ANYTHING_ELSE` somehow.
+            if ANYTHING_ELSE in string:
+                if otherchar is None:
+                    raise Exception("Please choose an `otherchar`")
+                string = [
+                    otherchar if char == ANYTHING_ELSE else char
+                    for char in string
+                ]
+
+            yield "".join(string)
+
+    def __iter__(self):
+        '''
+            This allows you to do `for string in pattern` as a list
+            comprehension!
+        '''
+        return self.strings()
+
+
+@dataclass(frozen=True)
+class Mult:
+    '''
+        A `Mult` is a combination of a multiplicand with a multiplier (a min
+        and a max). The vast majority of characters in regular expressions
+        occur without a specific multiplier, which is implicitly equivalent to
+        a min of 1 and a max of 1, but many more have explicit multipliers like
+        "*" (min = 0, max = INF) and so on.
+
+        e.g. a, b{2}, c?, d*, [efg]{2,5}, f{2,}, (anysubpattern)+, .*, ...
+    '''
+    multiplicand: Union[Charclass, Pattern]
+    multiplier: Multiplier
+
+    def __eq__(self, other):
+        return isinstance(other.multiplicand, type(self.multiplicand)) \
+               and self.multiplicand == other.multiplicand \
+               and self.multiplier == other.multiplier
+
+    def __hash__(self):
+        return hash((self.multiplicand, self.multiplier))
+
+    def __repr__(self):
+        return f"Mult({repr(self.multiplicand)}, {repr(self.multiplier)})"
+
+    def dock(self, other):
+        '''
+            "Dock" another `Mult` from this one (i.e. remove part of the tail)
+            and return the result. The reverse of concatenation. This is a lot
+            trickier.
+            e.g. a{4,5} - a{3} = a{1,2}
+        '''
+        if other.multiplicand != self.multiplicand:
+            raise Exception(
+                f"Can't subtract {repr(other)} from {repr(self)}"
+            )
+        return Mult(self.multiplicand, self.multiplier - other.multiplier)
+
+    def common(self, other):
+        '''
+            Return the common part of these two mults. This is the largest
+            `Mult` which can be safely subtracted from both the originals. The
+            multiplier on this `Mult` could be `ZERO`: this is the case if, for
+            example, the multiplicands disagree.
+        '''
+        if self.multiplicand == other.multiplicand:
+            return Mult(
+                self.multiplicand,
+                self.multiplier.common(other.multiplier)
+            )
+
+        # Multiplicands disagree, no common part at all.
+        return Mult(NULLCHARCLASS, ZERO)
+
+    def alphabet(self):
+        return {ANYTHING_ELSE} | self.multiplicand.alphabet()
+
+    def empty(self):
+        return self.multiplicand.empty() and self.multiplier.min > Bound(0)
+
+    def reduce(self):
+        if self == NULLMULT:
+            return self
+
+        # Can't match anything: reduce to empty `Mult`
+        if self.empty():
+            return NULLMULT
+
+        # Try recursively reducing our multiplicand
+        reduced = self.multiplicand.reduce()
+        if reduced != self.multiplicand:
+            return Mult(reduced, self.multiplier).reduce()
+
+        # If our multiplicand is a `Pattern` containing an empty `Conc`
+        # we can pull that "optional" bit out into our own multiplier
+        # instead.
+        # e.g. (A|B|C|) -> (A|B|C)?
+        # e.g. (A|B|C|){2} -> (A|B|C){0,2}
+        if isinstance(self.multiplicand, Pattern) \
+           and EMPTYSTRING in self.multiplicand.concs \
+           and self.multiplier.canmultiplyby(QM):
+            return Mult(
+                Pattern(
+                    *filter(
+                        lambda conc: len(conc.mults) != 0,
+                        self.multiplicand.concs
+                    )
+                ),
+                self.multiplier * QM,
+            ).reduce()
+
+        # If our multiplicand is a `Pattern` containing a single `Conc`
+        # containing a single `Mult`, we can scrap the `Pattern` in favour of
+        # that `Mult`'s multiplicand
+        # e.g. ([ab])* -> [ab]*
+        # e.g. ((a))* -> (a)* -> a*
+        # NOTE: this logic lives here at the `Mult` level, NOT in
+        # `Pattern.reduce` because we want to return another `Mult` (same type)
+        if isinstance(self.multiplicand, Pattern) \
+           and len(self.multiplicand.concs) == 1:
+            (conc,) = self.multiplicand.concs
+            if len(conc.mults) == 1 \
+               and conc.mults[0].multiplier.canmultiplyby(self.multiplier):
+                return Mult(
+                    conc.mults[0].multiplicand,
+                    conc.mults[0].multiplier * self.multiplier
+                ).reduce()
+
+        # no reduction possible
+        return self
+
+    def __str__(self):
+        if isinstance(self.multiplicand, Pattern):
+            return f"({str(self.multiplicand)}){str(self.multiplier)}"
+        if isinstance(self.multiplicand, Charclass):
+            return f"{str(self.multiplicand)}{str(self.multiplier)}"
+        raise Exception(f"Unknown type {str(type(self.inner))}")
+
+    def to_fsm(self, alphabet=None):
+        if alphabet is None:
+            alphabet = self.alphabet()
+
+        # worked example: (min, max) = (5, 7) or (5, INF)
+        # (mandatory, optional) = (5, 2) or (5, INF)
+
+        unit = self.multiplicand.to_fsm(alphabet)
+        # accepts e.g. "ab"
+
+        # accepts "ababababab"
+        mandatory = unit * self.multiplier.mandatory.v
+
+        # unlimited additional copies
+        if self.multiplier.optional == INF:
+            optional = unit.star()
+            # accepts "(ab)*"
+
+        else:
+            optional = epsilon(alphabet) | unit
+            # accepts "(ab)?"
+
+            optional *= self.multiplier.optional.v
+            # accepts "(ab)?(ab)?"
+
+        return mandatory + optional
+
+    def reversed(self):
+        return Mult(self.multiplicand.reversed(), self.multiplier)
+
+
+NULLMULT = Mult(NULLCHARCLASS, ONE)
+NULLCONC = Conc(NULLMULT)
+EMPTYSTRING = Conc()
+NULLPATTERN = Pattern(NULLCONC)
diff --git a/main.py b/main.py
index 43eff8c..d0a7c77 100644
--- a/main.py
+++ b/main.py
@@ -1,17 +1,10 @@
-import sys
-from greenery.lego import lego, parse
+from greenery import Bound, INF, Multiplier, PLUS, QM, STAR, parse
 
-regexes = sys.argv[1:]
+pattern = parse("a")
+print(pattern)  # "a"
 
-if len(regexes) < 2:
-    print(
-        "Please supply several regexes to compute their intersection, " \
-        "union and concatenation."
-    )
-    print("E.g. \"19.*\" \"\\d{4}-\\d{2}-\\d{2}\"")
+pattern = pattern * PLUS * QM * STAR * Multiplier(Bound(3), INF)
+print(pattern)  # "((((a)+)?)*){3,}"
 
-else:
-    regexes = [parse(regex) for regex in regexes]
-    print("Intersection:  %s" % (lego.intersection(*regexes).reduce()))
-    print("Union:         %s" % (lego.union(*regexes).reduce()))
-    print("Concatenation: %s" % (lego.concatenate(*regexes).reduce()))
+pattern = pattern.reduce()
+print(pattern)  # "a*"
diff --git a/module map.txt b/module map.txt
deleted file mode 100644
index 391233c..0000000
--- a/module map.txt
+++ /dev/null
@@ -1,37 +0,0 @@
-greenery
-greenery.fsm
-greenery.fsm.anything_else
-greenery.fsm.OblivionError
-greenery.fsm.fsm
-greenery.fsm.null
-greenery.fsm.epsilon
-greenery.fsm.parallel
-greenery.fsm.crawl
-greenery.lego
-greenery.lego.parse
-greenery.lego.from_fsm
-greenery.lego.lego
-greenery.lego.charclass
-greenery.lego.bound
-greenery.lego.bound.inf
-greenery.lego.multiplier
-greenery.lego.multiplier.zero
-greenery.lego.multiplier.qm
-greenery.lego.multiplier.one
-greenery.lego.multiplier.star
-greenery.lego.multiplier.plus
-greenery.lego.mult
-greenery.lego.conc
-greenery.lego.pattern
-greenery.lego.w
-greenery.lego.d
-greenery.lego.s
-greenery.lego.W
-greenery.lego.D
-greenery.lego.S
-greenery.lego.dot
-greenery.lego.nothing
-greenery.lego.shorthand
-greenery.lego.escapes
-greenery.lego.symbolic
-greenery.lego.emptystring
diff --git a/setup.py b/setup.py
index b30da19..07f4a8d 100644
--- a/setup.py
+++ b/setup.py
@@ -1,19 +1,25 @@
 from setuptools import setup
 
 setup(
-    name = "greenery",
-    version = "3.3.7",
-    tests_require = [ "pytest" ],
-    packages = [ "greenery" ],
-    package_dir = { "greenery": "greenery" },
-    author = "qntm",
-    author_email = "qntm <qntm@users.noreply.github.com>",
-    description = "Greenery allows manipulation of regular expressions as finite state machines",
-    license = "MIT License",
-    keywords = "re regex regexp regular expression deterministic finite state machine automaton fsm dfsm fsa dfsa greenery",
-    url = "https://github.com/qntm/greenery",
-    classifiers = [
+    name="greenery",
+    version="4.0.0",
+    tests_require=["pytest"],
+    packages=["greenery"],
+    package_dir={
+        "greenery": "greenery"
+    },
+    author="qntm",
+    author_email="qntm <qntm@users.noreply.github.com>",
+    description="Greenery allows manipulation of regular expressions",
+    license="MIT License",
+    keywords=" ".join([
+        "re", "regex", "regexp", "regular", "expression", "deterministic",
+        "finite", "state", "machine", "automaton", "fsm", "dfsm", "fsa",
+        "dfsa", "greenery",
+    ]),
+    url="https://github.com/qntm/greenery",
+    classifiers=[
         "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.8",
     ],
 )


Test patch:
diff --git a/greenery/bound_test.py b/greenery/bound_test.py
new file mode 100644
index 0000000..25e82e8
--- /dev/null
+++ b/greenery/bound_test.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+from .bound import Bound, INF
+
+
+def test_bound_str():
+    assert str(Bound(2)) == "2"
+    assert str(INF) == ""
+
+
+def test_bound():
+    assert min(Bound(0), INF) == Bound(0)
+    assert min(Bound(1), INF) == Bound(1)
diff --git a/greenery/charclass_test.py b/greenery/charclass_test.py
new file mode 100644
index 0000000..11fa28a
--- /dev/null
+++ b/greenery/charclass_test.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+
+from .charclass import Charclass, WORDCHAR, DIGIT, SPACECHAR, NONWORDCHAR, \
+    NONDIGITCHAR, NONSPACECHAR, DOT, NULLCHARCLASS
+from .fsm import ANYTHING_ELSE
+
+
+def test_charclass_equality():
+    assert Charclass("a") == Charclass("a")
+    assert ~Charclass("a") == ~Charclass("a")
+    assert ~Charclass("a") != Charclass("a")
+    assert Charclass("ab") == Charclass("ba")
+
+
+def test_repr():
+    assert repr(~Charclass("a")) == "~Charclass('a')"
+
+
+def test_charclass_str():
+    assert str(WORDCHAR) == "\\w"
+    assert str(DIGIT) == "\\d"
+    assert str(SPACECHAR) == "\\s"
+    assert str(Charclass("a")) == "a"
+    assert str(Charclass("{")) == "\\{"
+    assert str(Charclass("\t")) == "\\t"
+    assert str(Charclass("ab")) == "[ab]"
+    assert str(Charclass("a{")) == "[a{]"
+    assert str(Charclass("a\t")) == "[\\ta]"
+    assert str(Charclass("a-")) == "[\\-a]"
+    assert str(Charclass("a[")) == "[\\[a]"
+    assert str(Charclass("a]")) == "[\\]a]"
+    assert str(Charclass("ab")) == "[ab]"
+    assert str(Charclass("abc")) == "[abc]"
+    assert str(Charclass("abcd")) == "[a-d]"
+    assert str(Charclass("abcdfghi")) == "[a-df-i]"
+    assert str(Charclass("^")) == "^"
+    assert str(Charclass("\\")) == "\\\\"
+    assert str(Charclass("a^")) == "[\\^a]"
+    assert str(Charclass("0123456789a")) == "[0-9a]"
+    assert str(Charclass("\t\v\r A")) == "[\\t\\v\\r A]"
+    assert str(Charclass("\n\f A")) == "[\\n\\f A]"
+    assert str(Charclass("\t\n\v\f\r A")) == "[\\t-\\r A]"
+    assert str(Charclass(
+        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz|"
+    )) == "[0-9A-Z_a-z|]"
+    assert str(NONWORDCHAR) == "\\W"
+    assert str(NONDIGITCHAR) == "\\D"
+    assert str(NONSPACECHAR) == "\\S"
+    assert str(DOT) == "."
+    assert str(~Charclass("")) == "."
+    assert str(~Charclass("a")) == "[^a]"
+    assert str(~Charclass("{")) == "[^{]"
+    assert str(~Charclass("\t")) == "[^\\t]"
+    assert str(~Charclass("^")) == "[^\\^]"
+
+
+def test_charclass_fsm():
+    # "[^a]"
+    nota = (~Charclass("a")).to_fsm()
+    assert nota.alphabet == {"a", ANYTHING_ELSE}
+    assert nota.accepts("b")
+    assert nota.accepts(["b"])
+    assert nota.accepts([ANYTHING_ELSE])
+
+
+def test_charclass_negation():
+    assert ~~Charclass("a") == Charclass("a")
+    assert Charclass("a") == ~~Charclass("a")
+
+
+def test_empty():
+    assert NULLCHARCLASS.empty()
+    assert not DOT.empty()
diff --git a/greenery/conc_test.py b/greenery/conc_test.py
new file mode 100644
index 0000000..22a22ad
--- /dev/null
+++ b/greenery/conc_test.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+
+from .bound import Bound
+from .multiplier import Multiplier, ZERO, QM, ONE, STAR, PLUS
+from .charclass import Charclass
+from .rxelems import Conc, Mult, EMPTYSTRING
+
+
+def test_conc_equality():
+    a = Conc(Mult(Charclass("a"), ONE))
+    assert a == Conc(Mult(Charclass("a"), ONE))
+    assert a != Conc(Mult(Charclass("b"), ONE))
+    assert a != Conc(Mult(Charclass("a"), QM))
+    assert a != Conc(Mult(
+        Charclass("a"),
+        Multiplier(Bound(1), Bound(2)))
+    )
+    assert a != EMPTYSTRING
+
+
+def test_conc_str():
+    assert str(Conc(
+        Mult(Charclass("a"), ONE),
+        Mult(Charclass("b"), ONE),
+        Mult(Charclass("c"), ONE),
+        Mult(Charclass("d"), ONE),
+        Mult(Charclass("e"), ONE),
+        Mult(~Charclass("fg"), STAR),
+        Mult(Charclass("h"), Multiplier(Bound(5), Bound(5))),
+        Mult(Charclass("abcdefghijklmnopqrstuvwxyz"), PLUS),
+    )) == "abcde[^fg]*h{5}[a-z]+"
+
+
+def test_conc_common():
+    a = Mult(Charclass("A"), ONE)
+    b = Mult(Charclass("B"), ONE)
+    c = Mult(Charclass("C"), ONE)
+    y = Mult(Charclass("y"), ONE)
+    z = Mult(Charclass("Z"), ONE)
+    zstar = Mult(Charclass("Z"), STAR)
+
+    assert Conc(a, a, z, y).common(Conc(b, b, z, y), suffix=True) == Conc(z, y)
+    assert Conc(c, z).common(Conc(c, z), suffix=True) == Conc(c, z)
+    assert Conc(c, y).common(Conc(c, z), suffix=True) == Conc()
+    assert Conc(a, z).common(Conc(b, z), suffix=True) == Conc(z)
+    assert Conc(a, zstar).common(Conc(b, z), suffix=True) == Conc()
+    assert Conc(a).common(Conc(b), suffix=True) == Conc()
+
+
+def test_conc_dock():
+    a = Mult(Charclass("A"), ONE)
+    b = Mult(Charclass("B"), ONE)
+    x = Mult(Charclass("X"), ONE)
+    x2 = Mult(Charclass("X"), Multiplier(Bound(2), Bound(2)))
+    yplus = Mult(Charclass("y"), PLUS)
+    z = Mult(Charclass("Z"), ONE)
+
+    assert Conc(a, z).dock(Conc(z)) == Conc(a)
+    assert Conc(a, b, x, yplus, z).dock(Conc(x, yplus, z)) == Conc(a, b)
+    assert Conc(a, b, x, yplus, z).behead(Conc(a, b, x, yplus)) == Conc(z)
+    assert Conc(a).dock(Conc()) == Conc(a)
+
+    try:
+        Conc(x2, yplus, z).behead(Conc(x, yplus))
+        assert False
+    except AssertionError:
+        assert False
+    except Exception:
+        pass
+
+
+def test_mult_reduction_easy():
+    assert Conc(Mult(
+        Charclass("a"),
+        ZERO
+    )).reduce() == Conc()
diff --git a/greenery/fsm_test.py b/greenery/fsm_test.py
index 37bf5ba..9a37a64 100644
--- a/greenery/fsm_test.py
+++ b/greenery/fsm_test.py
@@ -1,35 +1,33 @@
 # -*- coding: utf-8 -*-
 
-if __name__ == "__main__":
-    raise Exception("Test files can't be run directly. Use `python -m pytest greenery`")
-
-import pickle
 import pytest
-from greenery.fsm import fsm, null, epsilon, anything_else
+
+from .fsm import Fsm, null, epsilon, ANYTHING_ELSE
+
 
 def test_addbug():
-    # Odd bug with fsm.__add__(), exposed by "[bc]*c"
-    int5A = fsm(
-        alphabet = {"a", "b", "c", anything_else},
-        states   = {0, 1},
-        initial  = 1,
-        finals   = {1},
-        map      = {
-            0: {anything_else: 0, "a": 0, "b": 0, "c": 0},
-            1: {anything_else: 0, "a": 0, "b": 1, "c": 1},
+    # Odd bug with Fsm.__add__(), exposed by "[bc]*c"
+    int5A = Fsm(
+        alphabet={"a", "b", "c", ANYTHING_ELSE},
+        states={0, 1},
+        initial=1,
+        finals={1},
+        map={
+            0: {ANYTHING_ELSE: 0, "a": 0, "b": 0, "c": 0},
+            1: {ANYTHING_ELSE: 0, "a": 0, "b": 1, "c": 1},
         }
     )
     assert int5A.accepts("")
 
-    int5B = fsm(
-        alphabet = {"a", "b", "c", anything_else},
-        states   = {0, 1, 2},
-        initial  = 1,
-        finals   = {0},
-        map      = {
-            0: {anything_else: 2, "a": 2, "b": 2, "c": 2},
-            1: {anything_else: 2, "a": 2, "b": 2, "c": 0},
-            2: {anything_else: 2, "a": 2, "b": 2, "c": 2},
+    int5B = Fsm(
+        alphabet={"a", "b", "c", ANYTHING_ELSE},
+        states={0, 1, 2},
+        initial=1,
+        finals={0},
+        map={
+            0: {ANYTHING_ELSE: 2, "a": 2, "b": 2, "c": 2},
+            1: {ANYTHING_ELSE: 2, "a": 2, "b": 2, "c": 0},
+            2: {ANYTHING_ELSE: 2, "a": 2, "b": 2, "c": 2},
         }
     )
     assert int5B.accepts("c")
@@ -38,51 +36,57 @@ def test_addbug():
     assert int5C.accepts("c")
     # assert int5C.initial == 0
 
+
 def test_builtins():
     assert not null("a").accepts("a")
     assert epsilon("a").accepts("")
     assert not epsilon("a").accepts("a")
 
+
 @pytest.fixture
 def a():
-    a = fsm(
-        alphabet = {"a", "b"},
-        states   = {0, 1, "ob"},
-        initial  = 0,
-        finals   = {1},
-        map      = {
-            0    : {"a" : 1   , "b" : "ob"},
-            1    : {"a" : "ob", "b" : "ob"},
-            "ob" : {"a" : "ob", "b" : "ob"},
+    a = Fsm(
+        alphabet={"a", "b"},
+        states={0, 1, "ob"},
+        initial=0,
+        finals={1},
+        map={
+            0: {"a": 1, "b": "ob"},
+            1: {"a": "ob", "b": "ob"},
+            "ob": {"a": "ob", "b": "ob"},
         },
     )
     return a
 
+
 def test_a(a):
     assert not a.accepts("")
     assert a.accepts("a")
     assert not a.accepts("b")
 
+
 @pytest.fixture
 def b():
-    b = fsm(
-        alphabet = {"a", "b"},
-        states   = {0, 1, "ob"},
-        initial  = 0,
-        finals   = {1},
-        map      = {
-            0    : {"a" : "ob", "b" : 1   },
-            1    : {"a" : "ob", "b" : "ob"},
-            "ob" : {"a" : "ob", "b" : "ob"},
+    b = Fsm(
+        alphabet={"a", "b"},
+        states={0, 1, "ob"},
+        initial=0,
+        finals={1},
+        map={
+            0: {"a": "ob", "b": 1},
+            1: {"a": "ob", "b": "ob"},
+            "ob": {"a": "ob", "b": "ob"},
         },
     )
     return b
 
+
 def test_b(b):
     assert not b.accepts("")
     assert not b.accepts("a")
     assert b.accepts("b")
 
+
 def test_concatenation_aa(a):
     concAA = a + a
     assert not concAA.accepts("")
@@ -96,6 +100,7 @@ def test_concatenation_aa(a):
     assert concAA.accepts("aa")
     assert not concAA.accepts("aaa")
 
+
 def test_concatenation_ab(a, b):
     concAB = a + b
     assert not concAB.accepts("")
@@ -106,11 +111,13 @@ def test_concatenation_ab(a, b):
     assert not concAB.accepts("ba")
     assert not concAB.accepts("bb")
 
+
 def test_alternation_a(a):
     altA = a | null({"a", "b"})
     assert not altA.accepts("")
     assert altA.accepts("a")
 
+
 def test_alternation_ab(a, b):
     altAB = a | b
     assert not altAB.accepts("")
@@ -121,6 +128,7 @@ def test_alternation_ab(a, b):
     assert not altAB.accepts("ba")
     assert not altAB.accepts("bb")
 
+
 def test_star(a):
     starA = a.star()
     assert starA.accepts("")
@@ -128,17 +136,20 @@ def test_star(a):
     assert not starA.accepts("b")
     assert starA.accepts("aaaaaaaaa")
 
+
 def test_multiply_0(a):
     zeroA = a * 0
     assert zeroA.accepts("")
     assert not zeroA.accepts("a")
 
+
 def test_multiply_1(a):
     oneA = a * 1
     assert not oneA.accepts("")
     assert oneA.accepts("a")
     assert not oneA.accepts("aa")
 
+
 def test_multiply_2(a):
     twoA = a * 2
     assert not twoA.accepts("")
@@ -146,12 +157,14 @@ def test_multiply_2(a):
     assert twoA.accepts("aa")
     assert not twoA.accepts("aaa")
 
+
 def test_multiply_7(a):
     sevenA = a * 7
     assert not sevenA.accepts("aaaaaa")
     assert sevenA.accepts("aaaaaaa")
     assert not sevenA.accepts("aaaaaaaa")
 
+
 def test_optional_mul(a, b):
     unit = a + b
     # accepts "ab"
@@ -176,12 +189,14 @@ def test_optional_mul(a, b):
     assert not optional.accepts(["a", "a", "a"])
     assert optional.accepts(["a", "b", "a", "b"])
 
+
 def test_intersection_ab(a, b):
     intAB = a & b
     assert not intAB.accepts("")
     assert not intAB.accepts("a")
     assert not intAB.accepts("b")
 
+
 def test_negation(a):
     everythingbutA = a.everythingbut()
     assert everythingbutA.accepts("")
@@ -190,35 +205,37 @@ def test_negation(a):
     assert everythingbutA.accepts("aa")
     assert everythingbutA.accepts("ab")
 
+
 def test_crawl_reduction():
     # this is "0*1" in heavy disguise. crawl should resolve this duplication
     # Notice how states 2 and 3 behave identically. When resolved together,
     # states 1 and 2&3 also behave identically, so they, too should be resolved
     # (this is impossible to spot before 2 and 3 have been combined).
     # Finally, the oblivion state should be omitted.
-    merged = fsm(
-        alphabet = {"0", "1"},
-        states   = {1, 2, 3, 4, "oblivion"},
-        initial  = 1,
-        finals   = {4},
-        map      = {
-            1          : {"0" : 2         , "1" : 4         },
-            2          : {"0" : 3         , "1" : 4         },
-            3          : {"0" : 3         , "1" : 4         },
-            4          : {"0" : "oblivion", "1" : "oblivion"},
-            "oblivion" : {"0" : "oblivion", "1" : "oblivion"},
+    merged = Fsm(
+        alphabet={"0", "1"},
+        states={1, 2, 3, 4, "oblivion"},
+        initial=1,
+        finals={4},
+        map={
+            1: {"0": 2, "1": 4},
+            2: {"0": 3, "1": 4},
+            3: {"0": 3, "1": 4},
+            4: {"0": "oblivion", "1": "oblivion"},
+            "oblivion": {"0": "oblivion", "1": "oblivion"},
         }
     ).reduce()
     assert len(merged.states) == 2
 
+
 def test_bug_28():
     # This is (ab*)* and it caused some defects.
-    abstar = fsm(
-        alphabet = {'a', 'b'},
-        states   = {0, 1},
-        initial  = 0,
-        finals   = {1},
-        map = {
+    abstar = Fsm(
+        alphabet={'a', 'b'},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={
             0: {'a': 1},
             1: {'b': 1}
         }
@@ -233,19 +250,20 @@ def test_bug_28():
     assert abstarstar.accepts("ab")
     assert not abstar.star().accepts("bb")
 
+
 def test_star_advanced():
     # This is (a*ba)*. Naively connecting the final states to the initial state
     # gives the incorrect result here.
-    starred = fsm(
-        alphabet = {"a", "b"},
-        states   = {0, 1, 2, "oblivion"},
-        initial  = 0,
-        finals   = {2},
-        map      = {
-            0          : {"a" : 0         , "b" : 1         },
-            1          : {"a" : 2         , "b" : "oblivion"},
-            2          : {"a" : "oblivion", "b" : "oblivion"},
-            "oblivion" : {"a" : "oblivion", "b" : "oblivion"},
+    starred = Fsm(
+        alphabet={"a", "b"},
+        states={0, 1, 2, "oblivion"},
+        initial=0,
+        finals={2},
+        map={
+            0: {"a": 0, "b": 1},
+            1: {"a": 2, "b": "oblivion"},
+            2: {"a": "oblivion", "b": "oblivion"},
+            "oblivion": {"a": "oblivion", "b": "oblivion"},
         }
     ).star()
     assert starred.alphabet == frozenset(["a", "b"])
@@ -259,52 +277,55 @@ def test_star_advanced():
     assert not starred.accepts("aabb")
     assert starred.accepts("abababa")
 
+
 def test_reduce():
     # FSM accepts no strings but has 3 states, needs only 1
-    asdf = fsm(
-        alphabet = {None},
-        states   = {0, 1, 2},
-        initial  = 0,
-        finals   = {1},
-        map = {
-            0 : {None : 2},
-            1 : {None : 2},
-            2 : {None : 2},
+    asdf = Fsm(
+        alphabet={None},
+        states={0, 1, 2},
+        initial=0,
+        finals={1},
+        map={
+            0: {None: 2},
+            1: {None: 2},
+            2: {None: 2},
         },
     )
     asdf = asdf.reduce()
     assert len(asdf.states) == 1
 
+
 def test_reverse_abc():
-    abc = fsm(
-        alphabet = {"a", "b", "c"},
-        states   = {0, 1, 2, 3, None},
-        initial  = 0,
-        finals   = {3},
-        map = {
-            0    : {"a" : 1   , "b" : None, "c" : None},
-            1    : {"a" : None, "b" : 2   , "c" : None},
-            2    : {"a" : None, "b" : None, "c" : 3   },
-            3    : {"a" : None, "b" : None, "c" : None},
-            None : {"a" : None, "b" : None, "c" : None},
+    abc = Fsm(
+        alphabet={"a", "b", "c"},
+        states={0, 1, 2, 3, None},
+        initial=0,
+        finals={3},
+        map={
+            0: {"a": 1, "b": None, "c": None},
+            1: {"a": None, "b": 2, "c": None},
+            2: {"a": None, "b": None, "c": 3},
+            3: {"a": None, "b": None, "c": None},
+            None: {"a": None, "b": None, "c": None},
         },
     )
     cba = reversed(abc)
     assert cba.accepts("cba")
 
+
 def test_reverse_brzozowski():
     # This is (a|b)*a(a|b)
-    brzozowski = fsm(
-        alphabet = {"a", "b"},
-        states = {"A", "B", "C", "D", "E"},
-        initial = "A",
-        finals = {"C", "E"},
-        map = {
-            "A" : {"a" : "B", "b" : "D"},
-            "B" : {"a" : "C", "b" : "E"},
-            "C" : {"a" : "C", "b" : "E"},
-            "D" : {"a" : "B", "b" : "D"},
-            "E" : {"a" : "B", "b" : "D"},
+    brzozowski = Fsm(
+        alphabet={"a", "b"},
+        states={"A", "B", "C", "D", "E"},
+        initial="A",
+        finals={"C", "E"},
+        map={
+            "A": {"a": "B", "b": "D"},
+            "B": {"a": "C", "b": "E"},
+            "C": {"a": "C", "b": "E"},
+            "D": {"a": "B", "b": "D"},
+            "E": {"a": "B", "b": "D"},
         },
     )
     assert brzozowski.accepts("aa")
@@ -343,26 +364,28 @@ def test_reverse_brzozowski():
     assert next(gen) == ["b", "a", "b"]
     assert next(gen) == ["a", "a", "a", "a"]
 
+
 def test_reverse_epsilon():
     # epsilon reversed is epsilon
     assert reversed(epsilon("a")).accepts("")
 
+
 def test_binary_3():
     # Binary numbers divisible by 3.
     # Disallows the empty string
     # Allows "0" on its own, but not leading zeroes.
-    div3 = fsm(
-        alphabet = {"0", "1"},
-        states = {"initial", "zero", 0, 1, 2, None},
-        initial = "initial",
-        finals = {"zero", 0},
-        map = {
-            "initial" : {"0" : "zero", "1" : 1   },
-            "zero"    : {"0" : None  , "1" : None},
-            0         : {"0" : 0     , "1" : 1   },
-            1         : {"0" : 2     , "1" : 0   },
-            2         : {"0" : 1     , "1" : 2   },
-            None      : {"0" : None  , "1" : None},
+    div3 = Fsm(
+        alphabet={"0", "1"},
+        states={"initial", "zero", 0, 1, 2, None},
+        initial="initial",
+        finals={"zero", 0},
+        map={
+            "initial": {"0": "zero", "1": 1},
+            "zero": {"0": None, "1": None},
+            0: {"0": 0, "1": 1},
+            1: {"0": 2, "1": 0},
+            2: {"0": 1, "1": 2},
+            None: {"0": None, "1": None},
         },
     )
     assert not div3.accepts("")
@@ -391,15 +414,16 @@ def test_binary_3():
     assert not div3.accepts("1000")
     assert div3.accepts("1001")
 
+
 def test_invalid_fsms():
     # initial state 1 is not a state
     try:
-        fsm(
-            alphabet = {},
-            states = {},
-            initial = 1,
-            finals = set(),
-            map = {}
+        Fsm(
+            alphabet={},
+            states={},
+            initial=1,
+            finals=set(),
+            map={}
         )
         assert False
     except AssertionError:
@@ -409,12 +433,12 @@ def test_invalid_fsms():
 
     # final state 2 not a state
     try:
-        fsm(
-            alphabet = {},
-            states = {1},
-            initial = 1,
-            finals = {2},
-            map = {}
+        Fsm(
+            alphabet={},
+            states={1},
+            initial=1,
+            finals={2},
+            map={}
         )
         assert False
     except AssertionError:
@@ -424,13 +448,13 @@ def test_invalid_fsms():
 
     # invalid transition for state 1, symbol "a"
     try:
-        fsm(
-            alphabet = {"a"},
-            states = {1},
-            initial = 1,
-            finals = set(),
-            map = {
-                1 : {"a" : 2}
+        Fsm(
+            alphabet={"a"},
+            states={1},
+            initial=1,
+            finals=set(),
+            map={
+                1: {"a": 2}
             }
         )
         assert False
@@ -439,37 +463,40 @@ def test_invalid_fsms():
     except Exception:
         pass
 
+
 def test_bad_multiplier(a):
     try:
-        x = a * -1
+        a * -1
         assert False
     except AssertionError:
         assert False
     except Exception:
         pass
 
+
 def test_anything_else_acceptance():
-    a = fsm(
-        alphabet = {"a", "b", "c", anything_else},
-        states = {1},
-        initial = 1,
-        finals = {1},
-        map = {
-            1 : {"a" : 1, "b" : 1, "c" : 1, anything_else : 1}
+    a = Fsm(
+        alphabet={"a", "b", "c", ANYTHING_ELSE},
+        states={1},
+        initial=1,
+        finals={1},
+        map={
+            1: {"a": 1, "b": 1, "c": 1, ANYTHING_ELSE: 1}
         },
     )
     assert a.accepts("d")
 
+
 def test_difference(a, b):
-    aorb = fsm(
-        alphabet = {"a", "b"},
-        states = {0, 1, None},
-        initial = 0,
-        finals = {1},
-        map = { 
-            0    : {"a" : 1   , "b" : 1   },
-            1    : {"a" : None, "b" : None},
-            None : {"a" : None, "b" : None},
+    aorb = Fsm(
+        alphabet={"a", "b"},
+        states={0, 1, None},
+        initial=0,
+        finals={1},
+        map={
+            0: {"a": 1, "b": 1},
+            1: {"a": None, "b": None},
+            None: {"a": None, "b": None},
         },
     )
 
@@ -478,102 +505,110 @@ def test_difference(a, b):
     assert list((a ^ b).strings()) == [["a"], ["b"]]
     assert list((aorb ^ a).strings()) == [["b"]]
 
+
 def test_empty(a, b):
     assert not a.empty()
     assert not b.empty()
 
-    assert fsm(
-        alphabet = {},
-        states = {0, 1},
-        initial = 0,
-        finals = {1},
-        map = {0:{}, 1:{}},
+    assert Fsm(
+        alphabet={},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={0: {}, 1: {}},
     ).empty()
 
-    assert not fsm(
-        alphabet = {},
-        states = {0},
-        initial = 0,
-        finals = {0},
-        map = {0:{}},
+    assert not Fsm(
+        alphabet={},
+        states={0},
+        initial=0,
+        finals={0},
+        map={0: {}},
     ).empty()
 
-    assert fsm(
-        alphabet = {"a", "b"},
-        states = {0, 1, None, 2},
-        initial = 0,
-        finals = {2},
-        map = { 
-            0    : {"a" : 1   , "b" : 1   },
-            1    : {"a" : None, "b" : None},
-            None : {"a" : None, "b" : None},
-            2    : {"a" : None, "b" : None},
+    assert Fsm(
+        alphabet={"a", "b"},
+        states={0, 1, None, 2},
+        initial=0,
+        finals={2},
+        map={
+            0: {"a": 1, "b": 1},
+            1: {"a": None, "b": None},
+            None: {"a": None, "b": None},
+            2: {"a": None, "b": None},
         },
     ).empty()
 
+
 def test_equivalent(a, b):
     assert (a | b).equivalent(b | a)
 
+
 def test_dead_default():
     '''
-        You may now omit a transition, or even an entire state, from the map. This
-        affects every usage of `fsm.map`.
+        You may now omit a transition, or even an entire state, from the map.
+        This affects every usage of `Fsm.map`.
     '''
-    blockquote = fsm(
-        alphabet = {"/", "*", anything_else},
-        states = {0, 1, 2, 3, 4, 5},
-        initial = 0,
-        finals = {4},
-        map = {
-                0 : {"/" : 1},
-                1 : {"*" : 2},
-                2 : {"/" : 2, anything_else : 2, "*" : 3},
-                3 : {"/" : 4, anything_else : 2, "*" : 3},
+    blockquote = Fsm(
+        alphabet={"/", "*", ANYTHING_ELSE},
+        states={0, 1, 2, 3, 4, 5},
+        initial=0,
+        finals={4},
+        map={
+                0: {"/": 1},
+                1: {"*": 2},
+                2: {"/": 2, ANYTHING_ELSE: 2, "*": 3},
+                3: {"/": 4, ANYTHING_ELSE: 2, "*": 3},
         }
     )
     assert blockquote.accepts(["/", "*", "whatever", "*", "/"])
     assert not blockquote.accepts(["*", "*", "whatever", "*", "/"])
     assert str(blockquote) == \
-      '  name final? * / anything_else \n' + \
-      '--------------------------------\n' + \
-      '* 0    False    1               \n' + \
-      '  1    False  2                 \n' + \
-      '  2    False  3 2 2             \n' + \
-      '  3    False  3 4 2             \n' + \
-      '  4    True                     \n' + \
-      '  5    False                    \n'
+        '  name final? * / ANYTHING_ELSE \n' + \
+        '--------------------------------\n' + \
+        '* 0    False    1               \n' + \
+        '  1    False  2                 \n' + \
+        '  2    False  3 2 2             \n' + \
+        '  3    False  3 4 2             \n' + \
+        '  4    True                     \n' + \
+        '  5    False                    \n'
     blockquote | blockquote
     blockquote & blockquote
     blockquote ^ blockquote
     reversed(blockquote)
-    assert not blockquote.everythingbut().accepts(["/", "*", "whatever", "*", "/"])
-    assert blockquote.everythingbut().accepts(["*"]) # deliberately seek oblivion
+    assert not blockquote.everythingbut() \
+        .accepts(["/", "*", "whatever", "*", "/"])
+
+    # deliberately seek oblivion
+    assert blockquote.everythingbut().accepts(["*"])
+
     assert blockquote.islive(3)
     assert blockquote.islive(4)
     assert not blockquote.islive(5)
     gen = blockquote.strings()
     assert next(gen) == ["/", "*", "*", "/"]
 
+
 def test_alphabet_unions():
-    # Thanks to sparse maps it should now be possible to compute the union of FSMs
-    # with disagreeing alphabets!
-    a = fsm(
-        alphabet = {"a"},
-        states   = {0, 1},
-        initial  = 0,
-        finals   = {1},
-        map      = {
-            0    : {"a" : 1},
+    # Thanks to sparse maps it should now be possible to compute the union of
+    # FSMs with disagreeing alphabets!
+    a = Fsm(
+        alphabet={"a"},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={
+            0: {"a": 1},
         },
     )
 
-    b = fsm(
-        alphabet = {"b"},
-        states   = {0, 1},
-        initial  = 0,
-        finals   = {1},
-        map      = {
-            0    : {"b" : 1},
+    b = Fsm(
+        alphabet={"b"},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={
+            0: {"b": 1},
         },
     )
 
@@ -584,9 +619,10 @@ def test_alphabet_unions():
     assert (a ^ b).accepts(["a"])
     assert (a ^ b).accepts(["b"])
 
+
 def test_new_set_methods(a, b):
-    # A whole bunch of new methods were added to the FSM module to enable FSMs to
-    # function exactly as if they were sets of strings (symbol lists), see:
+    # A whole bunch of new methods were added to the FSM module to enable FSMs
+    # to function exactly as if they were sets of strings (symbol lists), see:
     # https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset
     # But do they work?
     assert len(a) == 1
@@ -599,7 +635,6 @@ def test_new_set_methods(a, b):
 
     # "in"
     assert "a" in a
-    assert not "a" in b
     assert "a" not in b
 
     # List comprehension!
@@ -607,27 +642,36 @@ def test_new_set_methods(a, b):
     for string in four:
         assert string == ["a", "a"]
         break
-    assert [s for s in four] == [["a", "a"], ["a", "b"], ["b", "a"], ["b", "b"]]
+    assert [s for s in four] == [
+        ["a", "a"],
+        ["a", "b"],
+        ["b", "a"],
+        ["b", "b"]
+    ]
 
     # set.union() imitation
-    assert fsm.union(a, b) == a.union(b)
-    assert len(fsm.union()) == 0
-    assert fsm.intersection(a, b) == a.intersection(b)
+    assert Fsm.union(a, b) == a.union(b)
+    assert len(Fsm.union()) == 0
+    assert Fsm.intersection(a, b) == a.intersection(b)
 
     # This takes a little explaining. In general, `a & b & c` is equivalent to
     # `EVERYTHING & a & b & c` where `EVERYTHING` is an FSM accepting every
     # possible string. Similarly `a` is equivalent to `EVERYTHING & a`, and the
-    # intersection of no sets at all is... `EVERYTHING`.
-    # However, since we compute the union of alphabets, and there are no
-    # alphabets, the union is the empty set. So the only string which `EVERYTHING`
-    # actually recognises is the empty string, [] (or "" if you prefer).
-    int_none = fsm.intersection()
+    # intersection of no sets at all is... `EVERYTHING`. However, since we
+    # compute the union of alphabets, and there are no alphabets, the union is
+    # the empty set. So the only string which `EVERYTHING` actually recognises
+    # is the empty string, [] (or "" if you prefer).
+    int_none = Fsm.intersection()
     assert len(int_none) == 1
     assert [] in int_none
 
-    assert (a | b).difference(a) == fsm.difference((a | b), a) == (a | b) - a == b
-    assert (a | b).difference(a, b) == fsm.difference((a | b), a, b) == (a | b) - a - b == null("ab")
-    assert a.symmetric_difference(b) == fsm.symmetric_difference(a, b) == a ^ b
+    assert (a | b).difference(a) \
+        == Fsm.difference((a | b), a) \
+        == (a | b) - a == b
+    assert (a | b).difference(a, b) \
+        == Fsm.difference((a | b), a, b) \
+        == (a | b) - a - b == null("ab")
+    assert a.symmetric_difference(b) == Fsm.symmetric_difference(a, b) == a ^ b
     assert a.isdisjoint(b)
     assert a <= (a | b)
     assert a < (a | b)
@@ -637,22 +681,23 @@ def test_new_set_methods(a, b):
 
     assert list(a.concatenate(a, a).strings()) == [["a", "a", "a"]]
     assert list(a.concatenate().strings()) == [["a"]]
-    assert list(fsm.concatenate(b, a, b).strings()) == [["b", "a", "b"]]
-    assert list(fsm.concatenate().strings()) == []
+    assert list(Fsm.concatenate(b, a, b).strings()) == [["b", "a", "b"]]
+    assert list(Fsm.concatenate().strings()) == []
     assert not a.copy() is a
 
+
 def test_oblivion_crawl(a):
     # When crawling a new FSM, we should avoid generating an oblivion state.
     # `abc` has no oblivion state... all the results should not as well!
-    abc = fsm(
-        alphabet = {"a", "b", "c"},
-        states = {0, 1, 2, 3},
-        initial = 0,
-        finals = {3},
-        map = {
-            0 : {"a" : 1},
-            1 : {"b" : 2},
-            2 : {"c" : 3},
+    abc = Fsm(
+        alphabet={"a", "b", "c"},
+        states={0, 1, 2, 3},
+        initial=0,
+        finals={3},
+        map={
+            0: {"a": 1},
+            1: {"b": 2},
+            2: {"c": 3},
         }
     )
     assert len((abc + abc).states) == 7
@@ -664,10 +709,12 @@ def test_oblivion_crawl(a):
     assert len((abc ^ abc).states) == 1
     assert len((abc - abc).states) == 1
 
+
 def test_concatenate_bug(a):
-    # This exposes a defect in fsm.concatenate.
-    assert fsm.concatenate(a, epsilon({"a"}), a).accepts("aa")
-    assert fsm.concatenate(a, epsilon({"a"}), epsilon({"a"}), a).accepts("aa")
+    # This exposes a defect in Fsm.concatenate.
+    assert Fsm.concatenate(a, epsilon({"a"}), a).accepts("aa")
+    assert Fsm.concatenate(a, epsilon({"a"}), epsilon({"a"}), a).accepts("aa")
+
 
 def test_derive(a, b):
     # Just some basic tests because this is mainly a regex thing.
@@ -681,52 +728,57 @@ def test_derive(a, b):
     assert (a * 3).derive("a") == a * 2
     assert (a.star() - epsilon({"a", "b"})).derive("a") == a.star()
 
+
 def test_bug_36():
-    etc1 = fsm(
-        alphabet = {anything_else},
-        states = {0},
-        initial = 0,
-        finals = {0},
-        map = {
+    etc1 = Fsm(
+        alphabet={ANYTHING_ELSE},
+        states={0},
+        initial=0,
+        finals={0},
+        map={
             0: {
-                anything_else: 0
+                ANYTHING_ELSE: 0
             }
         }
     )
-    etc2 = fsm(
-        alphabet = {'s', anything_else},
-        states = {0, 1},
-        initial = 0,
-        finals = {1},
-        map = {
+    etc2 = Fsm(
+        alphabet={'s', ANYTHING_ELSE},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={
             0: {
                 's': 1
             },
             1: {
                 's': 1,
-                anything_else: 1
+                ANYTHING_ELSE: 1
             }
         }
     )
     both = etc1 & etc2
     assert etc1.accepts(["s"])
     assert etc2.accepts(["s"])
-    assert both.alphabet == {anything_else, "s"}
+    assert both.alphabet == {ANYTHING_ELSE, "s"}
     assert both.accepts(["s"])
 
+
 def test_add_anything_else():
-    fsm1 = fsm( # [^a]
-        alphabet = {"a", anything_else},
-        states = {0, 1},
-        initial = 0,
-        finals = {1},
-        map = {0: {anything_else: 1}}
+    # [^a]
+    fsm1 = Fsm(
+        alphabet={"a", ANYTHING_ELSE},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={0: {ANYTHING_ELSE: 1}}
     )
-    fsm2 = fsm( # [^b]
-        alphabet = {"b", anything_else},
-        states = {0, 1},
-        initial = 0,
-        finals = {1},
-        map = {0: {anything_else: 1}}
+
+    # [^b]
+    fsm2 = Fsm(
+        alphabet={"b", ANYTHING_ELSE},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={0: {ANYTHING_ELSE: 1}}
     )
     assert (fsm1 + fsm2).accepts("ba")
diff --git a/greenery/lego_test.py b/greenery/lego_test.py
deleted file mode 100644
index 261565f..0000000
--- a/greenery/lego_test.py
+++ /dev/null
@@ -1,1472 +0,0 @@
-# -*- coding: utf-8 -*-
-
-if __name__ == "__main__":
-    raise Exception("Test files can't be run directly. Use `python -m pytest greenery`")
-
-import pickle
-from greenery.lego import conc, mult, charclass, one, emptystring, star, plus, nothing, pattern, qm, d, multiplier, bound, w, s, W, D, S, dot, nomatch, inf, zero, parse, from_fsm
-from greenery import fsm
-
-# In general the idea for unit tests is that every unit test relies only on
-# functionality which has already been unit-tested. If this isn't possible, then
-# additional tests are required!
-
-###############################################################################
-# Equality tests. No point in comparing different lego pieces in tests unless
-# this part works
-
-def test_charclass_equality():
-    assert charclass("a") == charclass("a")
-    assert ~charclass("a") == ~charclass("a")
-    assert ~charclass("a") != charclass("a")
-    assert charclass("ab") == charclass("ba")
-
-def test_mult_equality():
-    assert mult(charclass("a"), one) == mult(charclass("a"), one)
-    assert mult(charclass("a"), one) != mult(charclass("b"), one)
-    assert mult(charclass("a"), one) != mult(charclass("a"), qm)
-    assert mult(charclass("a"), one) != mult(charclass("a"), multiplier(bound(1), bound(2)))
-    assert mult(charclass("a"), one) != charclass("a")
-
-def test_conc_equality():
-    assert conc(mult(charclass("a"), one)) == conc(mult(charclass("a"), one))
-    assert conc(mult(charclass("a"), one)) != conc(mult(charclass("b"), one))
-    assert conc(mult(charclass("a"), one)) != conc(mult(charclass("a"), qm))
-    assert conc(mult(charclass("a"), one)) != conc(mult(charclass("a"), multiplier(bound(1), bound(2))))
-    assert conc(mult(charclass("a"), one)) != emptystring
-
-def test_pattern_equality():
-    assert pattern(
-        conc(mult(charclass("a"), one)),
-        conc(mult(charclass("b"), one)),
-    ) == pattern(
-        conc(mult(charclass("b"), one)),
-        conc(mult(charclass("a"), one)),
-    )
-    assert pattern(
-        conc(mult(charclass("a"), one)),
-        conc(mult(charclass("a"), one)),
-    ) == pattern(
-        conc(mult(charclass("a"), one)),
-    )
-
-###############################################################################
-# Parsing tests. Absolutely no cleverness is applied at parsing time, we just
-# return the exact object which was just parsed. Call reduce() if you wish...
-
-def test_charclass_parsing():
-    assert charclass.match("a", 0) == (charclass("a"), 1)
-    assert charclass.parse("a") == charclass("a")
-    assert charclass.match("aa", 1) == (charclass("a"), 2)
-    assert charclass.match("a$", 1) == (charclass("$"), 2)
-    assert charclass.match(".", 0) == (dot, 1)
-    try:
-        charclass.match("[", 0)
-        assert False
-    except IndexError:
-        pass
-    try:
-        charclass.match("a", 1)
-        assert False
-    except nomatch:
-        pass
-
-def test_charclass_ranges():
-    # Should accept arbitrary ranges of characters in charclasses. No longer
-    # limited to alphanumerics. (User beware...)
-    assert parse("[z{|}~]") == parse("[z-~]")
-    assert parse("[\\w:;<=>?@\\[\\\\\\]\\^`]") == parse("[0-z]")
-
-def test_hex_escapes():
-    # Should be able to parse e.g. "\\x40"
-    assert parse("\\x00") == parse("\x00")
-    assert parse("\\x40") == parse("@")
-    assert parse("[\\x40]") == parse("[@]")
-    assert parse("[\\x41-\\x5a]") == parse("[A-Z]")
-
-def test_w_d_s():
-    # Allow "\w", "\d" and "\s" in charclasses
-    assert charclass.parse("\\w") == charclass.parse("[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz]")
-    assert charclass.parse("[\\w~]") == charclass.parse("[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~]")
-    assert charclass.parse("[\\da]") == charclass.parse("[0123456789a]")
-    assert charclass.parse("[\\s]") == charclass.parse("[\t\n\r\f\v ]")
-
-def test_mult_parsing():
-    assert mult.parse("[a-g]+") == mult(charclass("abcdefg"), plus)
-    assert mult.parse("[a-g0-8$%]+") == mult(charclass("abcdefg012345678$%"), plus)
-    assert mult.parse("[a-g0-8$%\\^]+") == mult(charclass("abcdefg012345678$%^"), plus)
-    assert mult.match("abcde[^fg]*", 5) == (
-        mult(~charclass("fg"), star),
-        11
-    )
-    assert mult.match("abcde[^fg]*h{5}[a-z]+", 11) == (
-        mult(charclass("h"), multiplier(bound(5), bound(5))),
-        15
-    )
-    assert mult.match("abcde[^fg]*h{5}[a-z]+T{1,}", 15) == (
-        mult(charclass("abcdefghijklmnopqrstuvwxyz"), plus),
-        21
-    )
-    assert mult.match("abcde[^fg]*h{5}[a-z]+T{2,}", 21) == (
-        mult(charclass("T"), multiplier(bound(2), inf)),
-        26
-    )
-
-def test_conc_parsing():
-    assert conc.parse("abcde[^fg]*h{5}[a-z]+") == conc(
-        mult(charclass("a"), one),
-        mult(charclass("b"), one),
-        mult(charclass("c"), one),
-        mult(charclass("d"), one),
-        mult(charclass("e"), one),
-        mult(~charclass("fg"), star),
-        mult(charclass("h"), multiplier(bound(5), bound(5))),
-        mult(charclass("abcdefghijklmnopqrstuvwxyz"), plus),
-    )
-    assert conc.parse("[bc]*[ab]*") == conc(
-        mult(charclass("bc"), star),
-        mult(charclass("ab"), star),
-    )
-    assert conc.parse("abc...") == conc(
-        mult(charclass("a"), one),
-        mult(charclass("b"), one),
-        mult(charclass("c"), one),
-        mult(dot, one),
-        mult(dot, one),
-        mult(dot, one),
-    )
-    assert conc.parse("\\d{4}-\\d{2}-\\d{2}") == conc(
-        mult(charclass("0123456789"), multiplier(bound(4), bound(4))),
-        mult(charclass("-"), one),
-        mult(charclass("0123456789"), multiplier(bound(2), bound(2))),
-        mult(charclass("-"), one),
-        mult(charclass("0123456789"), multiplier(bound(2), bound(2))),
-    )
-
-def test_pattern_parsing():
-    assert pattern.parse("abc|def(ghi|jkl)") == pattern(
-        conc(
-            mult(charclass("a"), one),
-            mult(charclass("b"), one),
-            mult(charclass("c"), one),
-        ),
-        conc(
-            mult(charclass("d"), one),
-            mult(charclass("e"), one),
-            mult(charclass("f"), one),
-            mult(
-                pattern(
-                    conc(
-                        mult(charclass("g"), one),
-                        mult(charclass("h"), one),
-                        mult(charclass("i"), one),
-                    ),
-                    conc(
-                        mult(charclass("j"), one),
-                        mult(charclass("k"), one),
-                        mult(charclass("l"), one),
-                    ),
-                ), one
-            ),
-        )
-    )
-
-    # Accept the "non-capturing group" syntax, "(?: ... )" but give it no
-    # special significance
-    assert parse("(?:)") == parse("()")
-    assert parse("(?:abc|def)") == parse("(abc|def)")
-    parse("(:abc)") # should give no problems
-
-    # Named groups
-    assert pattern.parse("(?P<ng1>abc)") == parse("(abc)")
-
-###############################################################################
-# repr() tests
-
-def test_repr():
-    assert repr(~charclass("a")) == "~charclass('a')"
-
-###############################################################################
-# Stringification tests
-
-def test_charclass_str():
-    assert str(w) == "\\w"
-    assert str(d) == "\\d"
-    assert str(s) == "\\s"
-    assert str(charclass("a")) == "a"
-    assert str(charclass("{")) == "\\{"
-    assert str(charclass("\t")) == "\\t"
-    assert str(charclass("ab")) == "[ab]"
-    assert str(charclass("a{")) == "[a{]"
-    assert str(charclass("a\t")) == "[\\ta]"
-    assert str(charclass("a-")) == "[\\-a]"
-    assert str(charclass("a[")) == "[\\[a]"
-    assert str(charclass("a]")) == "[\\]a]"
-    assert str(charclass("ab")) == "[ab]"
-    assert str(charclass("abc")) == "[abc]"
-    assert str(charclass("abcd")) == "[a-d]"
-    assert str(charclass("abcdfghi")) == "[a-df-i]"
-    assert str(charclass("^")) == "^"
-    assert str(charclass("\\")) == "\\\\"
-    assert str(charclass("a^")) == "[\\^a]"
-    assert str(charclass("0123456789a")) == "[0-9a]"
-    assert str(charclass("\t\v\r A")) == "[\\t\\v\\r A]"
-    assert str(charclass("\n\f A")) == "[\\n\\f A]"
-    assert str(charclass("\t\n\v\f\r A")) == "[\\t-\\r A]"
-    assert str(charclass("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz|")) == "[0-9A-Z_a-z|]"
-    assert str(W) == "\\W"
-    assert str(D) == "\\D"
-    assert str(S) == "\\S"
-    assert str(dot) == "."
-    assert str(~charclass("")) == "."
-    assert str(~charclass("a")) == "[^a]"
-    assert str(~charclass("{")) == "[^{]"
-    assert str(~charclass("\t")) == "[^\\t]"
-    assert str(~charclass("^")) == "[^\\^]"
-
-    # Arbitrary ranges
-    assert str(parse("[\\w:;<=>?@\\[\\\\\\]\\^`]")) == "[0-z]"
-    # TODO: what if \d is a proper subset of `chars`?
-
-    # escape sequences are not preserved
-    assert str(parse("\\x09")) == "\\t"
-
-    # Printing ASCII control characters? You should get hex escapes
-    assert str(parse("\\x00")) == "\\x00"
-
-def test_mult_str():
-    assert str(bound(2)) == "2"
-    assert str(inf) == ""
-    assert str(multiplier(bound(2), inf)) == "{2,}"
-
-    a = charclass("a")
-    assert str(mult(a, one)) == "a"
-    assert str(mult(a, multiplier(bound(2), bound(2)))) == "a{2}"
-    assert str(mult(a, multiplier(bound(3), bound(3)))) == "a{3}"
-    assert str(mult(a, multiplier(bound(4), bound(4)))) == "a{4}"
-    assert str(mult(a, multiplier(bound(5), bound(5)))) == "a{5}"
-    assert str(mult(a, qm)) == "a?"
-    assert str(mult(a, star)) == "a*"
-    assert str(mult(a, plus)) == "a+"
-    assert str(mult(a, multiplier(bound(2), bound(5)))) == "a{2,5}"
-    assert str(mult(a, multiplier(bound(2), inf))) == "a{2,}"
-    assert str(mult(d, one)) == "\\d"
-    assert str(mult(d, multiplier(bound(2), bound(2)))) == "\\d{2}"
-    assert str(mult(d, multiplier(bound(3), bound(3)))) == "\\d{3}"
-
-def test_conc_str():
-    assert str(conc(
-        mult(charclass("a"), one),
-        mult(charclass("b"), one),
-        mult(charclass("c"), one),
-        mult(charclass("d"), one),
-        mult(charclass("e"), one),
-        mult(~charclass("fg"), star),
-        mult(charclass("h"), multiplier(bound(5), bound(5))),
-        mult(charclass("abcdefghijklmnopqrstuvwxyz"), plus),
-    )) == "abcde[^fg]*h{5}[a-z]+"
-
-def test_pattern_str():
-    assert str(pattern(
-        conc(mult(charclass("a"), one)),
-        conc(mult(charclass("b"), one)),
-    )) == "a|b"
-    assert str(pattern(
-        conc(mult(charclass("a"), one)),
-        conc(mult(charclass("a"), one)),
-    )) == "a"
-    assert str(pattern(
-        conc(
-            mult(charclass("a"), one),
-            mult(charclass("b"), one),
-            mult(charclass("c"), one),
-        ),
-        conc(
-            mult(charclass("d"), one),
-            mult(charclass("e"), one),
-            mult(charclass("f"), one),
-            mult(
-                pattern(
-                    conc(
-                        mult(charclass("g"), one),
-                        mult(charclass("h"), one),
-                        mult(charclass("i"), one),
-                    ),
-                    conc(
-                        mult(charclass("j"), one),
-                        mult(charclass("k"), one),
-                        mult(charclass("l"), one),
-                    ),
-                ), one
-            ),
-        ),
-    )) == "abc|def(ghi|jkl)"
-
-def test_parse_str_round_trip():
-    assert str(parse("a.b")) == "a.b" # not "a[ab]b"
-    assert str(parse("\\d{4}")) == "\\d{4}"
-    assert str(parse("a.b()()")) == "a.b()()"
-
-###############################################################################
-# Test to_fsm() and alphabet-related functionality
-
-def test_alphabet():
-    # lego.alphabet() should include `fsm.anything_else`
-    assert parse("").alphabet() == {fsm.anything_else}
-
-def test_charclass_fsm():
-    # "[^a]"
-    nota = (~charclass("a")).to_fsm()
-    assert nota.alphabet == {"a", fsm.anything_else}
-    assert nota.accepts("b")
-    assert nota.accepts(["b"])
-    assert nota.accepts([fsm.anything_else])
-
-def test_pattern_fsm():
-    # "a[^a]"
-    anota = pattern.parse("a[^a]").to_fsm()
-    assert len(anota.states) == 3
-    assert not anota.accepts("a")
-    assert not anota.accepts(["a"])
-    assert not anota.accepts("b")
-    assert not anota.accepts(["b"])
-    assert not anota.accepts("aa")
-    assert not anota.accepts(["a", "a"])
-    assert anota.accepts("ab")
-    assert anota.accepts(["a", "b"])
-    assert anota.accepts(["a", fsm.anything_else])
-    assert not anota.accepts("ba")
-    assert not anota.accepts("bb")
-
-    # "0\\d"
-    zeroD = pattern.parse("0\\d").to_fsm(d.chars)
-    assert zeroD.accepts("01")
-    assert not zeroD.accepts("10")
-
-    # "\\d{2}"
-    d2 = pattern.parse("\\d{2}").to_fsm(d.chars)
-    assert not d2.accepts("")
-    assert not d2.accepts("1")
-    assert d2.accepts("11")
-    assert not d2.accepts("111")
-
-    # abc|def(ghi|jkl)
-    conventional = pattern.parse("abc|def(ghi|jkl)").to_fsm(w.chars)
-    assert not conventional.accepts("a")
-    assert not conventional.accepts("ab")
-    assert conventional.accepts("abc")
-    assert not conventional.accepts("abcj")
-    assert conventional.accepts("defghi")
-    assert conventional.accepts("defjkl")
-
-def test_fsm():
-    # You should be able to to_fsm() a single lego piece without supplying a specific
-    # alphabet. That should be determinable from context.
-    assert parse("a.b").to_fsm().accepts("acb")
-
-    bad = parse("0{2}|1{2}").to_fsm({"0", "1", fsm.anything_else})
-    assert bad.accepts("00")
-    assert bad.accepts("11")
-    assert not bad.accepts("01")
-
-    bad = parse("0{2}|1{2}").to_fsm()
-    assert bad.accepts("00")
-    assert bad.accepts("11")
-    assert not bad.accepts("01")
-
-def test_odd_bug():
-    # Odd bug with ([bc]*c)?[ab]*
-    int5A = mult(charclass("bc"), star).to_fsm({"a", "b", "c", fsm.anything_else})
-    assert int5A.accepts([])
-    assert int5A.accepts("")
-    int5B = mult(charclass("c"), one).to_fsm({"a", "b", "c", fsm.anything_else})
-    assert int5B.accepts("c")
-    assert int5B.accepts(["c"])
-    int5C = int5A + int5B
-    assert int5C.accepts("c")
-    assert int5C.accepts(["c"])
-
-def test_bug_28():
-    # Starification is broken in FSMs
-    assert not parse("(ab*)").to_fsm().star().accepts("bb")
-    assert not parse("(ab*)*").to_fsm().accepts("bb")
-
-###############################################################################
-# Test matches(). Quite sparse at the moment
-
-def test_wildcards_in_charclasses():
-    # Allow "\w", "\d" and "\s" in charclasses
-    assert parse("[\\w~]*").matches("a0~")
-    assert parse("[\\da]*").matches("0129a")
-    assert parse("[\\s]+").matches(" \t \t ")
-
-def test_block_comment_regex():
-    # I went through several incorrect regexes for C block comments. Here we show
-    # why the first few attempts were incorrect
-    a = parse("/\\*(([^*]|\\*+[^*/])*)\\*/")
-    assert a.matches("/**/")
-    assert not a.matches("/***/")
-    assert not a.matches("/****/")
-
-    b = parse("/\\*(([^*]|\\*[^/])*)\\*/")
-    assert b.matches("/**/")
-    assert not b.matches("/***/")
-    assert b.matches("/****/")
-
-    c = parse("/\\*(([^*]|\\*+[^*/])*)\\*+/")
-    assert c.matches("/**/")
-    assert c.matches("/***/")
-    assert c.matches("/****/")
-
-def test_named_groups():
-    a = parse("(?P<ng1>abc)")
-    assert a.matches("abc")
-
-def test_in():
-    assert "a" in parse("a")
-    assert "abcdsasda" in parse("\\w{4,10}")
-    assert "abc" in parse("abc|def(ghi|jkl)")
-
-###############################################################################
-# Test string generators
-
-def test_charclass_gen():
-    gen = charclass("xyz").strings()
-    assert next(gen) == "x"
-    assert next(gen) == "y"
-    assert next(gen) == "z"
-    try:
-        next(gen)
-        assert False
-    except StopIteration:
-        assert True
-
-def test_mult_gen():
-    # One term
-    gen = mult.parse("[ab]").strings()
-    assert next(gen) == "a"
-    assert next(gen) == "b"
-    try:
-        next(gen)
-        assert False
-    except StopIteration:
-        assert True
-
-    # No terms
-    gen = mult.parse("[ab]{0}").strings()
-    assert next(gen) == ""
-    try:
-        next(gen)
-        assert False
-    except StopIteration:
-        assert True
-
-    # Many terms
-    gen = mult.parse("[ab]*").strings()
-    assert next(gen) == ""
-    assert next(gen) == "a"
-    assert next(gen) == "b"
-    assert next(gen) == "aa"
-    assert next(gen) == "ab"
-    assert next(gen) == "ba"
-    assert next(gen) == "bb"
-    assert next(gen) == "aaa"
-
-def test_conc_generator():
-    gen = conc.parse("[ab][cd]").strings()
-    assert next(gen) == "ac"
-    assert next(gen) == "ad"
-    assert next(gen) == "bc"
-    assert next(gen) == "bd"
-    try:
-        next(gen)
-        assert False
-    except StopIteration:
-        assert True
-
-def test_pattern_generator():
-    gen = pattern.parse("[ab]|[cde]").strings()
-    assert next(gen) == "a"
-    assert next(gen) == "b"
-    assert next(gen) == "c"
-    assert next(gen) == "d"
-    assert next(gen) == "e"
-    try:
-        next(gen)
-        assert False
-    except StopIteration:
-        assert True
-
-    # more complex
-    gen = pattern.parse("abc|def(ghi|jkl)").strings()
-    assert next(gen) == "abc"
-    assert next(gen) == "defghi"
-    assert next(gen) == "defjkl"
-
-    gen = mult.parse("[0-9a-fA-F]{3,10}").strings()
-    assert next(gen) == "000"
-    assert next(gen) == "001"
-    assert next(gen) == "002"
-
-def test_infinite_generation():
-    # Infinite generator, flummoxes both depth-first and breadth-first searches
-    gen = parse("a*b*").strings()
-    assert next(gen) == ""
-    assert next(gen) == "a"
-    assert next(gen) == "b"
-    assert next(gen) == "aa"
-    assert next(gen) == "ab"
-    assert next(gen) == "bb"
-    assert next(gen) == "aaa"
-    assert next(gen) == "aab"
-    assert next(gen) == "abb"
-    assert next(gen) == "bbb"
-    assert next(gen) == "aaaa"
-
-def test_wildcard_generator():
-    # Generator needs to handle wildcards as well. Wildcards come last.
-    gen = parse("a.b").strings(otherchar="*")
-    assert next(gen) == "aab"
-    assert next(gen) == "abb"
-    assert next(gen) == "a*b"
-    try:
-        next(gen)
-        assert False
-    except StopIteration:
-        assert True
-
-def test_forin():
-    assert [s for s in parse("abc|def(ghi|jkl)")] == ["abc", "defghi", "defjkl"]
-
-###############################################################################
-# Test cardinality() and len()
-
-def test_cardinality():
-    assert charclass.parse("[]").cardinality() == 0
-    assert mult.parse("[]?").cardinality() == 1
-    assert mult.parse("[]{0,6}").cardinality() == 1
-    assert mult.parse("[ab]{3}").cardinality() == 8
-    assert mult.parse("[ab]{2,3}").cardinality() == 12
-    assert len(pattern.parse("abc|def(ghi|jkl)")) == 3
-    try:
-        len(pattern.parse(".*"))
-        assert False
-    except OverflowError:
-        assert True
-
-###############################################################################
-
-def test_copy():
-    x = pattern.parse("abc|def(ghi|jkl)")
-    assert x.copy() == x
-
-###############################################################################
-# Test from_fsm()
-
-def test_dot():
-    assert str(from_fsm(parse("a.b").to_fsm())) == "a.b" # not "a[ab]b"
-
-def test_abstar():
-    # Buggggs.
-    abstar = fsm.fsm(
-        alphabet={'a', fsm.anything_else, 'b'},
-        states={0, 1},
-        initial=0,
-        finals={0},
-        map={
-            0: {'a': 0, fsm.anything_else: 1, 'b': 0},
-            1: {'a': 1, fsm.anything_else: 1, 'b': 1}
-        }
-    )
-    assert str(from_fsm(abstar)) == "[ab]*"
-
-def test_adotb():
-    adotb = fsm.fsm(
-        alphabet={'a', fsm.anything_else, 'b'},
-        states={0, 1, 2, 3, 4},
-        initial=0,
-        finals={4},
-        map={
-            0: {'a': 2, fsm.anything_else: 1, 'b': 1},
-            1: {'a': 1, fsm.anything_else: 1, 'b': 1},
-            2: {'a': 3, fsm.anything_else: 3, 'b': 3},
-            3: {'a': 1, fsm.anything_else: 1, 'b': 4},
-            4: {'a': 1, fsm.anything_else: 1, 'b': 1}
-        }
-    )
-    assert str(from_fsm(adotb)) == "a.b"
-
-def test_lego_recursion_error():
-    # Catch a recursion error
-    assert str(from_fsm(fsm.fsm(
-        alphabet={"0", "1"},
-        states={0, 1, 2, 3},
-        initial=3,
-        finals={1},
-        map={
-            0: {"0": 1, "1": 1},
-            1: {"0": 2, "1": 2},
-            2: {"0": 2, "1": 2},
-            3: {"0": 0, "1": 2},
-        }
-    ))) == "0[01]"
-
-def test_even_star_bug():
-    # Bug fix. This is a(a{2})* (i.e. accepts an odd number of "a" chars in a
-    # row), but when from_fsm() is called, the result is "a+". Turned out to be
-    # a fault in the lego.multiplier.__mul__() routine
-    elesscomplex = fsm.fsm(
-        alphabet={"a"},
-        states={0, 1},
-        initial=0,
-        finals={1},
-        map={
-            0 : {"a" : 1},
-            1 : {"a" : 0},
-        },
-    )
-    assert not elesscomplex.accepts("")
-    assert elesscomplex.accepts("a")
-    assert not elesscomplex.accepts("aa")
-    assert elesscomplex.accepts("aaa")
-    elesscomplex = from_fsm(elesscomplex)
-    assert str(elesscomplex) in {"a(aa)*", "(aa)*a"}
-    elesscomplex = elesscomplex.to_fsm()
-    assert not elesscomplex.accepts("")
-    assert elesscomplex.accepts("a")
-    assert not elesscomplex.accepts("aa")
-    assert elesscomplex.accepts("aaa")
-    gen = elesscomplex.strings()
-    assert next(gen) == ["a"]
-    assert next(gen) == ["a", "a", "a"]
-    assert next(gen) == ["a", "a", "a", "a", "a"]
-    assert next(gen) == ["a", "a", "a", "a", "a", "a", "a"]
-
-def test_binary_3():
-    # Binary numbers divisible by 3.
-    # Disallows the empty string
-    # Allows "0" on its own, but not leading zeroes.
-    div3 = from_fsm(fsm.fsm(
-        alphabet={"0", "1"},
-        states={"initial", "zero", 0, 1, 2, None},
-        initial="initial",
-        finals={"zero", 0},
-        map={
-            "initial" : {"0" : "zero", "1" : 1},
-            "zero"    : {"0" : None  , "1" : None},
-            0         : {"0" : 0     , "1" : 1},
-            1         : {"0" : 2     , "1" : 0},
-            2         : {"0" : 1     , "1" : 2},
-            None      : {"0" : None  , "1" : None},
-        },
-    ))
-    assert str(div3) == "0|1(01*0|10*1)*10*"
-    gen = div3.strings()
-    assert next(gen) == "0"
-    assert next(gen) == "11"
-    assert next(gen) == "110"
-    assert next(gen) == "1001"
-    assert next(gen) == "1100"
-
-def test_base_N():
-    # Machine accepts only numbers in selected base (e.g. 2, 10) that are
-    # divisible by N (e.g. 3, 7).
-    # "0" alone is acceptable, but leading zeroes (e.g. "00", "07") are not
-    base = 2
-    N = 3
-    assert base <= 10
-    divN = from_fsm(fsm.fsm(
-        alphabet=set(str(i) for i in range(base)),
-        states=set(range(N)) | {"initial", "zero", None},
-        initial="initial",
-        finals={"zero", 0},
-        map=dict(
-            [
-                ("initial", dict([(str(j), j              % N) for j in range(1, base)] + [("0", "zero")])),
-                ("zero"   , dict([(str(j), None              ) for j in range(   base)]                  )),
-                (None     , dict([(str(j), None              ) for j in range(   base)]                  )),
-            ] + [
-                (i        , dict([(str(j), (i * base + j) % N) for j in range(   base)]                  ))
-                for i in range(N)
-            ]
-        ),
-    ))
-    gen = divN.strings()
-    a = next(gen)
-    assert a == "0"
-    for i in range(7):
-        b = next(gen)
-        assert int(a, base) + N == int(b, base)
-        a = b
-
-def test_bad_alphabet():
-    # You can use anything you like in your FSM alphabet, but if you try to
-    # convert it to a `lego` object then the only acceptable symbols are single
-    # characters or `fsm.anything_else`.
-    for bad_symbol in [None, (), 0, ("a",), "", "aa", "ab", True]:
-        f = fsm.fsm(
-            alphabet={bad_symbol},
-            states={0},
-            initial=0,
-            finals=set(),
-            map={
-                0 : {bad_symbol : 0}
-            },
-        )
-        try:
-            from_fsm(f)
-            assert False
-        except AssertionError as e:
-            raise Exception("Accepted bad symbol: " + repr(bad_symbol))
-        except Exception as e:
-            pass
-
-def test_dead_default():
-    blockquote = from_fsm(fsm.fsm(
-        alphabet={"/", "*", fsm.anything_else},
-        states={0, 1, 2, 3, 4},
-        initial=0,
-        finals={4},
-        map={
-                0    : {"/" : 1},
-                1    : {"*" : 2},
-                2    : {"/" : 2, fsm.anything_else : 2, "*" : 3},
-                3    : {"/" : 4, fsm.anything_else : 2, "*" : 3},
-        }
-    ))
-
-###############################################################################
-# charclass set operations
-
-def test_charclass_negation():
-    assert ~~charclass("a") == charclass("a")
-    assert charclass("a") == ~~charclass("a")
-
-def test_charclass_union():
-    # [ab] u [bc] = [abc]
-    assert charclass("ab") | charclass("bc") == charclass("abc")
-    # [ab] u [^bc] = [^c]
-    assert charclass("ab") | ~charclass("bc") == ~charclass("c")
-    # [^a] u [bc] = [^a]
-    assert ~charclass("ab") | charclass("bc") == ~charclass("a")
-    # [^ab] u [^bc] = [^b]
-    assert ~charclass("ab") | ~charclass("bc") == ~charclass("b")
-
-def test_charclass_intersection():
-    # [ab] n [bc] = [b]
-    assert charclass("ab") & charclass("bc") == charclass("b")
-    # [ab] n [^bc] = [a]
-    assert charclass("ab") & ~charclass("bc") == charclass("a")
-    # [^ab] n [bc] = [c]
-    assert ~charclass("ab") & charclass("bc") == charclass("c")
-    # [^ab] n [^bc] = [^abc]
-    assert ~charclass("ab") & ~charclass("bc") == ~charclass("abc")
-
-###############################################################################
-# Emptiness detection
-
-def test_empty():
-    assert nothing.empty()
-    assert charclass("").empty()
-    assert not dot.empty()
-    assert not mult.parse("a{0}").empty()
-    assert mult.parse("[]").empty()
-    assert not mult.parse("[]?").empty()
-    assert conc.parse("a[]").empty()
-    assert not conc.parse("a[]?").empty()
-    assert pattern().empty()
-    assert not pattern.parse("a{0}").empty()
-    assert not pattern.parse("[]?").empty()
-
-###############################################################################
-# Test everythingbut()
-
-def test_everythingbut():
-    # Regexes are usually gibberish but we make a few claims
-    a = parse("a")
-    notA = a.everythingbut().to_fsm()
-    assert notA.accepts("")
-    assert not notA.accepts("a")
-    assert notA.accepts("aa")
-
-    # everythingbut(), called twice, should take us back to where we started.
-    beer = parse("beer")
-    notBeer = beer.everythingbut()
-    beer2 = notBeer.everythingbut()
-    assert str(beer2) == "be{2}r"
-
-    # ".*" becomes "[]" and vice versa under this call.
-    everything = parse(".*")
-    assert str(everything.everythingbut()) == str(nothing)
-    assert str(nothing.everythingbut()) == str(everything)
-
-def test_isinstance_bug():
-    # Problem relating to isinstance(). The class "mult" was occurring as both
-    # lego.mult and as __main__.mult and apparently these count as different
-    # classes for some reason, so isinstance(m, mult) was returning false.
-    starfree = (parse("").everythingbut() + parse("aa") + parse("").everythingbut()).everythingbut()
-
-###############################################################################
-
-def test_equivalence():
-    assert parse("aa*").equivalent(parse("a*a"))
-    assert parse("([ab]*a|[bc]*c)?b*").equivalent(parse("b*(a[ab]*|c[bc]*)?"))
-
-###############################################################################
-# Test reversed()
-
-def test_regex_reversal():
-    assert reversed(parse("b")) == parse("b")
-    assert reversed(parse("e*")) == parse("e*")
-    assert reversed(parse("bear")) == parse("raeb")
-    assert reversed(parse("beer")) == parse("reeb")
-    assert reversed(parse("abc|def|ghi")) == parse("cba|fed|ihg")
-    assert reversed(parse("(abc)*d")) == parse("d(cba)*")
-
-###############################################################################
-# Bound and multiplier operation tests
-
-def test_bound():
-    assert min(bound(0), inf) == bound(0)
-    assert min(bound(1), inf) == bound(1)
-    assert qm.mandatory == bound(0)
-    assert qm.optional == bound(1)
-
-def test_multiplier_common():
-    assert zero.common(zero) == zero
-    assert zero.common(qm  ) == zero
-    assert zero.common(one ) == zero
-    assert zero.common(star) == zero
-    assert zero.common(plus) == zero
-    assert qm  .common(zero) == zero
-    assert qm  .common(qm  ) == qm
-    assert qm  .common(one ) == zero
-    assert qm  .common(star) == qm
-    assert qm  .common(plus) == qm
-    assert one .common(zero) == zero
-    assert one .common(qm  ) == zero
-    assert one .common(one ) == one
-    assert one .common(star) == zero
-    assert one .common(plus) == one
-    assert star.common(zero) == zero
-    assert star.common(qm  ) == qm
-    assert star.common(one ) == zero
-    assert star.common(star) == star
-    assert star.common(plus) == star
-    assert plus.common(zero) == zero
-    assert plus.common(qm  ) == qm
-    assert plus.common(one ) == one
-    assert plus.common(star) == star
-    assert plus.common(plus) == plus
-
-def test_multiplier_subtraction():
-    # a{3,4}, a{2,5} -> a{2,3} (with a{1,1}, a{0,2} left over)
-    assert multiplier.parse("{3,4}").common(multiplier.parse("{2,5}")) == multiplier.parse("{2,3}")
-    assert multiplier.parse("{3,4}") - multiplier.parse("{2,3}") == one
-    assert multiplier.parse("{2,5}") - multiplier.parse("{2,3}") == multiplier.parse("{0,2}")
-
-    # a{2,}, a{1,5} -> a{1,5} (with a{1,}, a{0,0} left over)
-    assert multiplier.parse("{2,}").common(multiplier.parse("{1,5}")) == multiplier.parse("{1,5}")
-    assert multiplier.parse("{2,}") - multiplier.parse("{1,5}") == plus
-    assert multiplier.parse("{1,5}") - multiplier.parse("{1,5}") == zero
-
-    # a{3,}, a{2,} -> a{2,} (with a, epsilon left over)
-    assert multiplier.parse("{3,}").common(multiplier.parse("{2,}")) == multiplier.parse("{2,}")
-    assert multiplier.parse("{3,}") - multiplier.parse("{2,}") == one
-    assert multiplier.parse("{2,}") - multiplier.parse("{2,}") == zero
-
-    # a{3,}, a{3,} -> a{3,} (with zero, zero left over)
-    assert multiplier.parse("{3,}").common(multiplier.parse("{3,}")) == multiplier.parse("{3,}")
-    assert multiplier.parse("{3,}") - multiplier.parse("{3,}") == zero
-
-def test_multiplier_union():
-    assert zero | zero == zero
-    assert zero | qm   == qm
-    assert zero | one  == qm
-    assert zero | star == star
-    assert zero | plus == star
-    assert qm   | zero == qm
-    assert qm   | qm   == qm
-    assert qm   | one  == qm
-    assert qm   | star == star
-    assert qm   | plus == star
-    assert one  | zero == qm
-    assert one  | qm   == qm
-    assert one  | one  == one
-    assert one  | star == star
-    assert one  | plus == plus
-    assert star | zero == star
-    assert star | qm   == star
-    assert star | one  == star
-    assert star | star == star
-    assert star | plus == star
-    assert plus | zero == star
-    assert plus | qm   == star
-    assert plus | one  == plus
-    assert plus | star == star
-    assert plus | plus == plus
-    assert not zero.canunion(multiplier(bound(2), inf))
-    assert not one.canunion(multiplier(bound(3), bound(4)))
-    assert not multiplier(bound(8), inf).canunion(multiplier(bound(3), bound(4)))
-    try:
-        zero | multiplier(bound(7), bound(8))
-        assert False
-    except AssertionError:
-        assert False
-    except Exception:
-        pass
-
-###############################################################################
-# Tests for some more set operations
-
-def test_set_ops():
-    assert parse("[abcd]") - parse("a") == charclass.parse("[bcd]")
-    assert parse("[abcd]") ^ parse("[cdef]") == charclass.parse("[abef]")
-
-###############################################################################
-# Concatenation tests (+)
-
-def test_concatenation():
-    assert charclass.parse("a") + charclass.parse("b") == conc.parse("ab")
-    assert charclass.parse("a") + mult.parse("b{0,8}") == conc.parse("ab{0,8}")
-    assert charclass.parse("a") + conc.parse("bc") == conc.parse("abc")
-    assert charclass.parse("a") + pattern.parse("b|cd") == conc.parse("a(b|cd)")
-    assert mult.parse("b{0,8}") + charclass.parse("c") == conc.parse("b{0,8}c")
-    assert mult.parse("a{3,4}") + mult.parse("b?") == conc.parse("a{3,4}b?")
-    assert mult.parse("a{2}") + conc.parse("bc") == conc.parse("a{2}bc")
-    assert mult.parse("a{2,3}") + pattern.parse("b|cd") == conc.parse("a{2,3}(b|cd)")
-    assert conc.parse("ab") + charclass.parse("c") == conc.parse("abc")
-    assert conc.parse("ab") + mult.parse("c*") == conc.parse("abc*")
-    assert conc.parse("") + conc.parse("") == conc.parse("")
-    assert conc.parse("ab") + conc.parse("cd") == conc.parse("abcd")
-    assert conc.parse("za{2,3}") + pattern.parse("b|cd") == conc.parse("za{2,3}(b|cd)")
-    assert pattern.parse("a|bd") + charclass.parse("c") == conc.parse("(a|bd)c")
-    assert pattern.parse("b|cd") + mult.parse("a{2,3}") == conc.parse("(b|cd)a{2,3}")
-    assert pattern.parse("b|cd") + conc.parse("za{2,3}") == conc.parse("(b|cd)za{2,3}")
-    assert pattern.parse("a|bc") + pattern.parse("c|de") == conc.parse("(a|bc)(c|de)")
-
-###############################################################################
-# Test methods for finding common parts of regular expressions.
-
-def test_mult_common():
-    assert mult.parse("a{3,4}").common(mult.parse("a{2,5}")) == mult.parse("a{2,3}")
-    assert mult.parse("a{2,}").common(mult.parse("a{1,5}")) == mult.parse("a{1,5}")
-    assert mult.parse("a{3,}").common(mult.parse("a{2,}")) == mult.parse("a{2,}")
-
-def test_conc_common():
-    assert conc.parse("AAZY").common(conc.parse("BBZY"), suffix=True) == conc.parse("ZY")
-    assert conc.parse("CZ").common(conc.parse("CZ"), suffix=True) == conc.parse("CZ")
-    assert conc.parse("CY").common(conc.parse("CZ"), suffix=True) == conc.parse("")
-    assert conc.parse("AZ").common(conc.parse("BZ"), suffix=True) == conc.parse("Z")
-    assert conc.parse("AZ*").common(conc.parse("BZ"), suffix=True) == conc.parse("")
-    assert conc.parse("A").common(conc.parse("B"), suffix=True) == conc.parse("")
-
-def test_pattern_commonconc():
-    assert pattern.parse("aa|aa")._commonconc() == conc.parse("aa")
-    assert pattern.parse("abc|aa")._commonconc() == conc.parse("a")
-    assert pattern.parse("a|bc")._commonconc() == conc.parse("")
-    assert pattern.parse("cf{1,2}|cf")._commonconc() == conc.parse("cf")
-    assert pattern.parse("ZA|ZB|ZC")._commonconc() == conc.parse("Z")
-    assert pattern.parse("Z+A|ZB|ZZC")._commonconc() == conc.parse("Z")
-    assert pattern.parse("a{2}b|a+c")._commonconc() == conc.parse("a")
-
-def test_pattern_commonconc_suffix():
-    assert pattern.parse("a|bc")._commonconc(suffix=True) == conc.parse("")
-    assert pattern.parse("aa|bca")._commonconc(suffix=True) == conc.parse("a")
-    assert pattern.parse("xyza|abca|a")._commonconc(suffix=True) == conc.parse("a")
-    assert pattern.parse("f{2,3}c|fc")._commonconc(suffix=True) == conc.parse("fc")
-    assert pattern.parse("aa")._commonconc(suffix=True) == conc.parse("aa")
-
-###############################################################################
-# Inverse concatenation tests: behead() and dock()
-
-def test_mult_dock():
-    assert mult.parse("a{4,5}").dock(mult.parse("a{3}")) == mult.parse("a{1,2}")
-
-def test_conc_dock():
-    assert conc.parse("AZ").dock(conc.parse("Z")) == conc.parse("A")
-    assert conc.parse("ABXY+Z").dock(conc.parse("XY+Z")) == conc.parse("AB")
-    assert conc.parse("ABXY+Z").behead(conc.parse("ABXY+")) == conc.parse("Z")
-    assert conc.parse("A").dock(conc.parse("")) == conc.parse("A")
-
-    try:
-        conc.parse("X{2}Y+Z").behead(conc.parse("XY+"))
-        assert False
-    except AssertionError:
-        assert False
-    except Exception:
-        pass
-
-def test_pattern_dock():
-    assert pattern.parse("a|bc").dock(conc.parse("")) == pattern.parse("a|bc")
-    assert pattern.parse("aa|bca").dock(conc.parse("a")) == pattern.parse("a|bc")
-    assert pattern.parse("xyza|abca|a").dock(conc.parse("a")) == pattern.parse("xyz|abc|")
-    assert pattern.parse("f{2,3}c|fc").dock(conc.parse("fc")) == pattern.parse("f{1,2}|")
-    assert pattern.parse("aa").dock(conc.parse("aa")) == pattern.parse("")
-
-def test_pattern_beheading():
-    assert pattern.parse("aa").behead(conc.parse("a")) == pattern.parse("a")
-    assert pattern.parse("abc|aa").behead(conc.parse("a")) == pattern.parse("a|bc")
-    assert pattern.parse("cf{1,2}|cf").behead(conc.parse("c")) == pattern.parse("f{1,2}|f")
-    assert pattern.parse("aa|aa").behead(conc.parse("aa")) == pattern.parse("")
-    assert pattern.parse("abc|aa").behead(conc.parse("a")) == pattern.parse("a|bc")
-    assert pattern.parse("a|bc").behead(conc.parse("")) == pattern.parse("a|bc")
-    assert pattern.parse("cf{1,2}|cf").behead(conc.parse("cf")) == pattern.parse("f?|")
-    assert pattern.parse("ZA|ZB|ZC").behead(conc.parse("Z")) == pattern.parse("A|B|C")
-    assert pattern.parse("Z+A|ZB|ZZC").behead(conc.parse("Z")) == pattern.parse("Z*A|B|ZC")
-    assert pattern.parse("a{2}b|a+c").behead(conc.parse("a")) == pattern.parse("ab|a*c")
-
-###############################################################################
-# Basic concatenation reduction tests
-
-def test_reduce_concatenations():
-    assert parse("aa").reduce() == mult.parse("a{2}")
-    assert parse("bb").reduce() == mult.parse("b{2}")
-    assert parse("b*b").reduce() == mult.parse("b+")
-    assert parse("aa{2,}").reduce() == mult.parse("a{3,}")
-    assert parse("a*a{2}").reduce() == mult.parse("a{2,}")
-    assert parse("aa{0,8}").reduce() == mult.parse("a{1,9}")
-    assert parse("b{0,8}b").reduce() == mult.parse("b{1,9}")
-    assert parse("aab").reduce() == conc.parse("a{2}b")
-    assert parse("abb").reduce() == conc.parse("ab{2}")
-    assert parse("abb*").reduce() == conc.parse("ab+")
-    assert parse("abbc").reduce() == conc.parse("ab{2}c")
-    assert parse("a?ab").reduce() == conc.parse("a{1,2}b")
-    assert parse("(ac{2}|bc+)c").reduce() == conc.parse("(ac|bc*)c{2}")
-    assert parse("a(a{2}b|a+c)").reduce() == conc.parse("a{2}(ab|a*c)")
-    assert parse("a{2,3}(a{2}b|a+c)").reduce() == conc.parse("a{3,4}(ab|a*c)")
-    assert parse("(ba{2}|ca+)a{2,3}").reduce() == conc.parse("(ba|ca*)a{3,4}")
-    assert parse("za{2,3}(a{2}b|a+c)").reduce() == conc.parse("za{3,4}(ab|a*c)")
-    assert parse("(ba{2}|ca+)a{2,3}z").reduce() == conc.parse("(ba|ca*)a{3,4}z")
-    assert parse("(a|bc)(a|bc)").reduce() == mult.parse("(a|bc){2}")
-    assert parse("a+[ab]+").reduce() == conc.parse("a[ab]+")
-    assert parse("a{3,8}[ab]+").reduce() == conc.parse("a{3}[ab]+")
-    assert parse("[ab]+b+").reduce() == conc.parse("[ab]+b")
-    assert parse("[ab]+a{3,8}").reduce() == conc.parse("[ab]+a{3}")
-    assert parse("\\d+\\w+").reduce() == conc.parse("\\d\\w+")
-    assert parse("[ab]+a?").reduce() == mult.parse("[ab]+")
-
-###############################################################################
-# Multiplication tests
-
-def test_charclass_multiplication():
-    assert charclass("a") * one == charclass("a")
-    assert charclass("a") * multiplier.parse("{1,3}") == mult.parse("a{1,3}")
-    assert charclass("a") * multiplier.parse("{4,}") == mult.parse("a{4,}")
-
-def test_mult_multiplication():
-    assert mult.parse("a{2,3}") * one == \
-        mult.parse("a{2,3}")
-    assert mult.parse("a{2,3}") * multiplier.parse("{4,5}") == \
-        mult.parse("a{8,15}")
-    assert mult.parse("a{2,}") * multiplier.parse("{2,}") == \
-        mult.parse("a{4,}")
-
-def test_conc_multiplication():
-    assert conc.parse("ab?") * qm == mult.parse("(ab?)?")
-
-def test_pattern_multiplication():
-    assert pattern.parse("ab?|ba?") * multiplier.parse("{2,3}") == \
-        mult.parse("(ab?|ba?){2,3}")
-    # TODO
-    #assert pattern.parse("(ab?|ba?)") * multiplier.parse("{2,3}") == \
-    #    mult.parse("(ab?|ba?){2,3}")
-
-def test_even_star_bug():
-    # Defect: (a{2})* should NOT reduce to a*
-    a2 = mult.parse("a{2}")
-    a2star = a2 * star
-    assert a2star == mult.parse("(a{2})*")
-
-def test_two_or_more_qm_bug():
-    assert mult.parse("a{2,}") * qm == mult.parse("(a{2,})?")
-
-def test_two_two_bug():
-    assert mult.parse("a{2}") * multiplier.parse("{2}") == mult.parse("a{4}")
-
-###############################################################################
-# Test intersection (&)
-
-def test_mult_intersection():
-    assert mult.parse("a") & mult.parse("b?") == charclass("")
-    assert mult.parse("a") & mult.parse("b?") == nothing
-    assert mult.parse("a") & mult.parse("a?") == charclass.parse("a")
-    assert mult.parse("a{2}") & mult.parse("a{2,}") == mult.parse("a{2}")
-    assert mult.parse("a") & mult.parse("b") == charclass.parse("[]")
-    assert mult.parse("a") & mult.parse("a") == charclass.parse("a")
-    assert mult.parse("a*") & mult.parse("a") == charclass.parse("a")
-    assert mult.parse("a*") & mult.parse("b*") == conc.parse("")
-    assert mult.parse("a*") & mult.parse("a+") == mult.parse("a+")
-    assert mult.parse("a{2}") & mult.parse("a{4}") == charclass.parse("[]")
-    assert mult.parse("a{3,}") & mult.parse("a{3,}") == mult.parse("a{3,}")
-
-def test_parse_regex_intersection():
-    assert str(parse("a*") & parse("b*")) == ""
-    assert str(parse("a") & parse("b")) == "[]"
-    assert str(parse("\\d") & parse(".")) == "\\d"
-    assert str(parse("\\d{2}") & parse("0.")) == "0\\d"
-    assert str(parse("\\d{2}") & parse("19.*")) == "19"
-    assert str(parse("\\d{3}") & parse("19.*")) == "19\\d"
-    assert str(parse("abc...") & parse("...def")) == "abcdef"
-    assert str(parse("[bc]*[ab]*") & parse("[ab]*[bc]*")) in {
-        "([ab]*a|[bc]*c)?b*",
-        "b*(a[ab]*|c[bc]*)?"
-    }
-    assert str(parse("\\W*")) == "\\W*"
-    assert str(parse("[a-g0-8$%\\^]+")) == "[$%0-8\\^a-g]+"
-    assert str(parse("[^d]{2,8}")) == "[^d]{2,8}"
-    assert str(parse("\\W*") & parse("[a-g0-8$%\\^]+")) == "[$%\\^]+"
-    assert str(parse("[ab]{1,2}") & parse("[^a]{1,2}")) == "b{1,2}"
-    assert str(parse("[ab]?") & parse("[^a]?")) == "b?"
-    assert parse("a{0,2}").matches("")
-    assert parse("[ab]{0,2}").matches("")
-    assert parse("[^a]{0,2}").matches("")
-    assert parse("b{0,2}").matches("")
-    assert str(parse("[ab]{0,2}") & parse("[^a]{0,2}")) == \
-        "b{0,2}"
-    assert str(parse("[ab]{0,4}") & parse("[^a]{0,4}")) == \
-        "b{0,4}"
-    assert str(parse("[abc]{0,8}") & parse("[^a]{0,8}")) == \
-        "[bc]{0,8}"
-    assert str(parse("[a-g0-8$%\\^]{0,8}") & parse("[^d]{0,8}")) == \
-        "[$%0-8\\^abcefg]{0,8}"
-    assert str(parse("[a-g0-8$%\\^]+") & parse("[^d]{0,8}")) == \
-        "[$%0-8\\^abcefg]{1,8}"
-    assert str(parse("[a-g0-8$%\\^]+") & parse("[^d]{2,8}")) == \
-        "[$%0-8\\^abcefg]{2,8}"
-    assert str(
-        parse("\\W*") &
-        parse("[a-g0-8$%\\^]+") &
-        parse("[^d]{2,8}")
-    ) == \
-        "[$%\\^]{2,8}"
-    assert str(parse("\\d{4}-\\d{2}-\\d{2}") & parse("19.*")) == \
-        "19\\d{2}-\\d{2}-\\d{2}"
-
-def test_complexify():
-    # Complexify!
-    gen = (parse("[bc]*[ab]*") & parse("[ab]*[bc]*")).strings()
-    assert next(gen) == ""
-    assert next(gen) == "a"
-    assert next(gen) == "b"
-    assert next(gen) == "c"
-    assert next(gen) == "aa"
-    assert next(gen) == "ab"
-    # no "ac"
-    assert next(gen) == "ba"
-    assert next(gen) == "bb"
-    assert next(gen) == "bc"
-    # no "ca"
-    assert next(gen) == "cb"
-    assert next(gen) == "cc"
-    assert next(gen) == "aaa"
-
-def test_silly_reduction():
-    # This one is horrendous and we have to jump through some hoops to get to
-    # a sensible result. Probably not a good unit test actually.
-    long = \
-    "(aa|bb*aa)a*|((ab|bb*ab)|(aa|bb*aa)a*b)((ab|bb*ab)|(aa|bb*aa)a*b)*" + \
-    "(aa|bb*aa)a*|((ab|bb*ab)|(aa|bb*aa)a*b)((ab|bb*ab)|(aa|bb*aa)a*b)*"
-    long = parse(long)
-    long = reversed(long.to_fsm())
-    long = reversed(from_fsm(long))
-    assert str(long) == "[ab]*a[ab]"
-    short = "[ab]*a?b*|[ab]*b?a*"
-    assert str(parse(".*") & parse(short)) == "[ab]*"
-
-###############################################################################
-# reduce() tests
-
-def test_mult_reduction_easy():
-    assert mult.parse("a").reduce() == charclass.parse("a")
-    assert mult.parse("a").reduce() == charclass("a")
-    assert mult.parse("a?").reduce() == mult(charclass("a"), qm)
-    assert mult.parse("a{0}").reduce() == emptystring
-    assert mult.parse("[]").reduce() == nothing
-    assert mult.parse("[]?").reduce() == emptystring
-    assert mult.parse("[]{0}").reduce() == emptystring
-    assert mult.parse("[]{0,5}").reduce() == emptystring
-    assert mult(pattern(), one).reduce() == nothing
-    assert mult(pattern(), qm).reduce() == emptystring
-    assert mult(pattern(), zero).reduce() == emptystring
-    assert mult(pattern(), multiplier.parse("{0,5}")).reduce() == emptystring
-
-
-def test_conc_reduction_basic():
-    assert conc.parse("a").reduce() == charclass.parse("a")
-    assert conc.parse("a{3,4}").reduce() == mult.parse("a{3,4}")
-    assert conc.parse("ab").reduce() == conc.parse("ab")
-    assert conc.parse("a[]b").reduce() == charclass.parse("[]")
-
-
-def test_pattern_reduce_basic():
-    assert pattern.parse("ab|cd").reduce() == pattern.parse("ab|cd")
-    assert pattern.parse("a{2}b{2}").reduce() == conc.parse("a{2}b{2}")
-    assert pattern.parse("a{2}").reduce() == mult.parse("a{2}")
-    assert pattern.parse("a").reduce() == charclass.parse("a")
-
-
-def test_empty_pattern_reduction():
-    assert pattern().reduce() == charclass("")
-
-
-def test_empty_mult_suppression():
-    assert conc.parse("[]0\\d").reduce() == charclass.parse("[]")
-    assert conc(
-        mult(pattern(), one), # this can never actually match anything
-        mult(charclass("0"), one),
-        mult(charclass("0123456789"), one),
-    ).reduce() == charclass.parse("[]")
-
-
-def test_empty_conc_suppression():
-    assert pattern.parse("[]0\\d").reduce() == charclass.parse("[]")
-    assert pattern(
-        conc(
-            mult(pattern(), one),  # this can never actually match anything
-            mult(charclass("0"), one),
-            mult(charclass("0123456789"), one),
-        ) # so neither can this conc
-    ).reduce() == charclass.parse("[]")
-
-
-def test_nested_pattern_reduction():
-    # a(d(ab|a*c)) -> ad(ab|a*c)
-    assert conc.parse("a(d(ab|a*c))").reduce() == conc.parse("ad(ab|a*c)")
-
-
-def test_mult_factor_out_qm():
-    # mult contains a pattern containing an empty conc? Pull the empty
-    # part out where it's external
-    assert mult.parse("(a|b*|)").reduce() == mult.parse("(a|b*)?")
-    assert conc.parse("(a|b*|)c").reduce() == conc.parse("(a|b*)?c")
-    # This happens even if emptystring is the only thing left inside the mult
-    assert mult.parse("()").reduce() == conc.parse("")
-    assert mult.parse("([$%\\^]|){1}").reduce() == mult.parse("[$%\\^]?")
-
-
-def test_remove_unnecessary_parens():
-    # mult contains a pattern containing a single conc containing a single
-    # mult? That can be reduced greatly
-    assert conc.parse("(a){2}b").reduce() == conc.parse("a{2}b")
-    assert conc.parse("(a?)+b").reduce() == conc.parse("a*b")
-    assert mult.parse("([ab])*").reduce() == mult.parse("[ab]*")
-    assert mult.parse("(c{1,2}){3,4}").reduce() == mult.parse("c{3,8}")
-
-
-def test_obvious_reduction():
-    assert mult.parse("(a|b)*").reduce() == mult.parse("[ab]*")
-
-
-def test_mult_squoosh():
-    # sequence squooshing of mults within a conc
-    assert conc.parse("[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]").reduce() == \
-        mult.parse("[0-9A-Fa-f]{3}")
-    assert conc.parse("[$%\\^]?[$%\\^]").reduce() == \
-        mult.parse("[$%\\^]{1,2}")
-    assert conc.parse(
-        "(|(|(|(|(|[$%\\^])[$%\\^])[$%\\^])[$%\\^])[$%\\^])[$%\\^][$%\\^]"
-    ).reduce() == \
-        mult.parse("[$%\\^]{2,7}")
-
-
-def test_bad_reduction_bug():
-    # DEFECT: "0{2}|1{2}" was erroneously reduced() to "[01]{2}"
-    assert parse("0{2}|1{2}").reduce() != \
-        parse("[01]{2}")
-    assert parse("0|[1-9]|ab").reduce() == \
-        pattern.parse("\\d|ab")
-    assert parse("0|[1-9]|a{5,7}").reduce() == \
-        pattern.parse("\\d|a{5,7}")
-    assert parse("0|(0|[1-9]|a{5,7})").reduce() == \
-        pattern.parse("0|(\\d|a{5,7})")
-    # TODO: should do better than this! Merge that 0
-
-
-def test_common_prefix_pattern_reduction():
-    assert pattern.parse("a{2}b|a+c").reduce() == conc.parse("a(ab|a*c)")
-
-
-def test_epsilon_reduction():
-    assert parse("|(ab)*|def").reduce() == pattern.parse("(ab)*|def")
-    assert parse("|(ab)+|def").reduce() == pattern.parse("(ab)*|def")
-    assert parse("|.+").reduce() == mult.parse(".*")
-    assert parse("|a+|b+").reduce() in {
-        pattern.parse("a+|b*"),
-        pattern.parse("a*|b+")
-    }
-
-
-def test_charclass_intersection_2():
-    assert (parse("[A-z]") & parse("[^g]")).reduce() == \
-        charclass.parse("[A-fh-z]")
-
-
-def test_reduce_boom():
-    # make sure recursion problem in reduce() has gone away
-    assert conc.parse("([1-9]|0)").reduce() == d
-
-
-def test_new_reduce():
-    # The @reduce_after decorator has been removed from many methods since it
-    # takes unnecessary time which the user may not wish to spend.
-    # This alters the behaviour of several methods and also exposes a new
-    # opportunity for conc.reduce()
-    assert conc.parse("a()").reduce() == charclass.parse("a")
-    assert conc.parse("a()()").reduce() == charclass.parse("a")
-    assert conc.parse("a.b()()").reduce() == conc.parse("a.b")
-
-
-def test_main_bug():
-    assert parse("a*").reduce() == \
-        mult.parse("a*")
-    assert parse("a|a*").reduce() == \
-        mult.parse("a*")
-    assert parse("a{1,2}|a{3,4}|bc").reduce() == \
-        pattern.parse("a{1,4}|bc")
-    assert parse("a{1,2}|bc|a{3,4}").reduce() == \
-        pattern.parse("a{1,4}|bc")
-    assert parse("a{1,2}|a{3,4}|a{5,6}|bc").reduce() == \
-        pattern.parse("a{1,6}|bc")
-    assert parse("a{1,2}|a{3}|a{5,6}").reduce() == \
-        conc.parse("a{1,2}(a?|a{4})")
-    assert parse("a{1,2}|a{3}|a{5,6}|bc").reduce() == \
-        pattern.parse("a{1,3}|a{5,6}|bc")
-    assert parse("a{1,2}|a{4}|a{5,6}").reduce() == \
-        conc.parse("a{1,2}(a{3,4})?")
-    assert parse("a{1,2}|a{4}|a{5,6}|bc").reduce() == \
-        pattern.parse("a{1,2}|a{4,6}|bc")
-    assert (parse("a") | parse("a*")).reduce() == \
-        mult.parse("a*")
-
-
-def test_bug_28_b():
-    # Defect in lego.to_fsm()
-    assert not parse("(ab*)*").to_fsm().accepts("bb")
-
-
-def test_derive():
-    assert parse("a+").derive("a") == mult.parse("a*")
-    assert parse("a+|b+").derive("a") == mult.parse("a*")
-    assert parse("abc|ade").derive("a") == pattern.parse("bc|de")
-    assert parse("abc|ade").derive("ab") == charclass.parse("c")
-
-
-def test_bug_36_1():
-    etc1 = parse(".*").to_fsm()
-    etc2 = parse("s.*").to_fsm()
-    assert etc1.accepts("s")
-    assert etc2.accepts("s")
-    assert not etc1.isdisjoint(etc2)
-    assert not etc2.isdisjoint(etc1)
-
-
-def test_bug_36_2():
-    etc1 = parse("/etc/.*").to_fsm()
-    etc2 = parse("/etc/something.*").to_fsm()
-    assert etc1.accepts("/etc/something")
-    assert etc2.accepts("/etc/something")
-    assert not etc1.isdisjoint(etc2)
-    assert not etc2.isdisjoint(etc1)
-
-
-def test_bug_slow():
-    # issue #43
-    import time
-    m = fsm.fsm(
-        alphabet={'R', 'L', 'U', 'D'},
-        states={
-            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
-            11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
-        initial=0,
-        finals={20},
-        map={
-            0: {'D': 1, 'U': 2},
-            1: {'L': 3},
-            2: {'L': 4},
-            3: {'U': 5},
-            4: {'D': 6},
-            5: {'R': 7},
-            6: {'R': 8},
-            7: {'U': 9},
-            8: {'D': 10},
-            9: {'L': 11},
-            10: {'L': 12},
-            11: {'L': 13},
-            12: {'L': 14},
-            13: {'D': 15},
-            14: {'U': 16},
-            15: {'R': 17},
-            16: {'R': 18},
-            17: {'D': 19},
-            18: {'U': 19},
-            19: {'L': 20},
-            20: {}
-        }
-    )
-    t1 = time.time()
-    ll = from_fsm(m)
-    t2 = time.time()
-    assert (t2 - t1) < 60  # should finish in way under 1s
-    assert ll == parse("(DLURULLDRD|ULDRDLLURU)L").reduce()
-
-
-def test_bug_48_simpler():
-    assert str(from_fsm(fsm.fsm(
-        alphabet={'d'},
-        states={0, 1},
-        initial=0,
-        finals={1},
-        map={
-            0: {'d': 1},
-        },
-    ))) == 'd'
-
-
-def test_bug_48():
-    S5, S26, S45, S63, S80, S97, S113, S127, S140, S152, S163, S175, S182 = \
-        range(13)
-    char0, char1, char2, char3, char4, char5, char6, char7, char8 = \
-        '_', 'a', 'd', 'e', 'g', 'm', 'n', 'o', 'p'
-
-    machine = fsm.fsm(
-        alphabet={
-            char0, char1, char2, char3, char4,
-            char5, char6, char7, char8
-        },
-        states={
-            S5, S26, S45, S63, S80, S97, S113,
-            S127, S140, S152, S163, S175, S182
-        },
-        initial=S5,
-        finals={S182},
-        map={
-            S113: {char0: S127},
-            S127: {char7: S140},
-            S140: {char6: S152},
-            S152: {char0: S163},
-            S163: {char5: S175},
-            S175: {char8: S182},
-            S26: {char1: S45},
-            S45: {char5: S63},
-            S5: {char2: S26},
-            S63: {char1: S80},
-            S80: {char4: S97},
-            S97: {char3: S113},
-        },
-    )
-
-    rex = from_fsm(machine)
-    assert str(rex) == 'damage_on_mp'
-
-
-def test_pickle():
-    f1 = parse("a{0,4}").to_fsm()
-    f2 = parse("a{0,3}").to_fsm()
-
-    assert f2 < f1
-
-    f1_unpickled = pickle.loads(pickle.dumps(f1))
-    f2_unpickled = pickle.loads(pickle.dumps(f2))
-
-    assert f2_unpickled < f1_unpickled
diff --git a/greenery/mult_test.py b/greenery/mult_test.py
new file mode 100644
index 0000000..beb036c
--- /dev/null
+++ b/greenery/mult_test.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+
+from .fsm import ANYTHING_ELSE
+from .charclass import Charclass, DIGIT
+from .bound import Bound, INF
+from .multiplier import Multiplier, ONE, QM, STAR, PLUS
+from .rxelems import Mult
+
+
+def test_mult_equality():
+    a = Mult(Charclass("a"), ONE)
+    assert a == a
+    assert a != Mult(Charclass("b"), ONE)
+    assert a != Mult(Charclass("a"), QM)
+    assert a != Mult(
+        Charclass("a"),
+        Multiplier(Bound(1), Bound(2))
+    )
+
+
+def test_mult_str():
+    a = Charclass("a")
+    assert str(Mult(a, ONE)) == "a"
+    assert str(Mult(a, Multiplier(Bound(2), Bound(2)))) == "a{2}"
+    assert str(Mult(a, Multiplier(Bound(3), Bound(3)))) == "a{3}"
+    assert str(Mult(a, Multiplier(Bound(4), Bound(4)))) == "a{4}"
+    assert str(Mult(a, Multiplier(Bound(5), Bound(5)))) == "a{5}"
+    assert str(Mult(a, QM)) == "a?"
+    assert str(Mult(a, STAR)) == "a*"
+    assert str(Mult(a, PLUS)) == "a+"
+    assert str(Mult(a, Multiplier(Bound(2), Bound(5)))) == "a{2,5}"
+    assert str(Mult(a, Multiplier(Bound(2), INF))) == "a{2,}"
+
+    assert str(Mult(DIGIT, ONE)) == "\\d"
+    assert str(Mult(DIGIT, Multiplier(Bound(2), Bound(2)))) == "\\d{2}"
+    assert str(Mult(DIGIT, Multiplier(Bound(3), Bound(3)))) == "\\d{3}"
+
+
+def test_odd_bug():
+    # Odd bug with ([bc]*c)?[ab]*
+    int5A = Mult(
+        Charclass("bc"),
+        STAR,
+    ).to_fsm({"a", "b", "c", ANYTHING_ELSE})
+    assert int5A.accepts([])
+    assert int5A.accepts("")
+
+    int5B = Mult(
+        Charclass("c"),
+        ONE,
+    ).to_fsm({"a", "b", "c", ANYTHING_ELSE})
+    assert int5B.accepts("c")
+    assert int5B.accepts(["c"])
+
+    int5C = int5A + int5B
+    assert int5C.accepts("c")
+    assert int5C.accepts(["c"])
+
+
+def test_mult_common():
+    a = Charclass("a")
+    assert Mult(a, Multiplier(Bound(3), Bound(4))) \
+        .common(Mult(a, Multiplier(Bound(2), Bound(5)))) == \
+        Mult(a, Multiplier(Bound(2), Bound(3)))
+    assert Mult(a, Multiplier(Bound(2), INF)) \
+        .common(Mult(a, Multiplier(Bound(1), Bound(5)))) == \
+        Mult(a, Multiplier(Bound(1), Bound(5)))
+    assert Mult(a, Multiplier(Bound(3), INF)) \
+        .common(Mult(a, Multiplier(Bound(2), INF))) == \
+        Mult(a, Multiplier(Bound(2), INF))
+
+
+def test_mult_dock():
+    a = Charclass("a")
+    assert Mult(a, Multiplier(Bound(4), Bound(5))) \
+        .dock(Mult(a, Multiplier(Bound(3), Bound(3)))) == \
+        Mult(a, Multiplier(Bound(1), Bound(2)))
diff --git a/greenery/multiplier_test.py b/greenery/multiplier_test.py
new file mode 100644
index 0000000..671d6d3
--- /dev/null
+++ b/greenery/multiplier_test.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+
+from .bound import Bound, INF
+from .multiplier import Multiplier, ZERO, ONE, QM, STAR, PLUS
+
+
+def test_multiplier_str():
+    assert str(Multiplier(Bound(2), INF)) == "{2,}"
+
+
+def test_bound_qm():
+    assert QM.mandatory == Bound(0)
+    assert QM.optional == Bound(1)
+
+
+def test_multiplier_common():
+    assert ZERO.common(ZERO) == ZERO
+    assert ZERO.common(QM) == ZERO
+    assert ZERO.common(ONE) == ZERO
+    assert ZERO.common(STAR) == ZERO
+    assert ZERO.common(PLUS) == ZERO
+    assert QM.common(ZERO) == ZERO
+    assert QM.common(QM) == QM
+    assert QM.common(ONE) == ZERO
+    assert QM.common(STAR) == QM
+    assert QM.common(PLUS) == QM
+    assert ONE.common(ZERO) == ZERO
+    assert ONE.common(QM) == ZERO
+    assert ONE.common(ONE) == ONE
+    assert ONE.common(STAR) == ZERO
+    assert ONE.common(PLUS) == ONE
+    assert STAR.common(ZERO) == ZERO
+    assert STAR.common(QM) == QM
+    assert STAR.common(ONE) == ZERO
+    assert STAR.common(STAR) == STAR
+    assert STAR.common(PLUS) == STAR
+    assert PLUS.common(ZERO) == ZERO
+    assert PLUS.common(QM) == QM
+    assert PLUS.common(ONE) == ONE
+    assert PLUS.common(STAR) == STAR
+    assert PLUS.common(PLUS) == PLUS
+
+
+def test_multiplier_subtraction():
+    # a{3,4}, a{2,5} -> a{2,3} (with a{1,1}, a{0,2} left over)
+    assert Multiplier(Bound(3), Bound(4)) \
+        .common(Multiplier(Bound(2), Bound(5))) \
+        == Multiplier(Bound(2), Bound(3))
+    assert Multiplier(Bound(3), Bound(4)) - Multiplier(Bound(2), Bound(3)) \
+        == ONE
+    assert Multiplier(Bound(2), Bound(5)) - Multiplier(Bound(2), Bound(3)) \
+        == Multiplier(Bound(0), Bound(2))
+
+    # a{2,}, a{1,5} -> a{1,5} (with a{1,}, a{0,0} left over)
+    assert Multiplier(Bound(2), INF).common(Multiplier(Bound(1), Bound(5))) \
+        == Multiplier(Bound(1), Bound(5))
+    assert Multiplier(Bound(2), INF) - Multiplier(Bound(1), Bound(5)) \
+        == PLUS
+    assert Multiplier(Bound(1), Bound(5)) - Multiplier(Bound(1), Bound(5)) \
+        == ZERO
+
+    # a{3,}, a{2,} -> a{2,} (with a, epsilon left over)
+    assert Multiplier(Bound(3), INF).common(Multiplier(Bound(2), INF)) \
+        == Multiplier(Bound(2), INF)
+    assert Multiplier(Bound(3), INF) - Multiplier(Bound(2), INF) == ONE
+    assert Multiplier(Bound(2), INF) - Multiplier(Bound(2), INF) == ZERO
+
+    # a{3,}, a{3,} -> a{3,} (with ZERO, ZERO left over)
+    assert Multiplier(Bound(3), INF).common(Multiplier(Bound(3), INF)) \
+        == Multiplier(Bound(3), INF)
+    assert Multiplier(Bound(3), INF) - Multiplier(Bound(3), INF) == ZERO
+
+
+def test_multiplier_union():
+    assert ZERO | ZERO == ZERO
+    assert ZERO | QM == QM
+    assert ZERO | ONE == QM
+    assert ZERO | STAR == STAR
+    assert ZERO | PLUS == STAR
+    assert QM | ZERO == QM
+    assert QM | QM == QM
+    assert QM | ONE == QM
+    assert QM | STAR == STAR
+    assert QM | PLUS == STAR
+    assert ONE | ZERO == QM
+    assert ONE | QM == QM
+    assert ONE | ONE == ONE
+    assert ONE | STAR == STAR
+    assert ONE | PLUS == PLUS
+    assert STAR | ZERO == STAR
+    assert STAR | QM == STAR
+    assert STAR | ONE == STAR
+    assert STAR | STAR == STAR
+    assert STAR | PLUS == STAR
+    assert PLUS | ZERO == STAR
+    assert PLUS | QM == STAR
+    assert PLUS | ONE == PLUS
+    assert PLUS | STAR == STAR
+    assert PLUS | PLUS == PLUS
+    assert not ZERO.canunion(Multiplier(Bound(2), INF))
+    assert not ONE.canunion(Multiplier(Bound(3), Bound(4)))
+    assert not Multiplier(
+        Bound(8),
+        INF
+    ).canunion(
+        Multiplier(
+            Bound(3),
+            Bound(4)
+        )
+    )
+    try:
+        ZERO | Multiplier(Bound(7), Bound(8))
+        assert False
+    except AssertionError:
+        assert False
+    except Exception:
+        pass
diff --git a/greenery/parse_test.py b/greenery/parse_test.py
new file mode 100644
index 0000000..0c999b5
--- /dev/null
+++ b/greenery/parse_test.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+
+if __name__ == "__main__":
+    raise Exception(
+        "Test files can't be run directly. Use `python -m pytest greenery`"
+    )
+
+from .bound import Bound, INF
+from .charclass import Charclass, DOT, NULLCHARCLASS, DIGIT
+from .multiplier import Multiplier, ONE, STAR, PLUS
+from .rxelems import Mult, Conc, Pattern
+from .parse import NoMatch, match_charclass, parse, match_mult
+
+
+def test_charclass_matching():
+    assert match_charclass("a", 0) == (Charclass("a"), 1)
+    assert match_charclass("aa", 1) == (Charclass("a"), 2)
+    assert match_charclass("a$", 1) == (Charclass("$"), 2)
+    assert match_charclass(".", 0) == (DOT, 1)
+    try:
+        match_charclass("[", 0)
+        assert False
+    except IndexError:
+        pass
+    try:
+        match_charclass("a", 1)
+        assert False
+    except NoMatch:
+        pass
+    assert match_charclass("[\\d]", 0) == (DIGIT, 4)
+
+
+def test_negatives_inside_charclasses():
+    assert match_charclass("[\\D]", 0) == (~DIGIT, 4)
+    assert match_charclass("[a\\D]", 0) == (~DIGIT, 5)
+    assert match_charclass("[a1\\D]", 0) == (~Charclass("023456789"), 6)
+    assert match_charclass("[1a\\D]", 0) == (~Charclass("023456789"), 6)
+    assert match_charclass("[1\\D]", 0) == (~Charclass("023456789"), 5)
+    assert match_charclass("[\\Da]", 0) == (~DIGIT, 5)
+    assert match_charclass("[\\D1]", 0) == (~Charclass("023456789"), 5)
+    assert match_charclass("[\\D1a]", 0) == (~Charclass("023456789"), 6)
+    assert match_charclass("[\\D\\d]", 0) == (DOT, 6)
+    assert match_charclass("[\\D\\D]", 0) == (~DIGIT, 6)
+    assert match_charclass("[\\S\\D]", 0) == \
+        (~Charclass("\t\n\v\f\r 0123456789"), 6)
+    assert match_charclass("[\\S \\D]", 0) == \
+        (~Charclass("\t\n\v\f\r0123456789"), 7)
+
+
+def test_negated_negatives_inside_charclasses():
+    assert match_charclass("[^\\D]", 0) == (DIGIT, 5)
+    assert match_charclass("[^a\\D]", 0) == (DIGIT, 6)
+    assert match_charclass("[^a1\\D]", 0) == (Charclass("023456789"), 7)
+    assert match_charclass("[^1a\\D]", 0) == (Charclass("023456789"), 7)
+    assert match_charclass("[^1\\D]", 0) == (Charclass("023456789"), 6)
+    assert match_charclass("[^\\Da]", 0) == (DIGIT, 6)
+    assert match_charclass("[^\\D1]", 0) == (Charclass("023456789"), 6)
+    assert match_charclass("[^\\D1a]", 0) == (Charclass("023456789"), 7)
+    assert match_charclass("[^\\D\\d]", 0) == (NULLCHARCLASS, 7)
+    assert match_charclass("[^\\D\\D]", 0) == (DIGIT, 7)
+    assert match_charclass("[^\\S\\D]", 0) == \
+        (Charclass("\t\n\v\f\r 0123456789"), 7)
+    assert match_charclass("[^\\S \\D]", 0) == \
+        (Charclass("\t\n\v\f\r0123456789"), 8)
+
+
+def test_mult_matching():
+    assert match_mult("abcde[^fg]*", 5) == (
+        Mult(~Charclass("fg"), STAR),
+        11
+    )
+    assert match_mult("abcde[^fg]*h{5}[a-z]+", 11) == (
+        Mult(Charclass("h"), Multiplier(Bound(5), Bound(5))),
+        15
+    )
+    assert match_mult("abcde[^fg]*h{5}[a-z]+T{1,}", 15) == (
+        Mult(Charclass("abcdefghijklmnopqrstuvwxyz"), PLUS),
+        21
+    )
+    assert match_mult("abcde[^fg]*h{5}[a-z]+T{2,}", 21) == (
+        Mult(Charclass("T"), Multiplier(Bound(2), INF)),
+        26
+    )
+
+
+def test_charclass_ranges():
+    # Should accept arbitrary ranges of characters in charclasses. No longer
+    # limited to alphanumerics. (User beware...)
+    assert parse("[z{|}~]") == parse("[z-~]")
+    assert parse("[\\w:;<=>?@\\[\\\\\\]\\^`]") == parse("[0-z]")
+
+
+def test_hex_escapes():
+    # Should be able to parse e.g. "\\x40"
+    assert parse("\\x00") == parse("\x00")
+    assert parse("\\x40") == parse("@")
+    assert parse("[\\x40]") == parse("[@]")
+    assert parse("[\\x41-\\x5a]") == parse("[A-Z]")
+
+
+def test_w_d_s():
+    # Allow "\w", "\d" and "\s" in charclasses
+    assert parse("\\w") == parse("[0-9A-Z_a-z]")
+    assert parse("[\\w~]") == parse("[0-9A-Z_a-z~]")
+    assert parse("[\\da]") == parse("[0123456789a]")
+    assert parse("[\\s]") == parse("[\t\n\r\f\v ]")
+
+
+def test_mult_parsing():
+    assert parse("[a-g]+") == Pattern(
+        Conc(
+            Mult(
+                Charclass("abcdefg"),
+                PLUS
+            )
+        )
+    )
+    assert parse("[a-g0-8$%]+") == Pattern(
+        Conc(
+            Mult(
+                Charclass("abcdefg012345678$%"),
+                PLUS
+            )
+        )
+    )
+    assert parse("[a-g0-8$%\\^]+") == Pattern(
+        Conc(
+            Mult(
+                Charclass("abcdefg012345678$%^"),
+                PLUS
+            )
+        )
+    )
+
+
+def test_conc_parsing():
+    assert parse("abcde[^fg]*h{5}[a-z]+") == Pattern(
+        Conc(
+            Mult(Charclass("a"), ONE),
+            Mult(Charclass("b"), ONE),
+            Mult(Charclass("c"), ONE),
+            Mult(Charclass("d"), ONE),
+            Mult(Charclass("e"), ONE),
+            Mult(~Charclass("fg"), STAR),
+            Mult(Charclass("h"), Multiplier(Bound(5), Bound(5))),
+            Mult(Charclass("abcdefghijklmnopqrstuvwxyz"), PLUS),
+        )
+    )
+    assert parse("[bc]*[ab]*") == Pattern(
+        Conc(
+            Mult(Charclass("bc"), STAR),
+            Mult(Charclass("ab"), STAR),
+        )
+    )
+    assert parse("abc...") == Pattern(
+        Conc(
+            Mult(Charclass("a"), ONE),
+            Mult(Charclass("b"), ONE),
+            Mult(Charclass("c"), ONE),
+            Mult(DOT, ONE),
+            Mult(DOT, ONE),
+            Mult(DOT, ONE),
+        )
+    )
+    assert parse("\\d{4}-\\d{2}-\\d{2}") == Pattern(
+        Conc(
+            Mult(DIGIT, Multiplier(Bound(4), Bound(4))),
+            Mult(Charclass("-"), ONE),
+            Mult(DIGIT, Multiplier(Bound(2), Bound(2))),
+            Mult(Charclass("-"), ONE),
+            Mult(DIGIT, Multiplier(Bound(2), Bound(2))),
+        )
+    )
+
+
+def test_pattern_parsing():
+    assert parse("abc|def(ghi|jkl)") == Pattern(
+        Conc(
+            Mult(Charclass("a"), ONE),
+            Mult(Charclass("b"), ONE),
+            Mult(Charclass("c"), ONE),
+        ),
+        Conc(
+            Mult(Charclass("d"), ONE),
+            Mult(Charclass("e"), ONE),
+            Mult(Charclass("f"), ONE),
+            Mult(
+                Pattern(
+                    Conc(
+                        Mult(Charclass("g"), ONE),
+                        Mult(Charclass("h"), ONE),
+                        Mult(Charclass("i"), ONE),
+                    ),
+                    Conc(
+                        Mult(Charclass("j"), ONE),
+                        Mult(Charclass("k"), ONE),
+                        Mult(Charclass("l"), ONE),
+                    ),
+                ),
+                ONE,
+            ),
+        ),
+    )
+
+    # Accept the "non-capturing group" syntax, "(?: ... )" but give it no
+    # special significance
+    assert parse("(?:)") == parse("()")
+    assert parse("(?:abc|def)") == parse("(abc|def)")
+    parse("(:abc)")  # should give no problems
+
+    # Named groups
+    assert parse("(?P<ng1>abc)") == parse("(abc)")
diff --git a/greenery/pattern_test.py b/greenery/pattern_test.py
new file mode 100644
index 0000000..e0d1fa7
--- /dev/null
+++ b/greenery/pattern_test.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+
+from .charclass import Charclass
+from .multiplier import ONE, ZERO
+from .rxelems import Pattern, Conc, Mult
+from .parse import parse
+
+
+def test_pattern_equality():
+    assert Pattern(
+        Conc(Mult(Charclass("a"), ONE)),
+        Conc(Mult(Charclass("b"), ONE)),
+    ) == Pattern(
+        Conc(Mult(Charclass("b"), ONE)),
+        Conc(Mult(Charclass("a"), ONE)),
+    )
+    assert Pattern(
+        Conc(Mult(Charclass("a"), ONE)),
+        Conc(Mult(Charclass("a"), ONE)),
+    ) == Pattern(
+        Conc(Mult(Charclass("a"), ONE)),
+    )
+
+
+def test_pattern_str():
+    assert str(Pattern(
+        Conc(Mult(Charclass("a"), ONE)),
+        Conc(Mult(Charclass("b"), ONE)),
+    )) == "a|b"
+    assert str(Pattern(
+        Conc(Mult(Charclass("a"), ONE)),
+        Conc(Mult(Charclass("a"), ONE)),
+    )) == "a"
+    assert str(Pattern(
+        Conc(
+            Mult(Charclass("a"), ONE),
+            Mult(Charclass("b"), ONE),
+            Mult(Charclass("c"), ONE),
+        ),
+        Conc(
+            Mult(Charclass("d"), ONE),
+            Mult(Charclass("e"), ONE),
+            Mult(Charclass("f"), ONE),
+            Mult(
+                Pattern(
+                    Conc(
+                        Mult(Charclass("g"), ONE),
+                        Mult(Charclass("h"), ONE),
+                        Mult(Charclass("i"), ONE),
+                    ),
+                    Conc(
+                        Mult(Charclass("j"), ONE),
+                        Mult(Charclass("k"), ONE),
+                        Mult(Charclass("l"), ONE),
+                    ),
+                ),
+                ONE,
+            ),
+        ),
+    )) == "abc|def(ghi|jkl)"
+
+
+def test_empty():
+    assert Pattern().empty()
+
+
+def test_mult_reduction_easy():
+    assert Pattern(Conc()).reduce() == Pattern(Conc())
+    assert Pattern(
+        Conc(
+            Mult(
+                Charclass("a"),
+                ZERO,
+            )
+        )
+    ).reduce() == Pattern(
+        Conc()
+    )
+    assert str(
+        Pattern(
+            Conc(
+                Mult(
+                    Charclass("a"),
+                    ZERO,
+                )
+            )
+        ).reduce()
+    ) == ""
+
+
+def test_empty_pattern_reduction():
+    assert str(Pattern().reduce()) == "[]"
+
+
+def test_empty_conc_suppression():
+    assert str(Pattern(
+        Conc(
+            # this `Mult` can never actually match anything
+            Mult(Pattern(), ONE),
+            Mult(Charclass("0"), ONE),
+            Mult(Charclass("0123456789"), ONE),
+        )  # so neither can this `Conc`
+    ).reduce()) == "[]"
+
+
+def test_pattern_dock():
+    a = Mult(Charclass("a"), ONE)
+    c = Mult(Charclass("c"), ONE)
+    f = Mult(Charclass("f"), ONE)
+
+    assert parse("a|bc").dock(Conc()) == parse("a|bc")
+    assert parse("aa|bca").dock(Conc(a)) == parse("a|bc")
+    assert parse("xyza|abca|a").dock(Conc(a)) == parse("xyz|abc|")
+    assert parse("f{2,3}c|fc").dock(Conc(f, c)) == parse("f{1,2}|")
+    assert parse("aa").dock(Conc(a, a)) == parse("")
+
+
+def test_pattern_beheading():
+    a = Mult(Charclass("a"), ONE)
+    c = Mult(Charclass("c"), ONE)
+    f = Mult(Charclass("f"), ONE)
+    z = Mult(Charclass("Z"), ONE)
+
+    assert parse("aa").behead(Conc(a)) == parse("a")
+    assert parse("abc|aa").behead(Conc(a)) == parse("a|bc")
+    assert parse("cf{1,2}|cf").behead(Conc(c)) == parse("f{1,2}|f")
+    assert parse("aa|aa").behead(Conc(a, a)) == parse("")
+    assert parse("abc|aa").behead(Conc(a)) == parse("a|bc")
+    assert parse("a|bc").behead(Conc()) == parse("a|bc")
+    assert parse("cf{1,2}|cf").behead(Conc(c, f)) == parse("f?|")
+    assert parse("ZA|ZB|ZC").behead(Conc(z)) == parse("A|B|C")
+    assert parse("Z+A|ZB|ZZC").behead(Conc(z)) == parse("Z*A|B|ZC")
+    assert parse("a{2}b|a+c").behead(Conc(a)) == parse("ab|a*c")
diff --git a/greenery/rxelems_test.py b/greenery/rxelems_test.py
new file mode 100644
index 0000000..44621e8
--- /dev/null
+++ b/greenery/rxelems_test.py
@@ -0,0 +1,1039 @@
+# -*- coding: utf-8 -*-
+
+if __name__ == "__main__":
+    raise Exception(
+        "Test files can't be run directly. Use `python -m pytest greenery`"
+    )
+
+import pickle
+
+from .fsm import Fsm, ANYTHING_ELSE
+from .rxelems import from_fsm
+from .charclass import DIGIT, WORDCHAR
+from .parse import parse
+
+
+###############################################################################
+# Stringification tests
+
+def test_charclass_str():
+    # Arbitrary ranges
+    assert str(parse("[\\w:;<=>?@\\[\\\\\\]\\^`]")) == "[0-z]"
+    # TODO: what if \d is a proper subset of `chars`?
+
+    # escape sequences are not preserved
+    assert str(parse("\\x09")) == "\\t"
+
+    # Printing ASCII control characters? You should get hex escapes
+    assert str(parse("\\x00")) == "\\x00"
+
+
+def test_parse_str_round_trip():
+    # not "a[ab]b"
+    assert str(parse("a.b")) == "a.b"
+    assert str(parse("\\d{4}")) == "\\d{4}"
+    assert str(parse("a.b()()")) == "a.b()()"
+
+
+###############################################################################
+# Test to_fsm() and alphabet-related functionality
+
+def test_alphabet():
+    # `.alphabet()` should include `ANYTHING_ELSE`
+    assert parse("").alphabet() == {ANYTHING_ELSE}
+
+
+def test_pattern_fsm():
+    # "a[^a]"
+    anota = parse("a[^a]").to_fsm()
+    assert len(anota.states) == 3
+    assert not anota.accepts("a")
+    assert not anota.accepts(["a"])
+    assert not anota.accepts("b")
+    assert not anota.accepts(["b"])
+    assert not anota.accepts("aa")
+    assert not anota.accepts(["a", "a"])
+    assert anota.accepts("ab")
+    assert anota.accepts(["a", "b"])
+    assert anota.accepts(["a", ANYTHING_ELSE])
+    assert not anota.accepts("ba")
+    assert not anota.accepts("bb")
+
+    # "0\\d"
+    zeroD = parse("0\\d").to_fsm(DIGIT.chars)
+    assert zeroD.accepts("01")
+    assert not zeroD.accepts("10")
+
+    # "\\d{2}"
+    d2 = parse("\\d{2}").to_fsm(DIGIT.chars)
+    assert not d2.accepts("")
+    assert not d2.accepts("1")
+    assert d2.accepts("11")
+    assert not d2.accepts("111")
+
+    # abc|def(ghi|jkl)
+    conventional = parse("abc|def(ghi|jkl)").to_fsm(WORDCHAR.chars)
+    assert not conventional.accepts("a")
+    assert not conventional.accepts("ab")
+    assert conventional.accepts("abc")
+    assert not conventional.accepts("abcj")
+    assert conventional.accepts("defghi")
+    assert conventional.accepts("defjkl")
+
+
+def test_fsm():
+    # You should be able to to_fsm() a single regular expression element
+    # without supplying a specific alphabet. That should be determinable from
+    # context.
+    assert parse("a.b").to_fsm().accepts("acb")
+
+    bad = parse("0{2}|1{2}").to_fsm({"0", "1", ANYTHING_ELSE})
+    assert bad.accepts("00")
+    assert bad.accepts("11")
+    assert not bad.accepts("01")
+
+    bad = parse("0{2}|1{2}").to_fsm()
+    assert bad.accepts("00")
+    assert bad.accepts("11")
+    assert not bad.accepts("01")
+
+
+def test_bug_28():
+    # Starification was broken in FSMs
+    assert not parse("(ab*)").to_fsm().star().accepts("bb")
+    assert not parse("(ab*)*").to_fsm().accepts("bb")
+
+
+###############################################################################
+# Test matches(). Quite sparse at the moment
+
+def test_wildcards_in_charclasses():
+    # Allow "\w", "\d" and "\s" in `Charclass`es
+    assert parse("[\\w~]*").matches("a0~")
+    assert parse("[\\da]*").matches("0129a")
+    assert parse("[\\s]+").matches(" \t \t ")
+
+
+def test_block_comment_regex():
+    # I went through several incorrect regexes for C block comments. Here we
+    # show why the first few attempts were incorrect
+    a = parse("/\\*(([^*]|\\*+[^*/])*)\\*/")
+    assert a.matches("/**/")
+    assert not a.matches("/***/")
+    assert not a.matches("/****/")
+
+    b = parse("/\\*(([^*]|\\*[^/])*)\\*/")
+    assert b.matches("/**/")
+    assert not b.matches("/***/")
+    assert b.matches("/****/")
+
+    c = parse("/\\*(([^*]|\\*+[^*/])*)\\*+/")
+    assert c.matches("/**/")
+    assert c.matches("/***/")
+    assert c.matches("/****/")
+
+
+def test_named_groups():
+    a = parse("(?P<ng1>abc)")
+    assert a.matches("abc")
+
+
+def test_in():
+    assert "a" in parse("a")
+    assert "abcdsasda" in parse("\\w{4,10}")
+    assert "abc" in parse("abc|def(ghi|jkl)")
+
+
+###############################################################################
+# Test string generators
+
+def test_charclass_gen():
+    gen = parse("[xyz]").strings()
+    assert next(gen) == "x"
+    assert next(gen) == "y"
+    assert next(gen) == "z"
+    try:
+        next(gen)
+        assert False
+    except StopIteration:
+        assert True
+
+
+def test_mult_gen():
+    # One term
+    gen = parse("[ab]").strings()
+    assert next(gen) == "a"
+    assert next(gen) == "b"
+    try:
+        next(gen)
+        assert False
+    except StopIteration:
+        assert True
+
+    # No terms
+    gen = parse("[ab]{0}").strings()
+    assert next(gen) == ""
+    try:
+        next(gen)
+        assert False
+    except StopIteration:
+        assert True
+
+    # Many terms
+    gen = parse("[ab]*").strings()
+    assert next(gen) == ""
+    assert next(gen) == "a"
+    assert next(gen) == "b"
+    assert next(gen) == "aa"
+    assert next(gen) == "ab"
+    assert next(gen) == "ba"
+    assert next(gen) == "bb"
+    assert next(gen) == "aaa"
+
+
+def test_conc_generator():
+    gen = parse("[ab][cd]").strings()
+    assert next(gen) == "ac"
+    assert next(gen) == "ad"
+    assert next(gen) == "bc"
+    assert next(gen) == "bd"
+    try:
+        next(gen)
+        assert False
+    except StopIteration:
+        assert True
+
+
+def test_pattern_generator():
+    gen = parse("[ab]|[cde]").strings()
+    assert next(gen) == "a"
+    assert next(gen) == "b"
+    assert next(gen) == "c"
+    assert next(gen) == "d"
+    assert next(gen) == "e"
+    try:
+        next(gen)
+        assert False
+    except StopIteration:
+        assert True
+
+    # more complex
+    gen = parse("abc|def(ghi|jkl)").strings()
+    assert next(gen) == "abc"
+    assert next(gen) == "defghi"
+    assert next(gen) == "defjkl"
+
+    gen = parse("[0-9a-fA-F]{3,10}").strings()
+    assert next(gen) == "000"
+    assert next(gen) == "001"
+    assert next(gen) == "002"
+
+
+def test_infinite_generation():
+    # Infinite generator, flummoxes both depth-first and breadth-first searches
+    gen = parse("a*b*").strings()
+    assert next(gen) == ""
+    assert next(gen) == "a"
+    assert next(gen) == "b"
+    assert next(gen) == "aa"
+    assert next(gen) == "ab"
+    assert next(gen) == "bb"
+    assert next(gen) == "aaa"
+    assert next(gen) == "aab"
+    assert next(gen) == "abb"
+    assert next(gen) == "bbb"
+    assert next(gen) == "aaaa"
+
+
+def test_wildcard_generator():
+    # Generator needs to handle wildcards as well. Wildcards come last.
+    gen = parse("a.b").strings(otherchar="*")
+    assert next(gen) == "aab"
+    assert next(gen) == "abb"
+    assert next(gen) == "a*b"
+    try:
+        next(gen)
+        assert False
+    except StopIteration:
+        assert True
+
+
+def test_forin():
+    assert [s for s in parse("abc|def(ghi|jkl)")] \
+        == ["abc", "defghi", "defjkl"]
+
+
+###############################################################################
+# Test cardinality() and len()
+
+def test_cardinality():
+    assert parse("[]").cardinality() == 0
+    assert parse("[]?").cardinality() == 1
+    assert parse("[]{0,6}").cardinality() == 1
+    assert parse("[ab]{3}").cardinality() == 8
+    assert parse("[ab]{2,3}").cardinality() == 12
+    assert len(parse("abc|def(ghi|jkl)")) == 3
+    try:
+        len(parse(".*"))
+        assert False
+    except OverflowError:
+        assert True
+
+
+###############################################################################
+
+def test_copy():
+    x = parse("abc|def(ghi|jkl)")
+    assert x.copy() == x
+
+
+###############################################################################
+# Test from_fsm()
+
+def test_dot():
+    # not "a[ab]b"
+    assert str(from_fsm(parse("a.b").to_fsm())) == "a.b"
+
+
+def test_abstar():
+    # Buggggs.
+    abstar = Fsm(
+        alphabet={'a', ANYTHING_ELSE, 'b'},
+        states={0, 1},
+        initial=0,
+        finals={0},
+        map={
+            0: {'a': 0, ANYTHING_ELSE: 1, 'b': 0},
+            1: {'a': 1, ANYTHING_ELSE: 1, 'b': 1}
+        }
+    )
+    assert str(from_fsm(abstar)) == "[ab]*"
+
+
+def test_adotb():
+    adotb = Fsm(
+        alphabet={'a', ANYTHING_ELSE, 'b'},
+        states={0, 1, 2, 3, 4},
+        initial=0,
+        finals={4},
+        map={
+            0: {'a': 2, ANYTHING_ELSE: 1, 'b': 1},
+            1: {'a': 1, ANYTHING_ELSE: 1, 'b': 1},
+            2: {'a': 3, ANYTHING_ELSE: 3, 'b': 3},
+            3: {'a': 1, ANYTHING_ELSE: 1, 'b': 4},
+            4: {'a': 1, ANYTHING_ELSE: 1, 'b': 1}
+        }
+    )
+    assert str(from_fsm(adotb)) == "a.b"
+
+
+def test_rxelems_recursion_error():
+    # Catch a recursion error
+    assert str(from_fsm(Fsm(
+        alphabet={"0", "1"},
+        states={0, 1, 2, 3},
+        initial=3,
+        finals={1},
+        map={
+            0: {"0": 1, "1": 1},
+            1: {"0": 2, "1": 2},
+            2: {"0": 2, "1": 2},
+            3: {"0": 0, "1": 2},
+        }
+    ))) == "0[01]"
+
+
+def test_even_star_bug1():
+    # Bug fix. This is a(a{2})* (i.e. accepts an odd number of "a" chars in a
+    # row), but when from_fsm() is called, the result is "a+". Turned out to be
+    # a fault in the rxelems.multiplier.__mul__() routine
+    elesscomplex = Fsm(
+        alphabet={"a"},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={
+            0: {"a": 1},
+            1: {"a": 0},
+        },
+    )
+    assert not elesscomplex.accepts("")
+    assert elesscomplex.accepts("a")
+    assert not elesscomplex.accepts("aa")
+    assert elesscomplex.accepts("aaa")
+    elesscomplex = from_fsm(elesscomplex)
+    assert str(elesscomplex) in {"a(a{2})*", "(a{2})*a"}
+    elesscomplex = elesscomplex.to_fsm()
+    assert not elesscomplex.accepts("")
+    assert elesscomplex.accepts("a")
+    assert not elesscomplex.accepts("aa")
+    assert elesscomplex.accepts("aaa")
+    gen = elesscomplex.strings()
+    assert next(gen) == ["a"]
+    assert next(gen) == ["a", "a", "a"]
+    assert next(gen) == ["a", "a", "a", "a", "a"]
+    assert next(gen) == ["a", "a", "a", "a", "a", "a", "a"]
+
+
+def test_binary_3():
+    # Binary numbers divisible by 3.
+    # Disallows the empty string
+    # Allows "0" on its own, but not leading zeroes.
+    div3 = from_fsm(Fsm(
+        alphabet={"0", "1"},
+        states={"initial", "zero", 0, 1, 2, None},
+        initial="initial",
+        finals={"zero", 0},
+        map={
+            "initial": {"0": "zero", "1": 1},
+            "zero": {"0": None, "1": None},
+            0: {"0": 0, "1": 1},
+            1: {"0": 2, "1": 0},
+            2: {"0": 1, "1": 2},
+            None: {"0": None, "1": None},
+        },
+    ))
+    assert str(parse("(0|1)").reduce()) == "[01]"
+    assert str(parse("(0|12)").reduce()) == "0|12"
+    assert str(parse("(0|1(01*0|10*1)*10*)").reduce()) == "0|1(01*0|10*1)*10*"
+    assert str(div3) == "0|1(01*0|10*1)*10*"
+    gen = div3.strings()
+    assert next(gen) == "0"
+    assert next(gen) == "11"
+    assert next(gen) == "110"
+    assert next(gen) == "1001"
+    assert next(gen) == "1100"
+
+
+def test_base_N():
+    # Machine accepts only numbers in selected base (e.g. 2, 10) that are
+    # divisible by N (e.g. 3, 7).
+    # "0" alone is acceptable, but leading zeroes (e.g. "00", "07") are not
+    base = 2
+    N = 3
+    assert base <= 10
+    divN = from_fsm(Fsm(
+        alphabet=set(str(i) for i in range(base)),
+        states=set(range(N)) | {"initial", "zero", None},
+        initial="initial",
+        finals={"zero", 0},
+        map=dict(
+            [
+                (
+                    "initial",
+                    dict(
+                        [(str(j), j % N) for j in range(1, base)] +
+                        [("0", "zero")]
+                    )
+                ),
+                (
+                    "zero",
+                    dict(
+                        [(str(j), None) for j in range(base)]
+                    )
+                ),
+                (
+                    None,
+                    dict(
+                        [(str(j), None) for j in range(base)]
+                    )
+                ),
+            ] + [
+                (
+                    i,
+                    dict(
+                        [(str(j), (i * base + j) % N) for j in range(base)]
+                    )
+                )
+                for i in range(N)
+            ]
+        ),
+    ))
+    gen = divN.strings()
+    a = next(gen)
+    assert a == "0"
+    for i in range(7):
+        b = next(gen)
+        assert int(a, base) + N == int(b, base)
+        a = b
+
+
+def test_bad_alphabet():
+    # You can use anything you like in your FSM alphabet, but if you try to
+    # convert it to an `rxelems` object then the only acceptable symbols are
+    # single characters or `ANYTHING_ELSE`.
+    for bad_symbol in [None, (), 0, ("a",), "", "aa", "ab", True]:
+        f = Fsm(
+            alphabet={bad_symbol},
+            states={0},
+            initial=0,
+            finals=set(),
+            map={
+                0: {bad_symbol: 0}
+            },
+        )
+        try:
+            from_fsm(f)
+            assert False
+        except AssertionError:
+            raise Exception(f"Accepted bad symbol: {repr(bad_symbol)}")
+        except Exception:
+            pass
+
+
+def test_dead_default():
+    blockquote = from_fsm(Fsm(
+        alphabet={"/", "*", ANYTHING_ELSE},
+        states={0, 1, 2, 3, 4},
+        initial=0,
+        finals={4},
+        map={
+                0: {"/": 1},
+                1: {"*": 2},
+                2: {"/": 2, ANYTHING_ELSE: 2, "*": 3},
+                3: {"/": 4, ANYTHING_ELSE: 2, "*": 3},
+        }
+    ))
+    assert str(blockquote) == "/\\*([^*]|\\*+[^*/])*\\*+/"
+
+
+###############################################################################
+# charclass set operations
+
+def test_charclass_union():
+    assert (parse("[ab]") | parse("[bc]")).reduce() == parse("[abc]")
+    assert (parse("[ab]") | parse("[^bc]")).reduce() == parse("[^c]")
+    assert (parse("[^ab]") | parse("[bc]")).reduce() == parse("[^a]")
+    assert (parse("[^ab]") | parse("[^bc]")).reduce() == parse("[^b]")
+
+
+def test_charclass_intersection():
+    assert parse("[ab]") & parse("[bc]") == parse("b")
+    assert parse("[ab]") & parse("[^bc]") == parse("a")
+    assert parse("[^ab]") & parse("[bc]") == parse("c")
+    assert parse("[^ab]") & parse("[^bc]") == parse("[^abc]")
+
+
+###############################################################################
+# Emptiness detection
+
+def test_empty():
+    assert not parse("a{0}").empty()
+    assert parse("[]").empty()
+    assert not parse("[]?").empty()
+    assert parse("a[]").empty()
+    assert not parse("a[]?").empty()
+    assert not parse("a{0}").empty()
+    assert not parse("[]?").empty()
+
+
+###############################################################################
+# Test everythingbut()
+
+def test_everythingbut():
+    # Regexes are usually gibberish but we make a few claims
+    a = parse("a")
+    notA = a.everythingbut().to_fsm()
+    assert notA.accepts("")
+    assert not notA.accepts("a")
+    assert notA.accepts("aa")
+
+    # everythingbut(), called twice, should take us back to where we started.
+    beer = parse("beer")
+    notBeer = beer.everythingbut()
+    beer2 = notBeer.everythingbut()
+    assert str(beer2) == "be{2}r"
+
+    # ".*" becomes "[]" and vice versa under this call.
+    assert str(parse(".*").everythingbut()) == "[]"
+    assert str(parse("[]").everythingbut()) == ".*"
+
+
+def test_isinstance_bug():
+    # Problem relating to isinstance(). The class `Mult` was occurring as both
+    # rxelems.Mult and as __main__.Mult and apparently these count as different
+    # classes for some reason, so isinstance(m, Mult) was returning false.
+    var = str(parse("").everythingbut()) \
+        + "aa" \
+        + str(parse("").everythingbut())
+    assert var == ".+aa.+"
+
+    starfree = parse(var).everythingbut()
+    assert str(starfree) == "(.(a?[^a])*a{0,2})?"
+
+
+###############################################################################
+
+def test_equivalence():
+    assert parse("aa*").equivalent(parse("a*a"))
+    assert parse("([ab]*a|[bc]*c)?b*").equivalent(parse("b*(a[ab]*|c[bc]*)?"))
+
+
+###############################################################################
+# Test reversed()
+
+def test_regex_reversal():
+    assert reversed(parse("b")) == parse("b")
+    assert reversed(parse("e*")) == parse("e*")
+    assert reversed(parse("bear")) == parse("raeb")
+    assert reversed(parse("beer")) == parse("reeb")
+    assert reversed(parse("abc|def|ghi")) == parse("cba|fed|ihg")
+    assert reversed(parse("(abc)*d")) == parse("d(cba)*")
+
+
+###############################################################################
+# Tests for some more set operations
+
+def test_set_ops():
+    assert parse("[abcd]") - parse("a") == parse("[bcd]")
+    assert parse("[abcd]") ^ parse("[cdef]") == parse("[abef]")
+
+
+###############################################################################
+# Test methods for finding common parts of regular expressions.
+
+def test_pattern_commonconc():
+    assert str(parse("aa|aa")._commonconc()) == "aa"
+    assert str(parse("abc|aa")._commonconc()) == "a"
+    assert str(parse("a|bc")._commonconc()) == ""
+    assert str(parse("cf{1,2}|cf")._commonconc()) == "cf"
+    assert str(parse("ZA|ZB|ZC")._commonconc()) == "Z"
+    assert str(parse("Z+A|ZB|ZZC")._commonconc()) == "Z"
+    assert str(parse("a{2}b|a+c")._commonconc()) == "a"
+
+
+def test_pattern_commonconc_suffix():
+    assert str(parse("a|bc")._commonconc(suffix=True)) == ""
+    assert str(parse("aa|bca")._commonconc(suffix=True)) == "a"
+    assert str(parse("xyza|abca|a")._commonconc(suffix=True)) == "a"
+    assert str(parse("f{2,3}c|fc")._commonconc(suffix=True)) == "fc"
+    assert str(parse("aa")._commonconc(suffix=True)) == "aa"
+
+
+###############################################################################
+# Basic concatenation reduction tests
+
+def test_reduce_concatenations():
+    assert str(parse("aa").reduce()) == "a{2}"
+    assert str(parse("bb").reduce()) == "b{2}"
+    assert str(parse("b*b").reduce()) == "b+"
+    assert str(parse("aa{2,}").reduce()) == "a{3,}"
+    assert str(parse("a*a{2}").reduce()) == "a{2,}"
+    assert str(parse("aa{0,8}").reduce()) == "a{1,9}"
+    assert str(parse("b{0,8}b").reduce()) == "b{1,9}"
+    assert str(parse("aab").reduce()) == "a{2}b"
+    assert str(parse("abb").reduce()) == "ab{2}"
+    assert str(parse("abb*").reduce()) == "ab+"
+    assert str(parse("abbc").reduce()) == "ab{2}c"
+    assert str(parse("a?ab").reduce()) == "a{1,2}b"
+    assert str(parse("(ac{2}|bc+)c").reduce()) == "(ac|bc*)c{2}"
+    assert str(parse("a(a{2}b|a+c)").reduce()) == "a{2}(a*c|ab)"
+    assert str(parse("a{2,3}(a{2}b|a+c)").reduce()) == "a{3,4}(a*c|ab)"
+    assert str(parse("(ba{2}|ca+)a{2,3}").reduce()) == "(ba|ca*)a{3,4}"
+    assert str(parse("za{2,3}(a{2}b|a+c)").reduce()) == "za{3,4}(a*c|ab)"
+    assert str(parse("(ba{2}|ca+)a{2,3}z").reduce()) == "(ba|ca*)a{3,4}z"
+    assert str(parse("(a|bc)(a|bc)").reduce()) == "(a|bc){2}"
+    assert str(parse("a+[ab]+").reduce()) == "a[ab]+"
+    assert str(parse("a{3,8}[ab]+").reduce()) == "a{3}[ab]+"
+    assert str(parse("[ab]+b+").reduce()) == "[ab]+b"
+    assert str(parse("[ab]+a{3,8}").reduce()) == "[ab]+a{3}"
+    assert str(parse("\\d+\\w+").reduce()) == "\\d\\w+"
+    assert str(parse("[ab]+a?").reduce()) == "[ab]+"
+
+
+###############################################################################
+# Multiplication tests
+
+def test_mult_multiplication():
+    assert parse("(a{2,3}){1,1}").reduce() == \
+        parse("a{2,3}").reduce()
+    assert parse("(a{2,3}){1}").reduce() == \
+        parse("a{2,3}").reduce()
+    assert parse("(a{2,3})").reduce() == \
+        parse("a{2,3}").reduce()
+    assert parse("(a{2,3}){4,5}").reduce() == \
+        parse("a{8,15}").reduce()
+    assert parse("(a{2,}){2,}").reduce() == \
+        parse("a{4,}").reduce()
+
+
+def test_even_star_bug2():
+    # Defect: (a{2})* should NOT reduce to a*
+    assert parse("(a{2})*").reduce() != parse("a*").reduce()
+
+
+def test_two_or_more_qm_bug():
+    assert str(parse("(a{2,})?").reduce()) == "(a{2,})?"
+
+
+def test_two_two_bug():
+    assert str(parse("(a{2}){2}").reduce()) == "a{4}"
+
+
+###############################################################################
+# Test intersection (&)
+
+def test_mult_intersection():
+    assert str(parse("a") & parse("a")) == "a"
+    assert str(parse("a*") & parse("a")) == "a"
+    assert str(parse("a") & parse("a?")) == "a"
+    assert str(parse("a{2}") & parse("a{2,}")) == "a{2}"
+    assert str(parse("a*") & parse("a+")) == "a+"
+    assert str(parse("a{2}") & parse("a{4}")) == "[]"
+    assert str(parse("a{3,}") & parse("a{3,}")) == "a{3,}"
+
+
+def test_parse_regex_intersection():
+    assert str(parse("a*") & parse("b*")) == ""
+    assert str(parse("a") & parse("b")) == "[]"
+    assert str(parse("\\d") & parse(".")) == "\\d"
+    assert str(parse("\\d{2}") & parse("0.")) == "0\\d"
+    assert str(parse("\\d{2}") & parse("19.*")) == "19"
+    assert str(parse("\\d{3}") & parse("19.*")) == "19\\d"
+    assert str(parse("abc...") & parse("...def")) == "abcdef"
+    assert str(parse("[bc]*[ab]*") & parse("[ab]*[bc]*")) in {
+        "([ab]*a|[bc]*c)?b*",
+        "b*(a[ab]*|c[bc]*)?"
+    }
+    assert str(parse("\\W*")) == "\\W*"
+    assert str(parse("[a-g0-8$%\\^]+")) == "[$%0-8\\^a-g]+"
+    assert str(parse("[^d]{2,8}")) == "[^d]{2,8}"
+    assert str(parse("\\W*") & parse("[a-g0-8$%\\^]+")) == "[$%\\^]+"
+    assert str(parse("[ab]{1,2}") & parse("[^a]{1,2}")) == "b{1,2}"
+    assert str(parse("[ab]?") & parse("[^a]?")) == "b?"
+    assert parse("a{0,2}").matches("")
+    assert parse("[ab]{0,2}").matches("")
+    assert parse("[^a]{0,2}").matches("")
+    assert parse("b{0,2}").matches("")
+    assert str(parse("[ab]{0,2}") & parse("[^a]{0,2}")) == \
+        "b{0,2}"
+    assert str(parse("[ab]{0,4}") & parse("[^a]{0,4}")) == \
+        "b{0,4}"
+    assert str(parse("[abc]{0,8}") & parse("[^a]{0,8}")) == \
+        "[bc]{0,8}"
+    assert str(parse("[a-g0-8$%\\^]{0,8}") & parse("[^d]{0,8}")) == \
+        "[$%0-8\\^abcefg]{0,8}"
+    assert str(parse("[a-g0-8$%\\^]+") & parse("[^d]{0,8}")) == \
+        "[$%0-8\\^abcefg]{1,8}"
+    assert str(parse("[a-g0-8$%\\^]+") & parse("[^d]{2,8}")) == \
+        "[$%0-8\\^abcefg]{2,8}"
+    assert str(
+        parse("\\W*") &
+        parse("[a-g0-8$%\\^]+") &
+        parse("[^d]{2,8}")
+    ) == \
+        "[$%\\^]{2,8}"
+    assert str(parse("\\d{4}-\\d{2}-\\d{2}") & parse("19.*")) == \
+        "19\\d{2}-\\d{2}-\\d{2}"
+
+
+def test_complexify():
+    # Complexify!
+    gen = (parse("[bc]*[ab]*") & parse("[ab]*[bc]*")).strings()
+    assert next(gen) == ""
+    assert next(gen) == "a"
+    assert next(gen) == "b"
+    assert next(gen) == "c"
+    assert next(gen) == "aa"
+    assert next(gen) == "ab"
+    # no "ac"
+    assert next(gen) == "ba"
+    assert next(gen) == "bb"
+    assert next(gen) == "bc"
+    # no "ca"
+    assert next(gen) == "cb"
+    assert next(gen) == "cc"
+    assert next(gen) == "aaa"
+
+
+def test_silly_reduction():
+    # This one is horrendous and we have to jump through some hoops to get to
+    # a sensible result. Probably not a good unit test actually.
+    long = \
+        "(aa|bb*aa)a*|((ab|bb*ab)|(aa|bb*aa)a*b)" + \
+        "((ab|bb*ab)|(aa|bb*aa)a*b)*" + \
+        "(aa|bb*aa)a*|((ab|bb*ab)|(aa|bb*aa)a*b)" + \
+        "((ab|bb*ab)|(aa|bb*aa)a*b)*"
+    long = parse(long)
+    long = reversed(long.to_fsm())
+    long = reversed(from_fsm(long))
+    assert str(long) == "[ab]*a[ab]"
+    short = "[ab]*a?b*|[ab]*b?a*"
+    assert str(parse(".*") & parse(short)) == "[ab]*"
+
+
+###############################################################################
+# reduce() tests
+
+def test_mult_reduction_easy():
+    assert str(parse("a").reduce()) == "a"
+    assert str(parse("a").reduce()) == "a"
+    assert str(parse("a?").reduce()) == "a?"
+    assert str(parse("a{0}").reduce()) == ""
+    assert str(parse("[]").reduce()) == "[]"
+    assert str(parse("[]?").reduce()) == ""
+    assert str(parse("[]{0}").reduce()) == ""
+    assert str(parse("[]{0,5}").reduce()) == ""
+
+
+def test_conc_reduction_basic():
+    assert str(parse("a").reduce()) == "a"
+    assert str(parse("a{3,4}").reduce()) == "a{3,4}"
+    assert str(parse("ab").reduce()) == "ab"
+    assert str(parse("a[]b").reduce()) == "[]"
+
+
+def test_pattern_reduce_basic():
+    assert str(parse("ab|cd").reduce()) == "ab|cd"
+    assert str(parse("a{2}b{2}").reduce()) == "a{2}b{2}"
+    assert str(parse("a{2}").reduce()) == "a{2}"
+    assert str(parse("a").reduce()) == "a"
+    assert str(parse("(((a)))").reduce()) == "a"
+
+
+def test_empty_conc_suppression():
+    assert str(parse("[]0\\d").reduce()) == "[]"
+
+
+def test_nested_pattern_reduction():
+    # a(d(ab|a*c)) -> ad(ab|a*c)
+    assert str(parse("a(d(ab|a*c))").reduce()) == "ad(a*c|ab)"
+
+
+def test_mult_factor_out_qm():
+    # `Mult` contains a `Pattern` containing an empty `Conc`? Pull the empty
+    # part out where it's external
+    assert str(parse("a|b*|").reduce()) == "a|b*"
+    assert str(parse("(a|b*|)").reduce()) == "a|b*"
+    assert str(parse("(a|b*|)c").reduce()) == "(a|b*)c"
+    # This happens even if `EMPTYSTRING` is the only thing left inside the
+    # `Mult`
+    assert str(parse("()").reduce()) == ""
+    assert str(parse("([$%\\^]|){1}").reduce()) == "[$%\\^]?"
+
+
+def test_remove_unnecessary_parens():
+    # `Mult` contains a `Pattern` containing a single `Conc` containing a
+    # single `Mult`? That can be reduced greatly
+    assert str(parse("(a){2}b").reduce()) == "a{2}b"
+    assert str(parse("(a?)+b").reduce()) == "a*b"
+    assert str(parse("([ab])*").reduce()) == "[ab]*"
+    assert str(parse("(c{1,2}){3,4}").reduce()) == "c{3,8}"
+
+
+def test_obvious_reduction():
+    assert str(parse("(a|b)*").reduce()) == "[ab]*"
+
+
+def test_mult_squoosh():
+    # sequence squooshing of mults within a `Conc`
+    assert str(parse("[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]").reduce()) == \
+        "[0-9A-Fa-f]{3}"
+    assert str(parse("[$%\\^]?[$%\\^]").reduce()) == \
+        "[$%\\^]{1,2}"
+    assert str(parse(
+        "(|(|(|(|(|[$%\\^])[$%\\^])[$%\\^])[$%\\^])[$%\\^])[$%\\^][$%\\^]"
+    ).reduce()) == \
+        "[$%\\^]{2,7}"
+
+
+def test_bad_reduction_bug():
+    # DEFECT: "0{2}|1{2}" was erroneously reduced() to "[01]{2}"
+    assert parse("0{2}|1{2}").reduce() != \
+        parse("[01]{2}")
+    assert parse("0|[1-9]|ab").reduce() == \
+        parse("\\d|ab")
+    assert parse("0|[1-9]|a{5,7}").reduce() == \
+        parse("\\d|a{5,7}")
+    assert parse("0|(0|[1-9]|a{5,7})").reduce() == \
+        parse("0|(\\d|a{5,7})")
+    # TODO: should do better than this! Merge that 0
+
+
+def test_common_prefix_pattern_reduction():
+    assert str(parse("a{2}b|a+c").reduce()) == "a(a*c|ab)"
+
+
+def test_epsilon_reduction():
+    assert str(parse("|(ab)*|def").reduce()) == "(ab)*|def"
+    assert str(parse("|(ab)+|def").reduce()) == "(ab)*|def"
+    assert str(parse("|.+").reduce()) == ".*"
+    assert str(parse("|a+|b+").reduce()) in {
+        "a+|b*",
+        "a*|b+"
+    }
+
+
+def test_charclass_intersection_2():
+    assert (parse("[A-z]") & parse("[^g]")).reduce() == \
+        parse("[A-fh-z]").reduce()
+
+
+def test_reduce_boom():
+    # make sure recursion problem in reduce() has gone away
+    assert str(parse("([1-9]|0)").reduce()) == "\\d"
+
+
+def test_new_reduce():
+    # The @reduce_after decorator has been removed from many methods since it
+    # takes unnecessary time which the user may not wish to spend.
+    # This alters the behaviour of several methods and also exposes a new
+    # opportunity for `Conc.reduce()`
+    assert str(parse("a()").reduce()) == "a"
+    assert str(parse("a()()").reduce()) == "a"
+    assert str(parse("a.b()()").reduce()) == "a.b"
+
+
+def test_main_bug():
+    assert str(parse("a*").reduce()) == \
+        "a*"
+    assert str(parse("a|a*").reduce()) == \
+        "a*"
+    assert str(parse("a{1,2}|a{3,4}|bc").reduce()) == \
+        "a{1,4}|bc"
+    assert str(parse("a{1,2}|bc|a{3,4}").reduce()) == \
+        "a{1,4}|bc"
+    assert str(parse("a{1,2}|a{3,4}|a{5,6}|bc").reduce()) == \
+        "a{1,6}|bc"
+    assert str(parse("a{1,2}|a{3}|a{5,6}").reduce()) == \
+        "a{1,2}(a?|a{4})"
+    assert str(parse("a{1,2}|a{3}|a{5,6}|bc").reduce()) == \
+        "a{1,3}|a{5,6}|bc"
+    assert str(parse("a{1,2}|a{4}|a{5,6}").reduce()) == \
+        "a{1,2}(a{3,4})?"
+    assert str(parse("a{1,2}|a{4}|a{5,6}|bc").reduce()) == \
+        "a{1,2}|a{4,6}|bc"
+    assert str((parse("a") | parse("a*")).reduce()) == \
+        "a*"
+
+
+def test_bug_28_b():
+    # Defect in rxelems.to_fsm()
+    assert not parse("(ab*)*").to_fsm().accepts("bb")
+
+
+def test_derive():
+    assert str(parse("a+").derive("a")) == "a*"
+    assert str(parse("a+|b+").derive("a")) == "a*"
+    assert str(parse("abc|ade").derive("a")) == "bc|de"
+    assert str(parse("abc|ade").derive("ab")) == "c"
+
+
+def test_bug_36_1():
+    etc1 = parse(".*").to_fsm()
+    etc2 = parse("s.*").to_fsm()
+    assert etc1.accepts("s")
+    assert etc2.accepts("s")
+    assert not etc1.isdisjoint(etc2)
+    assert not etc2.isdisjoint(etc1)
+
+
+def test_bug_36_2():
+    etc1 = parse("/etc/.*").to_fsm()
+    etc2 = parse("/etc/something.*").to_fsm()
+    assert etc1.accepts("/etc/something")
+    assert etc2.accepts("/etc/something")
+    assert not etc1.isdisjoint(etc2)
+    assert not etc2.isdisjoint(etc1)
+
+
+def test_bug_slow():
+    # issue #43
+    import time
+    m = Fsm(
+        alphabet={'R', 'L', 'U', 'D'},
+        states={
+            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+            11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
+        initial=0,
+        finals={20},
+        map={
+            0: {'D': 1, 'U': 2},
+            1: {'L': 3},
+            2: {'L': 4},
+            3: {'U': 5},
+            4: {'D': 6},
+            5: {'R': 7},
+            6: {'R': 8},
+            7: {'U': 9},
+            8: {'D': 10},
+            9: {'L': 11},
+            10: {'L': 12},
+            11: {'L': 13},
+            12: {'L': 14},
+            13: {'D': 15},
+            14: {'U': 16},
+            15: {'R': 17},
+            16: {'R': 18},
+            17: {'D': 19},
+            18: {'U': 19},
+            19: {'L': 20},
+            20: {}
+        }
+    )
+    t1 = time.time()
+    ll = from_fsm(m)
+    t2 = time.time()
+    assert (t2 - t1) < 60  # should finish in way under 1s
+    assert ll == parse("(DLURULLDRD|ULDRDLLURU)L").reduce()
+
+
+def test_bug_48_simpler():
+    assert str(from_fsm(Fsm(
+        alphabet={'d'},
+        states={0, 1},
+        initial=0,
+        finals={1},
+        map={
+            0: {'d': 1},
+        },
+    ))) == 'd'
+
+
+def test_bug_48():
+    S5, S26, S45, S63, S80, S97, S113, S127, S140, S152, S163, S175, S182 = \
+        range(13)
+    char0, char1, char2, char3, char4, char5, char6, char7, char8 = \
+        '_', 'a', 'd', 'e', 'g', 'm', 'n', 'o', 'p'
+
+    machine = Fsm(
+        alphabet={
+            char0, char1, char2, char3, char4,
+            char5, char6, char7, char8
+        },
+        states={
+            S5, S26, S45, S63, S80, S97, S113,
+            S127, S140, S152, S163, S175, S182
+        },
+        initial=S5,
+        finals={S182},
+        map={
+            S113: {char0: S127},
+            S127: {char7: S140},
+            S140: {char6: S152},
+            S152: {char0: S163},
+            S163: {char5: S175},
+            S175: {char8: S182},
+            S26: {char1: S45},
+            S45: {char5: S63},
+            S5: {char2: S26},
+            S63: {char1: S80},
+            S80: {char4: S97},
+            S97: {char3: S113},
+        },
+    )
+
+    rex = from_fsm(machine)
+    assert str(rex) == 'damage_on_mp'
+
+
+def test_pickle():
+    f1 = parse("a{0,4}").to_fsm()
+    f2 = parse("a{0,3}").to_fsm()
+
+    assert f2 < f1
+
+    f1_unpickled = pickle.loads(pickle.dumps(f1))
+    f2_unpickled = pickle.loads(pickle.dumps(f2))
+
+    assert f2_unpickled < f1_unpickled