dbrownell-parserlib 0.10.2__tar.gz → 0.12.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.
Files changed (18) hide show
  1. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/PKG-INFO +23 -21
  2. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/README.md +22 -20
  3. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/pyproject.toml +1 -1
  4. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/antlr/antlr_parser.py +25 -0
  5. dbrownell_parserlib-0.12.0/src/dbrownell_ParserLib/antlr/antlr_visitor_mixin.py +150 -0
  6. dbrownell_parserlib-0.10.2/src/dbrownell_ParserLib/expression.py → dbrownell_parserlib-0.12.0/src/dbrownell_ParserLib/element.py +33 -33
  7. dbrownell_parserlib-0.10.2/src/dbrownell_ParserLib/terminal_expression.py → dbrownell_parserlib-0.12.0/src/dbrownell_ParserLib/terminal_element.py +3 -3
  8. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/visitors.py +34 -36
  9. dbrownell_parserlib-0.10.2/src/dbrownell_ParserLib/antlr/antlr_visitor_mixins.py +0 -169
  10. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/__init__.py +0 -0
  11. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/antlr/__init__.py +0 -0
  12. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/__init__.py +0 -0
  13. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/antlr-4.13.2-complete.jar +0 -0
  14. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/errors.py +0 -0
  15. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/location.py +0 -0
  16. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/multiline_strings.py +0 -0
  17. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/src/dbrownell_ParserLib/py.typed +0 -0
  18. {dbrownell_parserlib-0.10.2 → dbrownell_parserlib-0.12.0}/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.10.2
3
+ Version: 0.12.0
4
4
  Summary: Functionality useful when creating parsers.
5
5
  Author: Dave Brownell
6
6
  Author-email: Dave Brownell <github@DavidBrownell.com>
@@ -51,13 +51,14 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  ### Key Features
53
53
  - **ANTLR Integration**: Streamlined wrapper around ANTLR for Python, handling grammar compilation and parser generation
54
- - **AST Representation**: Expression-based AST nodes with automatic parent-child relationship management
54
+ - **AST Representation**: Element-based AST nodes with automatic parent-child relationship management
55
55
  - **Source Location Tracking**: Every AST node tracks its source file location (filename, line, column ranges) for detailed error reporting
56
56
  - **Visitor Pattern**: Flexible visitor pattern implementation with fine-grained traversal control
57
57
  - **Error Handling**: Rich error objects that associate messages with source locations and extract Python exception tracebacks
58
58
  - **Multi-threaded Parsing**: Built-in support for parsing multiple files concurrently
59
- - **Workspace Support**: Parse entire directory trees with customizable file filtering
60
- - **Whitespace Handling**: Mixins for both insignificant and significant whitespace grammars (e.g., Python-like indentation)
59
+ - **Workspace Support**: Parse translation units organized across multiple workspace roots
60
+ - **Whitespace Handling**: A single visitor mixin supports both insignificant and significant whitespace grammars (e.g., Python-like indentation); grammars that define `INDENT`/`DEDENT` tokens are detected automatically
61
+ - **Multiline String Extraction**: Extract multiline string content that must be vertically aligned with its opening token, with detailed errors on misalignment
61
62
 
62
63
  ### How to use `dbrownell_ParserLib`
63
64
 
@@ -96,23 +97,23 @@ BuildAntlrGrammar(
96
97
  ```
97
98
 
98
99
  #### 3. Create Custom Visitor
99
- Implement a visitor that converts ANTLR parse trees to Expression objects:
100
+ Implement a visitor that converts ANTLR parse trees to Element objects:
100
101
 
101
102
  ```python
102
103
  from dataclasses import dataclass
103
104
 
104
- from dbrownell_ParserLib.antlr.antlr_visitor_mixins import InsignificantWhitespaceAntlrVisitorMixin
105
- from dbrownell_ParserLib.expression import Expression
106
- from dbrownell_ParserLib.terminal_expression import TerminalExpression
105
+ from dbrownell_ParserLib.antlr.antlr_visitor_mixin import AntlrVisitorMixin
106
+ from dbrownell_ParserLib.element import Element
107
+ from dbrownell_ParserLib.terminal_element import TerminalElement
107
108
 
108
109
  from CalculatorVisitor import CalculatorVisitor as GeneratedVisitor
109
110
 
110
111
 
111
112
  @dataclass(eq=False)
112
- class BinaryExpression(Expression):
113
- left: Expression
114
- operator: TerminalExpression[str]
115
- right: Expression
113
+ class BinaryExpression(Element):
114
+ left: Element
115
+ operator: TerminalElement[str]
116
+ right: Element
116
117
 
117
118
  def _GenerateAcceptDetails(self):
118
119
  yield "left", self.left
@@ -120,8 +121,10 @@ class BinaryExpression(Expression):
120
121
  yield "right", self.right
121
122
 
122
123
 
123
- class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisitor):
124
- def visitExpr(self, ctx):
124
+ class CalculatorVisitor(AntlrVisitorMixin, GeneratedVisitor):
125
+ def visitBinaryOp(self, ctx):
126
+ operator = TerminalElement[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText())
127
+
125
128
  children = self.GetChildren(ctx)
126
129
  assert len(children) == 2, children
127
130
 
@@ -129,20 +132,19 @@ class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisit
129
132
  BinaryExpression(
130
133
  self.CreateRegion(ctx),
131
134
  children[0],
132
- TerminalExpression[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText()),
133
-
135
+ operator,
134
136
  children[1],
135
137
  )
136
138
  )
137
139
 
