All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 # Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
2 # Licensed to PSF under a Contributor Agreement.
4 """Parser engine for the grammar tables generated by pgen.
6 The grammar table must be loaded first.
8 See Parser/parser.c in the Python distribution for additional info on
9 how this parsing engine works.
12 from contextlib import contextmanager
27 from blib2to3.pgen2.grammar import Grammar
28 from blib2to3.pytree import NL, Context, Leaf, Node, RawNode, convert
31 from . import grammar, token, tokenize
34 from blib2to3.pgen2.driver import TokenProxy
37 Results = Dict[str, NL]
38 Convert = Callable[[Grammar, RawNode], Union[Node, Leaf]]
39 DFA = List[List[Tuple[int, int]]]
40 DFAS = Tuple[DFA, Dict[int, int]]
43 def lam_sub(grammar: Grammar, node: RawNode) -> NL:
44 assert node[3] is not None
45 return Node(type=node[0], children=node[3], context=node[2])
48 # A placeholder node, used when parser is backtracking.
49 DUMMY_NODE = (-1, None, None, None)
53 stack: List[Tuple[DFAS, int, RawNode]]
54 ) -> List[Tuple[DFAS, int, RawNode]]:
55 """Nodeless stack copy."""
56 return [(dfa, label, DUMMY_NODE) for dfa, label, _ in stack]
60 def __init__(self, parser: "Parser", ilabels: List[int], context: Context) -> None:
62 self._ilabels = ilabels
63 self.context = context # not really matter
65 self._dead_ilabels: Set[int] = set()
66 self._start_point = self.parser.stack
67 self._points = {ilabel: stack_copy(self._start_point) for ilabel in ilabels}
70 def ilabels(self) -> Set[int]:
71 return self._dead_ilabels.symmetric_difference(self._ilabels)
74 def switch_to(self, ilabel: int) -> Iterator[None]:
75 with self.backtrack():
76 self.parser.stack = self._points[ilabel]
80 self._dead_ilabels.add(ilabel)
82 self.parser.stack = self._start_point
85 def backtrack(self) -> Iterator[None]:
87 Use the node-level invariant ones for basic parsing operations (push/pop/shift).
88 These still will operate on the stack; but they won't create any new nodes, or
89 modify the contents of any other existing nodes.
91 This saves us a ton of time when we are backtracking, since we
92 want to restore to the initial state as quick as possible, which
93 can only be done by having as little mutatations as possible.
95 is_backtracking = self.parser.is_backtracking
97 self.parser.is_backtracking = True
100 self.parser.is_backtracking = is_backtracking
102 def add_token(self, tok_type: int, tok_val: str, raw: bool = False) -> None:
103 func: Callable[..., Any]
105 func = self.parser._addtoken
107 func = self.parser.addtoken
109 for ilabel in self.ilabels:
110 with self.switch_to(ilabel):
111 args = [tok_type, tok_val, self.context]
113 args.insert(0, ilabel)
117 self, value: Optional[str] = None, force: bool = False
119 alive_ilabels = self.ilabels
120 if len(alive_ilabels) == 0:
121 *_, most_successful_ilabel = self._dead_ilabels
122 raise ParseError("bad input", most_successful_ilabel, value, self.context)
124 ilabel, *rest = alive_ilabels
125 if force or not rest:
131 class ParseError(Exception):
132 """Exception to signal the parser is stuck."""
135 self, msg: str, type: Optional[int], value: Optional[str], context: Context
138 self, f"{msg}: type={type!r}, value={value!r}, context={context!r}"
143 self.context = context
149 The proper usage sequence is:
151 p = Parser(grammar, [converter]) # create instance
152 p.setup([start]) # prepare for parsing
153 <for each input token>:
154 if p.addtoken(...): # parse a token; may raise ParseError
156 root = p.rootnode # root of abstract syntax tree
158 A Parser instance may be reused by calling setup() repeatedly.
160 A Parser instance contains state pertaining to the current token
161 sequence, and should not be used concurrently by different threads
162 to parse separate token sequences.
164 See driver.py for how to get input tokens by tokenizing a file or
167 Parsing is complete when addtoken() returns True; the root of the
168 abstract syntax tree can then be retrieved from the rootnode
169 instance variable. When a syntax error occurs, addtoken() raises
170 the ParseError exception. There is no error recovery; the parser
171 cannot be used after a syntax error was reported (but it can be
172 reinitialized by calling setup()).
176 def __init__(self, grammar: Grammar, convert: Optional[Convert] = None) -> None:
179 The grammar argument is a grammar.Grammar instance; see the
180 grammar module for more information.
182 The parser is not ready yet for parsing; you must call the
183 setup() method to get it started.
185 The optional convert argument is a function mapping concrete
186 syntax tree nodes to abstract syntax tree nodes. If not
187 given, no conversion is done and the syntax tree produced is
188 the concrete syntax tree. If given, it must be a function of
189 two arguments, the first being the grammar (a grammar.Grammar
190 instance), and the second being the concrete syntax tree node
191 to be converted. The syntax tree is converted from the bottom
194 **post-note: the convert argument is ignored since for Black's
195 usage, convert will always be blib2to3.pytree.convert. Allowing
196 this to be dynamic hurts mypyc's ability to use early binding.
197 These docs are left for historical and informational value.
199 A concrete syntax tree node is a (type, value, context, nodes)
200 tuple, where type is the node type (a token or symbol number),
201 value is None for symbols and a string for tokens, context is
202 None or an opaque value used for error reporting (typically a
203 (lineno, offset) pair), and nodes is a list of children for
204 symbols, and None for tokens.
206 An abstract syntax tree node may be anything; this is entirely
207 up to the converter function.
210 self.grammar = grammar
211 # See note in docstring above. TL;DR this is ignored.
212 self.convert = convert or lam_sub
213 self.is_backtracking = False
215 def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None:
216 """Prepare for parsing.
218 This *must* be called before starting to parse.
220 The optional argument is an alternative start symbol; it
221 defaults to the grammar's start symbol.
223 You can use a Parser instance to parse any number of programs;
224 each time you call setup() the parser is reset to an initial
225 state determined by the (implicit or explicit) start symbol.
229 start = self.grammar.start
230 # Each stack entry is a tuple: (dfa, state, node).
231 # A node is a tuple: (type, value, context, children),
232 # where children is a list of nodes or None, and context may be None.
233 newnode: RawNode = (start, None, None, [])
234 stackentry = (self.grammar.dfas[start], 0, newnode)
235 self.stack: List[Tuple[DFAS, int, RawNode]] = [stackentry]
236 self.rootnode: Optional[NL] = None
237 self.used_names: Set[str] = set()
240 def addtoken(self, type: int, value: str, context: Context) -> bool:
241 """Add a token; return True iff this is the end of the program."""
242 # Map from token to label
243 ilabels = self.classify(type, value, context)
244 assert len(ilabels) >= 1
246 # If we have only one state to advance, we'll directly
248 if len(ilabels) == 1:
250 return self._addtoken(ilabel, type, value, context)
252 # If there are multiple states which we can advance (only
253 # happen under soft-keywords), then we will try all of them
254 # in parallel and as soon as one state can reach further than
255 # the rest, we'll choose that one. This is a pretty hacky
256 # and hopefully temporary algorithm.
258 # For a more detailed explanation, check out this post:
259 # https://tree.science/what-the-backtracking.html
261 with self.proxy.release() as proxy:
262 counter, force = 0, False
263 recorder = Recorder(self, ilabels, context)
264 recorder.add_token(type, value, raw=True)
266 next_token_value = value
267 while recorder.determine_route(next_token_value) is None:
268 if not proxy.can_advance(counter):
272 next_token_type, next_token_value, *_ = proxy.eat(counter)
273 if next_token_type in (tokenize.COMMENT, tokenize.NL):
277 if next_token_type == tokenize.OP:
278 next_token_type = grammar.opmap[next_token_value]
280 recorder.add_token(next_token_type, next_token_value)
283 ilabel = cast(int, recorder.determine_route(next_token_value, force=force))
284 assert ilabel is not None
286 return self._addtoken(ilabel, type, value, context)
288 def _addtoken(self, ilabel: int, type: int, value: str, context: Context) -> bool:
289 # Loop until the token is shifted; may raise exceptions
291 dfa, state, node = self.stack[-1]
294 # Look for a state with this label
295 for i, newstate in arcs:
296 t = self.grammar.labels[i][0]
298 # See if it's a symbol and if we're in its first set
299 itsdfa = self.grammar.dfas[t]
300 itsstates, itsfirst = itsdfa
301 if ilabel in itsfirst:
303 self.push(t, itsdfa, newstate, context)
304 break # To continue the outer while loop
307 # Look it up in the list of labels
308 # Shift a token; we're done with it
309 self.shift(type, value, newstate, context)
310 # Pop while we are in an accept-only state
312 while states[state] == [(0, state)]:
317 dfa, state, node = self.stack[-1]
319 # Done with this token
323 if (0, state) in arcs:
324 # An accepting state, pop it and try something else
327 # Done parsing, but another token is input
328 raise ParseError("too much input", type, value, context)
330 # No success finding a transition
331 raise ParseError("bad input", type, value, context)
333 def classify(self, type: int, value: str, context: Context) -> List[int]:
334 """Turn a token into a label. (Internal)
336 Depending on whether the value is a soft-keyword or not,
337 this function may return multiple labels to choose from."""
338 if type == token.NAME:
339 # Keep a listing of all used names
340 self.used_names.add(value)
341 # Check for reserved words
342 if value in self.grammar.keywords:
343 return [self.grammar.keywords[value]]
344 elif value in self.grammar.soft_keywords:
345 assert type in self.grammar.tokens
347 self.grammar.soft_keywords[value],
348 self.grammar.tokens[type],
351 ilabel = self.grammar.tokens.get(type)
353 raise ParseError("bad token", type, value, context)
356 def shift(self, type: int, value: str, newstate: int, context: Context) -> None:
357 """Shift a token. (Internal)"""
358 if self.is_backtracking:
359 dfa, state, _ = self.stack[-1]
360 self.stack[-1] = (dfa, newstate, DUMMY_NODE)
362 dfa, state, node = self.stack[-1]
363 rawnode: RawNode = (type, value, context, None)
364 newnode = convert(self.grammar, rawnode)
365 assert node[-1] is not None
366 node[-1].append(newnode)
367 self.stack[-1] = (dfa, newstate, node)
369 def push(self, type: int, newdfa: DFAS, newstate: int, context: Context) -> None:
370 """Push a nonterminal. (Internal)"""
371 if self.is_backtracking:
372 dfa, state, _ = self.stack[-1]
373 self.stack[-1] = (dfa, newstate, DUMMY_NODE)
374 self.stack.append((newdfa, 0, DUMMY_NODE))
376 dfa, state, node = self.stack[-1]
377 newnode: RawNode = (type, None, context, [])
378 self.stack[-1] = (dfa, newstate, node)
379 self.stack.append((newdfa, 0, newnode))
381 def pop(self) -> None:
382 """Pop a nonterminal. (Internal)"""
383 if self.is_backtracking:
386 popdfa, popstate, popnode = self.stack.pop()
387 newnode = convert(self.grammar, popnode)
389 dfa, state, node = self.stack[-1]
390 assert node[-1] is not None
391 node[-1].append(newnode)
393 self.rootnode = newnode
394 self.rootnode.used_names = self.used_names