dbrownell-parserlib 0.7.0__tar.gz → 0.8.1__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.
Files changed (16) hide show
  1. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/PKG-INFO +1 -1
  2. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/pyproject.toml +1 -1
  3. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/__init__.py +8 -0
  4. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/antlr/antlr_parser.py +5 -0
  5. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/antlr/antlr_visitor_mixins.py +39 -10
  6. dbrownell_parserlib-0.8.1/src/dbrownell_ParserLib/expression.py +288 -0
  7. dbrownell_parserlib-0.8.1/src/dbrownell_ParserLib/terminal_expression.py +21 -0
  8. dbrownell_parserlib-0.8.1/src/dbrownell_ParserLib/visitors.py +160 -0
  9. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/README.md +0 -0
  10. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/antlr/__init__.py +0 -0
  11. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/__init__.py +0 -0
  12. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/antlr-4.13.2-complete.jar +0 -0
  13. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/errors.py +0 -0
  14. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/location.py +0 -0
  15. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/py.typed +0 -0
  16. {dbrownell_parserlib-0.7.0 → dbrownell_parserlib-0.8.1}/src/dbrownell_ParserLib/region.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbrownell-parserlib
3
- Version: 0.7.0
3
+ Version: 0.8.1
4
4
  Summary: Functionality useful when creating parsers.
5
5
  Author: Dave Brownell
6
6
  Author-email: Dave Brownell <github@DavidBrownell.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbrownell-parserlib"
3
- version = "0.7.0"
3
+ version = "0.8.1"
4
4
  # ^^^^^
5
5
  # Wheel names will be generated according to this value. Do not manually modify this value; instead
6
6
  # update it according to committed changes by running this command from the root of the repository:
@@ -12,8 +12,11 @@ from dbrownell_ParserLib.antlr import (
12
12
  SignificantWhitespaceAntlrVisitorMixin,
13
13
  )
14
14
  from dbrownell_ParserLib.errors import Error, PythonError
15
+ from dbrownell_ParserLib.expression import Expression
15
16
  from dbrownell_ParserLib.location import Location
16
17
  from dbrownell_ParserLib.region import Region
18
+ from dbrownell_ParserLib.terminal_expression import TerminalExpression
19
+ from dbrownell_ParserLib.visitors import ExpressionVisitor, ExpressionVisitorHelper, VisitResult
17
20
 
18
21
 
19
22
  # ----------------------------------------------------------------------
@@ -24,11 +27,16 @@ __all__ = [
24
27
  "BuildAntlrGrammar",
25
28
  "CreateAntlrParser",
26
29
  "Error",
30
+ "Expression",
31
+ "ExpressionVisitor",
32
+ "ExpressionVisitorHelper",
27
33
  "InsignificantWhitespaceAntlrVisitorMixin",
28
34
  "Location",
29
35
  "PythonError",
30
36
  "Region",
31
37
  "SignificantWhitespaceAntlrVisitorMixin",
38
+ "TerminalExpression",
39
+ "VisitResult",
32
40
  "__version__",
33
41
  ]
34
42
 
@@ -479,6 +479,11 @@ class _ErrorListener(antlr4.DiagnosticErrorListener):
479
479
  msg: str,
480
480
  e: antlr4.RecognitionException | None,
481
481
  ) -> None:
482
+ if msg.startswith("report"):
483
+ # I don't think that these should be errors
484
+ assert e is None, e
485
+ return
486
+
482
487
  location = Location(line, column + 1)
483
488
  error = Error(msg, Region(self._source, location, location))
484
489
  ex = AntlrParserException(error)
