syntactes 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syntactes-0.1.0/PKG-INFO +20 -0
- syntactes-0.1.0/README.md +0 -0
- syntactes-0.1.0/pyproject.toml +37 -0
- syntactes-0.1.0/setup.cfg +4 -0
- syntactes-0.1.0/src/syntactes/__init__.py +4 -0
- syntactes-0.1.0/src/syntactes/_action.py +65 -0
- syntactes-0.1.0/src/syntactes/_item.py +61 -0
- syntactes-0.1.0/src/syntactes/_state.py +60 -0
- syntactes-0.1.0/src/syntactes/generator.py +255 -0
- syntactes-0.1.0/src/syntactes/grammar.py +22 -0
- syntactes-0.1.0/src/syntactes/rule.py +31 -0
- syntactes-0.1.0/src/syntactes/table.py +130 -0
- syntactes-0.1.0/src/syntactes/token.py +45 -0
- syntactes-0.1.0/src/syntactes.egg-info/PKG-INFO +20 -0
- syntactes-0.1.0/src/syntactes.egg-info/SOURCES.txt +15 -0
- syntactes-0.1.0/src/syntactes.egg-info/dependency_links.txt +1 -0
- syntactes-0.1.0/src/syntactes.egg-info/top_level.txt +1 -0
syntactes-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: syntactes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Parser generator
|
|
5
|
+
Author-email: Maximos Nikiforakis <nikiforos@live.co.uk>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Maxcode123/syntactes
|
|
8
|
+
Project-URL: Issues, https://github.com/Maxcode123/syntactes/issues
|
|
9
|
+
Keywords: parser,generator
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[tool.setuptools.packages.find]
|
|
2
|
+
where = ["src"]
|
|
3
|
+
include = ["syntactes"]
|
|
4
|
+
exclude = ["syntactes.tests*"]
|
|
5
|
+
namespaces = false
|
|
6
|
+
|
|
7
|
+
[build-system]
|
|
8
|
+
requires = ["setuptools"]
|
|
9
|
+
build-backend = "setuptools.build_meta"
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "syntactes"
|
|
13
|
+
version = "0.1.0"
|
|
14
|
+
authors = [
|
|
15
|
+
{ name="Maximos Nikiforakis", email="nikiforos@live.co.uk" }
|
|
16
|
+
]
|
|
17
|
+
description = "Parser generator"
|
|
18
|
+
keywords = ["parser", "generator"]
|
|
19
|
+
readme = "README.md"
|
|
20
|
+
license = {text = "MIT"}
|
|
21
|
+
requires-python = ">=3.10"
|
|
22
|
+
dependencies = []
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Programming Language :: Python",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Natural Language :: English",
|
|
31
|
+
"License :: OSI Approved :: MIT License",
|
|
32
|
+
"Topic :: Software Development :: Compilers",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/Maxcode123/syntactes"
|
|
37
|
+
Issues = "https://github.com/Maxcode123/syntactes/issues"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Actionable(Protocol):
|
|
6
|
+
"""
|
|
7
|
+
State or Rule.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
number: int
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ActionType(Enum):
|
|
14
|
+
SHIFT = "SHIFT"
|
|
15
|
+
REDUCE = "REDUCE"
|
|
16
|
+
ACCEPT = "ACCEPT"
|
|
17
|
+
REJECT = "REJECT"
|
|
18
|
+
|
|
19
|
+
def abbreviated(self) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Returns an abbreviated version of the action type.
|
|
22
|
+
"""
|
|
23
|
+
if self == self.SHIFT:
|
|
24
|
+
return "s"
|
|
25
|
+
if self == self.REDUCE:
|
|
26
|
+
return "r"
|
|
27
|
+
if self == self.ACCEPT:
|
|
28
|
+
return "a"
|
|
29
|
+
if self == self.REJECT:
|
|
30
|
+
return "e"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Action:
|
|
34
|
+
"""
|
|
35
|
+
Parsing action. Contains data relevant to the actionable object (State or Rule)
|
|
36
|
+
and the relevant action to be applied.
|
|
37
|
+
|
|
38
|
+
The `actionable_number` refers to the state number for `State` actionables and
|
|
39
|
+
to the rule number for `Rule` actionables.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, actionable: Actionable, action_type: ActionType) -> None:
|
|
43
|
+
self.actionable = actionable
|
|
44
|
+
self.action_type = action_type
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
return f"<Action: {self}>"
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
if self.action_type in {ActionType.ACCEPT, ActionType.REJECT}:
|
|
51
|
+
return str(self.action_type.abbreviated())
|
|
52
|
+
|
|
53
|
+
return f"{self.action_type.abbreviated()}{self.actionable.number}"
|
|
54
|
+
|
|
55
|
+
def __hash__(self) -> int:
|
|
56
|
+
return hash((self.actionable, self.action_type))
|
|
57
|
+
|
|
58
|
+
def __eq__(self, other) -> bool:
|
|
59
|
+
if not isinstance(other, Action):
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
self.actionable == other.actionable
|
|
64
|
+
and self.action_type == other.action_type
|
|
65
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from syntactes.rule import Rule
|
|
4
|
+
from syntactes.token import Token
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LR0Item:
|
|
8
|
+
"""
|
|
9
|
+
Item of LR0 parser. Contains rule and current position in rule.
|
|
10
|
+
Current position is denoted with the dot '.'.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, rule: Rule, position: int) -> None:
|
|
14
|
+
self.rule = rule
|
|
15
|
+
self.position = position
|
|
16
|
+
|
|
17
|
+
def dot_is_last(self) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Returns True if the dot in the item is in the last position of the rhs,
|
|
20
|
+
False otherwise.
|
|
21
|
+
"""
|
|
22
|
+
return self.position == self.rule.rhs_len
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def after_dot(self) -> Optional[Token]:
|
|
26
|
+
"""
|
|
27
|
+
Returns the symbol after the dot in the current item.
|
|
28
|
+
If the dot is in the last position, returns None
|
|
29
|
+
"""
|
|
30
|
+
if self.dot_is_last():
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return self.rule.rhs[self.position]
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def before_dot(self) -> Optional[Token]:
|
|
37
|
+
"""
|
|
38
|
+
Returns the symbol before the dot in the current item.
|
|
39
|
+
If the dot is in the first position, returns None.
|
|
40
|
+
"""
|
|
41
|
+
if self.position == 0:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
return self.rule.rhs[self.position - 1]
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
return f"<LR0Item: {self}>"
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
rhs = [s for s in self.rule.rhs]
|
|
51
|
+
rhs.insert(self.position, ".")
|
|
52
|
+
return f"{self.rule.lhs} -> " + " ".join(map(str, rhs))
|
|
53
|
+
|
|
54
|
+
def __hash__(self) -> int:
|
|
55
|
+
return hash((self.rule, self.position))
|
|
56
|
+
|
|
57
|
+
def __eq__(self, other) -> bool:
|
|
58
|
+
if not isinstance(other, LR0Item):
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
return self.rule == other.rule and self.position == other.position
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Iterable, Protocol
|
|
2
|
+
|
|
3
|
+
from syntactes._item import LR0Item
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class State(Protocol):
|
|
7
|
+
"""
|
|
8
|
+
State of parser automaton. A state is a set of items.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
items: set
|
|
12
|
+
|
|
13
|
+
def __repr__(self) -> str: ...
|
|
14
|
+
def __str__(self) -> str: ...
|
|
15
|
+
def __hash__(self) -> int: ...
|
|
16
|
+
def __eq__(self) -> bool: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LR0State:
|
|
20
|
+
"""
|
|
21
|
+
State of LR0 parser. A LR0 state is a set of LR0 items.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self.number = None
|
|
26
|
+
self.items = set()
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def from_items(items: Iterable[LR0Item]) -> "LR0State":
|
|
30
|
+
"""
|
|
31
|
+
Create an LR0 state from a set of LR0 items.
|
|
32
|
+
"""
|
|
33
|
+
state = LR0State()
|
|
34
|
+
{state.add_item(item) for item in items}
|
|
35
|
+
|
|
36
|
+
return state
|
|
37
|
+
|
|
38
|
+
def add_item(self, item: LR0Item) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Adds an item to the state.
|
|
41
|
+
"""
|
|
42
|
+
self.items.add(item)
|
|
43
|
+
|
|
44
|
+
def set_number(self, number: int) -> None:
|
|
45
|
+
self.number = number
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return f"<LR0State: {self.number}>"
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
return f"{self.number}:" + "(" + ", ".join(map(str, self.items)) + ")"
|
|
52
|
+
|
|
53
|
+
def __hash__(self) -> int:
|
|
54
|
+
return hash(frozenset(self.items))
|
|
55
|
+
|
|
56
|
+
def __eq__(self, other) -> bool:
|
|
57
|
+
if not isinstance(other, LR0State):
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
return self.items == other.items
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
from syntactes._action import Action, ActionType
|
|
2
|
+
from syntactes._item import LR0Item
|
|
3
|
+
from syntactes._state import LR0State
|
|
4
|
+
from syntactes.grammar import Grammar
|
|
5
|
+
from syntactes.table import Entry, ParsingTable
|
|
6
|
+
from syntactes.token import Token
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LR0Generator:
|
|
10
|
+
"""
|
|
11
|
+
Generator of LR0 parsing tables.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, grammar: Grammar) -> None:
|
|
15
|
+
self.grammar = grammar
|
|
16
|
+
|
|
17
|
+
def generate(self) -> ParsingTable:
|
|
18
|
+
"""
|
|
19
|
+
Generates an LR0 parsing table for the configured grammar.
|
|
20
|
+
"""
|
|
21
|
+
states, shift_entries = self._create_states_and_shift_entries()
|
|
22
|
+
reduce_entries = self._create_reduce_entries(states)
|
|
23
|
+
|
|
24
|
+
entries = shift_entries | reduce_entries
|
|
25
|
+
|
|
26
|
+
table = ParsingTable.from_entries(entries, self.grammar)
|
|
27
|
+
|
|
28
|
+
return table
|
|
29
|
+
|
|
30
|
+
def closure(self, items: set[LR0Item]) -> set[LR0Item]:
|
|
31
|
+
"""
|
|
32
|
+
Computes and returns the closure for the given set of items.
|
|
33
|
+
|
|
34
|
+
The closure operation adds more items to a set of items when there
|
|
35
|
+
is a dot to the left of a non-terminal symbol.
|
|
36
|
+
|
|
37
|
+
e.g.
|
|
38
|
+
for any item S -> . E in the given items, closure adds E -> . T
|
|
39
|
+
and T -> . x, where E -> T and T -> x are production rules.
|
|
40
|
+
"""
|
|
41
|
+
_set = {item for item in items}
|
|
42
|
+
|
|
43
|
+
for item in items:
|
|
44
|
+
if item.dot_is_last():
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
new_items = self._get_related_items(item.after_dot)
|
|
48
|
+
_set |= new_items
|
|
49
|
+
|
|
50
|
+
return _set
|
|
51
|
+
|
|
52
|
+
def goto(self, items: set[LR0Item], token: Token) -> set[LR0Item]:
|
|
53
|
+
"""
|
|
54
|
+
Computes and returns the GOTO set for the given set of items.
|
|
55
|
+
|
|
56
|
+
The goto operation creates a set where all items have the dot past the
|
|
57
|
+
given symbol.
|
|
58
|
+
"""
|
|
59
|
+
_set: set[LR0Item] = set()
|
|
60
|
+
|
|
61
|
+
for item in items:
|
|
62
|
+
if item.dot_is_last() or item.after_dot != token:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
next_item = LR0Item(item.rule, item.position + 1)
|
|
66
|
+
_set.add(next_item)
|
|
67
|
+
|
|
68
|
+
return self.closure(_set)
|
|
69
|
+
|
|
70
|
+
def get_states(self) -> set[LR0State]:
|
|
71
|
+
"""
|
|
72
|
+
Returns the set of automaton states for the configured grammar.
|
|
73
|
+
"""
|
|
74
|
+
states, _ = self._create_states_and_shift_entries()
|
|
75
|
+
return states
|
|
76
|
+
|
|
77
|
+
def _first(self, symbol: Token) -> set[Token]:
|
|
78
|
+
"""
|
|
79
|
+
Computes and returns the FIRST set for the given symbol.
|
|
80
|
+
|
|
81
|
+
The FIRST set of a symbol 'G' is the set of terminal symbols that are
|
|
82
|
+
first in the right-hand side of a rule where 'G' is the left-hand side.
|
|
83
|
+
|
|
84
|
+
e.g. 't', 'k' and 'a' would be the FIRST set of G for the below rules:
|
|
85
|
+
1. G -> t
|
|
86
|
+
2. G -> kM
|
|
87
|
+
3. G -> T
|
|
88
|
+
4. T -> a
|
|
89
|
+
where M is either terminal or non-terminal and T is non-terminal.
|
|
90
|
+
'a' would be included in the FIRST set because if rule 4 is substituted in
|
|
91
|
+
rule 3, 'a' (which is a terminal) could be derived from 'G'.
|
|
92
|
+
"""
|
|
93
|
+
if symbol.is_terminal:
|
|
94
|
+
return {symbol}
|
|
95
|
+
|
|
96
|
+
_set: set[Token] = set()
|
|
97
|
+
|
|
98
|
+
for rule in self.grammar.rules:
|
|
99
|
+
if rule.lhs != symbol:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if rule.rhs[0].is_terminal:
|
|
103
|
+
_set.add(rule.rhs[0])
|
|
104
|
+
elif rule.rhs_len == 1:
|
|
105
|
+
_set |= self._first(rule.rhs[0])
|
|
106
|
+
|
|
107
|
+
return _set
|
|
108
|
+
|
|
109
|
+
def _follow(self, symbol: Token) -> set[Token]:
|
|
110
|
+
"""
|
|
111
|
+
Computes and returns the FOLLOW set for the given symbol.
|
|
112
|
+
|
|
113
|
+
The FOLLOW set of a symbol 'G' is the set of terminals that can immediately
|
|
114
|
+
follow 'G' in a rule.
|
|
115
|
+
"""
|
|
116
|
+
if symbol.is_terminal:
|
|
117
|
+
return set()
|
|
118
|
+
|
|
119
|
+
_set: set[Token] = set()
|
|
120
|
+
|
|
121
|
+
for rule in self.grammar.rules:
|
|
122
|
+
for i, s in enumerate(rule.rhs):
|
|
123
|
+
if s != symbol:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
if i == rule.rhs_len - 1:
|
|
127
|
+
if rule.lhs == symbol:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
_set |= self._follow(rule.lhs)
|
|
131
|
+
else:
|
|
132
|
+
_set |= self._first(rule.rhs[i + 1])
|
|
133
|
+
|
|
134
|
+
return _set
|
|
135
|
+
|
|
136
|
+
def _get_related_items(self, symbol: Token) -> set[LR0Item]:
|
|
137
|
+
"""
|
|
138
|
+
e.g. the items X -> .g, Y -> .p would be returned for the below grammar rules:
|
|
139
|
+
1. X -> g
|
|
140
|
+
2. X -> Y
|
|
141
|
+
3. Y -> p
|
|
142
|
+
where 'g' and 'p' are terminals.
|
|
143
|
+
"""
|
|
144
|
+
_set: set[LR0Item] = set()
|
|
145
|
+
|
|
146
|
+
for rule in self.grammar.rules:
|
|
147
|
+
if rule.lhs == symbol:
|
|
148
|
+
_set.add(LR0Item(rule, 0))
|
|
149
|
+
|
|
150
|
+
if rule.rhs_len == 1 and not rule.rhs[0].is_terminal:
|
|
151
|
+
_set |= self._get_related_items(rule.rhs[0])
|
|
152
|
+
|
|
153
|
+
return _set
|
|
154
|
+
|
|
155
|
+
def _create_states_and_shift_entries(self) -> tuple[set[LR0State], set[Entry]]:
|
|
156
|
+
"""
|
|
157
|
+
Computes and returns the states and entries for shift actions.
|
|
158
|
+
"""
|
|
159
|
+
states, entries = dict(), set()
|
|
160
|
+
|
|
161
|
+
initial_items = self.closure({LR0Item(self.grammar.starting_rule, 0)})
|
|
162
|
+
initial_state = LR0State.from_items(initial_items)
|
|
163
|
+
initial_state.set_number(1)
|
|
164
|
+
states[initial_state] = 1
|
|
165
|
+
|
|
166
|
+
_states, _entries = dict(), set()
|
|
167
|
+
while (_states, _entries) != (states, entries):
|
|
168
|
+
_states = {s: n for s, n in states.items()}
|
|
169
|
+
_entries = {e for e in entries}
|
|
170
|
+
states, entries = self._extend_states_and_shift_entries(_states, _entries)
|
|
171
|
+
|
|
172
|
+
return set(states.keys()), entries
|
|
173
|
+
|
|
174
|
+
def _extend_states_and_shift_entries(
|
|
175
|
+
self, states: dict[LR0State, int], entries: set[Entry]
|
|
176
|
+
) -> tuple[dict[LR0State, int], set[Entry]]:
|
|
177
|
+
"""
|
|
178
|
+
Extends states and entries following the below algorithm:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
for each state S in states
|
|
182
|
+
for each item A -> a.Xb in S
|
|
183
|
+
J = goto(S, X)
|
|
184
|
+
states.add(J)
|
|
185
|
+
entries.add((S->J, X))
|
|
186
|
+
```
|
|
187
|
+
"""
|
|
188
|
+
_states = {s: n for s, n in states.items()}
|
|
189
|
+
_entries = {e for e in entries}
|
|
190
|
+
|
|
191
|
+
EOF = Token.eof()
|
|
192
|
+
for state in states:
|
|
193
|
+
for item in state.items:
|
|
194
|
+
if item.dot_is_last() or item.after_dot == EOF:
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
new_items = self.goto(state.items, item.after_dot)
|
|
198
|
+
|
|
199
|
+
if len(new_items) == 0:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
new = LR0State.from_items(new_items)
|
|
203
|
+
|
|
204
|
+
number = _states.setdefault(new, len(_states) + 1)
|
|
205
|
+
new.set_number(number)
|
|
206
|
+
|
|
207
|
+
action = Action(new, ActionType.SHIFT)
|
|
208
|
+
_entries.add(Entry(state, item.after_dot, action))
|
|
209
|
+
|
|
210
|
+
return _states, _entries
|
|
211
|
+
|
|
212
|
+
def _create_reduce_entries(self, states: set[LR0State]) -> set[Entry]:
|
|
213
|
+
"""
|
|
214
|
+
Computes and returns the entries for reduce actions and the accept action.
|
|
215
|
+
"""
|
|
216
|
+
entries: set[Entry] = set()
|
|
217
|
+
|
|
218
|
+
for state in states:
|
|
219
|
+
for item in state.items:
|
|
220
|
+
if item.after_dot == Token.eof():
|
|
221
|
+
action = Action(item.rule, ActionType.ACCEPT)
|
|
222
|
+
entries.add(Entry(state, Token.eof(), action))
|
|
223
|
+
|
|
224
|
+
if not item.dot_is_last():
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
action = Action(item.rule, ActionType.REDUCE)
|
|
228
|
+
for token in self.grammar.tokens:
|
|
229
|
+
if token.is_terminal:
|
|
230
|
+
entries.add(Entry(state, token, action))
|
|
231
|
+
|
|
232
|
+
return entries
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class SLRGenerator(LR0Generator):
|
|
236
|
+
def _create_reduce_entries(self, states: set[LR0State]) -> set[Entry]:
|
|
237
|
+
"""
|
|
238
|
+
Computes and returns the entries for reduce actions and the accept action.
|
|
239
|
+
"""
|
|
240
|
+
entries: set[Entry] = set()
|
|
241
|
+
|
|
242
|
+
for state in states:
|
|
243
|
+
for item in state.items:
|
|
244
|
+
if item.after_dot == Token.eof():
|
|
245
|
+
action = Action(item.rule, ActionType.ACCEPT)
|
|
246
|
+
entries.add(Entry(state, Token.eof(), action))
|
|
247
|
+
|
|
248
|
+
if not item.dot_is_last():
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
action = Action(item.rule, ActionType.REDUCE)
|
|
252
|
+
for token in self._follow(item.rule.lhs):
|
|
253
|
+
entries.add(Entry(state, token, action))
|
|
254
|
+
|
|
255
|
+
return entries
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
|
|
3
|
+
from syntactes.rule import Rule
|
|
4
|
+
from syntactes.token import Token
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Grammar:
|
|
8
|
+
"""
|
|
9
|
+
A grammar is a set of rules that describe a language.
|
|
10
|
+
|
|
11
|
+
The only valid 'words' of the language are the given tokens.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, starting_rule: Rule, rules: Iterable[Rule], tokens: set[Token]
|
|
16
|
+
) -> None:
|
|
17
|
+
"""
|
|
18
|
+
`starting_rule` should also be included in `rules`.
|
|
19
|
+
"""
|
|
20
|
+
self.starting_rule = starting_rule
|
|
21
|
+
self.rules = rules
|
|
22
|
+
self.tokens = tokens
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from syntactes.token import Token
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Rule:
|
|
5
|
+
"""
|
|
6
|
+
Production rule. Describes the break-down of a non-terminal symbol to
|
|
7
|
+
other symbols.
|
|
8
|
+
|
|
9
|
+
LHS -> RHS1 RHS2...
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, number: int, lhs: Token, *args: tuple[Token]) -> None:
|
|
13
|
+
self.number = number
|
|
14
|
+
self.lhs = lhs
|
|
15
|
+
self.rhs = args
|
|
16
|
+
self.rhs_len = len(args)
|
|
17
|
+
|
|
18
|
+
def __repr__(self) -> str:
|
|
19
|
+
return f"<Rule: {self}>"
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return f"{self.lhs} -> " + " ".join(map(str, self.rhs))
|
|
23
|
+
|
|
24
|
+
def __hash__(self) -> int:
|
|
25
|
+
return hash((self.lhs, self.rhs))
|
|
26
|
+
|
|
27
|
+
def __eq__(self, other) -> bool:
|
|
28
|
+
if not isinstance(other, Rule):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
return self.lhs == other.lhs and self.rhs == other.rhs
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from typing import Iterable, Optional, TypeAlias
|
|
2
|
+
|
|
3
|
+
from syntactes._action import Action
|
|
4
|
+
from syntactes._state import State
|
|
5
|
+
from syntactes.grammar import Grammar
|
|
6
|
+
from syntactes.token import Token
|
|
7
|
+
|
|
8
|
+
Row: TypeAlias = dict[Token, list[Action]]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Entry:
|
|
12
|
+
"""
|
|
13
|
+
An entry of the parsing table. Holds the information of a transition from
|
|
14
|
+
a state to another state via a symbol.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, from_state: State, token: Token, action: Action) -> None:
|
|
18
|
+
self.from_state = from_state
|
|
19
|
+
self.token = token
|
|
20
|
+
self.action = action
|
|
21
|
+
|
|
22
|
+
def __repr__(self) -> str:
|
|
23
|
+
return f"<Entry: {str(self)}>"
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return f"{self.from_state.number}, {self.action}, {self.token}"
|
|
27
|
+
|
|
28
|
+
def __hash__(self) -> int:
|
|
29
|
+
return hash((self.from_state, self.token, self.action))
|
|
30
|
+
|
|
31
|
+
def __eq__(self, other) -> bool:
|
|
32
|
+
if not isinstance(other, Entry):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
self.from_state == other.from_state
|
|
37
|
+
and self.token == other.token
|
|
38
|
+
and self.action == other.action
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ParsingTable:
|
|
43
|
+
"""
|
|
44
|
+
Table that contains all the transitions from state to state with a symbol.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, grammar: Grammar) -> None:
|
|
48
|
+
self.rows: dict[State, Row] = dict()
|
|
49
|
+
self._grammar = grammar
|
|
50
|
+
|
|
51
|
+
def get_actions(self, state: State, token: Token) -> Optional[list[Action]]:
|
|
52
|
+
"""
|
|
53
|
+
Get the actions from state with given number with `token`.
|
|
54
|
+
If there are no actions, returns None.
|
|
55
|
+
"""
|
|
56
|
+
return self.rows.get(state, {}).get(token, None)
|
|
57
|
+
|
|
58
|
+
def get(self, state: State) -> Optional[Row]:
|
|
59
|
+
"""
|
|
60
|
+
Get the mapping of tokens to actions for the given state number.
|
|
61
|
+
Returns None if the state is not found.
|
|
62
|
+
"""
|
|
63
|
+
return self.rows.get(state, None)
|
|
64
|
+
|
|
65
|
+
def add_entry(self, entry: Entry) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Adds an entry to the parsing table.
|
|
68
|
+
"""
|
|
69
|
+
self.rows.setdefault(entry.from_state, {}).setdefault(
|
|
70
|
+
entry.token, list()
|
|
71
|
+
).append(entry.action)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def from_entries(
|
|
75
|
+
entries: Iterable[Entry], tokens: Iterable[Token]
|
|
76
|
+
) -> "ParsingTable":
|
|
77
|
+
"""
|
|
78
|
+
Create a parsing table from the given entries.
|
|
79
|
+
"""
|
|
80
|
+
table = ParsingTable(tokens)
|
|
81
|
+
{table.add_entry(entry) for entry in entries}
|
|
82
|
+
return table
|
|
83
|
+
|
|
84
|
+
def pretty_str(self) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Returns a pretty-formatted string representation of the table.
|
|
87
|
+
"""
|
|
88
|
+
return self._rules_pretty_str() + "\n\n" + self._table_pretty_str()
|
|
89
|
+
|
|
90
|
+
def _rules_pretty_str(self) -> str:
|
|
91
|
+
rules = [str(i) + ". " + str(r) for i, r in enumerate(self._grammar.rules)]
|
|
92
|
+
rules_str = "\n".join(rules)
|
|
93
|
+
rules_str = "GRAMMAR RULES\n" + "-" * max(map(len, rules)) + "\n" + rules_str
|
|
94
|
+
rules_str += "\n" + "-" * max(map(len, rules))
|
|
95
|
+
|
|
96
|
+
return rules_str
|
|
97
|
+
|
|
98
|
+
def _table_pretty_str(self) -> str:
|
|
99
|
+
rows = []
|
|
100
|
+
tokens = sorted(self._grammar.tokens)
|
|
101
|
+
for number, row in sorted(
|
|
102
|
+
map(lambda tpl: (tpl[0].number, tpl[1]), self.rows.items())
|
|
103
|
+
):
|
|
104
|
+
r = [str(number)]
|
|
105
|
+
for token in sorted(tokens):
|
|
106
|
+
actions = row.get(token, [])
|
|
107
|
+
if len(actions) >= 1:
|
|
108
|
+
actions_str = ",".join(map(str, actions))
|
|
109
|
+
else:
|
|
110
|
+
actions_str = "--"
|
|
111
|
+
|
|
112
|
+
r.append(actions_str)
|
|
113
|
+
|
|
114
|
+
rows.append(r)
|
|
115
|
+
|
|
116
|
+
table = "| | "
|
|
117
|
+
table += " | ".join(str(token) for token in tokens) + " |\n"
|
|
118
|
+
|
|
119
|
+
header = self._header_str() + "\n" + "-" * len(table) + "\n"
|
|
120
|
+
table += "-" * len(table) + "\n"
|
|
121
|
+
|
|
122
|
+
for row in rows:
|
|
123
|
+
new_row = "| " + " | ".join(row) + " |" + "\n"
|
|
124
|
+
table += new_row
|
|
125
|
+
table += "-" * len(new_row) + "\n"
|
|
126
|
+
|
|
127
|
+
return header + table
|
|
128
|
+
|
|
129
|
+
def _header_str(self) -> str:
|
|
130
|
+
return "LR0 PARSING TABLE"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Token:
|
|
2
|
+
"""
|
|
3
|
+
A token of the grammar. Can be a terminal or non-terminal symbol.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, symbol: str, is_terminal: bool) -> None:
|
|
7
|
+
self.symbol = symbol
|
|
8
|
+
self.is_terminal = is_terminal
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def null() -> "Token":
|
|
12
|
+
"""
|
|
13
|
+
Returns the NULL token.
|
|
14
|
+
"""
|
|
15
|
+
return Token("ε", True)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def eof() -> "Token":
|
|
19
|
+
"""
|
|
20
|
+
Returns the EOF token.
|
|
21
|
+
"""
|
|
22
|
+
return Token("$", True)
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return f"<Token: {self}>"
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return self.symbol
|
|
29
|
+
|
|
30
|
+
def __hash__(self) -> int:
|
|
31
|
+
return hash(str(self))
|
|
32
|
+
|
|
33
|
+
def __eq__(self, other) -> bool:
|
|
34
|
+
if not isinstance(other, Token):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
return self.symbol == other.symbol and self.is_terminal is other.is_terminal
|
|
38
|
+
|
|
39
|
+
def __lt__(self, other) -> bool:
|
|
40
|
+
if not isinstance(other, Token):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"'<' not supported between instances of 'Token' and {type(other).__name__}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return self.symbol < other.symbol
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: syntactes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Parser generator
|
|
5
|
+
Author-email: Maximos Nikiforakis <nikiforos@live.co.uk>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Maxcode123/syntactes
|
|
8
|
+
Project-URL: Issues, https://github.com/Maxcode123/syntactes/issues
|
|
9
|
+
Keywords: parser,generator
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/syntactes/__init__.py
|
|
4
|
+
src/syntactes/_action.py
|
|
5
|
+
src/syntactes/_item.py
|
|
6
|
+
src/syntactes/_state.py
|
|
7
|
+
src/syntactes/generator.py
|
|
8
|
+
src/syntactes/grammar.py
|
|
9
|
+
src/syntactes/rule.py
|
|
10
|
+
src/syntactes/table.py
|
|
11
|
+
src/syntactes/token.py
|
|
12
|
+
src/syntactes.egg-info/PKG-INFO
|
|
13
|
+
src/syntactes.egg-info/SOURCES.txt
|
|
14
|
+
src/syntactes.egg-info/dependency_links.txt
|
|
15
|
+
src/syntactes.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
syntactes
|