Repository: neogeny/TatSu

Issue:
misformatted output of unknown rules since 5.6.0
Since version 5.6.0, TatSu's output of unknown rules in grammars is misformatted:

```

import tatsu

GRAMMAR = '''
@@grammar::TestGrammar

  block = test ;


'''
if __name__ == '__main__':
    import pprint
    from tatsu import parse

    ast = parse(GRAMMAR, 'abc')
    pprint.pprint(ast, indent=2, width=20)

```

will output
```
tatsu.exceptions.GrammarError: Unknown rules, no parser generated:
e
s
t
```

In version 5.5.0, it was
```
tatsu.exceptions.GrammarError: Unknown rules, no parser generated:
test
```

Discussion:

Patch:
diff --git a/.pylintrc b/.pylintrc
index 9d0055d..e192645 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -166,6 +166,7 @@ disable=C0102, C0103, C0111, C0122, C0123,
         W0621, W0622,
         W0702, W0703, W0706,
         W0707, # consider using `raise from`
+        W1116, # pylint doesn'get tuples of type for isinstance()
         W1201, W1202,
         W1401,
         W1618
diff --git a/examples/g2e/python.ebnf b/examples/g2e/python.ebnf
index 46a8804..e69de29 100644
--- a/examples/g2e/python.ebnf
+++ b/examples/g2e/python.ebnf
@@ -1,741 +0,0 @@
-single_input
-    =
-    newline | simple_stmt | compound_stmt newline
-    ;
-
-
-file_input
-    =
-    {newline | stmt}
-    ;
-
-
-eval_input
-    =
-    {newline} testlist {newline}
-    ;
-
-
-decorators
-    =
-    {decorator}+
-    ;
-
-
-decorator
-    =
-    '@' dotted_attr ['(' [arglist] ')'] newline
-    ;
-
-
-dotted_attr
-    =
-    name {'.' name}
-    ;
-
-
-funcdef
-    =
-    [decorators] 'def' name parameters ':' suite
-    ;
-
-
-parameters
-    =
-    '(' [varargslist] ')'
-    ;
-
-
-varargslist
-    =
-        defparameter
-        {',' defparameter}
-        [','
-        [
-        '*' name [',' '**' name] | '**' name
-        ]
-        ]
-    |
-        '*' name [',' '**' name]
-    |
-        '**' name
-    ;
-
-
-defparameter
-    =
-    fpdef ['=' test]
-    ;
-
-
-fpdef
-    =
-    name | '(' fplist ')'
-    ;
-
-
-fplist
-    =
-    fpdef {',' fpdef} [',']
-    ;
-
-
-stmt
-    =
-    simple_stmt | compound_stmt
-    ;
-
-
-simple_stmt
-    =
-    small_stmt {';' small_stmt} [';'] newline
-    ;
-
-
-small_stmt
-    =
-    | expr_stmt
-    | print_stmt
-    | del_stmt
-    | pass_stmt
-    | flow_stmt
-    | import_stmt
-    | global_stmt
-    | exec_stmt
-    | assert_stmt
-    ;
-
-
-expr_stmt
-    =
-    testlist
-    [
-    augassign yield_expr | augassign testlist | assigns
-    ]
-    ;
-
-
-assigns
-    =
-    {assign_testlist}+ | {assign_yield}+
-    ;
-
-
-assign_testlist
-    =
-    '=' testlist
-    ;
-
-
-assign_yield
-    =
-    '=' yield_expr
-    ;
-
-
-augassign
-    =
-    | '+='
-    | '-='
-    | '*='
-    | '/='
-    | '%='
-    | '&='
-    | '|='
-    | '^='
-    | '<<='
-    | '>>='
-    | '**='
-    | '//='
-    ;
-
-
-print_stmt
-    =
-    'print'
-    [
-    printlist | '>>' printlist
-    ]
-    ;
-
-
-printlist
-    =
-    test {',' test} [',']
-    ;
-
-
-del_stmt
-    =
-    'del' exprlist
-    ;
-
-
-pass_stmt
-    =
-    'pass'
-    ;
-
-
-flow_stmt
-    =
-    break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
-    ;
-
-
-break_stmt
-    =
-    'break'
-    ;
-
-
-continue_stmt
-    =
-    'continue'
-    ;
-
-
-return_stmt
-    =
-    'return' [testlist]
-    ;
-
-
-yield_stmt
-    =
-    yield_expr
-    ;
-
-
-raise_stmt
-    =
-    'raise' [test [',' test [',' test]]]
-    ;
-
-
-import_stmt
-    =
-    import_name | import_from
-    ;
-
-
-import_name
-    =
-    'import' dotted_as_names
-    ;
-
-
-import_from
-    =
-    'from'
-    ({'.'} dotted_name | {'.'}+)
-    'import'
-    ('*' | import_as_names | '(' import_as_names ')')
-    ;
-
-
-import_as_names
-    =
-    import_as_name {',' import_as_name} [',']
-    ;
-
-
-import_as_name
-    =
-    name ['as' name]
-    ;
-
-
-dotted_as_name
-    =
-    dotted_name ['as' name]
-    ;
-
-
-dotted_as_names
-    =
-    dotted_as_name {',' dotted_as_name}
-    ;
-
-
-dotted_name
-    =
-    name {'.' name}
-    ;
-
-
-global_stmt
-    =
-    'global' name {',' name}
-    ;
-
-
-exec_stmt
-    =
-    'exec' expr ['in' test [',' test]]
-    ;
-
-
-assert_stmt
-    =
-    'assert' test [',' test]
-    ;
-
-
-compound_stmt
-    =
-    | if_stmt
-    | while_stmt
-    | for_stmt
-    | try_stmt
-    | with_stmt
-    | funcdef
-    | classdef
-    ;
-
-
-if_stmt
-    =
-    'if' test ':' suite {elif_clause} ['else' ':' suite]
-    ;
-
-
-elif_clause
-    =
-    'elif' test ':' suite
-    ;
-
-
-while_stmt
-    =
-    'while' test ':' suite ['else' ':' suite]
-    ;
-
-
-for_stmt
-    =
-    'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
-    ;
-
-
-try_stmt
-    =
-    'try'
-    ':'
-    suite
-    (
-        | {except_clause}+ ['else' ':' suite] ['finally' ':' suite]
-        | 'finally' ':' suite
-    )
-    ;
-
-
-with_stmt
-    =
-    'with' test [with_var] ':' suite
-    ;
-
-
-with_var
-    =
-    ('as' | name) expr
-    ;
-
-
-except_clause
-    =
-    'except' [test [',' test]] ':' suite
-    ;
-
-
-suite
-    =
-    simple_stmt | newline indent {stmt}+ dedent
-    ;
-
-
-indent
-    =
-    ()
-    ;
-
-
-dedent
-    =
-    ()
-    ;
-
-
-test
-    =
-    or_test ['if' or_test 'else' test] | lambdef
-    ;
-
-
-or_test
-    =
-    and_test {'or' and_test}
-    ;
-
-
-and_test
-    =
-    not_test {'and' not_test}
-    ;
-
-
-not_test
-    =
-    'not' not_test | comparison
-    ;
-
-
-comparison
-    =
-    expr {comp_op expr}
-    ;
-
-
-comp_op
-    =
-    | '<'
-    | '>'
-    | '=='
-    | '>='
-    | '<='
-    | '<>'
-    | '!='
-    | 'in'
-    | 'not' 'in'
-    | 'is'
-    | 'is' 'not'
-    ;
-
-
-expr
-    =
-    xor_expr {'|' xor_expr}
-    ;
-
-
-xor_expr
-    =
-    and_expr {'^' and_expr}
-    ;
-
-
-and_expr
-    =
-    shift_expr {'&' shift_expr}
-    ;
-
-
-shift_expr
-    =
-    arith_expr {('<<' | '>>') arith_expr}
-    ;
-
-
-arith_expr
-    =
-    term {('+' | '-') term}
-    ;
-
-
-term
-    =
-    factor {('*' | '/' | '%' | '//') factor}
-    ;
-
-
-factor
-    =
-    '+' factor | '-' factor | '~' factor | power
-    ;
-
-
-power
-    =
-    atom {trailer} ['**' factor]
-    ;
-
-
-atom
-    =
-        '('
-        [
-        yield_expr | testlist_gexp
-        ]
-
-        ')'
-    |
-        '[' [listmaker] ']'
-    |
-        '{' [dictmaker] '}'
-    |
-        '`' testlist '`'
-    |
-        name
-    |
-        int
-    |
-        LONGINT
-    |
-        float
-    |
-        complex
-    |
-        {STRING}+
-    ;
-
-
-listmaker
-    =
-    test (list_for | {',' test}) [',']
-    ;
-
-
-testlist_gexp
-    =
-    test ({',' test} [','] | gen_for)
-    ;
-
-
-lambdef
-    =
-    'lambda' [varargslist] ':' test
-    ;
-
-
-trailer
-    =
-    '(' [arglist] ')' | '[' subscriptlist ']' | '.' name
-    ;
-
-
-subscriptlist
-    =
-    subscript {',' subscript} [',']
-    ;
-
-
-subscript
-    =
-    '.' '.' '.' | test [':' [test] [sliceop]] | ':' [test] [sliceop]
-    ;
-
-
-sliceop
-    =
-    ':' [test]
-    ;
-
-
-exprlist
-    =
-    expr {',' expr} [',']
-    ;
-
-
-testlist
-    =
-    test {',' test} [',']
-    ;
-
-
-dictmaker
-    =
-    test ':' test {',' test ':' test} [',']
-    ;
-
-
-classdef
-    =
-    'class' name ['(' [testlist] ')'] ':' suite
-    ;
-
-
-arglist
-    =
-        argument
-        {',' argument}
-        [','
-        [
-        '*' test [',' '**' test] | '**' test
-        ]
-        ]
-    |
-        '*' test [',' '**' test]
-    |
-        '**' test
-    ;
-
-
-argument
-    =
-    test
-    [
-    ('=' test) | gen_for
-    ]
-    ;
-
-
-list_iter
-    =
-    list_for | list_if
-    ;
-
-
-list_for
-    =
-    'for' exprlist 'in' testlist [list_iter]
-    ;
-
-
-list_if
-    =
-    'if' test [list_iter]
-    ;
-
-
-gen_iter
-    =
-    gen_for | gen_if
-    ;
-
-
-gen_for
-    =
-    'for' exprlist 'in' or_test [gen_iter]
-    ;
-
-
-gen_if
-    =
-    'if' test [gen_iter]
-    ;
-
-
-yield_expr
-    =
-    'yield' [testlist]
-    ;
-
-
-float
-    =
-    | '.' DIGITS [EXPONENT]
-    | DIGITS '.' EXPONENT
-    | DIGITS ('.' [DIGITS [EXPONENT]] | EXPONENT)
-    ;
-
-
-LONGINT
-    =
-    int /[lL]/
-    ;
-
-
-EXPONENT
-    =
-    /[eE][+-]?/ DIGITS
-    ;
-
-
-int
-    =
-    '0' /[xX][0-9a-fA-F]+/ | '0' {DIGITS} | /[1-9]/ {DIGITS}
-    ;
-
-
-complex
-    =
-    int /[jJ]/ | float /[jJ]/
-    ;
-
-
-DIGITS
-    =
-    /[0-9]+/
-    ;
-
-
-name
-    =
-    /[a-zA-Z_][a-zA-Z_0-9]*/
-    ;
-
-
-STRING
-    =
-    [
-    /[ru]/ | 'ur'
-    ]
-
-    (
-        | "\\'\\'\\'" {TRIAPOS} "\\'\\'\\'"
-        | '"""' {TRIQUOTE} '"""'
-        | '"' {ESC | !/[\\\n"]/ /./} /["\']/ {ESC | !/[\\\n\']/ /./} "\\'"
-    )
-    ;
-
-
-TRIQUOTE
-    =
-    ['"'] ['"'] {ESC | !/[\\"]/ /./}+
-    ;
-
-
-TRIAPOS
-    =
-    ["\\'"] ["\\'"] {ESC | !/[\\\']/ /./}+
-    ;
-
-
-ESC
-    =
-    '\\\\' /\w+|\S+/
-    ;
-
-
-CONTINUED_LINE
-    =
-    '\\\\' ['\\r'] '\\n' /[ \t]*/ (nl:newline | ())
-    ;
-
-
-newline
-    =
-    {['\\u000C'] ['\\r'] '\\n'}+
-    ;
-
-
-ws
-    =
-    /[ \t\u000C]+/
-    ;
-
-
-leading_ws
-    =
-    (/[ \t]+/ | {' ' | '\\t'}+ {['\\r'] '\\n'})
-    ;
-
-
-comment
-    =
-    /[ \t]*/ '#' {!'\\n' /./} {'\\n'}+ | '#' {!'\\n' /./}
-    ;
-
-
-INDENT
-    =
-    !()
-    ;
-
-
-DEDENT
-    =
-    !()
-    ;
-
diff --git a/tatsu/collections/orderedset.py b/tatsu/collections/orderedset.py
index dd5a10f..7393a12 100644
--- a/tatsu/collections/orderedset.py
+++ b/tatsu/collections/orderedset.py
@@ -1,14 +1,15 @@
-# from https://github.com/LuminosoInsight/ordered-set/blob/master/ordered_set.py
-# removed Sequence behavior
+# NOTE: from https://github.com/LuminosoInsight/ordered-set/blob/master/ordered_set.py
 import itertools
 from typing import (
     Any,
     Dict,
     Iterable,
     Iterator,
+    Mapping,
     MutableSet,
     Optional,
     Sequence,
+    MutableSequence,
     Set,
     TypeVar,
 )
@@ -82,7 +83,12 @@ class OrderedSet(MutableSet[T], Sequence[T]):
         return all(item in other for item in self)
 
     def union(self, *other: Iterable[T]) -> "OrderedSet[T]":
-        inner = itertools.chain([self], *other)  # type: ignore
+        # do not split `str`
+        outer = tuple(
+            [o] if not isinstance(o, (Set, Mapping, MutableSequence)) else o
+            for o in other
+        )
+        inner = itertools.chain([self], *outer)  # type: ignore
         items = itertools.chain.from_iterable(inner)  # type: ignore
         return type(self)(itertools.chain(items))
 
diff --git a/tatsu/grammars.py b/tatsu/grammars.py
index 1bc1827..402986c 100644
--- a/tatsu/grammars.py
+++ b/tatsu/grammars.py
@@ -788,7 +788,7 @@ class RuleRef(Model):
 
     def missing_rules(self, rules):
         if self.name not in rules:
-            return {self.name}
+            return oset({self.name})
         return oset()
 
     def _used_rule_names(self):
@@ -977,7 +977,7 @@ class Grammar(Model):
 
         self._adopt_children(rules)
 
-        missing = self.missing_rules({r.name for r in self.rules})
+        missing = self.missing_rules(oset(r.name for r in self.rules))
         if missing:
             msg = '\n'.join([''] + list(sorted(missing)))
             raise GrammarError('Unknown rules, no parser generated:' + msg)


Test patch:
diff --git a/test/grammar/error_test.py b/test/grammar/error_test.py
new file mode 100644
index 0000000..ab4c8d2
--- /dev/null
+++ b/test/grammar/error_test.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+from tatsu.tool import parse
+from tatsu.exceptions import GrammarError
+
+
+def test_missing_rule():
+    grammar = '''
+        @@grammar::TestGrammar
+          block = test ;
+    '''
+    try:
+        parse(grammar, 'abc')
+    except GrammarError as e:
+        assert str(e) == 'Unknown rules, no parser generated:\ntest'
+
+
+def test_missing_rules():
+    grammar = '''
+        @@grammar::TestGrammar
+          block = test | test2;
+    '''
+    try:
+        parse(grammar, 'abc')
+    except GrammarError as e:
+        assert str(e) == 'Unknown rules, no parser generated:\ntest\ntest2'