138
140
  def visitNumber(self, ctx):
139
141
  self._stack.append(
140
- TerminalExpression(self.CreateRegion(ctx), int(ctx.getText()))
142
+ TerminalElement[int](self.CreateRegion(ctx), int(ctx.getText()))
141
143
  )
142
144
  ```
143
145
 
144
146
  #### 4. Create Parser Instance
145
- Use `CreateAntlrParser` to build a callable parser:
147
+ Use `CreateAntlrParser` to build a callable parser. Grammars that define `INDENT` and `DEDENT` tokens (significant whitespace) are detected automatically; no additional configuration is required.
146
148
 
147
149
  ```python
148
150
  from dbrownell_ParserLib.antlr.antlr_parser import CreateAntlrParser
@@ -185,12 +187,12 @@ Use the visitor pattern to process your AST:
185
187
  ```python
186
188
  from contextlib import contextmanager
187
189
 
188
- from dbrownell_ParserLib.visitors import ExpressionVisitorHelper, VisitResult
190
+ from dbrownell_ParserLib.visitors import ElementVisitorHelper, VisitResult
189
191
 
190
192
 
191
- class EvaluatorVisitor(ExpressionVisitorHelper):
193
+ class EvaluatorVisitor(ElementVisitorHelper):
192
194
  @contextmanager
193
- def OnBinaryExpression(self, expression):
195
+ def OnBinaryExpression(self, element):
194
196
  # Process binary expressions
195
197
  yield VisitResult.Continue
196
198
 
@@ -29,13 +29,14 @@
29
29
 
30
30
  ### Key Features
31
31
  - **ANTLR Integration**: Streamlined wrapper around ANTLR for Python, handling grammar compilation and parser generation
32
- - **AST Representation**: Expression-based AST nodes with automatic parent-child relationship management
32
+ - **AST Representation**: Element-based AST nodes with automatic parent-child relationship management
33
33
  - **Source Location Tracking**: Every AST node tracks its source file location (filename, line, column ranges) for detailed error reporting
34
34
  - **Visitor Pattern**: Flexible visitor pattern implementation with fine-grained traversal control
35
35
  - **Error Handling**: Rich error objects that associate messages with source locations and extract Python exception tracebacks
36
36
  - **Multi-threaded Parsing**: Built-in support for parsing multiple files concurrently
37
- - **Workspace Support**: Parse entire directory trees with customizable file filtering
38
- - **Whitespace Handling**: Mixins for both insignificant and significant whitespace grammars (e.g., Python-like indentation)
37
+ - **Workspace Support**: Parse translation units organized across multiple workspace roots
38
+ - **Whitespace Handling**: A single visitor mixin supports both insignificant and significant whitespace grammars (e.g., Python-like indentation); grammars that define `INDENT`/`DEDENT` tokens are detected automatically
39
+ - **Multiline String Extraction**: Extract multiline string content that must be vertically aligned with its opening token, with detailed errors on misalignment
39
40
 
40
41
  ### How to use `dbrownell_ParserLib`
41
42
 
@@ -74,23 +75,23 @@ BuildAntlrGrammar(
74
75
  ```
75
76
 
76
77
  #### 3. Create Custom Visitor
77
- Implement a visitor that converts ANTLR parse trees to Expression objects:
78
+ Implement a visitor that converts ANTLR parse trees to Element objects:
78
79
 
79
80
  ```python
80
81
  from dataclasses import dataclass
81
82
 
82
- from dbrownell_ParserLib.antlr.antlr_visitor_mixins import InsignificantWhitespaceAntlrVisitorMixin
83
- from dbrownell_ParserLib.expression import Expression
84
- from dbrownell_ParserLib.terminal_expression import TerminalExpression
83
+ from dbrownell_ParserLib.antlr.antlr_visitor_mixin import AntlrVisitorMixin
84
+ from dbrownell_ParserLib.element import Element
85
+ from dbrownell_ParserLib.terminal_element import TerminalElement
85
86
 
86
87
  from CalculatorVisitor import CalculatorVisitor as GeneratedVisitor
87
88
 
88
89
 
89
90
  @dataclass(eq=False)
90
- class BinaryExpression(Expression):
91
- left: Expression
92
- operator: TerminalExpression[str]
93
- right: Expression
91
+ class BinaryExpression(Element):
92
+ left: Element
93
+ operator: TerminalElement[str]
94
+ right: Element
94
95
 
95
96
  def _GenerateAcceptDetails(self):
96
97
  yield "left", self.left
@@ -98,8 +99,10 @@ class BinaryExpression(Expression):
98
99
  yield "right", self.right
99
100
 
100
101
 
101
- class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisitor):
102
- def visitExpr(self, ctx):
102
+ class CalculatorVisitor(AntlrVisitorMixin, GeneratedVisitor):
103
+ def visitBinaryOp(self, ctx):
104
+ operator = TerminalElement[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText())
105
+
103
106
  children = self.GetChildren(ctx)
104
107
  assert len(children) == 2, children
105
108
 
@@ -107,20 +110,19 @@ class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisit
107
110
  BinaryExpression(
108
111
  self.CreateRegion(ctx),
109
112
  children[0],
110
- TerminalExpression[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText()),
111
-
113
+ operator,
112
114
  children[1],
113
115
  )
114
116
  )
115
117
 
116
118
  def visitNumber(self, ctx):
117
119
  self._stack.append(
118
- TerminalExpression(self.CreateRegion(ctx), int(ctx.getText()))
120
+ TerminalElement[int](self.CreateRegion(ctx), int(ctx.getText()))
119
121
  )
120
122
  ```
