mirror of
https://github.com/facebook/sapling.git
synced 2024-10-09 16:31:02 +03:00
c51267a286
This class will keep syntax rules that are necessary to parse and expand aliases. The implementations will be extracted from the revset module. In order to make the porting easier, this class keeps parsedecl and parsedefn separately, which will be unified later. Also, getlist and funcnode will be refactored by future patches for better handling of the template aliases. The following public functions will be added: aliasrules.build(decl, defn) -> aliasobj parse decl and defn into an object that keeps alias name, arguments and replacement tree. aliasrules.buildmap(aliasitems) -> aliasdict helper to build() a dict of alias objects from a list of (decl, defn) aliasrules.expand(aliasdict, tree) -> tree expand aliases in tree recursively Because these functions aren't introduced by this series, there would remain a few wrapper functions in the revset module. These ugly wrappers should be eliminated by the next series. This class is considered an inheritable namespace, which will host only class/static methods. That's because it won't have no object-scope variables. I'm not a big fan of using class as a syntax sugar, but I admit it can improve code readability at some level. So let's give it a try.
267 lines
9.0 KiB
Python
267 lines
9.0 KiB
Python
# parser.py - simple top-down operator precedence parser for mercurial
|
|
#
|
|
# Copyright 2010 Matt Mackall <mpm@selenic.com>
|
|
#
|
|
# This software may be used and distributed according to the terms of the
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
# see http://effbot.org/zone/simple-top-down-parsing.htm and
|
|
# http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing/
|
|
# for background
|
|
|
|
# takes a tokenizer and elements
|
|
# tokenizer is an iterator that returns (type, value, pos) tuples
|
|
# elements is a mapping of types to binding strength, primary, prefix, infix
|
|
# and suffix actions
|
|
# an action is a tree node name, a tree label, and an optional match
|
|
# __call__(program) parses program into a labeled tree
|
|
|
|
from __future__ import absolute_import
|
|
|
|
from .i18n import _
|
|
from . import error
|
|
|
|
class parser(object):
|
|
def __init__(self, elements, methods=None):
|
|
self._elements = elements
|
|
self._methods = methods
|
|
self.current = None
|
|
def _advance(self):
|
|
'advance the tokenizer'
|
|
t = self.current
|
|
self.current = next(self._iter, None)
|
|
return t
|
|
def _hasnewterm(self):
|
|
'True if next token may start new term'
|
|
return any(self._elements[self.current[0]][1:3])
|
|
def _match(self, m):
|
|
'make sure the tokenizer matches an end condition'
|
|
if self.current[0] != m:
|
|
raise error.ParseError(_("unexpected token: %s") % self.current[0],
|
|
self.current[2])
|
|
self._advance()
|
|
def _parseoperand(self, bind, m=None):
|
|
'gather right-hand-side operand until an end condition or binding met'
|
|
if m and self.current[0] == m:
|
|
expr = None
|
|
else:
|
|
expr = self._parse(bind)
|
|
if m:
|
|
self._match(m)
|
|
return expr
|
|
def _parse(self, bind=0):
|
|
token, value, pos = self._advance()
|
|
# handle prefix rules on current token, take as primary if unambiguous
|
|
primary, prefix = self._elements[token][1:3]
|
|
if primary and not (prefix and self._hasnewterm()):
|
|
expr = (primary, value)
|
|
elif prefix:
|
|
expr = (prefix[0], self._parseoperand(*prefix[1:]))
|
|
else:
|
|
raise error.ParseError(_("not a prefix: %s") % token, pos)
|
|
# gather tokens until we meet a lower binding strength
|
|
while bind < self._elements[self.current[0]][0]:
|
|
token, value, pos = self._advance()
|
|
# handle infix rules, take as suffix if unambiguous
|
|
infix, suffix = self._elements[token][3:]
|
|
if suffix and not (infix and self._hasnewterm()):
|
|
expr = (suffix[0], expr)
|
|
elif infix:
|
|
expr = (infix[0], expr, self._parseoperand(*infix[1:]))
|
|
else:
|
|
raise error.ParseError(_("not an infix: %s") % token, pos)
|
|
return expr
|
|
def parse(self, tokeniter):
|
|
'generate a parse tree from tokens'
|
|
self._iter = tokeniter
|
|
self._advance()
|
|
res = self._parse()
|
|
token, value, pos = self.current
|
|
return res, pos
|
|
def eval(self, tree):
|
|
'recursively evaluate a parse tree using node methods'
|
|
if not isinstance(tree, tuple):
|
|
return tree
|
|
return self._methods[tree[0]](*[self.eval(t) for t in tree[1:]])
|
|
def __call__(self, tokeniter):
|
|
'parse tokens into a parse tree and evaluate if methods given'
|
|
t = self.parse(tokeniter)
|
|
if self._methods:
|
|
return self.eval(t)
|
|
return t
|
|
|
|
def buildargsdict(trees, funcname, keys, keyvaluenode, keynode):
|
|
"""Build dict from list containing positional and keyword arguments
|
|
|
|
Invalid keywords or too many positional arguments are rejected, but
|
|
missing arguments are just omitted.
|
|
"""
|
|
if len(trees) > len(keys):
|
|
raise error.ParseError(_("%(func)s takes at most %(nargs)d arguments")
|
|
% {'func': funcname, 'nargs': len(keys)})
|
|
args = {}
|
|
# consume positional arguments
|
|
for k, x in zip(keys, trees):
|
|
if x[0] == keyvaluenode:
|
|
break
|
|
args[k] = x
|
|
# remainder should be keyword arguments
|
|
for x in trees[len(args):]:
|
|
if x[0] != keyvaluenode or x[1][0] != keynode:
|
|
raise error.ParseError(_("%(func)s got an invalid argument")
|
|
% {'func': funcname})
|
|
k = x[1][1]
|
|
if k not in keys:
|
|
raise error.ParseError(_("%(func)s got an unexpected keyword "
|
|
"argument '%(key)s'")
|
|
% {'func': funcname, 'key': k})
|
|
if k in args:
|
|
raise error.ParseError(_("%(func)s got multiple values for keyword "
|
|
"argument '%(key)s'")
|
|
% {'func': funcname, 'key': k})
|
|
args[k] = x[2]
|
|
return args
|
|
|
|
def unescapestr(s):
|
|
try:
|
|
return s.decode("string_escape")
|
|
except ValueError as e:
|
|
# mangle Python's exception into our format
|
|
raise error.ParseError(str(e).lower())
|
|
|
|
def _prettyformat(tree, leafnodes, level, lines):
|
|
if not isinstance(tree, tuple) or tree[0] in leafnodes:
|
|
lines.append((level, str(tree)))
|
|
else:
|
|
lines.append((level, '(%s' % tree[0]))
|
|
for s in tree[1:]:
|
|
_prettyformat(s, leafnodes, level + 1, lines)
|
|
lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
|
|
|
|
def prettyformat(tree, leafnodes):
|
|
lines = []
|
|
_prettyformat(tree, leafnodes, 0, lines)
|
|
output = '\n'.join((' ' * l + s) for l, s in lines)
|
|
return output
|
|
|
|
def simplifyinfixops(tree, targetnodes):
|
|
"""Flatten chained infix operations to reduce usage of Python stack
|
|
|
|
>>> def f(tree):
|
|
... print prettyformat(simplifyinfixops(tree, ('or',)), ('symbol',))
|
|
>>> f(('or',
|
|
... ('or',
|
|
... ('symbol', '1'),
|
|
... ('symbol', '2')),
|
|
... ('symbol', '3')))
|
|
(or
|
|
('symbol', '1')
|
|
('symbol', '2')
|
|
('symbol', '3'))
|
|
>>> f(('func',
|
|
... ('symbol', 'p1'),
|
|
... ('or',
|
|
... ('or',
|
|
... ('func',
|
|
... ('symbol', 'sort'),
|
|
... ('list',
|
|
... ('or',
|
|
... ('or',
|
|
... ('symbol', '1'),
|
|
... ('symbol', '2')),
|
|
... ('symbol', '3')),
|
|
... ('negate',
|
|
... ('symbol', 'rev')))),
|
|
... ('and',
|
|
... ('symbol', '4'),
|
|
... ('group',
|
|
... ('or',
|
|
... ('or',
|
|
... ('symbol', '5'),
|
|
... ('symbol', '6')),
|
|
... ('symbol', '7'))))),
|
|
... ('symbol', '8'))))
|
|
(func
|
|
('symbol', 'p1')
|
|
(or
|
|
(func
|
|
('symbol', 'sort')
|
|
(list
|
|
(or
|
|
('symbol', '1')
|
|
('symbol', '2')
|
|
('symbol', '3'))
|
|
(negate
|
|
('symbol', 'rev'))))
|
|
(and
|
|
('symbol', '4')
|
|
(group
|
|
(or
|
|
('symbol', '5')
|
|
('symbol', '6')
|
|
('symbol', '7'))))
|
|
('symbol', '8')))
|
|
"""
|
|
if not isinstance(tree, tuple):
|
|
return tree
|
|
op = tree[0]
|
|
if op not in targetnodes:
|
|
return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:])
|
|
|
|
# walk down left nodes taking each right node. no recursion to left nodes
|
|
# because infix operators are left-associative, i.e. left tree is deep.
|
|
# e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3)
|
|
simplified = []
|
|
x = tree
|
|
while x[0] == op:
|
|
l, r = x[1:]
|
|
simplified.append(simplifyinfixops(r, targetnodes))
|
|
x = l
|
|
simplified.append(simplifyinfixops(x, targetnodes))
|
|
simplified.append(op)
|
|
return tuple(reversed(simplified))
|
|
|
|
def parseerrordetail(inst):
|
|
"""Compose error message from specified ParseError object
|
|
"""
|
|
if len(inst.args) > 1:
|
|
return _('at %s: %s') % (inst.args[1], inst.args[0])
|
|
else:
|
|
return inst.args[0]
|
|
|
|
class basealiasrules(object):
|
|
"""Parsing and expansion rule set of aliases
|
|
|
|
This is a helper for fileset/revset/template aliases. A concrete rule set
|
|
should be made by sub-classing this and implementing class/static methods.
|
|
|
|
It supports alias expansion of symbol and funciton-call styles::
|
|
|
|
# decl = defn
|
|
h = heads(default)
|
|
b($1) = ancestors($1) - ancestors(default)
|
|
"""
|
|
# typically a config section, which will be included in error messages
|
|
_section = None
|
|
# tags of symbol and function nodes
|
|
_symbolnode = 'symbol'
|
|
_funcnode = 'func'
|
|
|
|
def __new__(cls):
|
|
raise TypeError("'%s' is not instantiatable" % cls.__name__)
|
|
|
|
@staticmethod
|
|
def _parsedecl(spec):
|
|
"""Parse an alias name and arguments"""
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
def _parsedefn(spec):
|
|
"""Parse an alias definition"""
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
def _getlist(tree):
|
|
"""Extract a list of arguments from parsed tree"""
|
|
raise NotImplementedError
|