Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ cython_debug/

# project folders
.idea
.vscode/settings.json
build
dist
pyjapt.egg-info
7 changes: 0 additions & 7 deletions .vscode/settings.json

This file was deleted.

8 changes: 2 additions & 6 deletions build.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
30 changes: 18 additions & 12 deletions pyjapt/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -1084,6 +1086,10 @@ def error(parser: "ShiftReduceParser"):
f' "{parser.current_token.column}"',
)

@property
def has_conflicts(self) -> bool:
return self.conflicts != []

#############
# End #
#############
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
22 changes: 18 additions & 4 deletions tests/test_arithmetic_grammar.py
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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")
Expand Down Expand Up @@ -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"
33 changes: 33 additions & 0 deletions tests/test_lalr1_but_not_slr.py
Original file line number Diff line number Diff line change
@@ -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():
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing pytest marker for SLR test. Similar to the pattern in test_arithmetic_grammar.py, this test function should have the @pytest.mark.slr decorator to allow selective test execution. You'll also need to import pytest at the top of the file.

Copilot uses AI. Check for mistakes.
g = grammar()
parser = g.get_parser("slr")
assert parser.has_conflicts


@pytest.mark.lalr1
def test_lalr():
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing pytest marker for LALR1 test. Similar to the pattern in test_arithmetic_grammar.py (lines 71-74), this test function should have the @pytest.mark.lalr1 decorator to allow selective test execution. You'll also need to import pytest at the top of the file.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

g = grammar()
parser = g.get_parser("lalr1")
assert not parser.has_conflicts
31 changes: 31 additions & 0 deletions tests/test_lr1_but_not_lalr_grammar.py
Original file line number Diff line number Diff line change
@@ -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():
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing pytest marker for LALR1 test. Similar to the pattern in test_arithmetic_grammar.py (lines 71-74), this test function should have the @pytest.mark.lalr1 decorator to allow selective test execution. You'll also need to import pytest at the top of the file.

Copilot uses AI. Check for mistakes.
g = grammar()
parser = g.get_parser("lalr1")
assert parser.has_conflicts


def test_lr1():
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing pytest marker for LR1 test. Similar to the pattern in test_arithmetic_grammar.py (lines 65-68), this test function should have the @pytest.mark.lr1 decorator to allow selective test execution. You'll also need to import pytest at the top of the file.

Copilot uses AI. Check for mistakes.
g = grammar()
parser = g.get_parser("lr1")
assert not parser.has_conflicts