121
123
 
122
124
  #### 4. Create Parser Instance
123
- Use `CreateAntlrParser` to build a callable parser:
125
+ Use `CreateAntlrParser` to build a callable parser. Grammars that define `INDENT` and `DEDENT` tokens (significant whitespace) are detected automatically; no additional configuration is required.
124
126
 
125
127
  ```python
126
128
  from dbrownell_ParserLib.antlr.antlr_parser import CreateAntlrParser
@@ -163,12 +165,12 @@ Use the visitor pattern to process your AST:
163
165
  ```python
164
166
  from contextlib import contextmanager
165
167
 
166
- from dbrownell_ParserLib.visitors import ExpressionVisitorHelper, VisitResult
168
+ from dbrownell_ParserLib.visitors import ElementVisitorHelper, VisitResult
167
169
 
168
170
 
169
- class EvaluatorVisitor(ExpressionVisitorHelper):
171
+ class EvaluatorVisitor(ElementVisitorHelper):
170
172
  @contextmanager
171
- def OnBinaryExpression(self, expression):
173
+ def OnBinaryExpression(self, element):
172
174
  # Process binary expressions
173
175
  yield VisitResult.Continue
174
176
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbrownell-parserlib"
3
- version = "0.10.2"
3
+ version = "0.12.0"
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:
@@ -102,6 +102,9 @@ class CreateVisitorFuncType(Protocol):
102
102
  on_progress_func: Callable[[int], None],
103
103
  *,
104
104
  is_included_file: bool,
105
+ parser_newline_string: str | None,
106
+ indent_token_type: int | None,
107
+ dedent_token_type: int | None,
105
108
  ) -> antlr4.ParseTreeVisitor: ...
106
109
 
107
110
 
@@ -117,9 +120,18 @@ def CreateAntlrParser(
117
120
  get_ast_func: Callable[[antlr4.Parser], antlr4.ParserRuleContext],
118
121
  custom_lexer_init_func: Callable[[antlr4.Lexer], None] | None = None,
119
122
  custom_parser_init_func: Callable[[antlr4.Parser], None] | None = None,
123
+ parser_newline_string: str | bool | None = None, # noqa: FBT001
120
124
  ) -> AntlrParser:
121
125
  """Create an `AntlrParser` that uses the given code generated by ANTLR."""
122
126
 
127
+ indent_token_type = getattr(antlr_parser, "INDENT", None)
128
+ dedent_token_type = getattr(antlr_parser, "DEDENT", None)
129
+
130
+ if parser_newline_string is None and indent_token_type is not None and dedent_token_type is not None:
131
+ parser_newline_string = "newLine"
132
+ elif isinstance(parser_newline_string, bool):
133
+ parser_newline_string = None
134
+
123
135
  # ----------------------------------------------------------------------
124
136
  def CreateLexer(input_stream: antlr4.InputStream) -> antlr4.Lexer:
125
137
  lexer = antlr_lexer(input_stream)
@@ -145,6 +157,9 @@ def CreateAntlrParser(
145
157
  CreateParser,
146
158
  create_visitor_func,
147
159
  get_ast_func,
160
+ indent_token_type,
161
+ dedent_token_type,
162
+ parser_newline_string,
148
163
  )
149
164
 
150
165
 
@@ -162,12 +177,19 @@ class _AntlrParser(AntlrParser):
162
177
  create_parser_func: Callable[[antlr4.CommonTokenStream], antlr4.Parser],
163
178
  create_visitor_func: CreateVisitorFuncType,
164
179
  get_ast_func: Callable[[antlr4.Parser], antlr4.ParserRuleContext],
180
+ indent_token_type: int | None,
181
+ dedent_token_type: int | None,
182
+ parser_newline_string: str | None,
165
183
  ) -> None:
166
184
  self._create_lexer_func = create_lexer_func
167
185
  self._create_parser_func = create_parser_func
168
186
  self._create_visitor_func = create_visitor_func
169
187
  self._get_ast_func = get_ast_func
170
188
 
189
+ self._indent_token_type = indent_token_type
190
+ self._dedent_token_type = dedent_token_type
191
+ self._parser_newline_string = parser_newline_string
192
+
171
193
  self._execute_state: _AntlrParser._ExecuteState | None = None
172
194
 
173
195
  # ----------------------------------------------------------------------
@@ -433,6 +455,9 @@ class _AntlrParser(AntlrParser):
433
455
  fullpath,
434
456
  lambda line: cast(None, status.OnProgress(line, None)),
435
457
  is_included_file=is_included_file,
458
+ indent_token_type=self._indent_token_type,
459
+ dedent_token_type=self._dedent_token_type,
460
+ parser_newline_string=self._parser_newline_string,
436
461
  )
437
462
 
438
463
  ast = self._get_ast_func(parser)
