apalala: Could you take a look at these changes, @Victorious3? All tests are passing.
Victorious3: Another thing that happened is that the empty program no longer gives me `None` but instead I get 
```
>       assert parse("") == parse("()") == Program(None)
E       AssertionError: assert 
E         Program [
E          None
E         ]
E         ==
E         Program [
E          [
E           '('
E           ')'
E          ]
E         ]
```
I'll look into this more to figure out what commit causes it, it might actually be a problem with my grammar.
apalala: @Victorious3 , I tested my Java and COBOL parsers with this version, and had no changes to make to the grammars.
Victorious3: When I call trace I get this:
```
INFO     tatsu:_common.py:55 ↙t_ident↙identifier↙stmt_typedecl_lhs↙stmt_typedecl↙stmt↙ ~1:6
word(12.4)
INFO     tatsu:_common.py:55 ≡'word' /(?!\d)\w+/
```
Except `word` being a reserved word.
I could add you to my repo to investigate this. Should I do that?
Victorious3: This fixes my second issue, for the first one I would revert 3288a6a as it breaks `@name`
apalala: @Victorious3, now `_check_name()` is called automatically from `_invoke_semantic_rule()`. 

I think that the problem may be the

```python
        if not isinstance(name, str):
            return
```

That I added to `_check_name()`. Can you check again?
Victorious3: Same issue as before. I believe is_name is never set to true? There is no decorator for the generated parser.
apalala: > Same issue as before. I believe is_name is never set to true? There is no decorator for the generated parser.

Ah! Good catch, @Victorious3 ! 🥇 
apalala: @Victorious3 , I added a unit test for the case you report, and it failed. I added the `@isname` decorator for `@name` rules, and the issue is fixed. I'm merging now.
diff --git a/.pylintrc b/.pylintrc
index 280bbe7..0fd8660 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -145,7 +145,7 @@ confidence=
 #        comprehension-escape
 
 disable=C0102, C0103, C0111, C0122, C0123,
-        C0301, C0330,
+        C0301, C0321, C0330,
         C0411, C0412, C0415,
         C1801,
         E1101,
diff --git a/tatsu/codegen/python.py b/tatsu/codegen/python.py
index d01a874..e01f736 100644
--- a/tatsu/codegen/python.py
+++ b/tatsu/codegen/python.py
@@ -384,19 +384,18 @@ class Rule(_Decorator):
                 )
 
         fields.update(defines=sdefines)
-        fields.update(
-            check_name='\n    self._check_name()' if self.is_name else '',
-        )
         leftrec = self.node.is_leftrec
         fields.update(leftrec='\n@leftrec' if leftrec else '')
         fields.update(nomemo='\n@nomemo' if not self.node.is_memoizable and not leftrec else '')
