Repository: caleb531/automata

Issue:
How to express the empty string in regexes?
Is there a way to specify the empty string as part of regexes? For example, Python's built-in re.compile allows expressions like `'()'` or `'|'`, but the parser in this library throws an error on both.

Comments:

Patch:
diff --git a/automata/regex/parser.py b/automata/regex/parser.py
index 61f0e10..098e3cb 100644
--- a/automata/regex/parser.py
+++ b/automata/regex/parser.py
@@ -492,8 +492,9 @@ def val(self) -> NFARegexBuilder:
         return NFARegexBuilder.wildcard(self.input_symbols, self.counter)
 
 
-def add_concat_tokens(
+def add_concat_and_empty_string_tokens(
     token_list: List[Token[NFARegexBuilder]],
+    state_name_counter: count,
 ) -> List[Token[NFARegexBuilder]]:
     """Add concat tokens to list of parsed infix tokens."""
 
@@ -509,6 +510,9 @@ def add_concat_tokens(
         (PostfixOperator, LeftParen),
     ]
 
+    # Pairs of tokens to insert empty string literals between
+    empty_string_pairs = [(LeftParen, RightParen)]
+
     for curr_token, next_token in zip_longest(token_list, token_list[1:]):
         final_token_list.append(curr_token)
 
@@ -519,13 +523,20 @@ def add_concat_tokens(
                 ):
                     final_token_list.append(ConcatToken(""))
 
+            for firstClass, secondClass in empty_string_pairs:
+                if isinstance(curr_token, firstClass) and isinstance(
+                    next_token, secondClass
+                ):
+                    final_token_list.append(StringToken("", state_name_counter))
+
     return final_token_list
 
 
-def get_regex_lexer(input_symbols: AbstractSet[str]) -> Lexer[NFARegexBuilder]:
+def get_regex_lexer(
+    input_symbols: AbstractSet[str], state_name_counter: count
+) -> Lexer[NFARegexBuilder]:
     """Get lexer for parsing regular expressions."""
     lexer: Lexer[NFARegexBuilder] = Lexer()
-    state_name_counter = count(0)
 
     lexer.register_token(LeftParen.from_match, r"\(")
     lexer.register_token(RightParen.from_match, r"\)")
@@ -553,10 +564,14 @@ def parse_regex(regexstr: str, input_symbols: AbstractSet[str]) -> NFARegexBuild
     if len(regexstr) == 0:
         return NFARegexBuilder.from_string_literal(regexstr, count(0))
 
-    lexer = get_regex_lexer(input_symbols)
+    state_name_counter = count(0)
+
+    lexer = get_regex_lexer(input_symbols, state_name_counter)
     lexed_tokens = lexer.lex(regexstr)
     validate_tokens(lexed_tokens)
-    tokens_with_concats = add_concat_tokens(lexed_tokens)
+    tokens_with_concats = add_concat_and_empty_string_tokens(
+        lexed_tokens, state_name_counter
+    )
     postfix = tokens_to_postfix(tokens_with_concats)
 
     return parse_postfix_tokens(postfix)
diff --git a/automata/regex/postfix.py b/automata/regex/postfix.py
index a9ab999..2c173ea 100644
--- a/automata/regex/postfix.py
+++ b/automata/regex/postfix.py
@@ -103,7 +103,7 @@ def validate_tokens(token_list: List[Token]) -> None:
         # No left parens right before infix or postfix operators, or right
         # before a right paren
         elif isinstance(prev_token, LeftParen):
-            if isinstance(curr_token, (InfixOperator, PostfixOperator, RightParen)):
+            if isinstance(curr_token, (InfixOperator, PostfixOperator)):
                 raise exceptions.InvalidRegexError(
                     f"'{prev_token}' cannot appear immediately before '{curr_token}'."
                 )
diff --git a/automata/regex/regex.py b/automata/regex/regex.py
index 1fa02b6..9c9414c 100644
--- a/automata/regex/regex.py
+++ b/automata/regex/regex.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 """Methods for working with regular expressions"""
 
+from itertools import count
 from typing import AbstractSet, Literal, Optional
 
 import automata.base.exceptions as exceptions
@@ -23,7 +24,7 @@ def validate(regex: str) -> Literal[True]:
     """Raise an error if the regular expression is invalid"""
     input_symbols = set(regex) - RESERVED_CHARACTERS
 
-    validate_tokens(get_regex_lexer(input_symbols).lex(regex))
+    validate_tokens(get_regex_lexer(input_symbols, count(0)).lex(regex))
 
     return True
 
diff --git a/docs/regular-expressions.md b/docs/regular-expressions.md
index 325fa84..d1dc220 100644
--- a/docs/regular-expressions.md
+++ b/docs/regular-expressions.md
@@ -16,8 +16,9 @@ A regular expression with the following operations only are supported in this li
 - `&`: Intersection. Ex: `a&b`
 - `.`: Wildcard. Ex: `a.b`
 - `^`: Shuffle. Ex: `a^b`
-- `{}` : Quantifiers expressing finite repetitions. Ex: `a{1,2}`,`a{3,}`
-- `()`: Grouping.
+- `{}`: Quantifiers expressing finite repetitions. Ex: `a{1,2}`,`a{3,}`
+- `()`: The empty string.
+- `(...)`: Grouping.
 
 This is similar to the Python `re` module, but this library does not support any special
 characters other than those given above. All regular languages can be written with these.


Test patch:
diff --git a/tests/test_nfa.py b/tests/test_nfa.py
index c33d690..0aff08f 100644
--- a/tests/test_nfa.py
+++ b/tests/test_nfa.py
@@ -571,7 +571,6 @@ def test_validate_regex(self):
             exceptions.InvalidRegexError, NFA.from_regex, "((abc*)))((abd)"
         )
         self.assertRaises(exceptions.InvalidRegexError, NFA.from_regex, "*")
-        self.assertRaises(exceptions.InvalidRegexError, NFA.from_regex, "abcd()")
         self.assertRaises(
             exceptions.InvalidRegexError, NFA.from_regex, "ab(bc)*((bbcd)"
         )
diff --git a/tests/test_regex.py b/tests/test_regex.py
index cda0960..e115d3a 100644
--- a/tests/test_regex.py
+++ b/tests/test_regex.py
@@ -26,7 +26,6 @@ def test_validate_invalid(self):
         self.assertRaises(exceptions.InvalidRegexError, re.validate, "a||b")
         self.assertRaises(exceptions.InvalidRegexError, re.validate, "((abc*)))((abd)")
         self.assertRaises(exceptions.InvalidRegexError, re.validate, "*")
-        self.assertRaises(exceptions.InvalidRegexError, re.validate, "abcd()")
         self.assertRaises(exceptions.InvalidRegexError, re.validate, "ab(bc)*((bbcd)")
         self.assertRaises(exceptions.InvalidRegexError, re.validate, "a(*)")
         self.assertRaises(exceptions.InvalidRegexError, re.validate, "a(|)")
@@ -203,6 +202,13 @@ def test_quantifier(self):
             )
         )
 
+    def test_blank(self):
+        """Should correctly parse blank"""
+        self.assertTrue(re.isequal("()", ""))
+        self.assertTrue(re.isequal("a|()", "a?"))
+        self.assertTrue(re.isequal("a()", "a"))
+        self.assertTrue(re.isequal("a()b()()c()", "abc"))
+
     def test_invalid_symbols(self):
         """Should throw exception if reserved character is in input symbols"""
         with self.assertRaises(exceptions.InvalidSymbolError):