@@ -0,0 +1,150 @@
1
+ # noqa: D100
2
+ import re
3
+
4
+ from typing import cast, TYPE_CHECKING
5
+
6
+ import antlr4
7
+
8
+ from dbrownell_ParserLib.region import Location, Region
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+
14
+
15
+ # ----------------------------------------------------------------------
16
+ class AntlrVisitorMixin:
17
+ """Mixin that provides common visitation functionality; it is used along side ANTLR-generated visitors."""
18
+
19
+ # ----------------------------------------------------------------------
20
+ def __init__(
21
+ self,
22
+ filename: Path,
23
+ on_progress_func: Callable[[int], None],
24
+ *,
25
+ is_included_file: bool,
26
+ parser_newline_string: str | None,
27
+ indent_token_type: int | None,
28
+ dedent_token_type: int | None,
29
+ ) -> None:
30
+ self.filename = filename
31
+ self.is_included_file = is_included_file
32
+
33
+ # Protected data
34
+ self._on_progress_func = on_progress_func
35
+ self._parser_newline_string = parser_newline_string
36
+ self._indent_token_type = indent_token_type
37
+ self._dedent_token_type = dedent_token_type
38
+ self._stack: list[object] = []
39
+
40
+ # Private data
41
+ self._current_line: int = 0
42
+ self._stop_token_regex = re.compile(r"^\r?\n[ \t]*$")
43
+
44
+ # ----------------------------------------------------------------------
45
+ def CreateRegion(
46
+ self,
47
+ ctx: antlr4.TerminalNode | antlr4.ParserRuleContext,
48
+ ) -> Region:
49
+ """Create a `Region` from the given context."""
50
+
51
+ if isinstance(ctx, antlr4.TerminalNode):
52
+ start_token = cast(antlr4.Token, ctx.symbol) # ty: ignore[unresolved-attribute]
53
+ end_location = self._CreateEndLocation(start_token)
54
+ else:
55
+ start_token = ctx.start
56
+ stop_token = ctx.stop
57
+
58
+ assert ctx.children is not None
59
+ assert start_token is not None
60
+ assert stop_token is not None
61
+
62
+ end_location: Location | None = None
63
+
64
+ if self._parser_newline_string is not None:
65
+ if self._IsStopToken(stop_token):
66
+ assert len(ctx.children) == 2, ctx.children # noqa: PLR2004
67
+ end_location = self._CreateEndLocation(start_token)
68
+ elif stop_token.type == self._dedent_token_type:
69
+ end_location = Location(stop_token.line, stop_token.column + 1)
70
+
71
+ if end_location is None:
72
+ end_location = self._CreateEndLocation(stop_token)
73
+
74
+ assert start_token.line is not None
75
+ assert start_token.column is not None
76
+
77
+ region = Region(
78
+ self.filename,
79
+ Location(start_token.line, start_token.column + 1),
80
+ end_location,
81
+ )
82
+
83
+ self._OnProgress(region.end.line)
84
+
85
+ return region
86
+
87
+ # ----------------------------------------------------------------------
88
+ def GetChildren(self, ctx: antlr4.ParserRuleContext) -> list[object]:
89
+ """Return the results of visiting the children of the given context."""
90
+
91
+ prev_num_stack_items = len(self._stack)
92
+
93
+ cast(antlr4.ParseTreeVisitor, self).visitChildren(ctx)
94
+
95
+ results = self._stack[prev_num_stack_items:]
96
+ del self._stack[prev_num_stack_items:]
97
+
98
+ return results
99
+
100
+ # ----------------------------------------------------------------------
101
+ # | Protected Methods
102
+ def _OnProgress(self, end_line: int) -> None:
103
+ if end_line > self._current_line:
104
+ self._current_line = end_line
105
+ self._on_progress_func(end_line)
106
+
107
+ # ----------------------------------------------------------------------
108
+ # ----------------------------------------------------------------------
109
+ # ----------------------------------------------------------------------
110
+ @staticmethod
111
+ def _CreateEndLocation(token: antlr4.Token) -> Location:
112
+ assert token.text is not None
113
+ assert token.line is not None
114
+ assert token.column is not None
115
+
116
+ return CreateEndLocation(token.text, token.line, token.column + 1)
117
+
118
+ # ----------------------------------------------------------------------
119
+ def _IsStopToken(self, token: antlr4.Token) -> bool:
120
+ assert token.text is not None
121
+
122
+ return (
123
+ token.text == self._parser_newline_string or self._stop_token_regex.match(token.text) is not None
124
+ )
125
+
126
+
127
+ # ----------------------------------------------------------------------
128
+ def CreateEndLocation(text: str, line: int, column: int) -> Location:
129
+ """Create a `Location` based on the text and current line and column."""
130
+
131
+ # Note that this function will not be invoked outside of this file; it is implemented as a
132
+ # function so that it can be exercised by automated tests.
133
+
134
+ assert line > 0, line
135
+ assert column > 0, column
136
+
137
+ lines = text.splitlines()
138
+
139
+ last_nonblank_line = None
140
+
141
+ for line_index, line_text in enumerate(lines):
142
+ if line_text.strip():
143
+ last_nonblank_line = line_index
144
+
145
+ assert last_nonblank_line is not None
146
+
147
+ if last_nonblank_line == 0:
148
+ return Location(line, column + len(lines[0]))
149
+
150
+ return Location(line + last_nonblank_line, len(lines[last_nonblank_line]) + 1)
@@ -8,7 +8,7 @@ from typing import Self, TYPE_CHECKING
8
8
 
9
9
  from dbrownell_Common.Types import extension, override
10
10
 
11
- from dbrownell_ParserLib.visitors import ExpressionVisitor, ExpressionVisitorHelper, VisitResult
11
+ from dbrownell_ParserLib.visitors import ElementVisitor, ElementVisitorHelper, VisitResult
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from dbrownell_ParserLib.region import Region
@@ -16,8 +16,8 @@ if TYPE_CHECKING:
16
16
 
17
17
  # ----------------------------------------------------------------------
18
18
  @dataclass(eq=False)
19
- class Expression:
20
- """Abstract base class for all expressions encountered during the parsing process."""
19
+ class Element:
20
+ """Abstract base class for all elements encountered during the parsing process."""
21
21
 
