diff --git a/.gitignore b/.gitignore index 3708df5..1de3273 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,7 @@ cython_debug/ # project folders .idea +.vscode/settings.json build dist pyjapt.egg-info \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 9b38853..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true -} \ No newline at end of file diff --git a/build.py b/build.py index f4cf679..3e4c38f 100644 --- a/build.py +++ b/build.py @@ -1,13 +1,9 @@ import os import re +import pyjapt -with open("pyjapt/__init__.py", "r") as f: - version = ( - re.search(r"__version__ = \"\d\.\d\.\d\"", f.read()) - .group() - .replace('__version__ = "', "")[:-1] - ) +version = pyjapt.__version__ with open("pyproject.toml", "r") as f: s = f.read() diff --git a/pyjapt/parsing.py b/pyjapt/parsing.py index 7189aa0..087fca5 100644 --- a/pyjapt/parsing.py +++ b/pyjapt/parsing.py @@ -2,14 +2,15 @@ import re import sys from typing import ( - List, - FrozenSet, - Optional, - Tuple, - Iterable, Callable, Dict, + FrozenSet, + Iterable, + List, + Literal, + Optional, Set, + Tuple, Union, ) @@ -500,7 +501,7 @@ def get_lexer(self) -> Lexer: self.lexical_error_handler, ) - def get_parser(self, name: str, verbose: bool = False): + def get_parser(self, name: Literal["slr", "lalr1", "lr1"], verbose: bool = False): if name == "slr": return SLRParser(self, verbose) @@ -734,7 +735,7 @@ def compute_local_first(firsts, alpha): return first_alpha -def compute_firsts(grammar: Grammar): +def compute_firsts(grammar: Grammar) -> Dict[Symbol, ContainerSet]: firsts = {} change = True @@ -808,7 +809,7 @@ def compute_follows(grammar: Grammar, firsts): ######################### # LR0 AUTOMATA BUILDING # ######################### -def closure_lr0(items: Iterable[Item]): +def closure_lr0(items: Iterable[Item]) -> FrozenSet[Item]: closure = set(items) pending = set(items) @@ -926,7 +927,7 @@ def goto_lr1(items, symbol, firsts=None, just_kernel=False): return items if just_kernel else closure_lr1(items, firsts) -def build_lr1_automaton(grammar, firsts=None): +def build_lr1_automaton(grammar: Grammar, firsts=None): assert len(grammar.start_symbol.productions) == 1, "Grammar must be augmented" if not firsts: @@ -1057,6 +1058,7 @@ def __init__( sys.stderr.write( f"Warning: {self.shift_reduce_count} Shift-Reduce Conflicts\n" ) + sys.stderr.write( f"Warning: {self.reduce_reduce_count} Reduce-Reduce Conflicts\n" ) @@ -1084,6 +1086,10 @@ def error(parser: "ShiftReduceParser"): f' "{parser.current_token.column}"', ) + @property + def has_conflicts(self) -> bool: + return self.conflicts != [] + ############# # End # ############# @@ -1127,9 +1133,9 @@ def _register(self, table, key, value): action, tag = table[key] if action != value[0]: if action == self.SHIFT: - table[ - key - ] = value # By default shifting if exists a Shift-Reduce Conflict + table[key] = ( + value # By default shifting if exists a Shift-Reduce Conflict + ) self.shift_reduce_count += 1 self.conflicts.append(("SR", value[1], tag)) else: diff --git a/pyproject.toml b/pyproject.toml index fd96fe7..50d919a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,5 +21,7 @@ build-backend = "poetry.masonry.api" [tool.pytest.ini_options] markers = [ "slr: test using the slr parser", + "lr1: test using the lr1 parser", + "lalr1: test using the lalr1 parser", "serial", ] \ No newline at end of file diff --git a/tests/test_arithmetic_grammar.py b/tests/test_arithmetic_grammar.py index 152f808..7f8a4e2 100644 --- a/tests/test_arithmetic_grammar.py +++ b/tests/test_arithmetic_grammar.py @@ -1,7 +1,8 @@ +from typing import Literal import pytest from pyjapt import Grammar -from pyjapt.typing import RuleList, Lexer, SLRParser, LR1Parser, LALR1Parser +from pyjapt.typing import RuleList, Lexer tests = [ @@ -17,7 +18,7 @@ ] -def get_artithmetic_exception_grammar() -> Grammar: +def get_arithmetic_expressions_grammar() -> Grammar: g = Grammar() expr = g.add_non_terminal("expr", True) term, fact = g.add_non_terminals("term fact") @@ -47,14 +48,27 @@ def empty_expression(s: RuleList): return g -def parse(parser_name: str, text: str): - g = get_artithmetic_exception_grammar() +def parse(parser_name: Literal["slr", "lr1", "lalr1"], text: str): + g = get_arithmetic_expressions_grammar() lexer = g.get_lexer() parser = g.get_parser(parser_name) return parser(lexer(text)) +@pytest.mark.slr @pytest.mark.parametrize("test,expected", tests) def test_slr(test, expected): assert parse("slr", test) == expected, "Bad Parsing" + + +@pytest.mark.lr1 +@pytest.mark.parametrize("test,expected", tests) +def test_lr1(test, expected): + assert parse("lr1", test) == expected, "Bad Parsing" + + +@pytest.mark.lalr1 +@pytest.mark.parametrize("test,expected", tests) +def test_lalr1(test, expected): + assert parse("lalr1", test) == expected, "Bad Parsing" diff --git a/tests/test_lalr1_but_not_slr.py b/tests/test_lalr1_but_not_slr.py new file mode 100644 index 0000000..2498209 --- /dev/null +++ b/tests/test_lalr1_but_not_slr.py @@ -0,0 +1,33 @@ +import pytest + +from pyjapt import Grammar + + +def grammar(): + g = Grammar() + + S = g.add_non_terminal("S", True) + (A,) = g.add_non_terminals("A") + + g.add_terminals("a b c d") + + S %= "A a" + S %= "b A c" + S %= "d c" + S %= "b d a" + A %= "d" + + return g + + +def test_slr(): + g = grammar() + parser = g.get_parser("slr") + assert parser.has_conflicts + + +@pytest.mark.lalr1 +def test_lalr(): + g = grammar() + parser = g.get_parser("lalr1") + assert not parser.has_conflicts diff --git a/tests/test_lr1_but_not_lalr_grammar.py b/tests/test_lr1_but_not_lalr_grammar.py new file mode 100644 index 0000000..d2aab13 --- /dev/null +++ b/tests/test_lr1_but_not_lalr_grammar.py @@ -0,0 +1,31 @@ +from pyjapt import Grammar + + +def grammar(): + g = Grammar() + + S = g.add_non_terminal("S", True) + A, B = g.add_non_terminals("A B") + + g.add_terminals("a b c d") + + S %= "A a" + S %= "b A c" + S %= "B c" + S %= "b B a" + A %= "d" + B %= "d" + + return g + + +def test_lalr(): + g = grammar() + parser = g.get_parser("lalr1") + assert parser.has_conflicts + + +def test_lr1(): + g = grammar() + parser = g.get_parser("lr1") + assert not parser.has_conflicts