@@ -1,7 +1,7 @@
1
1
  """Various ANTLR visitor mixins that can be used to add functionality to the generated visitor classes."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import cast, TYPE_CHECKING
4
+ from typing import cast, override, TYPE_CHECKING
5
5
 
6
6
  import antlr4
7
7
 
@@ -70,23 +70,51 @@ class InsignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
70
70
  """Mixin that adds functionality used for grammars that ignore insignificant whitespace."""
71
71
 
72
72
  # ----------------------------------------------------------------------
73
+ @override
73
74
  def CreateRegion(
74
75
  self,
75
- ctx: antlr4.ParserRuleContext,
76
+ ctx: antlr4.ParserRuleContext | antlr4.TerminalNode,
76
77
  ) -> Region:
77
78
  """Create a `Region` from the given context."""
78
79
 
79
- start_token = ctx.start
80
- stop_token = ctx.stop
80
+ if isinstance(ctx, antlr4.ParserRuleContext):
81
+ start_token = ctx.start
82
+ stop_token = ctx.stop
83
+
84
+ if start_token is None or stop_token is None:
85
+ msg = "Context does not have start or stop token"
86
+ raise ValueError(msg)
87
+
88
+ start_line = start_token.line
89
+ start_col = start_token.column + 1
90
+ end_line = stop_token.line
91
+ end_col = stop_token.column + 1
92
+
93
+ if start_line == end_line:
94
+ end_col += len(stop_token.text)
95
+ else:
96
+ end_col += 1
97
+
98
+ elif isinstance(ctx, antlr4.TerminalNode):
99
+ token = cast(antlr4.Token, ctx.symbol) # ty: ignore[unresolved-attribute]
81
100
 
82
- if start_token is None or stop_token is None:
83
- msg = "Context does not have start or stop token"
84
- raise ValueError(msg)
101
+ assert token.column is not None
102
+
103
+ start_line = token.line
104
+ start_col = token.column + 1
105
+ end_line = token.line
106
+ end_col = token.column + 1 + len(token.text)
107
+
108
+ else:
109
+ assert False, ctx # noqa: B011, PT015 # pragma: no cover
110
+
111
+ assert start_line is not None
112
+ assert end_line is not None
85
113
 
86
114
  return Region(
87
115
  self.filename,
88
- Location(start_token.line, start_token.column),
89
- Location(stop_token.line, stop_token.column + len(stop_token.text)),
116
+ Location(start_line, start_col),
117
+ Location(end_line, end_col),
90
118
  )
91
119
 
92
120
 
@@ -106,7 +134,8 @@ class SignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
106
134
  self._newline_token_string = newline_token_string
107
135
 
108
136
  # ----------------------------------------------------------------------
109
- def CreateRegion( # noqa: D102
137
+ @override
138
+ def CreateRegion(
110
139
  self,
111
140
  ctx: antlr4.ParserRuleContext,
112
141
  ) -> Region:
@@ -0,0 +1,288 @@
1
+ # noqa: D100
2
+ from collections.abc import Generator, Iterable
3
+ from contextlib import contextmanager
4
+ from dataclasses import dataclass, field, fields, InitVar
5
+ from typing import Self, TYPE_CHECKING
6
+
7
+ from dbrownell_Common.Types import extension, override
8
+
9
+ from dbrownell_ParserLib.visitors import ExpressionVisitor, ExpressionVisitorHelper, VisitResult
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
14
+ from dbrownell_ParserLib.region import Region
15
+
16
+
17
+ # ----------------------------------------------------------------------
18
+ @dataclass(eq=False)
19
+ class Expression:
20
+ """Abstract base class for all expressions encountered during the parsing process."""
21
+
22
+ # ----------------------------------------------------------------------
23
+ # |
24
+ # | Public Data
25
+ # |
26
+ # ----------------------------------------------------------------------
27
+ region__: Region
28
+ finalize: InitVar[bool] = field(kw_only=True, default=True)
29
+
30
+ parent__: Expression | None = field(init=False)
31
+
32
+ _unique_id: tuple[object, ...] | None = field(init=False)
33
+ _disabled: bool = field(init=False)
34
+
35
+ _finalize_func: Callable[[], None] = field(init=False)
36
+
37
+ # ----------------------------------------------------------------------
38
+ # |
39
+ # | Public Methods
40
+ # |
41
+ # ----------------------------------------------------------------------
42
+ def __post_init__(
43
+ self,
44
+ finalize: bool,
45
+ ) -> None:
46
+ self.parent__ = None
47
+
48
+ self._unique_id = None
49
+ self._disabled = False
50
+
51
+ # ----------------------------------------------------------------------
52
+ def Finalize() -> None:
53
+ visitor = _UniqueIdVisitor(self)
54
+
55
+ self.Accept(visitor)
56
+
57
+ self._unique_id = visitor.unique_id
58
+
59
+ # ----------------------------------------------------------------------
60
+
61
+ self._finalize_func = Finalize
62
+
63
+ if finalize:
64
+ self._Finalize()
65
+
66
+ # ----------------------------------------------------------------------
67
+ @property
68
+ def unique_id__(self) -> tuple[object, ...]:
69
+ """A unique identifier for this expression, which can be used for caching and other purposes."""
70
+
71
+ assert self._unique_id is not None
72
+ return self._unique_id
73
+
74
+ @property
75
+ def is_disabled__(self) -> bool:
76
+ """Whether this expression is disabled, which can be used for caching and other purposes."""
77
+
78
+ return self._disabled
79
+
80
+ # ----------------------------------------------------------------------
81
+ def __hash__(self) -> int:
82
+ return hash(self.unique_id__)
83
+
84
+ # ----------------------------------------------------------------------
85
+ def __eq__(self, other: object) -> bool:
86
+ if not isinstance(other, Expression):
87
+ return False
88
+
89
+ return self.unique_id__ == other.unique_id__
90
+
91
+ # ----------------------------------------------------------------------
92
+ def __ne__(self, other: object) -> bool:
93
+ if not isinstance(other, Expression):
94
+ return True
95
+
96
+ return self.unique_id__ != other.unique_id__
97
+
98
+ # ----------------------------------------------------------------------
99
+ def Clone(self, **kwargs) -> Self:
100
+ """Create a copy of this expression, optionally overriding some of its fields."""
101
+
102
+ for field_value in fields(self.__class__):
103
+ if field_value.init and field_value.name != "finalize" and field_value.name not in kwargs:
104
+ kwargs[field_value.name] = getattr(self, field_value.name)
105
+
106
+ return self.__class__(**kwargs)
107
+
108
+ # ----------------------------------------------------------------------
109
+ def Disable(self) -> None:
110
+ """Disable this expression so that it doesn't participate in visitation."""
111
+
112
+ assert self.is_disabled__ is False
113
+ self._disabled = True
114
+
115
+ # ----------------------------------------------------------------------
116
+ def Accept( # noqa: C901
117
+ self,
118
+ visitor: ExpressionVisitor,
119
+ *,
120
+ include_disabled: bool = False,
121
+ ) -> VisitResult:
122
+ """Accept a visitor for visitation of this expression and its children (if any)."""
123
+
124
+ if self.is_disabled__ and not include_disabled:
125
+ return VisitResult.Continue
126
+
127
+ with visitor.OnExpression(self) as expression_visit_result:
128
+ if expression_visit_result & VisitResult.Terminate:
129
+ return expression_visit_result
130
+
131
+ if expression_visit_result & VisitResult.SkipAll:
132
+ return VisitResult.Continue
133
+
134
+ method_name = f"On{self.__class__.__name__}"
135
+
136
+ method = getattr(visitor, method_name, None)
137
+ assert method is not None, method_name
138
+
139
+ with method(self) as method_visit_result:
140
+ if method_visit_result & VisitResult.Terminate:
141
+ return method_visit_result
142
+
143
+ # Details
144
+ if not method_visit_result & VisitResult.SkipDetails:
145
+ all_details = list(self._GenerateAcceptDetails())
146
+
147
+ if all_details:
148
+ with visitor.OnExpressionDetails(self) as details_visit_result:
149
+ if details_visit_result & VisitResult.Terminate:
150
+ return details_visit_result
151
+
152
+ if not details_visit_result & VisitResult.SkipDetails:
153
+ method_name_prefix = f"On{self.__class__.__name__}__"
154
+
155
+ for detail_name, detail_value in all_details:
156
+ method_name = f"{method_name_prefix}{detail_name}"
157
+
158
+ method = getattr(visitor, method_name, None)
159
+ assert method is not None, method_name
160
+
161
+ detail_visit_result = method(
162
+ detail_value, include_disabled=include_disabled
163
+ )
164
+
165
+ if detail_visit_result & VisitResult.Terminate:
166
+ return detail_visit_result
167
+
168
+ if detail_visit_result & VisitResult.SkipDetails:
169
+ break
170
+
171
+ # Children
172
+ if not method_visit_result & VisitResult.SkipChildren:
173
+ children_result = self._GetAcceptChildren()
174
+
175
+ if children_result:
176
+ children_name, children = children_result
177
+
178
+ with visitor.OnExpressionChildren(
179
+ self,
180
+ children_name,
181
+ children,
182
+ ) as children_visit_result:
183
+ if children_visit_result & VisitResult.Terminate:
184
+ return children_visit_result
185
+
186
+ if not children_visit_result & VisitResult.SkipChildren:
187
+ for child in children:
188
+ child_accept_result = child.Accept(
189
+ visitor, include_disabled=include_disabled
190
+ )
191
+
192
+ if child_accept_result & VisitResult.Terminate:
193
+ return child_accept_result
194
+
195
+ if child_accept_result & VisitResult.SkipChildren:
196
+ break
197
+
198
+ return VisitResult.Continue
199
+
200
+ # ----------------------------------------------------------------------
201
+ # |
202
+ # | Protected Types
203
+ # |
204
+ # ----------------------------------------------------------------------
205
+ _GenerateAcceptDetailsResultType = Generator[tuple[str, Self]]
206
+ _GetAcceptChildrenResultType = tuple[str, Iterable[Self]] | None
207
+
208
+ # ----------------------------------------------------------------------
209
+ # |
210
+ # | Protected Methods
211
+ # |
212
+ # ----------------------------------------------------------------------
213
+ def _Finalize(self) -> None:
214
+ self._finalize_func()
215
+
216
+ # ----------------------------------------------------------------------
217
+ # |
218
+ # | Private Methods
219
+ # |
220
+ # ----------------------------------------------------------------------
221
+ @extension
222
+ def _GetTerminalUniqueId(self) -> tuple[object, ...]:
223
+ """Get a unique identifier for this expression that doesn't include any of its children."""
224
+
225
+ msg = "This functionality should be implemented by a terminal expression (i.e. an expression that doesn't have any children)."
226
+ raise Exception(msg)
227
+
228
+ # ----------------------------------------------------------------------
229
+ @extension
230
+ def _GenerateAcceptDetails(self) -> _GenerateAcceptDetailsResultType:
231
+ # Nothing by default
232
+ if False:
233
+ yield
234
+
235
+ # ----------------------------------------------------------------------
236
+ @extension
237
+ def _GetAcceptChildren(self) -> _GetAcceptChildrenResultType:
238
+ # Nothing by default
239
+ return None
240
+
241
+
242
+ # ----------------------------------------------------------------------
243
+ # |
244
+ # | Private Types
245
+ # |
246
+ # ----------------------------------------------------------------------
247
+ class _UniqueIdVisitor(ExpressionVisitorHelper):
248
+ # ----------------------------------------------------------------------
249
+ def __init__(
250
+ self,
251
+ root_expression: Expression,
252
+ ) -> None:
253
+ self._target_expression = root_expression
254
+
255
+ self._child_unique_ids: list[tuple[object, ...]] = []
256
+ self._result: tuple[object, ...] | None = None
257
+
258
+ # ----------------------------------------------------------------------
259
+ @property
260
+ def unique_id(self) -> tuple[object, ...]:
261
+ assert self._result is not None
262
+ return self._result
263
+
264
+ # ----------------------------------------------------------------------
265
+ @override
266
+ @contextmanager
267
+ def OnExpression(
268
+ self,
269
+ expression: Expression,
270
+ ) -> Generator[VisitResult]:
271
+ if expression is self._target_expression:
272
+ yield VisitResult.Continue
273
+
274
+ if not self._child_unique_ids:
275
+ result = expression._GetTerminalUniqueId() # noqa: SLF001
276
+ else:
277
+ result = tuple(self._child_unique_ids)
278
+
279
+ assert self._result is None
280
+ self._result = (type(expression).__name__, *result)
281
+
282
+ return
283
+
284
+ self._child_unique_ids.append(expression.unique_id__)
285
+
286
+ # Do not parse the children of this expression as its unique id already takes that information
287
+ # into account.
288
+ yield VisitResult.SkipChildren
@@ -0,0 +1,21 @@
1
+ # noqa: D100
2
+ from dataclasses import dataclass
3
+ from typing import override
4
+
5
+ from dbrownell_ParserLib.expression import Expression
6
+
7
+
8
+ # ----------------------------------------------------------------------
9
+ @dataclass(eq=False)
10
+ class TerminalExpression[T](Expression):
11
+ """Expression with a single value member, typically used with leaves in an abstract syntax tree."""
12
+
13
+ # ----------------------------------------------------------------------
14
+ value: T
15
+
16
+ # ----------------------------------------------------------------------
17
+ # ----------------------------------------------------------------------
18
+ # ----------------------------------------------------------------------
19
+ @override
20
+ def _GetTerminalUniqueId(self) -> tuple[object, ...]:
21
+ return (self.value,)
@@ -0,0 +1,160 @@
1
+ """Contains types related to Expression visitation."""
2
+
3
+ import types
4
+
5
+ from abc import ABC, abstractmethod
6
+ from contextlib import contextmanager
7
+ from enum import auto, Flag
8
+ from typing import override, TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Generator, Iterable
12
+
13
+ from dbrownell_ParserLib.expression import Expression
14
+
15
+
16
+ # ----------------------------------------------------------------------
17
+ class VisitResult(Flag):
18
+ """Result returned during visitation that controls the flow of the visitation process."""
19
+
20
+ Continue = 0
21
+ """Continue visiting the rest of the expressions as normal."""
22
+
23
+ SkipDetails = auto()
24
+ """Skip visiting the details of this expression, but continue visiting the rest of the expressions as normal."""
25
+
26
+ SkipChildren = auto()
27
+ """Skip visiting the children of this expression, but continue visiting the rest of the expressions as normal."""
28
+
29
+ Terminate = auto()
30
+ """Terminate the visitation process immediately."""
31
+
32
+ # Amalgamations
33
+ SkipAll = SkipDetails | SkipChildren
34
+
35
+
36
+ # ----------------------------------------------------------------------
37
+ class ExpressionVisitor(ABC):
38
+ """Abstract base class for a visitor that accepts Expressions."""
39
+
40
+ # ----------------------------------------------------------------------
41
+ @abstractmethod
42
+ @contextmanager
43
+ def OnExpression(
44
+ self,
45
+ expression: Expression,
46
+ ) -> Generator[VisitResult]:
47
+ """Call for every expression."""
48
+
49
+ raise NotImplementedError("Abstract method") # noqa: EM101 # pragma: no cover
50
+
51
+ # ----------------------------------------------------------------------
52
+ @abstractmethod
53
+ @contextmanager
54
+ def OnExpressionDetails(
55
+ self,
56
+ expression: Expression,
57
+ ) -> Generator[VisitResult]:
58
+ """Call when visiting the details of an expression."""
59
+
60
+ raise NotImplementedError("Abstract method") # noqa: EM101 # pragma: no cover
61
+
62
+ # ----------------------------------------------------------------------
63
+ @abstractmethod
64
+ @contextmanager
65
+ def OnExpressionChildren(
66
+ self,
67
+ expression: Expression,
68
+ children_name: str,
69
+ children: Iterable[Expression],
70
+ ) -> Generator[VisitResult]:
71
+ """Call when visiting the children of an expression."""
72
+
73
+ raise NotImplementedError("Abstract method") # noqa: EM101 # pragma: no cover
74
+
75
+ # ----------------------------------------------------------------------
76
+ # Derived classes should implement the following methods:
77
+ #
78
+ # @contextmanager
79
+ # def On<Expression Name>(self, expression: <Expression Name>) -> Generator[VisitResult]:
80
+ # ...
81
+ #
82
+ # def On<Expression Name>__<Detail Name>(self, expression_or_expressions: <Expression Name> | list[<Expression Name>], include_disabled: bool) -> VisitResult:
83
+ # ...
84
+ #
85
+
86
+
87
+ # ----------------------------------------------------------------------
88
+ class ExpressionVisitorHelper(ExpressionVisitor):
89
+ """Base class that makes writing custom visitors easier by providing default behavior for visitation methods."""
90
+
91
+ # ----------------------------------------------------------------------
92
+ @override
93
+ @contextmanager
94
+ def OnExpression(
95
+ self,
96
+ expression: Expression,
97
+ ) -> Generator[VisitResult]:
98
+ yield VisitResult.Continue
99
+
100
+ # ----------------------------------------------------------------------
101
+ @override
102
+ @contextmanager
103
+ def OnExpressionDetails(
104
+ self,
105
+ expression: Expression,
106
+ ) -> Generator[VisitResult]:
107
+ yield VisitResult.Continue
108
+
109
+ # ----------------------------------------------------------------------
110
+ @override
111
+ @contextmanager
112
+ def OnExpressionChildren(
113
+ self,
114
+ expression: Expression,
115
+ children_name: str,
116
+ children: Iterable[Expression],
117
+ ) -> Generator[VisitResult]:
118
+ yield VisitResult.Continue
119
+
120
+ # ----------------------------------------------------------------------
121
+ def __getattr__(
122
+ self,
123
+ method_name: str,
124
+ ) -> object:
125
+ if method_name.endswith("Expression"):
126
+ return self.__class__._DefaultExpressionMethod # noqa: SLF001
127
+
128
+ index = method_name.find("Expression__")
129
+ if index != -1 and index + len("Expression__") + 1 < len(method_name):
130
+ return types.MethodType(self.__class__._DefaultDetailMethod, self) # noqa: SLF001
131
+
132
+ raise AttributeError(method_name)
133
+
134
+ # ----------------------------------------------------------------------
135
+ # ----------------------------------------------------------------------
136
+ # ----------------------------------------------------------------------
137
+ @staticmethod
138
+ @contextmanager
139
+ def _DefaultExpressionMethod(*args, **kwargs) -> Generator[VisitResult]: # noqa: ARG004
140
+ yield VisitResult.Continue
141
+
142
+ # ----------------------------------------------------------------------
143
+ def _DefaultDetailMethod(
144
+ self,
145
+ expression_or_expressions: Expression | list[Expression],
146
+ *,
147
+ include_disabled: bool,
148
+ ) -> VisitResult:
149
+ expressions: list[Expression] = ( # ty: ignore[invalid-assignment]
150
+ expression_or_expressions
151
+ if isinstance(expression_or_expressions, list)
152
+ else [expression_or_expressions]
153
+ )
154
+
155
+ for expression in expressions:
156
+ result = expression.Accept(self, include_disabled=include_disabled)
157
+ if result & VisitResult.Terminate:
158
+ return result
159
+
160
+ return VisitResult.Continue