22
22
  # ----------------------------------------------------------------------
23
23
  # |
@@ -27,7 +27,7 @@ class Expression:
27
27
  region__: Region
28
28
  finalize: InitVar[bool] = field(kw_only=True, default=True)
29
29
 
30
- parent__: weakref.ref[Expression] | None = field(init=False)
30
+ parent__: weakref.ref[Element] | None = field(init=False)
31
31
 
32
32
  _unique_id: tuple[object, ...] | None = field(init=False)
33
33
  _disabled: bool = field(init=False)
@@ -52,14 +52,14 @@ class Expression:
52
52
  # ----------------------------------------------------------------------
53
53
  @property
54
54
  def unique_id__(self) -> tuple[object, ...]:
55
- """A unique identifier for this expression, which can be used for caching and other purposes."""
55
+ """A unique identifier for this element, which can be used for caching and other purposes."""
56
56
 
57
57
  assert self._unique_id is not None
58
58
  return self._unique_id
59
59
 
60
60
  @property
61
61
  def is_disabled__(self) -> bool:
62
- """Whether this expression is disabled, which can be used for caching and other purposes."""
62
+ """Whether this element is disabled, which can be used for caching and other purposes."""
63
63
 
64
64
  return self._disabled
65
65
 
@@ -69,21 +69,21 @@ class Expression:
69
69
 
70
70
  # ----------------------------------------------------------------------
71
71
  def __eq__(self, other: object) -> bool:
72
- if not isinstance(other, Expression):
72
+ if not isinstance(other, Element):
73
73
  return False
74
74
 
75
75
  return self.unique_id__ == other.unique_id__
76
76
 
77
77
  # ----------------------------------------------------------------------
78
78
  def __ne__(self, other: object) -> bool:
79
- if not isinstance(other, Expression):
79
+ if not isinstance(other, Element):
80
80
  return True
81
81
 
82
82
  return self.unique_id__ != other.unique_id__
83
83
 
84
84
  # ----------------------------------------------------------------------
85
85
  def Clone(self, **kwargs) -> Self:
86
- """Create a copy of this expression, optionally overriding some of its fields."""
86
+ """Create a copy of this element, optionally overriding some of its fields."""
87
87
 
88
88
  for field_value in fields(self.__class__):
89
89
  if field_value.init and field_value.name != "finalize" and field_value.name not in kwargs:
@@ -93,7 +93,7 @@ class Expression:
93
93
 
94
94
  # ----------------------------------------------------------------------
95
95
  def Disable(self) -> None:
96
- """Disable this expression so that it doesn't participate in visitation."""
96
+ """Disable this element so that it doesn't participate in visitation."""
97
97
 
98
98
  assert self.is_disabled__ is False
99
99
  self._disabled = True
@@ -101,20 +101,20 @@ class Expression:
101
101
  # ----------------------------------------------------------------------
102
102
  def Accept( # noqa: C901
103
103
  self,
104
- visitor: ExpressionVisitor,
104
+ visitor: ElementVisitor,
105
105
  *,
106
106
  include_disabled: bool = False,
107
107
  ) -> VisitResult:
108
- """Accept a visitor for visitation of this expression and its children (if any)."""
108
+ """Accept a visitor for visitation of this element and its children (if any)."""
109
109
 
110
110
  if self.is_disabled__ and not include_disabled:
111
111
  return VisitResult.Continue
112
112
 
113
- with visitor.OnExpression(self) as expression_visit_result:
114
- if expression_visit_result & VisitResult.Terminate:
115
- return expression_visit_result
113
+ with visitor.OnElement(self) as element_visit_result:
114
+ if element_visit_result & VisitResult.Terminate:
115
+ return element_visit_result
116
116
 
117
- if expression_visit_result & VisitResult.SkipAll:
117
+ if element_visit_result & VisitResult.SkipAll:
118
118
  return VisitResult.Continue
119
119
 
120
120
  method_name = f"On{self.__class__.__name__}"
@@ -131,7 +131,7 @@ class Expression:
131
131
  all_details = list(self._GenerateAcceptDetails())
132
132
 
133
133
  if all_details:
134
- with visitor.OnExpressionDetails(self) as details_visit_result:
134
+ with visitor.OnElementDetails(self) as details_visit_result:
135
135
  if details_visit_result & VisitResult.Terminate:
136
136
  return details_visit_result
137
137
 
@@ -161,7 +161,7 @@ class Expression:
161
161
  if children_result:
162
162
  children_name, children = children_result
163
163
 