+        fields.update(isname='\n@isname' if self.node.is_name else '')
 
     template = '''
         @tatsumasu({params})\
         {leftrec}\
-        {nomemo}
+        {nomemo}\
+        {isname}
         def _{name}_(self):  # noqa
-        {exp:1::}{check_name}{defines}
+        {exp:1::}{defines}
         '''
 
     define_template = '''\
@@ -499,8 +498,8 @@ class Grammar(Base):
 
                 from tatsu.buffering import Buffer
                 from tatsu.parsing import Parser
-                from tatsu.parsing import tatsumasu, leftrec, nomemo
-                from tatsu.parsing import leftrec, nomemo  # noqa
+                from tatsu.parsing import tatsumasu
+                from tatsu.parsing import leftrec, nomemo, isname # noqa
                 from tatsu.util import re, generic_main  # noqa
 
 
diff --git a/tatsu/contexts.py b/tatsu/contexts.py
index 0c82450..b291aef 100644
--- a/tatsu/contexts.py
+++ b/tatsu/contexts.py
@@ -4,6 +4,7 @@ from __future__ import generator_stop
 import sys
 import functools
 from contextlib import contextmanager
+from copy import copy
 
 from tatsu.util.unicode_characters import (
     C_DERIVE,
@@ -23,9 +24,9 @@ from .infos import (
     ParseInfo,
     RuleInfo,
     RuleResult,
-    TreeInfo,
+    ParseState,
 )
-from tatsu.exceptions import (
+from tatsu.exceptions import (  # noqa
     FailedCut,
     FailedExpectingEndOfText,
     FailedLeftRecursion,
@@ -34,6 +35,7 @@ from tatsu.exceptions import (
     FailedPattern,
     FailedRef,
     FailedSemantics,
+    FailedKeyword,
     FailedKeywordSemantics,
     FailedToken,
     OptionSucceeded
@@ -53,7 +55,16 @@ def tatsumasu(*params, **kwparams):
             name = name[1:-1]
             is_leftrec = getattr(impl, "is_leftrec", False)
             is_memoizable = getattr(impl, "is_memoizable", True)
-            ruleinfo = RuleInfo(name, impl, is_leftrec, is_memoizable, params, kwparams)
+            is_name = getattr(impl, 'is_name', False)
+            ruleinfo = RuleInfo(
+                name,
+                impl,
+                is_leftrec,
+                is_memoizable,
+                is_name,
+                params,
+                kwparams
+            )
             return self._call(ruleinfo)
         return wrapper
     return decorator
@@ -73,6 +84,12 @@ def nomemo(impl):
     return impl
 
 
+# Marks rules marked as @name in the grammar
+def isname(impl):
+    impl.is_name = True
+    return impl
+
+
 class closure(list):
     pass
 
@@ -124,12 +141,12 @@ class ParseContext(object):
         self._initialize_caches()
 
     def _initialize_caches(self):
-        self._tree_stack = [TreeInfo()]
+        self._statestack = [ParseState()]
         self._rule_stack = []
         self._cut_stack = [False]
 
         self._last_node = None
-        self._state = None
+        self.substate = None
         self._lookahead = 0
 
         self._clear_memoization_caches()
@@ -222,7 +239,6 @@ class ParseContext(object):
             )
             rule = self._find_rule(rule_name)
             result = rule()
-            self.ast[rule_name] = result
             return result
         except FailedCut as e:
             self._set_furthest_exception(e.nested)
@@ -271,13 +287,17 @@ class ParseContext(object):
             ast.update(self.ast)
             self.ast = ast
 
+    @property
+    def state(self):
+        return self._statestack[-1]
+
     @property
     def ast(self):
-        return self._tree_stack[-1].ast
+        return self.state.ast
 
     @ast.setter
     def ast(self, value):
-        self._tree_stack[-1].ast = value
+        self.state.ast = value
 
     def name_last_node(self, name):
         self.ast[name] = self.last_node
@@ -285,29 +305,56 @@ class ParseContext(object):
     def add_last_node_to_name(self, name):
         self.ast._setlist(name, self.last_node)
 
-    def _push_ast(self):
-        self._tree_stack.append(TreeInfo())
+    def _push_ast(self, copyast=False):
+        ast = copy(self.ast) if copyast else AST()
+        self.state.pos = self._pos
+        self._statestack.append(
+            ParseState(
+                ast=ast,
+                pos=self._pos,
+            )
+        )
 
     def _pop_ast(self):
-        self._tree_stack.pop()
+        self._statestack.pop()
+        self.tokenizer.goto(self.state.pos)
+
+    def _merge_ast(self):
+        pos = self._pos
+        ast = self.ast
+        cst = self.cst
+        self._statestack.pop()
+        self.ast = ast
+        self._extend_cst(cst)
+        self.tokenizer.goto(pos)
 
     @property
     def cst(self):
-        return self._tree_stack[-1].cst
+        return self.state.cst
 
     @cst.setter
     def cst(self, value):
-        self._tree_stack[-1].cst = value
+        self.state.cst = value
 
     def _push_cst(self):
-        self._tree_stack.append(TreeInfo(ast=self.ast))
+        self._statestack.append(ParseState(ast=self.ast))
 
     def _pop_cst(self):
         ast = self.ast
-        self._tree_stack.pop()
+        self._statestack.pop()
         self.ast = ast
 
-    def _add_cst_node(self, node):
+    def _merge_cst(self, extend=True):
+        cst = self.cst
+        self._pop_cst()
+        if extend:
+            self._extend_cst(cst)
+        else:
+            self._append_cst(cst)
+        return cst
+
+    def _append_cst(self, node):
+        self.last_node = node
         if node is None:
             return
         previous = self.cst
@@ -317,8 +364,10 @@ class ParseContext(object):
             previous.append(node)
         else:
             self.cst = [previous, node]
+        return node
 
     def _extend_cst(self, node):
+        self.last_node = node
         if node is None:
             return
         previous = self.cst
@@ -333,6 +382,7 @@ class ParseContext(object):
             previous.append(node)
         else:
             self.cst = [previous, node]
+        return node
 
     def _copy_node(self, node):
         if node is None:
@@ -489,7 +539,7 @@ class ParseContext(object):
 
     @property
     def memokey(self):
-        return MemoKey(self._pos, self.rule, self._state)
+        return MemoKey(self._pos, self.rule, self.substate)
 
     def _memoize(self, key, memo):
         if self._memoization() and key.rule.is_memoizable:
@@ -508,7 +558,7 @@ class ParseContext(object):
         return memo
 
     def _mkresult(self, node):
-        return RuleResult(node, self._pos, self._state)
+        return RuleResult(node, self._pos, self.substate)
 
     def _save_result(self, key, result):
         if is_list(result.node):
@@ -534,9 +584,8 @@ class ParseContext(object):
             node, newpos, newstate = result
 
             self._goto(newpos)
-            self._state = newstate
-            self._add_cst_node(node)
-            self._last_node = node
+            self.substate = newstate
+            self._append_cst(node)
 
             self._trace_success()
 
@@ -616,12 +665,11 @@ class ParseContext(object):
                 return result
             except FailedSemantics as e:
                 self._error(str(e))
-            finally:
-                self._pop_ast()
         except FailedParse as e:
             self._memoize(key, e)
-            self._goto(key.pos)
             raise
+        finally:
+            self._pop_ast()
 
     def _get_node(self, pos, ruleinfo):
         node = self.ast
@@ -639,6 +687,8 @@ class ParseContext(object):
             node = semantic_rule(node, *(rule.params or ()), **(rule.kwparams or {}))
         if postproc is not None:
             postproc(self, node)
+        if rule.is_name:
+            self._check_name(node)
         return node
 
     def _token(self, token):
@@ -647,15 +697,13 @@ class ParseContext(object):
             self._trace_match(token, failed=True)
             self._error(token, exclass=FailedToken)
         self._trace_match(token)
-        self._add_cst_node(token)
-        self._last_node = token
+        self._append_cst(token)
         return token
 
     def _constant(self, literal):
         self._next_token()
         self._trace_match(literal)
-        self._add_cst_node(literal)
-        self._last_node = literal
+        self._append_cst(literal)
         return literal
 
     def _pattern(self, pattern):
@@ -664,8 +712,7 @@ class ParseContext(object):
             self._trace_match('', pattern, failed=True)
             self._error(pattern, exclass=FailedPattern)
         self._trace_match(token, pattern)
-        self._add_cst_node(token)
-        self._last_node = token
+        self._append_cst(token)
         return token
 
     def _eof(self):
@@ -681,25 +728,16 @@ class ParseContext(object):
 
     @contextmanager
     def _try(self):
-        p = self._pos
-        s = self._state
-        ast_copy = self.ast.copy()
-        self._push_ast()
+        s = self.substate
+        self._push_ast(copyast=True)
         self.last_node = None
         try:
-            self.ast = ast_copy
             yield
-            ast = self.ast
-            cst = self.cst
+            self._merge_ast()
         except FailedParse:
-            self._goto(p)
-            self._state = s
-            raise
-        finally:
             self._pop_ast()
-        self.ast = ast
-        self._extend_cst(cst)
-        self.last_node = cst
+            self.substate = s
+            raise
 
     @contextmanager
     def _option(self):
@@ -737,27 +775,22 @@ class ParseContext(object):
         self._push_cst()
         try:
             yield
-            cst = self.cst
-        finally:
+            self._merge_cst(extend=True)
+        except Exception:
             self._pop_cst()
-        self._extend_cst(cst)
-        self.last_node = cst
+            raise
 
     @contextmanager
     def _if(self):
-        p = self._pos
-        s = self._state
+        s = self.substate
         self._push_ast()
         self._lookahead += 1
         try:
             yield
-            cst = self.cst
         finally:
-            self._lookahead -= 1
-            self._goto(p)
-            self._state = s
             self._pop_ast()  # simply discard
-        self.last_node = cst
+            self._lookahead -= 1
+            self.substate = s
 
     @contextmanager
     def _ifnot(self):
@@ -780,7 +813,7 @@ class ParseContext(object):
         if is_list(cst):
             cst = closure(cst)
         if not drop:
-            self._add_cst_node(cst)
+            self._append_cst(cst)
         return cst
 
     def _repeat(self, block, prefix=None, dropprefix=False):
@@ -807,12 +840,11 @@ class ParseContext(object):
                 block()
                 self.cst = [self.cst]
             self._repeat(block, prefix=sep, dropprefix=omitsep)
-            cst = closure(self.cst)
-        finally:
+            self.cst = closure(self.cst)
+            return self._merge_cst()
+        except Exception:
             self._pop_cst()
-        self._add_cst_node(cst)
-        self.last_node = cst
-        return cst
+            raise
 
     def _positive_closure(self, block, sep=None, omitsep=False):
         self._push_cst()
@@ -820,17 +852,15 @@ class ParseContext(object):
             block()
             self.cst = [self.cst]
             self._repeat(block, prefix=sep, dropprefix=omitsep)
-            cst = closure(self.cst)
-        finally:
+            self.cst = closure(self.cst)
+            return self._merge_cst()
+        except Exception:
             self._pop_cst()
-        self._add_cst_node(cst)
-        self.last_node = cst
-        return cst
+            raise
 
     def _empty_closure(self):
         cst = closure([])
-        self._add_cst_node(cst)
-        self.last_node = cst
+        self._append_cst(cst)
         return cst
 
     def _gather(self, block, sep):
@@ -855,9 +885,8 @@ class ParseContext(object):
         self.last_node = self.cst
         return self.cst
 
-    def _check_name(self, name=None):
-        if name is None:
-            name = str(self.last_node)
+    def _check_name(self, name):
+        name = str(name)
         if self.ignorecase or self.tokenizer.ignorecase:
             name = name.upper()
         if name in self.keywords:
@@ -872,8 +901,7 @@ class ParseContext(object):
             self._trace_match(c, failed=True)
             self._error(c, exclass=FailedToken)
         self._trace_match(c)
-        self._add_cst_node(c)
-        self._last_node = c
+        self._append_cst(c)
         return c
 
     def _skip_to(self, block):
diff --git a/tatsu/exceptions.py b/tatsu/exceptions.py
index 9b5f94f..e1b409e 100644
--- a/tatsu/exceptions.py
+++ b/tatsu/exceptions.py
@@ -138,3 +138,7 @@ class FailedLeftRecursion(FailedParse):
 
 class FailedExpectingEndOfText(FailedParse):
     pass
+
+
+class FailedKeyword(FailedParse):
+    pass
diff --git a/tatsu/grammars.py b/tatsu/grammars.py
index 1fd9d21..1d43f64 100644
--- a/tatsu/grammars.py
+++ b/tatsu/grammars.py
@@ -842,14 +842,16 @@ class Rule(Decorator):
     def parse(self, ctx):
         result = self._parse_rhs(ctx, self.exp)
         self._add_defined_attributes(result)
-        if self.is_name:
-            ctx._check_name()
         return result
 
     def _parse_rhs(self, ctx, exp):
         ruleinfo = RuleInfo(
             self.name, exp.parse,
-            self.is_leftrec, self.is_memoizable, self.params, self.kwparams
+            self.is_leftrec,
+            self.is_memoizable,
+            self.is_name,
+            self.params,
+            self.kwparams
         )
         result = ctx._call(ruleinfo)
         return result
diff --git a/tatsu/infos.py b/tatsu/infos.py
index 6113bc4..293bc1d 100644
--- a/tatsu/infos.py
+++ b/tatsu/infos.py
@@ -2,6 +2,8 @@
 from __future__ import generator_stop
 
 from collections import namedtuple
+from dataclasses import dataclass, field
+from typing import Any
 
 from .ast import AST
 
@@ -86,6 +88,7 @@ _RuleInfo = namedtuple(
         'impl',
         'is_leftrec',
         'is_memoizable',
+        'is_name',
         'params',
         'kwparams',
     ]
@@ -117,12 +120,8 @@ RuleResult = namedtuple(
 )
 
 
-class TreeInfo(object):
-    __slots__ = (
-        'ast',
-        'cst'
-    )
-
-    def __init__(self, ast=None, cst=None):
-        self.ast = AST() if ast is None else ast
-        self.cst = cst
+@dataclass
+class ParseState(object):
+    pos: int = 0
+    ast: AST = field(default_factory=AST)
+    cst: Any = None
diff --git a/tatsu/parsing.py b/tatsu/parsing.py
index f168228..29f66ff 100644
--- a/tatsu/parsing.py
+++ b/tatsu/parsing.py
@@ -3,8 +3,7 @@ from __future__ import generator_stop
 
 from tatsu.exceptions import FailedRef
 from tatsu.contexts import ParseContext
-from tatsu.contexts import tatsumasu, leftrec, nomemo
-assert tatsumasu and leftrec and nomemo
+from tatsu.contexts import tatsumasu, leftrec, nomemo, isname  # noqa
 
 
 class Parser(ParseContext):
diff --git a/tatsu/util/_common.py b/tatsu/util/_common.py
index 7839673..d1d7086 100644
--- a/tatsu/util/_common.py
+++ b/tatsu/util/_common.py
@@ -360,12 +360,13 @@ def right_assoc(elements):
     return assoc(iter(elements))
 
 
-def memory_use():
-    try:
-        import psutil
-    except ImportError:
+try:
+    import psutil
+except ImportError:
+    def memory_use():
         return 0
-    else:
+else:
+    def memory_use():
         process = psutil.Process(os.getpid())
         return process.memory_info().rss
 

Simplify ParseContext
`contextxs.ParseContext` still carries inneficiencies and complications from its original versions:

* Although the `ast` and `cst` stacks were merged, the `cut` and `pos` stacks remain separate, when there could be a single stack to hold the state at relevant points in the parse: rules, and optionals (used in choices and closures). It should basically be pushing a new state on `_try`,  pop-merge on success, and pop on failure, modifying `tokenizer.pos` on each pop.
* `invoke_rule` should use `with self._try()` instead of pulling its own.
* There should be no need to keep separate `cst` and `ast`, as the `ast` overrides the `cst` in all parser results.
* The `last_node` protocol is suspicious. Why isn't it `current_state.ast`?

The simplifications would allow for semantics and an implementation that can be ported to other programming language targets, such as `C`.
diff --git a/test/grammar/stateful_test.py b/test/grammar/stateful_test.py
index 908a39c..5b937cf 100644
--- a/test/grammar/stateful_test.py
+++ b/test/grammar/stateful_test.py
@@ -34,17 +34,17 @@ class StatefulTests(unittest.TestCase):
 
             def ul_start(self, ast):
                 ctx = self._context
-                ctx._state = 1 if ctx._state is None else ctx._state + 1
+                ctx.substate = 1 if ctx.substate is None else ctx.substate + 1
                 return ast
 
             def ul_end(self, ast):
                 ctx = self._context
-                ctx._state = None if ctx._state is None or ctx._state <= 1 else ctx._state - 1
+                ctx.substate = None if ctx.substate is None or ctx.substate <= 1 else ctx.substate - 1
                 return ast
 
             def ul_marker(self, ast):
                 ctx = self._context
-                if ctx._state is not None and not ctx.tokenizer.match("*" * ctx._state):
+                if ctx.substate is not None and not ctx.tokenizer.match("*" * ctx.substate):
                     raise FailedSemantics("not at correct level")
                 return ast
 
diff --git a/test/parser_equivalence_test.py b/test/parser_equivalence_test.py
index 421ae86..f810eeb 100644
--- a/test/parser_equivalence_test.py
+++ b/test/parser_equivalence_test.py
@@ -1,6 +1,5 @@
 
-import subprocess  # noqa
-import py_compile  # noqa
+import importlib
 from pathlib import Path
 
 import pytest  # noqa
@@ -24,7 +23,7 @@ GRAMMAR = """
 
     addition
         =
-        | left:addition op:('+' | '-') ~ right:addition
+        | left:dice_expr op:('+' | '-') ~ right:addition
         | dice_expr
         ;
 
@@ -42,22 +41,24 @@ GRAMMAR = """
 """
 
 
-def compile_run(grammar, input, output):
+def generate_and_load_parser(name, grammar):
+    init_filename = Path('./tmp/__init__')
+    init_filename.touch(exist_ok=True)
+
     parser = gencode(name='Test', grammar=grammar)
-    parser_filename = Path('./tmp/test_codegen_parser.py')
+    parser_filename = Path(f'./tmp/{name}.py')
     with open(parser_filename, 'wt') as f:
         f.write(parser)
     try:
+        importlib.invalidate_caches()
+        module = importlib.import_module(f'tmp.{name}', 'tmp')
+        importlib.reload(module)
         try:
-            from tmp.test_codegen_parser import UnknownParser as Parser  # pylint: disable=all
-        except ImportError:
-            from tmp.test_codegen_parser import TestParser as Parser  # pylint: disable=all
-        output = Parser().parse(input, parseinfo=False)
-        assert output == output
+            return module.UnknownParser()  # noqa
+        except (AttributeError, ImportError):
+            return module.TestParser()  # noqa
     finally:
         pass
-        # init_filename.unlink()
-        # input_filename.unlink()
         # parser_filename.unlink()
 
 
@@ -72,11 +73,9 @@ def test_codegen_parse():
     init_filename = Path('./tmp/__init__.py')
     init_filename.touch(exist_ok=True)
 
-    input_filename = Path('./tmp/input.txt')
-    with open(input_filename, 'wt') as f:
-        f.write(INPUT)
-
-    compile_run(GRAMMAR, INPUT, OUTPUT)
+    parser = generate_and_load_parser('test_codegen_parse', GRAMMAR)
+    output = parser.parse(INPUT, parseinfo=False)
+    assert output == OUTPUT
 
 
 # @pytest.mark.skip('work in progress')
@@ -98,3 +97,30 @@ def test_error_messages():
     except FailedParse as e:  # noqa
         e1 = str(e)
     assert "expecting one of: 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o'" in e1
+
+
+# @pytest.mark.skip('work in progress')
+def test_name_checked():
+    grammar = '''
+        @@grammar :: Test
+        @@ignorecase :: True
+        @@keyword :: if
+
+        start = rule ;
+        rule = @:word if_exp $ ;
+        if_exp = 'if' digit ;
+        @name word = /\w+/ ;
+        digit = /\d/ ;
+    '''
+
+    def subtest(parser):
+        parser.parse('nonIF if 1', trace=False)
+        with pytest.raises(FailedParse):
+            parser.parse('if if 1', trace=False)
+        with pytest.raises(FailedParse):
+            parser.parse('IF if 1', trace=False)
+
+    parser = compile(grammar, 'Test')
+    subtest(parser)
+    parser = generate_and_load_parser('test_name_checked', grammar)
+    subtest(parser)
diff --git a/test/zzz_bootstrap/bootstrap_test.py b/test/zzz_bootstrap/bootstrap_test.py
index a96e5dc..48404a3 100644
--- a/test/zzz_bootstrap/bootstrap_test.py
+++ b/test/zzz_bootstrap/bootstrap_test.py
@@ -45,9 +45,9 @@ class BootstrapTests(unittest.TestCase):
         with open('grammar/tatsu.ebnf') as f:
             text = str(f.read())
         g = GrammarGenerator('EBNFBootstrap')
-        g.parse(text)
+        result = g.parse(text)
 
-        generated_grammar1 = str(g.ast['start'])
+        generated_grammar1 = str(result)
         with open('./tmp/01.ebnf', 'w') as f:
             f.write(generated_grammar1)
 
@@ -55,8 +55,8 @@ class BootstrapTests(unittest.TestCase):
         with open('./tmp/01.ebnf') as f:
             text = str(f.read())
         g = GrammarGenerator('EBNFBootstrap')
-        g.parse(text)
-        generated_grammar2 = str(g.ast['start'])
+        result = g.parse(text)
+        generated_grammar2 = str(result)
         with open('./tmp/02.ebnf', 'w') as f:
             f.write(generated_grammar2)
         self.assertEqual(generated_grammar2, generated_grammar1)
@@ -73,8 +73,7 @@ class BootstrapTests(unittest.TestCase):
         with open('./tmp/02.ebnf') as f:
             text = f.read()
         g = GrammarGenerator('EBNFBootstrap')
-        g.parse(text)
-        parser = g.ast['start']
+        parser = g.parse(text)
     #    pprint(parser.first_sets, indent=2, depth=3)
         generated_grammar4 = str(parser)
         with open('./tmp/04.ebnf', 'w') as f:

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-flake8 pytest-mypy pytest-pylint
3.9
requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
astroid==3.3.9
asttokens==3.0.0
babel==2.17.0
backports.tarfile==1.2.0
cachetools==5.5.2
certifi==2025.1.31
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.1
colorama==0.4.6
cryptography==44.0.2
decorator==5.2.1
dill==0.3.9
distlib==0.3.9
docutils==0.21.2
exceptiongroup==1.2.2
executing==2.2.0
filelock==3.18.0
flake8==7.2.0
future==1.0.0
git-pylint-commit-hook==2.6.1
id==1.5.0
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
ipython==8.18.1
isort==6.0.1
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jedi==0.19.2
jeepney==0.9.0
Jinja2==3.1.6
keyring==25.6.0
markdown-it-py==3.0.0
MarkupSafe==3.0.2
matplotlib-inline==0.1.7
mccabe==0.7.0
mdurl==0.1.2
more-itertools==10.6.0
mypy==1.15.0
mypy-extensions==1.0.0
nh3==0.2.21
packaging==24.2
pandoc==2.4
parso==0.8.4
pexpect==4.9.0
platformdirs==4.3.7
pluggy==1.5.0
plumbum==1.9.0
ply==3.11
prompt_toolkit==3.0.50
psutil==7.0.0
ptyprocess==0.7.0
pure_eval==0.2.3
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.1
Pygments==2.19.1
pylint==3.3.6
pyproject-api==1.9.0
pytest==8.3.5
pytest-flake8==1.3.0
pytest-mypy==1.0.0
pytest-pylint==0.21.0
PyYAML==6.0.2
readme_renderer==44.0
regex==2024.11.6
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
SecretStorage==3.3.3
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
stack-data==0.6.3
-e git+https://github.com/neogeny/TatSu.git@22e741a684849aaebe9d3eb1c9a05b74605dfb2c#egg=TatSu
tomli==2.2.1
tomlkit==0.13.2
tox==4.25.0
traitlets==5.14.3
twine==6.1.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
wcwidth==0.2.13
zipp==3.21.0

name: TatSu
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - astroid==3.3.9
      - asttokens==3.0.0
      - babel==2.17.0
      - backports-tarfile==1.2.0
      - cachetools==5.5.2
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - colorama==0.4.6
      - cryptography==44.0.2
      - decorator==5.2.1
      - dill==0.3.9
      - distlib==0.3.9
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - executing==2.2.0
      - filelock==3.18.0
      - flake8==7.2.0
      - future==1.0.0
      - git-pylint-commit-hook==2.6.1
      - id==1.5.0
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - ipython==8.18.1
      - isort==6.0.1
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jedi==0.19.2
      - jeepney==0.9.0
      - jinja2==3.1.6
      - keyring==25.6.0
      - markdown-it-py==3.0.0
      - markupsafe==3.0.2
      - matplotlib-inline==0.1.7
      - mccabe==0.7.0
      - mdurl==0.1.2
      - more-itertools==10.6.0
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - nh3==0.2.21
      - packaging==24.2
      - pandoc==2.4
      - parso==0.8.4
      - pexpect==4.9.0
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - plumbum==1.9.0
      - ply==3.11
      - prompt-toolkit==3.0.50
      - psutil==7.0.0
      - ptyprocess==0.7.0
      - pure-eval==0.2.3
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pylint==3.3.6
      - pyproject-api==1.9.0
      - pytest==8.3.5
      - pytest-flake8==1.3.0
      - pytest-mypy==1.0.0
      - pytest-pylint==0.21.0
      - pyyaml==6.0.2
      - readme-renderer==44.0
      - regex==2024.11.6
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==14.0.0
      - secretstorage==3.3.3
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinx-rtd-theme==3.0.2
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - stack-data==0.6.3
      - tomli==2.2.1
      - tomlkit==0.13.2
      - tox==4.25.0
      - traitlets==5.14.3
      - twine==6.1.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - wcwidth==0.2.13
      - zipp==3.21.0
prefix: /opt/conda/envs/TatSu

test/grammar/stateful_test.py::StatefulTests::test_stateful
test/parser_equivalence_test.py::test_model_parse test/parser_equivalence_test.py::test_codegen_parse test/parser_equivalence_test.py::test_error_messages test/parser_equivalence_test.py::test_name_checked test/zzz_bootstrap/bootstrap_test.py::BootstrapTests::test_bootstrap
tatsu/infos.py .pylintrc tatsu/grammars.py tatsu/util/_common.py tatsu/contexts.py tatsu/parsing.py tatsu/codegen/python.py tatsu/exceptions.py
tatsu/infos.py .pylintrc tatsu/grammars.py tatsu/util/_common.py tatsu/contexts.py tatsu/parsing.py tatsu/codegen/python.py tatsu/exceptions.py