##// END OF EJS Templates
parser: reorder alias expansion routine to return early...
Yuya Nishihara -
r28896:4c76a032 default
parent child Browse files
Show More
@@ -1,544 +1,542 b''
1 1 # parser.py - simple top-down operator precedence parser for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # see http://effbot.org/zone/simple-top-down-parsing.htm and
9 9 # http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing/
10 10 # for background
11 11
12 12 # takes a tokenizer and elements
13 13 # tokenizer is an iterator that returns (type, value, pos) tuples
14 14 # elements is a mapping of types to binding strength, primary, prefix, infix
15 15 # and suffix actions
16 16 # an action is a tree node name, a tree label, and an optional match
17 17 # __call__(program) parses program into a labeled tree
18 18
19 19 from __future__ import absolute_import
20 20
21 21 from .i18n import _
22 22 from . import error
23 23
24 24 class parser(object):
25 25 def __init__(self, elements, methods=None):
26 26 self._elements = elements
27 27 self._methods = methods
28 28 self.current = None
29 29 def _advance(self):
30 30 'advance the tokenizer'
31 31 t = self.current
32 32 self.current = next(self._iter, None)
33 33 return t
34 34 def _hasnewterm(self):
35 35 'True if next token may start new term'
36 36 return any(self._elements[self.current[0]][1:3])
37 37 def _match(self, m):
38 38 'make sure the tokenizer matches an end condition'
39 39 if self.current[0] != m:
40 40 raise error.ParseError(_("unexpected token: %s") % self.current[0],
41 41 self.current[2])
42 42 self._advance()
43 43 def _parseoperand(self, bind, m=None):
44 44 'gather right-hand-side operand until an end condition or binding met'
45 45 if m and self.current[0] == m:
46 46 expr = None
47 47 else:
48 48 expr = self._parse(bind)
49 49 if m:
50 50 self._match(m)
51 51 return expr
52 52 def _parse(self, bind=0):
53 53 token, value, pos = self._advance()
54 54 # handle prefix rules on current token, take as primary if unambiguous
55 55 primary, prefix = self._elements[token][1:3]
56 56 if primary and not (prefix and self._hasnewterm()):
57 57 expr = (primary, value)
58 58 elif prefix:
59 59 expr = (prefix[0], self._parseoperand(*prefix[1:]))
60 60 else:
61 61 raise error.ParseError(_("not a prefix: %s") % token, pos)
62 62 # gather tokens until we meet a lower binding strength
63 63 while bind < self._elements[self.current[0]][0]:
64 64 token, value, pos = self._advance()
65 65 # handle infix rules, take as suffix if unambiguous
66 66 infix, suffix = self._elements[token][3:]
67 67 if suffix and not (infix and self._hasnewterm()):
68 68 expr = (suffix[0], expr)
69 69 elif infix:
70 70 expr = (infix[0], expr, self._parseoperand(*infix[1:]))
71 71 else:
72 72 raise error.ParseError(_("not an infix: %s") % token, pos)
73 73 return expr
74 74 def parse(self, tokeniter):
75 75 'generate a parse tree from tokens'
76 76 self._iter = tokeniter
77 77 self._advance()
78 78 res = self._parse()
79 79 token, value, pos = self.current
80 80 return res, pos
81 81 def eval(self, tree):
82 82 'recursively evaluate a parse tree using node methods'
83 83 if not isinstance(tree, tuple):
84 84 return tree
85 85 return self._methods[tree[0]](*[self.eval(t) for t in tree[1:]])
86 86 def __call__(self, tokeniter):
87 87 'parse tokens into a parse tree and evaluate if methods given'
88 88 t = self.parse(tokeniter)
89 89 if self._methods:
90 90 return self.eval(t)
91 91 return t
92 92
93 93 def buildargsdict(trees, funcname, keys, keyvaluenode, keynode):
94 94 """Build dict from list containing positional and keyword arguments
95 95
96 96 Invalid keywords or too many positional arguments are rejected, but
97 97 missing arguments are just omitted.
98 98 """
99 99 if len(trees) > len(keys):
100 100 raise error.ParseError(_("%(func)s takes at most %(nargs)d arguments")
101 101 % {'func': funcname, 'nargs': len(keys)})
102 102 args = {}
103 103 # consume positional arguments
104 104 for k, x in zip(keys, trees):
105 105 if x[0] == keyvaluenode:
106 106 break
107 107 args[k] = x
108 108 # remainder should be keyword arguments
109 109 for x in trees[len(args):]:
110 110 if x[0] != keyvaluenode or x[1][0] != keynode:
111 111 raise error.ParseError(_("%(func)s got an invalid argument")
112 112 % {'func': funcname})
113 113 k = x[1][1]
114 114 if k not in keys:
115 115 raise error.ParseError(_("%(func)s got an unexpected keyword "
116 116 "argument '%(key)s'")
117 117 % {'func': funcname, 'key': k})
118 118 if k in args:
119 119 raise error.ParseError(_("%(func)s got multiple values for keyword "
120 120 "argument '%(key)s'")
121 121 % {'func': funcname, 'key': k})
122 122 args[k] = x[2]
123 123 return args
124 124
125 125 def unescapestr(s):
126 126 try:
127 127 return s.decode("string_escape")
128 128 except ValueError as e:
129 129 # mangle Python's exception into our format
130 130 raise error.ParseError(str(e).lower())
131 131
132 132 def _prettyformat(tree, leafnodes, level, lines):
133 133 if not isinstance(tree, tuple) or tree[0] in leafnodes:
134 134 lines.append((level, str(tree)))
135 135 else:
136 136 lines.append((level, '(%s' % tree[0]))
137 137 for s in tree[1:]:
138 138 _prettyformat(s, leafnodes, level + 1, lines)
139 139 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
140 140
141 141 def prettyformat(tree, leafnodes):
142 142 lines = []
143 143 _prettyformat(tree, leafnodes, 0, lines)
144 144 output = '\n'.join((' ' * l + s) for l, s in lines)
145 145 return output
146 146
147 147 def simplifyinfixops(tree, targetnodes):
148 148 """Flatten chained infix operations to reduce usage of Python stack
149 149
150 150 >>> def f(tree):
151 151 ... print prettyformat(simplifyinfixops(tree, ('or',)), ('symbol',))
152 152 >>> f(('or',
153 153 ... ('or',
154 154 ... ('symbol', '1'),
155 155 ... ('symbol', '2')),
156 156 ... ('symbol', '3')))
157 157 (or
158 158 ('symbol', '1')
159 159 ('symbol', '2')
160 160 ('symbol', '3'))
161 161 >>> f(('func',
162 162 ... ('symbol', 'p1'),
163 163 ... ('or',
164 164 ... ('or',
165 165 ... ('func',
166 166 ... ('symbol', 'sort'),
167 167 ... ('list',
168 168 ... ('or',
169 169 ... ('or',
170 170 ... ('symbol', '1'),
171 171 ... ('symbol', '2')),
172 172 ... ('symbol', '3')),
173 173 ... ('negate',
174 174 ... ('symbol', 'rev')))),
175 175 ... ('and',
176 176 ... ('symbol', '4'),
177 177 ... ('group',
178 178 ... ('or',
179 179 ... ('or',
180 180 ... ('symbol', '5'),
181 181 ... ('symbol', '6')),
182 182 ... ('symbol', '7'))))),
183 183 ... ('symbol', '8'))))
184 184 (func
185 185 ('symbol', 'p1')
186 186 (or
187 187 (func
188 188 ('symbol', 'sort')
189 189 (list
190 190 (or
191 191 ('symbol', '1')
192 192 ('symbol', '2')
193 193 ('symbol', '3'))
194 194 (negate
195 195 ('symbol', 'rev'))))
196 196 (and
197 197 ('symbol', '4')
198 198 (group
199 199 (or
200 200 ('symbol', '5')
201 201 ('symbol', '6')
202 202 ('symbol', '7'))))
203 203 ('symbol', '8')))
204 204 """
205 205 if not isinstance(tree, tuple):
206 206 return tree
207 207 op = tree[0]
208 208 if op not in targetnodes:
209 209 return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:])
210 210
211 211 # walk down left nodes taking each right node. no recursion to left nodes
212 212 # because infix operators are left-associative, i.e. left tree is deep.
213 213 # e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3)
214 214 simplified = []
215 215 x = tree
216 216 while x[0] == op:
217 217 l, r = x[1:]
218 218 simplified.append(simplifyinfixops(r, targetnodes))
219 219 x = l
220 220 simplified.append(simplifyinfixops(x, targetnodes))
221 221 simplified.append(op)
222 222 return tuple(reversed(simplified))
223 223
224 224 def parseerrordetail(inst):
225 225 """Compose error message from specified ParseError object
226 226 """
227 227 if len(inst.args) > 1:
228 228 return _('at %s: %s') % (inst.args[1], inst.args[0])
229 229 else:
230 230 return inst.args[0]
231 231
232 232 class alias(object):
233 233 """Parsed result of alias"""
234 234
235 235 def __init__(self, name, tree, args, err, replacement):
236 236 self.name = name
237 237 self.tree = tree
238 238 self.args = args
239 239 self.error = err
240 240 self.replacement = replacement
241 241 # whether own `error` information is already shown or not.
242 242 # this avoids showing same warning multiple times at each `findaliases`.
243 243 self.warned = False
244 244
245 245 class basealiasrules(object):
246 246 """Parsing and expansion rule set of aliases
247 247
248 248 This is a helper for fileset/revset/template aliases. A concrete rule set
249 249 should be made by sub-classing this and implementing class/static methods.
250 250
251 251 It supports alias expansion of symbol and funciton-call styles::
252 252
253 253 # decl = defn
254 254 h = heads(default)
255 255 b($1) = ancestors($1) - ancestors(default)
256 256 """
257 257 # typically a config section, which will be included in error messages
258 258 _section = None
259 259 # tags of symbol and function nodes
260 260 _symbolnode = 'symbol'
261 261 _funcnode = 'func'
262 262
263 263 def __new__(cls):
264 264 raise TypeError("'%s' is not instantiatable" % cls.__name__)
265 265
266 266 @staticmethod
267 267 def _parse(spec):
268 268 """Parse an alias name, arguments and definition"""
269 269 raise NotImplementedError
270 270
271 271 @staticmethod
272 272 def _getlist(tree):
273 273 """Extract a list of arguments from parsed tree"""
274 274 raise NotImplementedError
275 275
276 276 @classmethod
277 277 def _builddecl(cls, decl):
278 278 """Parse an alias declaration into ``(name, tree, args, errorstr)``
279 279
280 280 This function analyzes the parsed tree. The parsing rule is provided
281 281 by ``_parse()``.
282 282
283 283 - ``name``: of declared alias (may be ``decl`` itself at error)
284 284 - ``tree``: parse result (or ``None`` at error)
285 285 - ``args``: list of argument names (or None for symbol declaration)
286 286 - ``errorstr``: detail about detected error (or None)
287 287
288 288 >>> sym = lambda x: ('symbol', x)
289 289 >>> symlist = lambda *xs: ('list',) + tuple(sym(x) for x in xs)
290 290 >>> func = lambda n, a: ('func', sym(n), a)
291 291 >>> parsemap = {
292 292 ... 'foo': sym('foo'),
293 293 ... '$foo': sym('$foo'),
294 294 ... 'foo::bar': ('dagrange', sym('foo'), sym('bar')),
295 295 ... 'foo()': func('foo', None),
296 296 ... '$foo()': func('$foo', None),
297 297 ... 'foo($1, $2)': func('foo', symlist('$1', '$2')),
298 298 ... 'foo(bar_bar, baz.baz)':
299 299 ... func('foo', symlist('bar_bar', 'baz.baz')),
300 300 ... 'foo(bar($1, $2))':
301 301 ... func('foo', func('bar', symlist('$1', '$2'))),
302 302 ... 'foo($1, $2, nested($1, $2))':
303 303 ... func('foo', (symlist('$1', '$2') +
304 304 ... (func('nested', symlist('$1', '$2')),))),
305 305 ... 'foo("bar")': func('foo', ('string', 'bar')),
306 306 ... 'foo($1, $2': error.ParseError('unexpected token: end', 10),
307 307 ... 'foo("bar': error.ParseError('unterminated string', 5),
308 308 ... 'foo($1, $2, $1)': func('foo', symlist('$1', '$2', '$1')),
309 309 ... }
310 310 >>> def parse(expr):
311 311 ... x = parsemap[expr]
312 312 ... if isinstance(x, Exception):
313 313 ... raise x
314 314 ... return x
315 315 >>> def getlist(tree):
316 316 ... if not tree:
317 317 ... return []
318 318 ... if tree[0] == 'list':
319 319 ... return list(tree[1:])
320 320 ... return [tree]
321 321 >>> class aliasrules(basealiasrules):
322 322 ... _parse = staticmethod(parse)
323 323 ... _getlist = staticmethod(getlist)
324 324 >>> builddecl = aliasrules._builddecl
325 325 >>> builddecl('foo')
326 326 ('foo', ('symbol', 'foo'), None, None)
327 327 >>> builddecl('$foo')
328 328 ('$foo', None, None, "'$' not for alias arguments")
329 329 >>> builddecl('foo::bar')
330 330 ('foo::bar', None, None, 'invalid format')
331 331 >>> builddecl('foo()')
332 332 ('foo', ('func', ('symbol', 'foo')), [], None)
333 333 >>> builddecl('$foo()')
334 334 ('$foo()', None, None, "'$' not for alias arguments")
335 335 >>> builddecl('foo($1, $2)')
336 336 ('foo', ('func', ('symbol', 'foo')), ['$1', '$2'], None)
337 337 >>> builddecl('foo(bar_bar, baz.baz)')
338 338 ('foo', ('func', ('symbol', 'foo')), ['bar_bar', 'baz.baz'], None)
339 339 >>> builddecl('foo($1, $2, nested($1, $2))')
340 340 ('foo($1, $2, nested($1, $2))', None, None, 'invalid argument list')
341 341 >>> builddecl('foo(bar($1, $2))')
342 342 ('foo(bar($1, $2))', None, None, 'invalid argument list')
343 343 >>> builddecl('foo("bar")')
344 344 ('foo("bar")', None, None, 'invalid argument list')
345 345 >>> builddecl('foo($1, $2')
346 346 ('foo($1, $2', None, None, 'at 10: unexpected token: end')
347 347 >>> builddecl('foo("bar')
348 348 ('foo("bar', None, None, 'at 5: unterminated string')
349 349 >>> builddecl('foo($1, $2, $1)')
350 350 ('foo', None, None, 'argument names collide with each other')
351 351 """
352 352 try:
353 353 tree = cls._parse(decl)
354 354 except error.ParseError as inst:
355 355 return (decl, None, None, parseerrordetail(inst))
356 356
357 357 if tree[0] == cls._symbolnode:
358 358 # "name = ...." style
359 359 name = tree[1]
360 360 if name.startswith('$'):
361 361 return (decl, None, None, _("'$' not for alias arguments"))
362 362 return (name, tree, None, None)
363 363
364 364 if tree[0] == cls._funcnode and tree[1][0] == cls._symbolnode:
365 365 # "name(arg, ....) = ...." style
366 366 name = tree[1][1]
367 367 if name.startswith('$'):
368 368 return (decl, None, None, _("'$' not for alias arguments"))
369 369 args = []
370 370 for arg in cls._getlist(tree[2]):
371 371 if arg[0] != cls._symbolnode:
372 372 return (decl, None, None, _("invalid argument list"))
373 373 args.append(arg[1])
374 374 if len(args) != len(set(args)):
375 375 return (name, None, None,
376 376 _("argument names collide with each other"))
377 377 return (name, tree[:2], args, None)
378 378
379 379 return (decl, None, None, _("invalid format"))
380 380
381 381 @classmethod
382 382 def _relabelargs(cls, tree, args):
383 383 """Mark alias arguments as ``_aliasarg``"""
384 384 if not isinstance(tree, tuple):
385 385 return tree
386 386 op = tree[0]
387 387 if op != cls._symbolnode:
388 388 return (op,) + tuple(cls._relabelargs(x, args) for x in tree[1:])
389 389
390 390 assert len(tree) == 2
391 391 sym = tree[1]
392 392 if sym in args:
393 393 op = '_aliasarg'
394 394 elif sym.startswith('$'):
395 395 raise error.ParseError(_("'$' not for alias arguments"))
396 396 return (op, sym)
397 397
398 398 @classmethod
399 399 def _builddefn(cls, defn, args):
400 400 """Parse an alias definition into a tree and marks substitutions
401 401
402 402 This function marks alias argument references as ``_aliasarg``. The
403 403 parsing rule is provided by ``_parse()``.
404 404
405 405 ``args`` is a list of alias argument names, or None if the alias
406 406 is declared as a symbol.
407 407
408 408 >>> parsemap = {
409 409 ... '$1 or foo': ('or', ('symbol', '$1'), ('symbol', 'foo')),
410 410 ... '$1 or $bar': ('or', ('symbol', '$1'), ('symbol', '$bar')),
411 411 ... '$10 or baz': ('or', ('symbol', '$10'), ('symbol', 'baz')),
412 412 ... '"$1" or "foo"': ('or', ('string', '$1'), ('string', 'foo')),
413 413 ... }
414 414 >>> class aliasrules(basealiasrules):
415 415 ... _parse = staticmethod(parsemap.__getitem__)
416 416 ... _getlist = staticmethod(lambda x: [])
417 417 >>> builddefn = aliasrules._builddefn
418 418 >>> def pprint(tree):
419 419 ... print prettyformat(tree, ('_aliasarg', 'string', 'symbol'))
420 420 >>> args = ['$1', '$2', 'foo']
421 421 >>> pprint(builddefn('$1 or foo', args))
422 422 (or
423 423 ('_aliasarg', '$1')
424 424 ('_aliasarg', 'foo'))
425 425 >>> try:
426 426 ... builddefn('$1 or $bar', args)
427 427 ... except error.ParseError as inst:
428 428 ... print parseerrordetail(inst)
429 429 '$' not for alias arguments
430 430 >>> args = ['$1', '$10', 'foo']
431 431 >>> pprint(builddefn('$10 or baz', args))
432 432 (or
433 433 ('_aliasarg', '$10')
434 434 ('symbol', 'baz'))
435 435 >>> pprint(builddefn('"$1" or "foo"', args))
436 436 (or
437 437 ('string', '$1')
438 438 ('string', 'foo'))
439 439 """
440 440 tree = cls._parse(defn)
441 441 if args:
442 442 args = set(args)
443 443 else:
444 444 args = set()
445 445 return cls._relabelargs(tree, args)
446 446
447 447 @classmethod
448 448 def build(cls, decl, defn):
449 449 """Parse an alias declaration and definition into an alias object"""
450 450 repl = efmt = None
451 451 name, tree, args, err = cls._builddecl(decl)
452 452 if err:
453 453 efmt = _('failed to parse the declaration of %(section)s '
454 454 '"%(name)s": %(error)s')
455 455 else:
456 456 try:
457 457 repl = cls._builddefn(defn, args)
458 458 except error.ParseError as inst:
459 459 err = parseerrordetail(inst)
460 460 efmt = _('failed to parse the definition of %(section)s '
461 461 '"%(name)s": %(error)s')
462 462 if err:
463 463 err = efmt % {'section': cls._section, 'name': name, 'error': err}
464 464 return alias(name, tree, args, err, repl)
465 465
466 466 @classmethod
467 467 def buildmap(cls, items):
468 468 """Parse a list of alias (name, replacement) pairs into a dict of
469 469 alias objects"""
470 470 aliases = {}
471 471 for decl, defn in items:
472 472 a = cls.build(decl, defn)
473 473 aliases[a.name] = a
474 474 return aliases
475 475
476 476 @classmethod
477 477 def _getalias(cls, aliases, tree):
478 478 """If tree looks like an unexpanded alias, return it. Return None
479 479 otherwise.
480 480 """
481 481 if not isinstance(tree, tuple):
482 482 return None
483 483 if tree[0] == cls._symbolnode:
484 484 name = tree[1]
485 485 a = aliases.get(name)
486 486 if a and a.args is None and a.tree == tree:
487 487 return a
488 488 if tree[0] == cls._funcnode and tree[1][0] == cls._symbolnode:
489 489 name = tree[1][1]
490 490 a = aliases.get(name)
491 491 if a and a.args is not None and a.tree == tree[:2]:
492 492 return a
493 493 return None
494 494
495 495 @classmethod
496 496 def _expandargs(cls, tree, args):
497 497 """Replace _aliasarg instances with the substitution value of the
498 498 same name in args, recursively.
499 499 """
500 500 if not isinstance(tree, tuple):
501 501 return tree
502 502 if tree[0] == '_aliasarg':
503 503 sym = tree[1]
504 504 return args[sym]
505 505 return tuple(cls._expandargs(t, args) for t in tree)
506 506
507 507 @classmethod
508 508 def _expand(cls, aliases, tree, expanding, cache):
509 509 if not isinstance(tree, tuple):
510 510 return tree
511 511 a = cls._getalias(aliases, tree)
512 if a is not None:
513 if a.error:
514 raise error.Abort(a.error)
515 if a in expanding:
516 raise error.ParseError(_('infinite expansion of %(section)s '
517 '"%(name)s" detected')
518 % {'section': cls._section,
519 'name': a.name})
520 expanding.append(a)
521 if a.name not in cache:
522 cache[a.name] = cls._expand(aliases, a.replacement, expanding,
523 cache)
524 result = cache[a.name]
525 expanding.pop()
526 if a.args is not None:
527 l = cls._getlist(tree[2])
528 if len(l) != len(a.args):
529 raise error.ParseError(_('invalid number of arguments: %d')
530 % len(l))
531 l = [cls._expand(aliases, t, [], cache) for t in l]
532 result = cls._expandargs(result, dict(zip(a.args, l)))
533 else:
534 result = tuple(cls._expand(aliases, t, expanding, cache)
535 for t in tree)
536 return result
512 if a is None:
513 return tuple(cls._expand(aliases, t, expanding, cache)
514 for t in tree)
515 if a.error:
516 raise error.Abort(a.error)
517 if a in expanding:
518 raise error.ParseError(_('infinite expansion of %(section)s '
519 '"%(name)s" detected')
520 % {'section': cls._section, 'name': a.name})
521 expanding.append(a)
522 if a.name not in cache:
523 cache[a.name] = cls._expand(aliases, a.replacement, expanding,
524 cache)
525 result = cache[a.name]
526 expanding.pop()
527 if a.args is None:
528 return result
529 l = cls._getlist(tree[2])
530 if len(l) != len(a.args):
531 raise error.ParseError(_('invalid number of arguments: %d')
532 % len(l))
533 l = [cls._expand(aliases, t, [], cache) for t in l]
534 return cls._expandargs(result, dict(zip(a.args, l)))
537 535
538 536 @classmethod
539 537 def expand(cls, aliases, tree):
540 538 """Expand aliases in tree, recursively.
541 539
542 540 'aliases' is a dictionary mapping user defined aliases to alias objects.
543 541 """
544 542 return cls._expand(aliases, tree, [], {})
General Comments 0
You need to be logged in to leave comments. Login now