larkita 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.
- larkita-0.1.0/LICENSE +19 -0
- larkita-0.1.0/PKG-INFO +301 -0
- larkita-0.1.0/README.md +276 -0
- larkita-0.1.0/larkita/__init__.py +29 -0
- larkita-0.1.0/larkita/ast_builder.py +100 -0
- larkita-0.1.0/larkita/common.py +33 -0
- larkita-0.1.0/larkita/exceptions.py +225 -0
- larkita-0.1.0/larkita/grammar.py +137 -0
- larkita-0.1.0/larkita/grammars/__init__.py +0 -0
- larkita-0.1.0/larkita/grammars/common.lark +59 -0
- larkita-0.1.0/larkita/grammars/unicode.lark +7 -0
- larkita-0.1.0/larkita/larkita.py +143 -0
- larkita-0.1.0/larkita/lexer.py +731 -0
- larkita-0.1.0/larkita/load_grammar.py +1305 -0
- larkita-0.1.0/larkita/lr_analysis.py +443 -0
- larkita-0.1.0/larkita/lr_parser.py +94 -0
- larkita-0.1.0/larkita/parse_tree_builder.py +43 -0
- larkita-0.1.0/larkita/parser_frontends.py +38 -0
- larkita-0.1.0/larkita/py.typed +0 -0
- larkita-0.1.0/larkita/report.py +209 -0
- larkita-0.1.0/larkita/tree.py +114 -0
- larkita-0.1.0/larkita/utils.py +206 -0
- larkita-0.1.0/larkita/visitors.py +245 -0
- larkita-0.1.0/larkita.egg-info/PKG-INFO +301 -0
- larkita-0.1.0/larkita.egg-info/SOURCES.txt +29 -0
- larkita-0.1.0/larkita.egg-info/dependency_links.txt +1 -0
- larkita-0.1.0/larkita.egg-info/requires.txt +6 -0
- larkita-0.1.0/larkita.egg-info/top_level.txt +1 -0
- larkita-0.1.0/pyproject.toml +58 -0
- larkita-0.1.0/setup.cfg +4 -0
- larkita-0.1.0/tests/test_larkita.py +461 -0
larkita-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright © 2017 Erez Shinan
|
|
2
|
+
Copyright © 2026 Karl Stroetmann
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
6
|
+
the Software without restriction, including without limitation the rights to
|
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
8
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
16
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
17
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
18
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
19
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
larkita-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: larkita
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A light-weight canonical LR(1) parser generator that returns abstract syntax trees as nested tuples
|
|
5
|
+
Author: Karl Stroetmann
|
|
6
|
+
Author-email: "Erez Shinan (Lark)" <erezshin@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/karlstroetmann/Larkita
|
|
9
|
+
Project-URL: Documentation, https://github.com/karlstroetmann/Larkita/blob/master/tutorial/larkita-tutorial.pdf
|
|
10
|
+
Keywords: LR(1),parser,parsing,ast
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Text Processing :: General
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: regex
|
|
21
|
+
Requires-Dist: regex; extra == "regex"
|
|
22
|
+
Provides-Extra: interegular
|
|
23
|
+
Requires-Dist: interegular<0.4.0,>=0.3.1; extra == "interegular"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Larkita
|
|
27
|
+
|
|
28
|
+
Larkita is a light-weight version of the Python parser generator
|
|
29
|
+
[Lark](https://github.com/lark-parser/lark). It differs from Lark in three ways:
|
|
30
|
+
|
|
31
|
+
* It generates a **canonical LR(1) parser** (not Earley or CYK). On request, it
|
|
32
|
+
merges states with the same core; for an LALR(1) grammar and the basic lexer,
|
|
33
|
+
the result is the LALR(1) parser.
|
|
34
|
+
* The parser returns the **abstract syntax tree as a nested tuple**, not as a tree
|
|
35
|
+
of objects. How the tuple is built follows from the grammar itself; you never
|
|
36
|
+
have to write tree-building code.
|
|
37
|
+
* Every time a parser is generated, Larkita writes an **output file** with all
|
|
38
|
+
the LR(1) states, every item annotated with its lookahead tokens. This makes
|
|
39
|
+
conflicts easy to debug.
|
|
40
|
+
|
|
41
|
+
Grammars are written in Lark's EBNF notation, with a few additions for building
|
|
42
|
+
the abstract syntax tree.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
Larkita needs Python 3.12 or newer and has no dependencies. Install it with
|
|
47
|
+
|
|
48
|
+
pip install larkita
|
|
49
|
+
|
|
50
|
+
The source code is on GitHub at <https://github.com/karlstroetmann/Larkita>. To
|
|
51
|
+
install the latest version from there, run
|
|
52
|
+
|
|
53
|
+
pip install git+https://github.com/karlstroetmann/Larkita.git
|
|
54
|
+
|
|
55
|
+
If the optional package `interegular` is installed, Larkita checks whether the
|
|
56
|
+
regular expressions of different terminals can match the same text. It reports
|
|
57
|
+
such collisions via `larkita.logger`, which is silent by default; call
|
|
58
|
+
`larkita.logger.setLevel(logging.WARNING)` to see them.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from larkita import Larkita
|
|
64
|
+
|
|
65
|
+
grammar = r'''
|
|
66
|
+
stmnt: IDENTIFIER ":=" expr ";"
|
|
67
|
+
| expr ";"
|
|
68
|
+
|
|
69
|
+
expr: expr "+" product
|
|
70
|
+
| expr "-" product
|
|
71
|
+
| product
|
|
72
|
+
|
|
73
|
+
product: product "*" factor
|
|
74
|
+
| product "/" factor
|
|
75
|
+
| factor
|
|
76
|
+
|
|
77
|
+
factor: "(" expr ")" %ignore "(", ")"
|
|
78
|
+
| NUMBER
|
|
79
|
+
| IDENTIFIER
|
|
80
|
+
|
|
81
|
+
%import common.CNAME -> IDENTIFIER
|
|
82
|
+
%import common.NUMBER
|
|
83
|
+
%import common.WS
|
|
84
|
+
%ignore WS
|
|
85
|
+
'''
|
|
86
|
+
|
|
87
|
+
parser = Larkita(grammar)
|
|
88
|
+
print(parser.parse('x := (a + 3) * b;'))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
prints
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
((':=', ';'), ('IDENTIFIER', 'x'), ('*', ('+', ('IDENTIFIER', 'a'), ('NUMBER', '3')), ('IDENTIFIER', 'b')))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
and writes the description of the automaton to `grammar.out`. To read the
|
|
98
|
+
grammar from a file, use
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
parser = Larkita.open('new-syntax/expr.g') # writes new-syntax/expr.out
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The start symbol is the first rule of the grammar (here `stmnt`).
|
|
105
|
+
|
|
106
|
+
## Tutorial
|
|
107
|
+
|
|
108
|
+
The tutorial
|
|
109
|
+
[`tutorial/larkita-tutorial.pdf`](https://github.com/karlstroetmann/Larkita/blob/master/tutorial/larkita-tutorial.pdf)
|
|
110
|
+
explains how to write grammars for Larkita, how the abstract syntax trees are
|
|
111
|
+
built, how to process them with `match`, and how to deal with syntax errors and
|
|
112
|
+
conflicts. Its grammars and programs are in the directory `tutorial/examples` of
|
|
113
|
+
the repository; running `make` in the directory `tutorial` runs them and rebuilds
|
|
114
|
+
the PDF.
|
|
115
|
+
|
|
116
|
+
## How the abstract syntax tree is built
|
|
117
|
+
|
|
118
|
+
Consider an alternative `a: X1 X2 ... Xn`. The literal tokens written in double
|
|
119
|
+
quotes form the **head** of the tuple; all other symbols become its **children**:
|
|
120
|
+
|
|
121
|
+
(head, ast(child_1), ..., ast(child_k))
|
|
122
|
+
|
|
123
|
+
| Grammar | Abstract syntax tree |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `expr: expr "+" product` | `("+", ast(expr), ast(product))` |
|
|
126
|
+
| `stmnt: expr ";"` | `(";", ast(expr))` |
|
|
127
|
+
| `stmnt: IDENTIFIER ":=" expr ";"` | `((":=", ";"), ("IDENTIFIER", name), ast(expr))` |
|
|
128
|
+
| `atom: IDENTIFIER` | `("IDENTIFIER", name)` |
|
|
129
|
+
| `expr: product` | `ast(product)` |
|
|
130
|
+
| `factor: "(" expr ")" %ignore "(", ")"` | `ast(expr)` |
|
|
131
|
+
| `atom: IDENTIFIER "(" args ")" -> "call"` | `("call", ("IDENTIFIER", name), ast(args))` |
|
|
132
|
+
| `args: expr ("," expr)*` | `(",", ast(expr_1), ..., ast(expr_n))` |
|
|
133
|
+
| `set: "{" expr ("," expr)* "}"` | `(("{", ",", "}"), ast(expr_1), ..., ast(expr_n))` |
|
|
134
|
+
| `program: statement*` | `("program", ast(statement_1), ..., ast(statement_n))` |
|
|
135
|
+
| `stmnt: "return" [expr] ";"` | `(("return", ";"),)` or `(("return", ";"), ast(expr))` |
|
|
136
|
+
|
|
137
|
+
In detail:
|
|
138
|
+
|
|
139
|
+
* **Head.** The head consists of all literals of the alternative that are not
|
|
140
|
+
ignored, in the order in which they are written, one entry per occurrence. If
|
|
141
|
+
there is exactly one literal, the head is that string; otherwise it is a tuple
|
|
142
|
+
of strings. The head is fixed by the grammar, not by the input: `f(a)` with the
|
|
143
|
+
rule for `args` above yields `(",", ast(a))`, even though no comma was read.
|
|
144
|
+
Literals inside repetitions, optional parts and groups belong to the head, too.
|
|
145
|
+
* **Named tokens** become `(NAME, text)`, e.g. `("NUMBER", "42")`. The text is
|
|
146
|
+
not converted. Inline regular expressions `/.../` are treated like named tokens.
|
|
147
|
+
* **Pass-through.** If an alternative has no head and consists of exactly one
|
|
148
|
+
symbol (with no `*`, `+`, `?`, `[...]` or `|`), it returns the abstract syntax
|
|
149
|
+
tree of that symbol unchanged.
|
|
150
|
+
* **Rule name as head.** If an alternative has no head and is not a pass-through,
|
|
151
|
+
the name of the rule is used as head, e.g. `("program", ...)`. An empty
|
|
152
|
+
alternative yields `("rule_name",)`.
|
|
153
|
+
* **Repetitions and groups are flattened** into the tuple of the alternative.
|
|
154
|
+
* **Missing optional parts are left out**; there is no `None` placeholder.
|
|
155
|
+
* **Aliases** replace the whole head: `-> "call"` and `-> call` are equivalent.
|
|
156
|
+
|
|
157
|
+
### Ignoring literals
|
|
158
|
+
|
|
159
|
+
`%ignore` has three forms:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
%ignore WS // the lexer skips the terminal WS
|
|
163
|
+
factor: "(" expr ")" %ignore "(", ")"
|
|
164
|
+
// "(" and ")" are left out of the head of this alternative
|
|
165
|
+
%ignore: "(", ")", "{", "}" // these literals are left out of the heads of all rules
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
With `%ignore: "(", ")"`, the rule
|
|
169
|
+
`statement: "while" "(" expr ")" statement` yields
|
|
170
|
+
`("while", ast(expr), ast(statement))`.
|
|
171
|
+
|
|
172
|
+
## Grammar syntax
|
|
173
|
+
|
|
174
|
+
Larkita reads Lark grammars:
|
|
175
|
+
|
|
176
|
+
* Rules have lowercase names, terminals have uppercase names.
|
|
177
|
+
* `|` separates alternatives, `( ... )` groups, `[ ... ]` and `?` mark optional
|
|
178
|
+
parts, `*` and `+` repeat, and `~ n` or `~ n..m` repeat a fixed number of times.
|
|
179
|
+
* String literals are written as `"..."` (append `i` for case-insensitive
|
|
180
|
+
matching), regular expressions as `/.../flags`.
|
|
181
|
+
* Terminals are defined with `NAME: ...`; `NAME.p: ...` gives a terminal the
|
|
182
|
+
priority `p`, and `rule.p: ...` gives a rule the priority `p` (used to resolve
|
|
183
|
+
reduce/reduce conflicts).
|
|
184
|
+
* `%import common.NUMBER` imports terminals from Lark's standard library
|
|
185
|
+
(`larkita/grammars/common.lark`), and `%import .file.rule` imports from a
|
|
186
|
+
grammar file next to the current one. `-> NAME` renames an import.
|
|
187
|
+
* `%declare`, `%extend` and `%override` work as in Lark.
|
|
188
|
+
* `//` starts a comment.
|
|
189
|
+
|
|
190
|
+
Not supported (compared with Lark): the rule modifiers `?rule` and `!rule`,
|
|
191
|
+
templates, post-lexers (such as Lark's `Indenter`), lexer callbacks, and
|
|
192
|
+
transformers. Rules and terminals whose names start with `_` are not inlined
|
|
193
|
+
or filtered.
|
|
194
|
+
|
|
195
|
+
## The output file
|
|
196
|
+
|
|
197
|
+
If the grammar is read from a file, the output file has the same name with the
|
|
198
|
+
extension `.out` (`expr.g` produces `expr.out`). Otherwise it is `grammar.out` in
|
|
199
|
+
the current directory. The parameter `out_file` sets another path.
|
|
200
|
+
|
|
201
|
+
The file contains
|
|
202
|
+
|
|
203
|
+
1. the rules after the EBNF operators have been expanded, each followed by the
|
|
204
|
+
abstract syntax tree that a reduction produces, e.g.
|
|
205
|
+
`(3) expr → expr "+" product ⇒ ("+", ast(expr), ast(product))`;
|
|
206
|
+
2. a list of all conflicts and how they were resolved;
|
|
207
|
+
3. all states. Each state lists its items with their lookahead tokens (closure
|
|
208
|
+
items are marked with `+`), followed by its actions and gotos:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
State 2
|
|
212
|
+
|
|
213
|
+
(1) stmnt → IDENTIFIER • ":=" expr ";" { $END }
|
|
214
|
+
(11) factor → IDENTIFIER • { "*", "+", "-", "/", ";" }
|
|
215
|
+
|
|
216
|
+
":=" shift and go to state 8
|
|
217
|
+
"*" reduce using rule (11) factor → IDENTIFIER
|
|
218
|
+
"+" reduce using rule (11) factor → IDENTIFIER
|
|
219
|
+
"-" reduce using rule (11) factor → IDENTIFIER
|
|
220
|
+
"/" reduce using rule (11) factor → IDENTIFIER
|
|
221
|
+
";" reduce using rule (11) factor → IDENTIFIER
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Conflicts
|
|
225
|
+
|
|
226
|
+
* **Shift/reduce conflicts** are resolved in favour of shift (so the dangling
|
|
227
|
+
`else` belongs to the innermost `if`). Larkita issues a `ConflictWarning`.
|
|
228
|
+
* **Reduce/reduce conflicts** are resolved in favour of the rule with the highest
|
|
229
|
+
priority. If no rule has a higher priority than the others, `Larkita` raises a
|
|
230
|
+
`GrammarError`; the output file is written anyway.
|
|
231
|
+
|
|
232
|
+
## Merging states
|
|
233
|
+
|
|
234
|
+
Canonical LR(1) automata can be large. With `merge_states=True`, Larkita merges
|
|
235
|
+
states with the same core (i.e. the same items without lookaheads), as long as
|
|
236
|
+
this is safe:
|
|
237
|
+
|
|
238
|
+
* Merging must not introduce a new conflict, neither in the merged states nor in
|
|
239
|
+
their successors.
|
|
240
|
+
* With the contextual lexer, merging must not change how the input is split into
|
|
241
|
+
tokens. For example, if a keyword like `"else"` is only acceptable in one of
|
|
242
|
+
two states, merging them could make the lexer read `else` as a keyword where
|
|
243
|
+
the other state reads an identifier.
|
|
244
|
+
|
|
245
|
+
The resulting parser accepts the same strings, builds the same abstract syntax
|
|
246
|
+
trees, and detects syntax errors at the same tokens as the canonical parser,
|
|
247
|
+
although it may perform a few more reductions before it reports an error. With
|
|
248
|
+
`lexer='basic'`, the lexer does not depend on the state of the parser, so for an
|
|
249
|
+
LALR(1) grammar the result is the LALR(1) automaton. The output file then
|
|
250
|
+
describes the merged states and explains why the remaining states with the same
|
|
251
|
+
core have been kept apart.
|
|
252
|
+
|
|
253
|
+
## API
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
Larkita(grammar, *, start=None, lexer='contextual', import_paths=(),
|
|
257
|
+
source_path=None, out_file=None, regex=False, g_regex_flags=0,
|
|
258
|
+
merge_states=False)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
* `grammar`: the grammar as a string or an open file.
|
|
262
|
+
* `start`: the start symbol; defaults to the first rule.
|
|
263
|
+
* `lexer`: `'contextual'` (only matches the terminals that the parser can accept
|
|
264
|
+
in its current state) or `'basic'`.
|
|
265
|
+
* `import_paths`: additional directories to search for imported grammars.
|
|
266
|
+
* `source_path`: the file name of the grammar, used for relative imports and to
|
|
267
|
+
name the output file.
|
|
268
|
+
* `out_file`: the path of the output file.
|
|
269
|
+
* `regex`: use the `regex` module instead of `re`.
|
|
270
|
+
* `g_regex_flags`: flags applied to all terminals.
|
|
271
|
+
* `merge_states`: merge states with the same core, as long as this is safe
|
|
272
|
+
(see above).
|
|
273
|
+
|
|
274
|
+
`Larkita.open(filename, rel_to=None, **options)` reads the grammar from a file.
|
|
275
|
+
|
|
276
|
+
`parser.parse(text)` returns the abstract syntax tree. It raises
|
|
277
|
+
`UnexpectedCharacters` if the lexer cannot match the input, and `UnexpectedToken`
|
|
278
|
+
if the parser receives a token it cannot accept (at the end of the input, the
|
|
279
|
+
token has the type `$END`). All exceptions derive from `LarkitaError`.
|
|
280
|
+
|
|
281
|
+
`parser.lex(text)` runs only the scanner and returns an iterator over the tokens.
|
|
282
|
+
Every token `t` provides `t.type`, `t.value`, `t.line`, and `t.column`; ignored
|
|
283
|
+
tokens are skipped.
|
|
284
|
+
|
|
285
|
+
After construction, `parser.conflicts`, `parser.rules`, `parser.terminals`,
|
|
286
|
+
`parser.parse_table` and `parser.out_file` describe the generated parser.
|
|
287
|
+
|
|
288
|
+
## Examples and tests
|
|
289
|
+
|
|
290
|
+
The directory `new-syntax` of the repository contains two example grammars:
|
|
291
|
+
`expr.g` (arithmetic expressions) and `interpreter.g` (a small imperative language
|
|
292
|
+
with sets).
|
|
293
|
+
|
|
294
|
+
In a clone of the repository, run the tests with
|
|
295
|
+
|
|
296
|
+
python3 -m tests
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT. Larkita is derived from [Lark](https://github.com/lark-parser/lark) by
|
|
301
|
+
Erez Shinan; see `LICENSE`.
|
larkita-0.1.0/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Larkita
|
|
2
|
+
|
|
3
|
+
Larkita is a light-weight version of the Python parser generator
|
|
4
|
+
[Lark](https://github.com/lark-parser/lark). It differs from Lark in three ways:
|
|
5
|
+
|
|
6
|
+
* It generates a **canonical LR(1) parser** (not Earley or CYK). On request, it
|
|
7
|
+
merges states with the same core; for an LALR(1) grammar and the basic lexer,
|
|
8
|
+
the result is the LALR(1) parser.
|
|
9
|
+
* The parser returns the **abstract syntax tree as a nested tuple**, not as a tree
|
|
10
|
+
of objects. How the tuple is built follows from the grammar itself; you never
|
|
11
|
+
have to write tree-building code.
|
|
12
|
+
* Every time a parser is generated, Larkita writes an **output file** with all
|
|
13
|
+
the LR(1) states, every item annotated with its lookahead tokens. This makes
|
|
14
|
+
conflicts easy to debug.
|
|
15
|
+
|
|
16
|
+
Grammars are written in Lark's EBNF notation, with a few additions for building
|
|
17
|
+
the abstract syntax tree.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Larkita needs Python 3.12 or newer and has no dependencies. Install it with
|
|
22
|
+
|
|
23
|
+
pip install larkita
|
|
24
|
+
|
|
25
|
+
The source code is on GitHub at <https://github.com/karlstroetmann/Larkita>. To
|
|
26
|
+
install the latest version from there, run
|
|
27
|
+
|
|
28
|
+
pip install git+https://github.com/karlstroetmann/Larkita.git
|
|
29
|
+
|
|
30
|
+
If the optional package `interegular` is installed, Larkita checks whether the
|
|
31
|
+
regular expressions of different terminals can match the same text. It reports
|
|
32
|
+
such collisions via `larkita.logger`, which is silent by default; call
|
|
33
|
+
`larkita.logger.setLevel(logging.WARNING)` to see them.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from larkita import Larkita
|
|
39
|
+
|
|
40
|
+
grammar = r'''
|
|
41
|
+
stmnt: IDENTIFIER ":=" expr ";"
|
|
42
|
+
| expr ";"
|
|
43
|
+
|
|
44
|
+
expr: expr "+" product
|
|
45
|
+
| expr "-" product
|
|
46
|
+
| product
|
|
47
|
+
|
|
48
|
+
product: product "*" factor
|
|
49
|
+
| product "/" factor
|
|
50
|
+
| factor
|
|
51
|
+
|
|
52
|
+
factor: "(" expr ")" %ignore "(", ")"
|
|
53
|
+
| NUMBER
|
|
54
|
+
| IDENTIFIER
|
|
55
|
+
|
|
56
|
+
%import common.CNAME -> IDENTIFIER
|
|
57
|
+
%import common.NUMBER
|
|
58
|
+
%import common.WS
|
|
59
|
+
%ignore WS
|
|
60
|
+
'''
|
|
61
|
+
|
|
62
|
+
parser = Larkita(grammar)
|
|
63
|
+
print(parser.parse('x := (a + 3) * b;'))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
prints
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
((':=', ';'), ('IDENTIFIER', 'x'), ('*', ('+', ('IDENTIFIER', 'a'), ('NUMBER', '3')), ('IDENTIFIER', 'b')))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
and writes the description of the automaton to `grammar.out`. To read the
|
|
73
|
+
grammar from a file, use
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
parser = Larkita.open('new-syntax/expr.g') # writes new-syntax/expr.out
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The start symbol is the first rule of the grammar (here `stmnt`).
|
|
80
|
+
|
|
81
|
+
## Tutorial
|
|
82
|
+
|
|
83
|
+
The tutorial
|
|
84
|
+
[`tutorial/larkita-tutorial.pdf`](https://github.com/karlstroetmann/Larkita/blob/master/tutorial/larkita-tutorial.pdf)
|
|
85
|
+
explains how to write grammars for Larkita, how the abstract syntax trees are
|
|
86
|
+
built, how to process them with `match`, and how to deal with syntax errors and
|
|
87
|
+
conflicts. Its grammars and programs are in the directory `tutorial/examples` of
|
|
88
|
+
the repository; running `make` in the directory `tutorial` runs them and rebuilds
|
|
89
|
+
the PDF.
|
|
90
|
+
|
|
91
|
+
## How the abstract syntax tree is built
|
|
92
|
+
|
|
93
|
+
Consider an alternative `a: X1 X2 ... Xn`. The literal tokens written in double
|
|
94
|
+
quotes form the **head** of the tuple; all other symbols become its **children**:
|
|
95
|
+
|
|
96
|
+
(head, ast(child_1), ..., ast(child_k))
|
|
97
|
+
|
|
98
|
+
| Grammar | Abstract syntax tree |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `expr: expr "+" product` | `("+", ast(expr), ast(product))` |
|
|
101
|
+
| `stmnt: expr ";"` | `(";", ast(expr))` |
|
|
102
|
+
| `stmnt: IDENTIFIER ":=" expr ";"` | `((":=", ";"), ("IDENTIFIER", name), ast(expr))` |
|
|
103
|
+
| `atom: IDENTIFIER` | `("IDENTIFIER", name)` |
|
|
104
|
+
| `expr: product` | `ast(product)` |
|
|
105
|
+
| `factor: "(" expr ")" %ignore "(", ")"` | `ast(expr)` |
|
|
106
|
+
| `atom: IDENTIFIER "(" args ")" -> "call"` | `("call", ("IDENTIFIER", name), ast(args))` |
|
|
107
|
+
| `args: expr ("," expr)*` | `(",", ast(expr_1), ..., ast(expr_n))` |
|
|
108
|
+
| `set: "{" expr ("," expr)* "}"` | `(("{", ",", "}"), ast(expr_1), ..., ast(expr_n))` |
|
|
109
|
+
| `program: statement*` | `("program", ast(statement_1), ..., ast(statement_n))` |
|
|
110
|
+
| `stmnt: "return" [expr] ";"` | `(("return", ";"),)` or `(("return", ";"), ast(expr))` |
|
|
111
|
+
|
|
112
|
+
In detail:
|
|
113
|
+
|
|
114
|
+
* **Head.** The head consists of all literals of the alternative that are not
|
|
115
|
+
ignored, in the order in which they are written, one entry per occurrence. If
|
|
116
|
+
there is exactly one literal, the head is that string; otherwise it is a tuple
|
|
117
|
+
of strings. The head is fixed by the grammar, not by the input: `f(a)` with the
|
|
118
|
+
rule for `args` above yields `(",", ast(a))`, even though no comma was read.
|
|
119
|
+
Literals inside repetitions, optional parts and groups belong to the head, too.
|
|
120
|
+
* **Named tokens** become `(NAME, text)`, e.g. `("NUMBER", "42")`. The text is
|
|
121
|
+
not converted. Inline regular expressions `/.../` are treated like named tokens.
|
|
122
|
+
* **Pass-through.** If an alternative has no head and consists of exactly one
|
|
123
|
+
symbol (with no `*`, `+`, `?`, `[...]` or `|`), it returns the abstract syntax
|
|
124
|
+
tree of that symbol unchanged.
|
|
125
|
+
* **Rule name as head.** If an alternative has no head and is not a pass-through,
|
|
126
|
+
the name of the rule is used as head, e.g. `("program", ...)`. An empty
|
|
127
|
+
alternative yields `("rule_name",)`.
|
|
128
|
+
* **Repetitions and groups are flattened** into the tuple of the alternative.
|
|
129
|
+
* **Missing optional parts are left out**; there is no `None` placeholder.
|
|
130
|
+
* **Aliases** replace the whole head: `-> "call"` and `-> call` are equivalent.
|
|
131
|
+
|
|
132
|
+
### Ignoring literals
|
|
133
|
+
|
|
134
|
+
`%ignore` has three forms:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
%ignore WS // the lexer skips the terminal WS
|
|
138
|
+
factor: "(" expr ")" %ignore "(", ")"
|
|
139
|
+
// "(" and ")" are left out of the head of this alternative
|
|
140
|
+
%ignore: "(", ")", "{", "}" // these literals are left out of the heads of all rules
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
With `%ignore: "(", ")"`, the rule
|
|
144
|
+
`statement: "while" "(" expr ")" statement` yields
|
|
145
|
+
`("while", ast(expr), ast(statement))`.
|
|
146
|
+
|
|
147
|
+
## Grammar syntax
|
|
148
|
+
|
|
149
|
+
Larkita reads Lark grammars:
|
|
150
|
+
|
|
151
|
+
* Rules have lowercase names, terminals have uppercase names.
|
|
152
|
+
* `|` separates alternatives, `( ... )` groups, `[ ... ]` and `?` mark optional
|
|
153
|
+
parts, `*` and `+` repeat, and `~ n` or `~ n..m` repeat a fixed number of times.
|
|
154
|
+
* String literals are written as `"..."` (append `i` for case-insensitive
|
|
155
|
+
matching), regular expressions as `/.../flags`.
|
|
156
|
+
* Terminals are defined with `NAME: ...`; `NAME.p: ...` gives a terminal the
|
|
157
|
+
priority `p`, and `rule.p: ...` gives a rule the priority `p` (used to resolve
|
|
158
|
+
reduce/reduce conflicts).
|
|
159
|
+
* `%import common.NUMBER` imports terminals from Lark's standard library
|
|
160
|
+
(`larkita/grammars/common.lark`), and `%import .file.rule` imports from a
|
|
161
|
+
grammar file next to the current one. `-> NAME` renames an import.
|
|
162
|
+
* `%declare`, `%extend` and `%override` work as in Lark.
|
|
163
|
+
* `//` starts a comment.
|
|
164
|
+
|
|
165
|
+
Not supported (compared with Lark): the rule modifiers `?rule` and `!rule`,
|
|
166
|
+
templates, post-lexers (such as Lark's `Indenter`), lexer callbacks, and
|
|
167
|
+
transformers. Rules and terminals whose names start with `_` are not inlined
|
|
168
|
+
or filtered.
|
|
169
|
+
|
|
170
|
+
## The output file
|
|
171
|
+
|
|
172
|
+
If the grammar is read from a file, the output file has the same name with the
|
|
173
|
+
extension `.out` (`expr.g` produces `expr.out`). Otherwise it is `grammar.out` in
|
|
174
|
+
the current directory. The parameter `out_file` sets another path.
|
|
175
|
+
|
|
176
|
+
The file contains
|
|
177
|
+
|
|
178
|
+
1. the rules after the EBNF operators have been expanded, each followed by the
|
|
179
|
+
abstract syntax tree that a reduction produces, e.g.
|
|
180
|
+
`(3) expr → expr "+" product ⇒ ("+", ast(expr), ast(product))`;
|
|
181
|
+
2. a list of all conflicts and how they were resolved;
|
|
182
|
+
3. all states. Each state lists its items with their lookahead tokens (closure
|
|
183
|
+
items are marked with `+`), followed by its actions and gotos:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
State 2
|
|
187
|
+
|
|
188
|
+
(1) stmnt → IDENTIFIER • ":=" expr ";" { $END }
|
|
189
|
+
(11) factor → IDENTIFIER • { "*", "+", "-", "/", ";" }
|
|
190
|
+
|
|
191
|
+
":=" shift and go to state 8
|
|
192
|
+
"*" reduce using rule (11) factor → IDENTIFIER
|
|
193
|
+
"+" reduce using rule (11) factor → IDENTIFIER
|
|
194
|
+
"-" reduce using rule (11) factor → IDENTIFIER
|
|
195
|
+
"/" reduce using rule (11) factor → IDENTIFIER
|
|
196
|
+
";" reduce using rule (11) factor → IDENTIFIER
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Conflicts
|
|
200
|
+
|
|
201
|
+
* **Shift/reduce conflicts** are resolved in favour of shift (so the dangling
|
|
202
|
+
`else` belongs to the innermost `if`). Larkita issues a `ConflictWarning`.
|
|
203
|
+
* **Reduce/reduce conflicts** are resolved in favour of the rule with the highest
|
|
204
|
+
priority. If no rule has a higher priority than the others, `Larkita` raises a
|
|
205
|
+
`GrammarError`; the output file is written anyway.
|
|
206
|
+
|
|
207
|
+
## Merging states
|
|
208
|
+
|
|
209
|
+
Canonical LR(1) automata can be large. With `merge_states=True`, Larkita merges
|
|
210
|
+
states with the same core (i.e. the same items without lookaheads), as long as
|
|
211
|
+
this is safe:
|
|
212
|
+
|
|
213
|
+
* Merging must not introduce a new conflict, neither in the merged states nor in
|
|
214
|
+
their successors.
|
|
215
|
+
* With the contextual lexer, merging must not change how the input is split into
|
|
216
|
+
tokens. For example, if a keyword like `"else"` is only acceptable in one of
|
|
217
|
+
two states, merging them could make the lexer read `else` as a keyword where
|
|
218
|
+
the other state reads an identifier.
|
|
219
|
+
|
|
220
|
+
The resulting parser accepts the same strings, builds the same abstract syntax
|
|
221
|
+
trees, and detects syntax errors at the same tokens as the canonical parser,
|
|
222
|
+
although it may perform a few more reductions before it reports an error. With
|
|
223
|
+
`lexer='basic'`, the lexer does not depend on the state of the parser, so for an
|
|
224
|
+
LALR(1) grammar the result is the LALR(1) automaton. The output file then
|
|
225
|
+
describes the merged states and explains why the remaining states with the same
|
|
226
|
+
core have been kept apart.
|
|
227
|
+
|
|
228
|
+
## API
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
Larkita(grammar, *, start=None, lexer='contextual', import_paths=(),
|
|
232
|
+
source_path=None, out_file=None, regex=False, g_regex_flags=0,
|
|
233
|
+
merge_states=False)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
* `grammar`: the grammar as a string or an open file.
|
|
237
|
+
* `start`: the start symbol; defaults to the first rule.
|
|
238
|
+
* `lexer`: `'contextual'` (only matches the terminals that the parser can accept
|
|
239
|
+
in its current state) or `'basic'`.
|
|
240
|
+
* `import_paths`: additional directories to search for imported grammars.
|
|
241
|
+
* `source_path`: the file name of the grammar, used for relative imports and to
|
|
242
|
+
name the output file.
|
|
243
|
+
* `out_file`: the path of the output file.
|
|
244
|
+
* `regex`: use the `regex` module instead of `re`.
|
|
245
|
+
* `g_regex_flags`: flags applied to all terminals.
|
|
246
|
+
* `merge_states`: merge states with the same core, as long as this is safe
|
|
247
|
+
(see above).
|
|
248
|
+
|
|
249
|
+
`Larkita.open(filename, rel_to=None, **options)` reads the grammar from a file.
|
|
250
|
+
|
|
251
|
+
`parser.parse(text)` returns the abstract syntax tree. It raises
|
|
252
|
+
`UnexpectedCharacters` if the lexer cannot match the input, and `UnexpectedToken`
|
|
253
|
+
if the parser receives a token it cannot accept (at the end of the input, the
|
|
254
|
+
token has the type `$END`). All exceptions derive from `LarkitaError`.
|
|
255
|
+
|
|
256
|
+
`parser.lex(text)` runs only the scanner and returns an iterator over the tokens.
|
|
257
|
+
Every token `t` provides `t.type`, `t.value`, `t.line`, and `t.column`; ignored
|
|
258
|
+
tokens are skipped.
|
|
259
|
+
|
|
260
|
+
After construction, `parser.conflicts`, `parser.rules`, `parser.terminals`,
|
|
261
|
+
`parser.parse_table` and `parser.out_file` describe the generated parser.
|
|
262
|
+
|
|
263
|
+
## Examples and tests
|
|
264
|
+
|
|
265
|
+
The directory `new-syntax` of the repository contains two example grammars:
|
|
266
|
+
`expr.g` (arithmetic expressions) and `interpreter.g` (a small imperative language
|
|
267
|
+
with sets).
|
|
268
|
+
|
|
269
|
+
In a clone of the repository, run the tests with
|
|
270
|
+
|
|
271
|
+
python3 -m tests
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT. Larkita is derived from [Lark](https://github.com/lark-parser/lark) by
|
|
276
|
+
Erez Shinan; see `LICENSE`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .exceptions import (
|
|
2
|
+
ConflictWarning,
|
|
3
|
+
GrammarError,
|
|
4
|
+
LarkitaError,
|
|
5
|
+
LexError,
|
|
6
|
+
ParseError,
|
|
7
|
+
UnexpectedCharacters,
|
|
8
|
+
UnexpectedInput,
|
|
9
|
+
UnexpectedToken,
|
|
10
|
+
)
|
|
11
|
+
from .larkita import Larkita
|
|
12
|
+
from .lexer import Token
|
|
13
|
+
from .utils import logger
|
|
14
|
+
|
|
15
|
+
__version__: str = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
"ConflictWarning",
|
|
19
|
+
"GrammarError",
|
|
20
|
+
"LarkitaError",
|
|
21
|
+
"LexError",
|
|
22
|
+
"ParseError",
|
|
23
|
+
"UnexpectedCharacters",
|
|
24
|
+
"UnexpectedInput",
|
|
25
|
+
"UnexpectedToken",
|
|
26
|
+
"Larkita",
|
|
27
|
+
"Token",
|
|
28
|
+
"logger",
|
|
29
|
+
)
|