164
- with visitor.OnExpressionChildren(
164
+ with visitor.OnElementChildren(
165
165
  self,
166
166
  children_name,
167
167
  children,
@@ -210,9 +210,9 @@ class Expression:
210
210
  # ----------------------------------------------------------------------
211
211
  @extension
212
212
  def _GetTerminalUniqueId(self) -> tuple[object, ...]:
213
- """Get a unique identifier for this expression that doesn't include any of its children."""
213
+ """Get a unique identifier for this element that doesn't include any of its children."""
214
214
 
215
- msg = f"This functionality should be implemented by a terminal expression (i.e. an expression that doesn't have any children or expression details) [{self.__class__.__name__}]"
215
+ msg = f"This functionality should be implemented by a terminal element (i.e. an element that doesn't have any children or element details) [{self.__class__.__name__}]"
216
216
  raise Exception(msg)
217
217
 
218
218
  # ----------------------------------------------------------------------
@@ -234,15 +234,15 @@ class Expression:
234
234
  # | Private Types
235
235
  # |
236
236
  # ----------------------------------------------------------------------
237
- class _FinalizeVisitor(ExpressionVisitorHelper):
238
- """Visitor that visits the children of an expression, but not anything below that; it sets the unique_id__ of the target expression and sets its children's parent attribute to the correct value."""
237
+ class _FinalizeVisitor(ElementVisitorHelper):
238
+ """Visitor that visits the children of an element, but not anything below that; it sets the unique_id__ of the target element and sets its children's parent attribute to the correct value."""
239
239
 
240
240
  # ----------------------------------------------------------------------
241
241
  def __init__(
242
242
  self,
243
- root_expression: Expression,
243
+ root_element: Element,
244
244
  ) -> None:
245
- self._target_expression = root_expression
245
+ self._target_element = root_element
246
246
 
247
247
  self._child_unique_ids: list[tuple[object, ...]] = []
248
248
  self._result: tuple[object, ...] | None = None
@@ -256,27 +256,27 @@ class _FinalizeVisitor(ExpressionVisitorHelper):
256
256
  # ----------------------------------------------------------------------
257
257
  @override
258
258
  @contextmanager
259
- def OnExpression(
259
+ def OnElement(
260
260
  self,
261
- expression: Expression,
261
+ element: Element,
262
262
  ) -> Generator[VisitResult]:
263
- # This code will visit the children of the target expression, but not anything below that.
264
- if expression is self._target_expression:
263
+ # This code will visit the children of the target element, but not anything below that.
264
+ if element is self._target_element:
265
265
  yield VisitResult.Continue
266
266
 
267
267
  if not self._child_unique_ids:
268
- result = expression._GetTerminalUniqueId() # noqa: SLF001
268
+ result = element._GetTerminalUniqueId() # noqa: SLF001
269
269
  else:
270
270
  result = tuple(self._child_unique_ids)
271
271
 
272
272
  assert self._result is None
273
- self._result = (type(expression).__name__, *result)
273
+ self._result = (type(element).__name__, *result)
274
274
 
275
275
  return
276
276
 
277
- expression.parent__ = weakref.ref(self._target_expression)
277
+ element.parent__ = weakref.ref(self._target_element)
278
278
 
279
- self._child_unique_ids.append(expression.unique_id__)
279
+ self._child_unique_ids.append(element.unique_id__)
280
280
 
281
- # Do not parse the grandchildren of the original target expression
281
+ # Do not parse the grandchildren of the original target element
282
282
  yield VisitResult.SkipChildren
@@ -2,13 +2,13 @@
2
2
  from dataclasses import dataclass
3
3
  from typing import override
4
4
 
5
- from dbrownell_ParserLib.expression import Expression
5
+ from dbrownell_ParserLib.element import Element
6
6
 
7
7
 
8
8
  # ----------------------------------------------------------------------
9
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."""
10
+ class TerminalElement[T](Element):
11
+ """Element with a single value member, typically used with leaves in an abstract syntax tree."""
12
12
 
13
13
  # ----------------------------------------------------------------------
14
14
  value: T
@@ -1,4 +1,4 @@
1
- """Contains types related to Expression visitation."""
1
+ """Contains types related to Element visitation."""
2
2
 
3
3
  import types
4
4
 
@@ -10,7 +10,7 @@ from typing import override, TYPE_CHECKING
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Generator, Iterable
12
12
 
13
- from dbrownell_ParserLib.expression import Expression
13
+ from dbrownell_ParserLib.element import Element
14
14
 
15
15
 
16
16
  # ----------------------------------------------------------------------
@@ -18,13 +18,13 @@ class VisitResult(Flag):
18
18
  """Result returned during visitation that controls the flow of the visitation process."""
19
19
 
20
20
  Continue = 0
21
- """Continue visiting the rest of the expressions as normal."""
21
+ """Continue visiting the rest of the elements as normal."""
22
22
 
23
23
  SkipDetails = auto()
24
- """Skip visiting the details of this expression, but continue visiting the rest of the expressions as normal."""
24
+ """Skip visiting the details of this element, but continue visiting the rest of the elements as normal."""
25
25
 
26
26
  SkipChildren = auto()
27
- """Skip visiting the children of this expression, but continue visiting the rest of the expressions as normal."""
27
+ """Skip visiting the children of this element, but continue visiting the rest of the elements as normal."""
28
28
 
29
29
  Terminate = auto()
30
30
  """Terminate the visitation process immediately."""
@@ -34,41 +34,41 @@ class VisitResult(Flag):
34
34
 
35
35
 
36
36
  # ----------------------------------------------------------------------
37
- class ExpressionVisitor(ABC):
38
- """Abstract base class for a visitor that accepts Expressions."""
37
+ class ElementVisitor(ABC):
38
+ """Abstract base class for a visitor that accepts Elements."""
39
39
 
40
40
  # ----------------------------------------------------------------------
41
41
  @abstractmethod
42
42
  @contextmanager
43
- def OnExpression(
43
+ def OnElement(
44
44
  self,
45
- expression: Expression,
45
+ element: Element,
46
46
  ) -> Generator[VisitResult]:
47
- """Call for every expression."""
47
+ """Call for every element."""
48
48
 
49
49
  raise NotImplementedError("Abstract method") # noqa: EM101 # pragma: no cover
50
50
 
51
51
  # ----------------------------------------------------------------------
52
52
  @abstractmethod
53
53
  @contextmanager
54
- def OnExpressionDetails(
54
+ def OnElementDetails(
55
55
  self,
56
- expression: Expression,
56
+ element: Element,
57
57
  ) -> Generator[VisitResult]:
58
- """Call when visiting the details of an expression."""
58
+ """Call when visiting the details of an element."""
59
59
 
60
60
  raise NotImplementedError("Abstract method") # noqa: EM101 # pragma: no cover
61
61
 
62
62
  # ----------------------------------------------------------------------
63
63
  @abstractmethod
64
64
  @contextmanager
65
- def OnExpressionChildren(
65
+ def OnElementChildren(
66
66
  self,
67
- expression: Expression,
67
+ element: Element,
68
68
  children_name: str,
69
- children: Iterable[Expression],
69
+ children: Iterable[Element],
70
70
  ) -> Generator[VisitResult]:
71
- """Call when visiting the children of an expression."""
71
+ """Call when visiting the children of an element."""
72
72
 
73
73
  raise NotImplementedError("Abstract method") # noqa: EM101 # pragma: no cover
74
74
 
@@ -76,44 +76,44 @@ class ExpressionVisitor(ABC):
76
76
  # Derived classes should implement the following methods:
77
77
  #
78
78
  # @contextmanager
79
- # def On<Expression Name>(self, expression: <Expression Name>) -> Generator[VisitResult]:
79
+ # def On<Element Name>(self, element: <Element Name>) -> Generator[VisitResult]:
80
80
  # ...
81
81
  #
82
- # def On<Expression Name>__<Detail Name>(self, expression_or_expressions: <Expression Name> | list[<Expression Name>], include_disabled: bool) -> VisitResult:
82
+ # def On<Element Name>__<Detail Name>(self, element_or_elements: <Element Name> | list[<Element Name>], include_disabled: bool) -> VisitResult:
83
83
  # ...
84
84
  #
85
85
 
86
86
 
87
87
  # ----------------------------------------------------------------------
88
- class ExpressionVisitorHelper(ExpressionVisitor):
88
+ class ElementVisitorHelper(ElementVisitor):
89
89
  """Base class that makes writing custom visitors easier by providing default behavior for visitation methods."""
90
90
 
91
91
  # ----------------------------------------------------------------------
92
92
  @override
93
93
  @contextmanager
94
- def OnExpression(
94
+ def OnElement(
95
95
  self,
96
- expression: Expression,
96
+ element: Element,
97
97
  ) -> Generator[VisitResult]:
98
98
  yield VisitResult.Continue
99
99
 
100
100
  # ----------------------------------------------------------------------
101
101
  @override
102
102
  @contextmanager
103
- def OnExpressionDetails(
103
+ def OnElementDetails(
104
104
  self,
105
- expression: Expression,
105
+ element: Element,
106
106
  ) -> Generator[VisitResult]:
107
107
  yield VisitResult.Continue
108
108
 
109
109
  # ----------------------------------------------------------------------
110
110
  @override
111
111
  @contextmanager
112
- def OnExpressionChildren(
112
+ def OnElementChildren(
113
113
  self,
114
- expression: Expression,
114
+ element: Element,
115
115
  children_name: str,
116
- children: Iterable[Expression],
116
+ children: Iterable[Element],
117
117
  ) -> Generator[VisitResult]:
118
118
  yield VisitResult.Continue
119
119
 
@@ -128,7 +128,7 @@ class ExpressionVisitorHelper(ExpressionVisitor):
128
128
  return types.MethodType(self.__class__._DefaultDetailMethod, self) # noqa: SLF001
129
129
 
130
130
  if index == -1:
131
- return self.__class__._DefaultExpressionMethod # noqa: SLF001
131
+ return self.__class__._DefaultElementMethod # noqa: SLF001
132
132
 
133
133
  raise AttributeError(method_name)
134
134
 
@@ -137,24 +137,22 @@ class ExpressionVisitorHelper(ExpressionVisitor):
137
137
  # ----------------------------------------------------------------------
138
138
  @staticmethod
139
139
  @contextmanager
140
- def _DefaultExpressionMethod(*args, **kwargs) -> Generator[VisitResult]: # noqa: ARG004
140
+ def _DefaultElementMethod(*args, **kwargs) -> Generator[VisitResult]: # noqa: ARG004
141
141
  yield VisitResult.Continue
142
142
 
143
143
  # ----------------------------------------------------------------------
144
144
  def _DefaultDetailMethod(
145
145
  self,
146
- expression_or_expressions: Expression | list[Expression],
146
+ element_or_elements: Element | list[Element],
147
147
  *,
148
148
  include_disabled: bool,
149
149
  ) -> VisitResult:
150
- expressions: list[Expression] = ( # ty: ignore[invalid-assignment]
151
- expression_or_expressions
152
- if isinstance(expression_or_expressions, list)
153
- else [expression_or_expressions]
150
+ elements: list[Element] = ( # ty: ignore[invalid-assignment]
151
+ element_or_elements if isinstance(element_or_elements, list) else [element_or_elements]
154
152
  )
155
153
 
156
- for expression in expressions:
157
- result = expression.Accept(self, include_disabled=include_disabled)
154
+ for element in elements:
155
+ result = element.Accept(self, include_disabled=include_disabled)
158
156
  if result & VisitResult.Terminate:
159
157
  return result
160
158
 
@@ -1,169 +0,0 @@
1
- """Various ANTLR visitor mixins that can be used to add functionality to the generated visitor classes."""
2
-
3
- import re
4
-
5
- from abc import ABC, abstractmethod
6
- from typing import cast, override, TYPE_CHECKING
7
-
8
- import antlr4
9
-
10
- from dbrownell_ParserLib.region import Location, Region
11
-
12
- if TYPE_CHECKING:
13
- from collections.abc import Callable
14
- from pathlib import Path
15
-
16
-
17
- # ----------------------------------------------------------------------
18
- class AntlrVisitorMixinBase(ABC):
19
- """Base class for visitor mixins. This class is not meant to be used directly, but rather to be inherited by other mixin classes."""
20
-
21
- # ----------------------------------------------------------------------
22
- def __init__(
23
- self,
24
- filename: Path,
25
- on_progress_func: Callable[[int], None],
26
- *,
27
- is_included_file: bool,
28
- ) -> None:
29
- self.filename = filename
30
- self.is_included_file = is_included_file
31
-
32
- # Protected data
33
- self._on_progress_func = on_progress_func
34
- self._stack: list[object] = []
35
-
36
- # Private data
37
- self._current_line: int = 0
38
-
39
- # ----------------------------------------------------------------------
40
- def CreateRegion(
41
- self,
42
- ctx: antlr4.TerminalNode | antlr4.ParserRuleContext,
43
- ) -> Region:
44
- """Create a `Region` from the given context."""
45
-
46
- if isinstance(ctx, antlr4.TerminalNode):
47
- token = cast(antlr4.Token, ctx.symbol) # ty: ignore[unresolved-attribute]
48
-
49
- assert token.column is not None
50
-
51
- start_line = token.line
52
- start_col = token.column + 1
53
- end_line = token.line
54
- end_col = token.column + 1 + len(token.text)
55
-
56
- assert start_line is not None
57
- assert end_line is not None
58
-
59
- region = Region(
60
- self.filename,
61
- Location(start_line, start_col),
62
- Location(end_line, end_col),
63
- )
64
- else:
65
- region = self._CreateRegionFromRule(ctx)
66
-
67
- self._OnProgress(region.end.line)
68
-
69
- return region
70
-
71
- # ----------------------------------------------------------------------
72
- def GetChildren(self, ctx: antlr4.ParserRuleContext) -> list[object]:
73
- """Return the results of visiting the children of the given context."""
74
-
75
- prev_num_stack_items = len(self._stack)
76
-
77
- cast(antlr4.ParseTreeVisitor, self).visitChildren(ctx)
78
-
79
- results = self._stack[prev_num_stack_items:]
80
- del self._stack[prev_num_stack_items:]
81
-
82
- return results
83
-
84
- # ----------------------------------------------------------------------
85
- # | Protected Methods
86
- def _OnProgress(self, end_line: int) -> None:
87
- if end_line > self._current_line:
88
- self._current_line = end_line
89
- self._on_progress_func(end_line)
90
-
91
- # ----------------------------------------------------------------------
92
- @abstractmethod
93
- def _CreateRegionFromRule(self, ctx: antlr4.ParserRuleContext) -> Region: ...
94
-
95
-
96
- # ----------------------------------------------------------------------
97
- class InsignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
98
- """Mixin that adds functionality used for grammars that ignore insignificant whitespace."""
99
-
100
- # ----------------------------------------------------------------------
101
- def _CreateRegionFromRule(self, ctx: antlr4.ParserRuleContext) -> Region:
102
- start_token = ctx.start
103
- stop_token = ctx.stop
104
-
105
- if start_token is None or stop_token is None:
106
- msg = "Context does not have start or stop token"
107
- raise ValueError(msg)
108
-
109
- start_line = start_token.line
110
- start_col = start_token.column + 1
111
- end_line = stop_token.line
112
- end_col = stop_token.column + 1 + len(stop_token.text)
113
-
114
- assert start_line is not None
115
- assert end_line is not None
116
-
117
- return Region(
118
- self.filename,
119
- Location(start_line, start_col),
120
- Location(end_line, end_col),
121
- )
122
-
123
-
124
- # ----------------------------------------------------------------------
125
- class SignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
126
- """Mixin that adds functionality used for grammars that treat whitespace as significant."""
127
-
128
- # ----------------------------------------------------------------------
129
- def __init__(
130
- self,
131
- dedent_token: int,
132
- newline_token: int,
133
- newline_token_string: str,
134
- ) -> None:
135
- self._dedent_token = dedent_token
136
- self._newline_token = newline_token
137
- self._newline_token_string = newline_token_string
138
-
139
- self._newline_indent_regex = re.compile(r"^\r?\n[ \t]*$")
140
-
141
- # ----------------------------------------------------------------------
142
- @override
143
- def _CreateRegionFromRule(
144
- self,
145
- ctx: antlr4.ParserRuleContext,
146
- ) -> Region:
147
- assert isinstance(ctx.start, antlr4.Token), ctx.start
148
- assert isinstance(ctx.stop, antlr4.Token), ctx.stop
149
-
150
- if ctx.stop.text == self._newline_token_string or self._newline_indent_regex.match(ctx.stop.text):
151
- assert ctx.stop.line == ctx.start.line or ctx.stop.line - 1 == ctx.start.line, (
152
- ctx.start.line,
153
- ctx.stop.line,
154
- )
155
-
156
- stop_line = ctx.start.line
157
- stop_column = ctx.start.column + 1 + len(ctx.start.text)
158
-
159
- return Region(
160
- self.filename,
161
- Location(ctx.start.line, ctx.start.column + 1),
162
- Location(stop_line, stop_column),
163
- )
164
-
165
- return Region(
166
- self.filename,
167
- Location(ctx.start.line, ctx.start.column + 1),
168
- Location(ctx.stop.line, ctx.stop.column + 1),
169
- )