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