comber 1.0.0__py3-none-any.whl
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.
- comber/__init__.py +8 -0
- comber/combinator.py +352 -0
- comber/extras.py +121 -0
- comber/parser.py +335 -0
- comber/py.typed +0 -0
- comber-1.0.0.dist-info/LICENSE.txt +504 -0
- comber-1.0.0.dist-info/METADATA +910 -0
- comber-1.0.0.dist-info/RECORD +10 -0
- comber-1.0.0.dist-info/WHEEL +5 -0
- comber-1.0.0.dist-info/top_level.txt +1 -0
comber/__init__.py
ADDED
comber/combinator.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Combinator definitions.
|
|
3
|
+
"""
|
|
4
|
+
from typing import cast, Optional, Tuple, List, Union, Any
|
|
5
|
+
import weakref
|
|
6
|
+
from math import inf
|
|
7
|
+
from abc import ABC
|
|
8
|
+
from .parser import Parser, State, Expect, Emitter, ParseError
|
|
9
|
+
|
|
10
|
+
Parseable = Union['Combinator', str]
|
|
11
|
+
|
|
12
|
+
class Combinator(Parser, ABC):
|
|
13
|
+
"""
|
|
14
|
+
Combinator definitions.
|
|
15
|
+
"""
|
|
16
|
+
def __matmul__(self, arg:Union[str, Emitter, Tuple[str, Emitter]]) -> 'Combinator':
|
|
17
|
+
if isinstance(arg, str):
|
|
18
|
+
self.name = arg
|
|
19
|
+
elif callable(arg):
|
|
20
|
+
self.emit = arg
|
|
21
|
+
elif isinstance(arg, tuple):
|
|
22
|
+
self.name = arg[0]
|
|
23
|
+
self.emit = arg[1]
|
|
24
|
+
else:
|
|
25
|
+
raise TypeError(
|
|
26
|
+
'Expected name or name-emitter tuple, e.g. '\
|
|
27
|
+
'combinator@"name", combinator@lambda x: int(x), or combinator@("name, lambda x: int(x)"')
|
|
28
|
+
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def __add__(self, right:Parseable) -> Parseable:
|
|
32
|
+
return Seq(self, right)
|
|
33
|
+
|
|
34
|
+
def __or__(self, right:Parseable) -> Parseable:
|
|
35
|
+
return Choice(self, right)
|
|
36
|
+
|
|
37
|
+
def __getitem__(self, args:Union[int,Tuple[int,int|float],Tuple[int,int|float,Parseable]]) -> 'Combinator':
|
|
38
|
+
minimum = args if isinstance(args, int) else args[0]
|
|
39
|
+
maximum = None if isinstance(args, int) else args[1]
|
|
40
|
+
separator = None if isinstance(args, int) or len(args) < 3 else cast(Tuple[int,int,Parseable], args)[2]
|
|
41
|
+
return Repeat(self, minimum, maximum, separator)
|
|
42
|
+
|
|
43
|
+
def __invert__(self) -> 'Combinator':
|
|
44
|
+
return Repeat(self, 0, 1, None)
|
|
45
|
+
|
|
46
|
+
def __pos__(self) -> 'Combinator':
|
|
47
|
+
return Repeat(self, 0, inf, None)
|
|
48
|
+
|
|
49
|
+
def __mul__(self, separator:Parseable) -> Parseable:
|
|
50
|
+
return Repeat(self, 0, inf, separator)
|
|
51
|
+
|
|
52
|
+
def simplify(self) -> 'Combinator':
|
|
53
|
+
""" Return a simplified version of the combinator """
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def analyze(self) -> None:
|
|
57
|
+
analyzed:set[Combinator] = set()
|
|
58
|
+
parsers:list[Combinator] = [self]
|
|
59
|
+
|
|
60
|
+
while parsers:
|
|
61
|
+
parser = parsers.pop()
|
|
62
|
+
|
|
63
|
+
if hasattr(parser, 'subparsers'):
|
|
64
|
+
subparsers = tuple(sub.simplify() for sub in getattr(parser, 'subparsers'))
|
|
65
|
+
setattr(parser, 'subparsers', subparsers)
|
|
66
|
+
parsers.extend(list(sub for sub in subparsers if sub not in analyzed))
|
|
67
|
+
analyzed.update(subparsers)
|
|
68
|
+
|
|
69
|
+
elif hasattr(parser, 'subparser'):
|
|
70
|
+
subparser = getattr(parser, 'subparser').simplify()
|
|
71
|
+
setattr(parser, 'subparser', subparser)
|
|
72
|
+
if parser not in analyzed:
|
|
73
|
+
parsers.append(subparser)
|
|
74
|
+
analyzed.add(subparser)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Lit(Combinator):
|
|
78
|
+
"""
|
|
79
|
+
A parser of an exact string
|
|
80
|
+
"""
|
|
81
|
+
instances:weakref.WeakValueDictionary[str,'Lit'] = weakref.WeakValueDictionary()
|
|
82
|
+
recurse = True # As an optimization - there's no way Lit can recurse, so don't check
|
|
83
|
+
|
|
84
|
+
def __new__(cls, string:str) -> 'Lit':
|
|
85
|
+
if string not in cls.instances:
|
|
86
|
+
instance = super().__new__(cls)
|
|
87
|
+
cls.__init__(instance, string)
|
|
88
|
+
cls.instances[string] = instance
|
|
89
|
+
else:
|
|
90
|
+
instance = cls.instances[string]
|
|
91
|
+
|
|
92
|
+
return instance
|
|
93
|
+
|
|
94
|
+
def __init__(self, string:str) -> None:
|
|
95
|
+
super().__init__()
|
|
96
|
+
self.string = string
|
|
97
|
+
self._hash = hash(string)
|
|
98
|
+
|
|
99
|
+
def expect(self, state:Expect) -> List[str]:
|
|
100
|
+
return [self.string]
|
|
101
|
+
|
|
102
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
103
|
+
if not state.text.startswith(self.string):
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
state.consume(len(self.string))
|
|
107
|
+
return state
|
|
108
|
+
|
|
109
|
+
def __hash__(self) -> int:
|
|
110
|
+
return self._hash
|
|
111
|
+
|
|
112
|
+
def repr(self) -> str:
|
|
113
|
+
return f'Lit({self.string})'
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def asCombinator(arg:Parseable) -> Combinator:
|
|
117
|
+
"""
|
|
118
|
+
Ensure a value is a Combinator
|
|
119
|
+
"""
|
|
120
|
+
if isinstance(arg, Combinator):
|
|
121
|
+
return arg
|
|
122
|
+
else:
|
|
123
|
+
return Lit(arg)
|
|
124
|
+
|
|
125
|
+
# TODO: Make a multi-parser parent of Seq and Choice
|
|
126
|
+
|
|
127
|
+
class Seq(Combinator):
|
|
128
|
+
"""
|
|
129
|
+
A sequence of parsers.
|
|
130
|
+
"""
|
|
131
|
+
compound = True
|
|
132
|
+
|
|
133
|
+
def __init__(self, left:Parseable, right:Parseable) -> None:
|
|
134
|
+
super().__init__()
|
|
135
|
+
self.subparsers:tuple[Combinator, ...]
|
|
136
|
+
|
|
137
|
+
# TODO: this doesn't flatten the rhs
|
|
138
|
+
if isinstance(left, Seq) and not left.emit:
|
|
139
|
+
subparsers = list(left.subparsers)
|
|
140
|
+
subparsers.append(asCombinator(right))
|
|
141
|
+
self.subparsers = tuple(subparsers)
|
|
142
|
+
else:
|
|
143
|
+
if left is not C:
|
|
144
|
+
self.subparsers = (asCombinator(left), asCombinator(right))
|
|
145
|
+
else:
|
|
146
|
+
self.subparsers = (asCombinator(right), )
|
|
147
|
+
|
|
148
|
+
self._hash:int = hash(self.subparsers)
|
|
149
|
+
|
|
150
|
+
def expect(self, state:Expect) -> List[str]:
|
|
151
|
+
return self.subparsers[0].expectCore(state)
|
|
152
|
+
|
|
153
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
154
|
+
first = True
|
|
155
|
+
for parser in self.subparsers:
|
|
156
|
+
try:
|
|
157
|
+
if not first:
|
|
158
|
+
state.shiftParser()
|
|
159
|
+
state = parser.parseCore(state)
|
|
160
|
+
finally:
|
|
161
|
+
if not first:
|
|
162
|
+
state.unshiftParser()
|
|
163
|
+
else:
|
|
164
|
+
first = False
|
|
165
|
+
|
|
166
|
+
return state
|
|
167
|
+
|
|
168
|
+
def __add__(self, right:Parseable) -> Parseable:
|
|
169
|
+
subparsers = list(self.subparsers)
|
|
170
|
+
subparsers.append(asCombinator(right))
|
|
171
|
+
self.subparsers = tuple(subparsers)
|
|
172
|
+
self._hash = hash(self.subparsers)
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def __hash__(self) -> int:
|
|
176
|
+
return self._hash
|
|
177
|
+
|
|
178
|
+
def repr(self) -> str:
|
|
179
|
+
return f'Seq{self.subparsers}'
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class Choice(Combinator):
|
|
183
|
+
"""
|
|
184
|
+
Parse as the first successful parse.
|
|
185
|
+
"""
|
|
186
|
+
recurse = True
|
|
187
|
+
compound = True
|
|
188
|
+
|
|
189
|
+
def __init__(self, left:Parseable, right:Parseable) -> None:
|
|
190
|
+
super().__init__()
|
|
191
|
+
self.subparsers:tuple[Combinator, ...]
|
|
192
|
+
|
|
193
|
+
if isinstance(left, Choice) and not left.emit:
|
|
194
|
+
subparsers = list(left.subparsers)
|
|
195
|
+
subparsers.append(asCombinator(right))
|
|
196
|
+
self.subparsers = tuple(subparsers)
|
|
197
|
+
else:
|
|
198
|
+
if left is not C:
|
|
199
|
+
self.subparsers = (asCombinator(left), asCombinator(right))
|
|
200
|
+
else:
|
|
201
|
+
self.subparsers = (asCombinator(right), )
|
|
202
|
+
|
|
203
|
+
self._hash:int = hash(self.subparsers)
|
|
204
|
+
|
|
205
|
+
def expect(self, state:Expect) -> List[str]:
|
|
206
|
+
return \
|
|
207
|
+
[ string
|
|
208
|
+
for subparser in self.subparsers
|
|
209
|
+
for string in subparser.expectCore(state)
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
213
|
+
bestState:State|None = None
|
|
214
|
+
|
|
215
|
+
for parser in self.subparsers:
|
|
216
|
+
if not state.inRecursion(parser):
|
|
217
|
+
try:
|
|
218
|
+
if parser.compound:
|
|
219
|
+
trialState = state.pushState()
|
|
220
|
+
else:
|
|
221
|
+
trialState = state
|
|
222
|
+
|
|
223
|
+
state = parser.parseCore(trialState)
|
|
224
|
+
|
|
225
|
+
if parser.compound:
|
|
226
|
+
state = state.popState()
|
|
227
|
+
bestState = state
|
|
228
|
+
break
|
|
229
|
+
except ParseError:
|
|
230
|
+
continue
|
|
231
|
+
return bestState
|
|
232
|
+
|
|
233
|
+
def __or__(self, right:Parseable) -> Parseable:
|
|
234
|
+
subparsers = list(self.subparsers)
|
|
235
|
+
subparsers.append(asCombinator(right))
|
|
236
|
+
self.subparsers = tuple(subparsers)
|
|
237
|
+
self._hash = hash(self.subparsers)
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
def __hash__(self) -> int:
|
|
241
|
+
return self._hash
|
|
242
|
+
|
|
243
|
+
def repr(self) -> str:
|
|
244
|
+
return f'Choice{self.subparsers}'
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class Repeat(Combinator):
|
|
248
|
+
"""
|
|
249
|
+
Repeat a combinator.
|
|
250
|
+
"""
|
|
251
|
+
compound = True
|
|
252
|
+
|
|
253
|
+
def __init__(self,
|
|
254
|
+
subparser:Combinator,
|
|
255
|
+
minimum:int,
|
|
256
|
+
maximum:Optional[int|float],
|
|
257
|
+
separator:Optional[Parseable]
|
|
258
|
+
) -> None:
|
|
259
|
+
super().__init__()
|
|
260
|
+
self.subparser = subparser
|
|
261
|
+
self.minimum = minimum
|
|
262
|
+
self.maximum = maximum
|
|
263
|
+
self.separator = None if separator is None else asCombinator(separator)
|
|
264
|
+
self._hash = hash(hash(self.subparser)+hash(self.minimum)+hash(self.maximum))
|
|
265
|
+
|
|
266
|
+
def expect(self, state:Expect) -> List[str]:
|
|
267
|
+
return self.subparser.expectCore(state)
|
|
268
|
+
|
|
269
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
270
|
+
parsed = 0
|
|
271
|
+
|
|
272
|
+
while parsed < self.minimum:
|
|
273
|
+
if parsed > 0 and self.separator:
|
|
274
|
+
state = self.separator.parseCore(state)
|
|
275
|
+
state = self.subparser.parseCore(state)
|
|
276
|
+
parsed += 1
|
|
277
|
+
|
|
278
|
+
if self.maximum is not None:
|
|
279
|
+
while parsed < self.maximum:
|
|
280
|
+
try:
|
|
281
|
+
mustPop = self.subparser.compound or self.separator and self.separator.compound
|
|
282
|
+
if mustPop:
|
|
283
|
+
trialState = state.pushState()
|
|
284
|
+
else:
|
|
285
|
+
trialState = state
|
|
286
|
+
|
|
287
|
+
if parsed > 0 and self.separator:
|
|
288
|
+
trialState = self.separator.parseCore(trialState)
|
|
289
|
+
|
|
290
|
+
state = self.subparser.parseCore(trialState)
|
|
291
|
+
|
|
292
|
+
if mustPop:
|
|
293
|
+
state = state.popState()
|
|
294
|
+
|
|
295
|
+
parsed += 1
|
|
296
|
+
except ParseError:
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
return state
|
|
300
|
+
|
|
301
|
+
def __hash__(self) -> int:
|
|
302
|
+
return self._hash
|
|
303
|
+
|
|
304
|
+
def repr(self) -> str:
|
|
305
|
+
return f'Repeat({self.subparser}, {self.minimum}, {self.maximum}, {self.separator})'
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class Id(Combinator):
|
|
309
|
+
"""
|
|
310
|
+
Parse exactly the subparser.
|
|
311
|
+
"""
|
|
312
|
+
compound = True
|
|
313
|
+
|
|
314
|
+
def __init__(self, subparser:Parseable) -> None:
|
|
315
|
+
super().__init__()
|
|
316
|
+
self.subparser = asCombinator(subparser)
|
|
317
|
+
|
|
318
|
+
def expect(self, state:Expect) -> List[str]:
|
|
319
|
+
return self.subparser.expectCore(state)
|
|
320
|
+
|
|
321
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
322
|
+
return self.subparser.parseCore(state)
|
|
323
|
+
|
|
324
|
+
def __eq__(self, right:Any) -> bool:
|
|
325
|
+
return isinstance(right, Id) and right.subparser == self.subparser
|
|
326
|
+
|
|
327
|
+
def __hash__(self) -> int:
|
|
328
|
+
return hash(self.subparser)
|
|
329
|
+
|
|
330
|
+
def repr(self) -> str:
|
|
331
|
+
return f'Id({self.subparser})'
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class CClass(Combinator):
|
|
335
|
+
"""
|
|
336
|
+
The combinator start's class. Don't instantiate.
|
|
337
|
+
"""
|
|
338
|
+
def expect(self, state:Expect) -> List[str]:
|
|
339
|
+
return []
|
|
340
|
+
|
|
341
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
342
|
+
return state
|
|
343
|
+
|
|
344
|
+
#pylint: disable=signature-differs
|
|
345
|
+
def __call__(self, arg:Parseable) -> Id:#type:ignore
|
|
346
|
+
return Id(arg)
|
|
347
|
+
|
|
348
|
+
def repr(self) -> str:
|
|
349
|
+
return 'C'
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
C = CClass()
|
comber/extras.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Additional non-core parsers.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Iterable, List, Optional
|
|
5
|
+
import re
|
|
6
|
+
from .parser import State, Expect
|
|
7
|
+
from .combinator import Combinator, asCombinator
|
|
8
|
+
|
|
9
|
+
#pylint: disable=invalid-name
|
|
10
|
+
class cs(Combinator):
|
|
11
|
+
"""
|
|
12
|
+
Parse one of a list of strings, or one of a character in a string.
|
|
13
|
+
"""
|
|
14
|
+
recurse = True # As an optimization - there's no way cs can recurse, so don't check
|
|
15
|
+
|
|
16
|
+
def __init__(self, string:Iterable) -> None:
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.string = tuple(set(string))
|
|
19
|
+
|
|
20
|
+
def expect(self, state:Expect) -> List[str]:
|
|
21
|
+
return list(self.string)
|
|
22
|
+
|
|
23
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
24
|
+
for string in self.string:
|
|
25
|
+
if state.text.startswith(string):
|
|
26
|
+
state.consume(len(string))
|
|
27
|
+
return state
|
|
28
|
+
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def __hash__(self) -> int:
|
|
32
|
+
return hash(self.string)
|
|
33
|
+
|
|
34
|
+
def repr(self) -> str:
|
|
35
|
+
return f'cs({self.string})'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
#pylint: disable=invalid-name
|
|
39
|
+
class rs(Combinator):
|
|
40
|
+
"""
|
|
41
|
+
Parse using a regular expression
|
|
42
|
+
"""
|
|
43
|
+
recurse = True # As an optimization - there's no way rs can recurse, so don't check
|
|
44
|
+
|
|
45
|
+
def __init__(self, regex:str, caseInsensitive=False) -> None:
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.raw = regex
|
|
48
|
+
self.regex = re.compile(
|
|
49
|
+
self.raw,
|
|
50
|
+
re.IGNORECASE if caseInsensitive else 0)
|
|
51
|
+
|
|
52
|
+
def expect(self, state:Expect) -> List[str]:
|
|
53
|
+
return [f'/{self.raw}/']
|
|
54
|
+
|
|
55
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
56
|
+
matched = self.regex.match(state.text)
|
|
57
|
+
if matched:
|
|
58
|
+
state.consume(len(matched[0]))
|
|
59
|
+
return state
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def __hash__(self) -> int:
|
|
64
|
+
return hash(self.raw)
|
|
65
|
+
|
|
66
|
+
def repr(self) -> str:
|
|
67
|
+
return f'rs({self.raw})'
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#pylint: disable=invalid-name
|
|
71
|
+
class defer(Combinator):
|
|
72
|
+
"""
|
|
73
|
+
A placeholder parser that can be filled in later with another parser.
|
|
74
|
+
Useful for recusive definitions.
|
|
75
|
+
"""
|
|
76
|
+
recurse = False
|
|
77
|
+
compound = True
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
super().__init__()
|
|
81
|
+
self._coreparser:Optional[Combinator] = None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def coreparser(self) -> Combinator:
|
|
85
|
+
"""
|
|
86
|
+
The parser this defer parser is the stand-in for.
|
|
87
|
+
"""
|
|
88
|
+
if self._coreparser is None:
|
|
89
|
+
message = 'Unfulfilled defer parser'
|
|
90
|
+
if self.name:
|
|
91
|
+
message += f' ({self.name})'
|
|
92
|
+
#pylint: disable=broad-exception-raised
|
|
93
|
+
raise Exception(message)
|
|
94
|
+
|
|
95
|
+
return self._coreparser
|
|
96
|
+
|
|
97
|
+
def fill(self, coreparser:Combinator) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Fill in the parser for this deferred parser.
|
|
100
|
+
"""
|
|
101
|
+
self._coreparser = asCombinator(coreparser)
|
|
102
|
+
|
|
103
|
+
def expect(self, state:Expect) -> List[str]:
|
|
104
|
+
return self.coreparser.expect(state)
|
|
105
|
+
|
|
106
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
107
|
+
raise NotImplementedError('Deferred parsers have no recogizer')
|
|
108
|
+
|
|
109
|
+
def parseCore(self, state:State) -> State:
|
|
110
|
+
return self.coreparser.parseCore(state)
|
|
111
|
+
|
|
112
|
+
def simplify(self) -> Combinator:
|
|
113
|
+
return self.coreparser
|
|
114
|
+
|
|
115
|
+
def __hash__(self) -> int:
|
|
116
|
+
# Where we care about this, we care about literal identity
|
|
117
|
+
return hash(id(self))
|
|
118
|
+
|
|
119
|
+
def repr(self) -> str:
|
|
120
|
+
return f'defer({self._coreparser})'
|
|
121
|
+
|