##// END OF EJS Templates
parser: make buildargsdict() precompute position where keyword args start...
Yuya Nishihara -
r30752:ffd324ea default
parent child Browse files
Show More
@@ -1,540 +1,541 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, expr)
68 expr = (suffix, 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 kwstart = next((i for i, x in enumerate(trees) if x[0] == keyvaluenode),
100 len(trees))
99 if len(trees) > len(keys):
101 if len(trees) > len(keys):
100 raise error.ParseError(_("%(func)s takes at most %(nargs)d arguments")
102 raise error.ParseError(_("%(func)s takes at most %(nargs)d arguments")
101 % {'func': funcname, 'nargs': len(keys)})
103 % {'func': funcname, 'nargs': len(keys)})
102 args = {}
104 args = {}
103 # consume positional arguments
105 # consume positional arguments
104 for k, x in zip(keys, trees):
106 for k, x in zip(keys, trees[:kwstart]):
105 if x[0] == keyvaluenode:
106 break
107 args[k] = x
107 args[k] = x
108 assert len(args) == kwstart
108 # remainder should be keyword arguments
109 # remainder should be keyword arguments
109 for x in trees[len(args):]:
110 for x in trees[kwstart:]:
110 if x[0] != keyvaluenode or x[1][0] != keynode:
111 if x[0] != keyvaluenode or x[1][0] != keynode:
111 raise error.ParseError(_("%(func)s got an invalid argument")
112 raise error.ParseError(_("%(func)s got an invalid argument")
112 % {'func': funcname})
113 % {'func': funcname})
113 k = x[1][1]
114 k = x[1][1]
114 if k not in keys:
115 if k not in keys:
115 raise error.ParseError(_("%(func)s got an unexpected keyword "
116 raise error.ParseError(_("%(func)s got an unexpected keyword "
116 "argument '%(key)s'")
117 "argument '%(key)s'")
117 % {'func': funcname, 'key': k})
118 % {'func': funcname, 'key': k})
118 if k in args:
119 if k in args:
119 raise error.ParseError(_("%(func)s got multiple values for keyword "
120 raise error.ParseError(_("%(func)s got multiple values for keyword "
120 "argument '%(key)s'")
121 "argument '%(key)s'")
121 % {'func': funcname, 'key': k})
122 % {'func': funcname, 'key': k})
122 args[k] = x[2]
123 args[k] = x[2]
123 return args
124 return args
124
125
125 def unescapestr(s):
126 def unescapestr(s):
126 try:
127 try:
127 return s.decode("string_escape")
128 return s.decode("string_escape")
128 except ValueError as e:
129 except ValueError as e:
129 # mangle Python's exception into our format
130 # mangle Python's exception into our format
130 raise error.ParseError(str(e).lower())
131 raise error.ParseError(str(e).lower())
131
132
132 def _prettyformat(tree, leafnodes, level, lines):
133 def _prettyformat(tree, leafnodes, level, lines):
133 if not isinstance(tree, tuple) or tree[0] in leafnodes:
134 if not isinstance(tree, tuple) or tree[0] in leafnodes:
134 lines.append((level, str(tree)))
135 lines.append((level, str(tree)))
135 else:
136 else:
136 lines.append((level, '(%s' % tree[0]))
137 lines.append((level, '(%s' % tree[0]))
137 for s in tree[1:]:
138 for s in tree[1:]:
138 _prettyformat(s, leafnodes, level + 1, lines)
139 _prettyformat(s, leafnodes, level + 1, lines)
139 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
140 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
140
141
141 def prettyformat(tree, leafnodes):
142 def prettyformat(tree, leafnodes):
142 lines = []
143 lines = []
143 _prettyformat(tree, leafnodes, 0, lines)
144 _prettyformat(tree, leafnodes, 0, lines)
144 output = '\n'.join((' ' * l + s) for l, s in lines)
145 output = '\n'.join((' ' * l + s) for l, s in lines)
145 return output
146 return output
146
147
147 def simplifyinfixops(tree, targetnodes):
148 def simplifyinfixops(tree, targetnodes):
148 """Flatten chained infix operations to reduce usage of Python stack
149 """Flatten chained infix operations to reduce usage of Python stack
149
150
150 >>> def f(tree):
151 >>> def f(tree):
151 ... print prettyformat(simplifyinfixops(tree, ('or',)), ('symbol',))
152 ... print prettyformat(simplifyinfixops(tree, ('or',)), ('symbol',))
152 >>> f(('or',
153 >>> f(('or',
153 ... ('or',
154 ... ('or',
154 ... ('symbol', '1'),
155 ... ('symbol', '1'),
155 ... ('symbol', '2')),
156 ... ('symbol', '2')),
156 ... ('symbol', '3')))
157 ... ('symbol', '3')))
157 (or
158 (or
158 ('symbol', '1')
159 ('symbol', '1')
159 ('symbol', '2')
160 ('symbol', '2')
160 ('symbol', '3'))
161 ('symbol', '3'))
161 >>> f(('func',
162 >>> f(('func',
162 ... ('symbol', 'p1'),
163 ... ('symbol', 'p1'),
163 ... ('or',
164 ... ('or',
164 ... ('or',
165 ... ('or',
165 ... ('func',
166 ... ('func',
166 ... ('symbol', 'sort'),
167 ... ('symbol', 'sort'),
167 ... ('list',
168 ... ('list',
168 ... ('or',
169 ... ('or',
169 ... ('or',
170 ... ('or',
170 ... ('symbol', '1'),
171 ... ('symbol', '1'),
171 ... ('symbol', '2')),
172 ... ('symbol', '2')),
172 ... ('symbol', '3')),
173 ... ('symbol', '3')),
173 ... ('negate',
174 ... ('negate',
174 ... ('symbol', 'rev')))),
175 ... ('symbol', 'rev')))),
175 ... ('and',
176 ... ('and',
176 ... ('symbol', '4'),
177 ... ('symbol', '4'),
177 ... ('group',
178 ... ('group',
178 ... ('or',
179 ... ('or',
179 ... ('or',
180 ... ('or',
180 ... ('symbol', '5'),
181 ... ('symbol', '5'),
181 ... ('symbol', '6')),
182 ... ('symbol', '6')),
182 ... ('symbol', '7'))))),
183 ... ('symbol', '7'))))),
183 ... ('symbol', '8'))))
184 ... ('symbol', '8'))))
184 (func
185 (func
185 ('symbol', 'p1')
186 ('symbol', 'p1')
186 (or
187 (or
187 (func
188 (func
188 ('symbol', 'sort')
189 ('symbol', 'sort')
189 (list
190 (list
190 (or
191 (or
191 ('symbol', '1')
192 ('symbol', '1')
192 ('symbol', '2')
193 ('symbol', '2')
193 ('symbol', '3'))
194 ('symbol', '3'))
194 (negate
195 (negate
195 ('symbol', 'rev'))))
196 ('symbol', 'rev'))))
196 (and
197 (and
197 ('symbol', '4')
198 ('symbol', '4')
198 (group
199 (group
199 (or
200 (or
200 ('symbol', '5')
201 ('symbol', '5')
201 ('symbol', '6')
202 ('symbol', '6')
202 ('symbol', '7'))))
203 ('symbol', '7'))))
203 ('symbol', '8')))
204 ('symbol', '8')))
204 """
205 """
205 if not isinstance(tree, tuple):
206 if not isinstance(tree, tuple):
206 return tree
207 return tree
207 op = tree[0]
208 op = tree[0]
208 if op not in targetnodes:
209 if op not in targetnodes:
209 return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:])
210 return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:])
210
211
211 # walk down left nodes taking each right node. no recursion to left nodes
212 # 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.
213 # because infix operators are left-associative, i.e. left tree is deep.
213 # e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3)
214 # e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3)
214 simplified = []
215 simplified = []
215 x = tree
216 x = tree
216 while x[0] == op:
217 while x[0] == op:
217 l, r = x[1:]
218 l, r = x[1:]
218 simplified.append(simplifyinfixops(r, targetnodes))
219 simplified.append(simplifyinfixops(r, targetnodes))
219 x = l
220 x = l
220 simplified.append(simplifyinfixops(x, targetnodes))
221 simplified.append(simplifyinfixops(x, targetnodes))
221 simplified.append(op)
222 simplified.append(op)
222 return tuple(reversed(simplified))
223 return tuple(reversed(simplified))
223
224
224 def parseerrordetail(inst):
225 def parseerrordetail(inst):
225 """Compose error message from specified ParseError object
226 """Compose error message from specified ParseError object
226 """
227 """
227 if len(inst.args) > 1:
228 if len(inst.args) > 1:
228 return _('at %s: %s') % (inst.args[1], inst.args[0])
229 return _('at %s: %s') % (inst.args[1], inst.args[0])
229 else:
230 else:
230 return inst.args[0]
231 return inst.args[0]
231
232
232 class alias(object):
233 class alias(object):
233 """Parsed result of alias"""
234 """Parsed result of alias"""
234
235
235 def __init__(self, name, args, err, replacement):
236 def __init__(self, name, args, err, replacement):
236 self.name = name
237 self.name = name
237 self.args = args
238 self.args = args
238 self.error = err
239 self.error = err
239 self.replacement = replacement
240 self.replacement = replacement
240 # whether own `error` information is already shown or not.
241 # whether own `error` information is already shown or not.
241 # this avoids showing same warning multiple times at each
242 # this avoids showing same warning multiple times at each
242 # `expandaliases`.
243 # `expandaliases`.
243 self.warned = False
244 self.warned = False
244
245
245 class basealiasrules(object):
246 class basealiasrules(object):
246 """Parsing and expansion rule set of aliases
247 """Parsing and expansion rule set of aliases
247
248
248 This is a helper for fileset/revset/template aliases. A concrete rule set
249 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.
250 should be made by sub-classing this and implementing class/static methods.
250
251
251 It supports alias expansion of symbol and function-call styles::
252 It supports alias expansion of symbol and function-call styles::
252
253
253 # decl = defn
254 # decl = defn
254 h = heads(default)
255 h = heads(default)
255 b($1) = ancestors($1) - ancestors(default)
256 b($1) = ancestors($1) - ancestors(default)
256 """
257 """
257 # typically a config section, which will be included in error messages
258 # typically a config section, which will be included in error messages
258 _section = None
259 _section = None
259 # tag of symbol node
260 # tag of symbol node
260 _symbolnode = 'symbol'
261 _symbolnode = 'symbol'
261
262
262 def __new__(cls):
263 def __new__(cls):
263 raise TypeError("'%s' is not instantiatable" % cls.__name__)
264 raise TypeError("'%s' is not instantiatable" % cls.__name__)
264
265
265 @staticmethod
266 @staticmethod
266 def _parse(spec):
267 def _parse(spec):
267 """Parse an alias name, arguments and definition"""
268 """Parse an alias name, arguments and definition"""
268 raise NotImplementedError
269 raise NotImplementedError
269
270
270 @staticmethod
271 @staticmethod
271 def _trygetfunc(tree):
272 def _trygetfunc(tree):
272 """Return (name, args) if tree is a function; otherwise None"""
273 """Return (name, args) if tree is a function; otherwise None"""
273 raise NotImplementedError
274 raise NotImplementedError
274
275
275 @classmethod
276 @classmethod
276 def _builddecl(cls, decl):
277 def _builddecl(cls, decl):
277 """Parse an alias declaration into ``(name, args, errorstr)``
278 """Parse an alias declaration into ``(name, args, errorstr)``
278
279
279 This function analyzes the parsed tree. The parsing rule is provided
280 This function analyzes the parsed tree. The parsing rule is provided
280 by ``_parse()``.
281 by ``_parse()``.
281
282
282 - ``name``: of declared alias (may be ``decl`` itself at error)
283 - ``name``: of declared alias (may be ``decl`` itself at error)
283 - ``args``: list of argument names (or None for symbol declaration)
284 - ``args``: list of argument names (or None for symbol declaration)
284 - ``errorstr``: detail about detected error (or None)
285 - ``errorstr``: detail about detected error (or None)
285
286
286 >>> sym = lambda x: ('symbol', x)
287 >>> sym = lambda x: ('symbol', x)
287 >>> symlist = lambda *xs: ('list',) + tuple(sym(x) for x in xs)
288 >>> symlist = lambda *xs: ('list',) + tuple(sym(x) for x in xs)
288 >>> func = lambda n, a: ('func', sym(n), a)
289 >>> func = lambda n, a: ('func', sym(n), a)
289 >>> parsemap = {
290 >>> parsemap = {
290 ... 'foo': sym('foo'),
291 ... 'foo': sym('foo'),
291 ... '$foo': sym('$foo'),
292 ... '$foo': sym('$foo'),
292 ... 'foo::bar': ('dagrange', sym('foo'), sym('bar')),
293 ... 'foo::bar': ('dagrange', sym('foo'), sym('bar')),
293 ... 'foo()': func('foo', None),
294 ... 'foo()': func('foo', None),
294 ... '$foo()': func('$foo', None),
295 ... '$foo()': func('$foo', None),
295 ... 'foo($1, $2)': func('foo', symlist('$1', '$2')),
296 ... 'foo($1, $2)': func('foo', symlist('$1', '$2')),
296 ... 'foo(bar_bar, baz.baz)':
297 ... 'foo(bar_bar, baz.baz)':
297 ... func('foo', symlist('bar_bar', 'baz.baz')),
298 ... func('foo', symlist('bar_bar', 'baz.baz')),
298 ... 'foo(bar($1, $2))':
299 ... 'foo(bar($1, $2))':
299 ... func('foo', func('bar', symlist('$1', '$2'))),
300 ... func('foo', func('bar', symlist('$1', '$2'))),
300 ... 'foo($1, $2, nested($1, $2))':
301 ... 'foo($1, $2, nested($1, $2))':
301 ... func('foo', (symlist('$1', '$2') +
302 ... func('foo', (symlist('$1', '$2') +
302 ... (func('nested', symlist('$1', '$2')),))),
303 ... (func('nested', symlist('$1', '$2')),))),
303 ... 'foo("bar")': func('foo', ('string', 'bar')),
304 ... 'foo("bar")': func('foo', ('string', 'bar')),
304 ... 'foo($1, $2': error.ParseError('unexpected token: end', 10),
305 ... 'foo($1, $2': error.ParseError('unexpected token: end', 10),
305 ... 'foo("bar': error.ParseError('unterminated string', 5),
306 ... 'foo("bar': error.ParseError('unterminated string', 5),
306 ... 'foo($1, $2, $1)': func('foo', symlist('$1', '$2', '$1')),
307 ... 'foo($1, $2, $1)': func('foo', symlist('$1', '$2', '$1')),
307 ... }
308 ... }
308 >>> def parse(expr):
309 >>> def parse(expr):
309 ... x = parsemap[expr]
310 ... x = parsemap[expr]
310 ... if isinstance(x, Exception):
311 ... if isinstance(x, Exception):
311 ... raise x
312 ... raise x
312 ... return x
313 ... return x
313 >>> def trygetfunc(tree):
314 >>> def trygetfunc(tree):
314 ... if not tree or tree[0] != 'func' or tree[1][0] != 'symbol':
315 ... if not tree or tree[0] != 'func' or tree[1][0] != 'symbol':
315 ... return None
316 ... return None
316 ... if not tree[2]:
317 ... if not tree[2]:
317 ... return tree[1][1], []
318 ... return tree[1][1], []
318 ... if tree[2][0] == 'list':
319 ... if tree[2][0] == 'list':
319 ... return tree[1][1], list(tree[2][1:])
320 ... return tree[1][1], list(tree[2][1:])
320 ... return tree[1][1], [tree[2]]
321 ... return tree[1][1], [tree[2]]
321 >>> class aliasrules(basealiasrules):
322 >>> class aliasrules(basealiasrules):
322 ... _parse = staticmethod(parse)
323 ... _parse = staticmethod(parse)
323 ... _trygetfunc = staticmethod(trygetfunc)
324 ... _trygetfunc = staticmethod(trygetfunc)
324 >>> builddecl = aliasrules._builddecl
325 >>> builddecl = aliasrules._builddecl
325 >>> builddecl('foo')
326 >>> builddecl('foo')
326 ('foo', None, None)
327 ('foo', None, None)
327 >>> builddecl('$foo')
328 >>> builddecl('$foo')
328 ('$foo', None, "invalid symbol '$foo'")
329 ('$foo', None, "invalid symbol '$foo'")
329 >>> builddecl('foo::bar')
330 >>> builddecl('foo::bar')
330 ('foo::bar', None, 'invalid format')
331 ('foo::bar', None, 'invalid format')
331 >>> builddecl('foo()')
332 >>> builddecl('foo()')
332 ('foo', [], None)
333 ('foo', [], None)
333 >>> builddecl('$foo()')
334 >>> builddecl('$foo()')
334 ('$foo()', None, "invalid function '$foo'")
335 ('$foo()', None, "invalid function '$foo'")
335 >>> builddecl('foo($1, $2)')
336 >>> builddecl('foo($1, $2)')
336 ('foo', ['$1', '$2'], None)
337 ('foo', ['$1', '$2'], None)
337 >>> builddecl('foo(bar_bar, baz.baz)')
338 >>> builddecl('foo(bar_bar, baz.baz)')
338 ('foo', ['bar_bar', 'baz.baz'], None)
339 ('foo', ['bar_bar', 'baz.baz'], None)
339 >>> builddecl('foo($1, $2, nested($1, $2))')
340 >>> builddecl('foo($1, $2, nested($1, $2))')
340 ('foo($1, $2, nested($1, $2))', None, 'invalid argument list')
341 ('foo($1, $2, nested($1, $2))', None, 'invalid argument list')
341 >>> builddecl('foo(bar($1, $2))')
342 >>> builddecl('foo(bar($1, $2))')
342 ('foo(bar($1, $2))', None, 'invalid argument list')
343 ('foo(bar($1, $2))', None, 'invalid argument list')
343 >>> builddecl('foo("bar")')
344 >>> builddecl('foo("bar")')
344 ('foo("bar")', None, 'invalid argument list')
345 ('foo("bar")', None, 'invalid argument list')
345 >>> builddecl('foo($1, $2')
346 >>> builddecl('foo($1, $2')
346 ('foo($1, $2', None, 'at 10: unexpected token: end')
347 ('foo($1, $2', None, 'at 10: unexpected token: end')
347 >>> builddecl('foo("bar')
348 >>> builddecl('foo("bar')
348 ('foo("bar', None, 'at 5: unterminated string')
349 ('foo("bar', None, 'at 5: unterminated string')
349 >>> builddecl('foo($1, $2, $1)')
350 >>> builddecl('foo($1, $2, $1)')
350 ('foo', None, 'argument names collide with each other')
351 ('foo', None, 'argument names collide with each other')
351 """
352 """
352 try:
353 try:
353 tree = cls._parse(decl)
354 tree = cls._parse(decl)
354 except error.ParseError as inst:
355 except error.ParseError as inst:
355 return (decl, None, parseerrordetail(inst))
356 return (decl, None, parseerrordetail(inst))
356
357
357 if tree[0] == cls._symbolnode:
358 if tree[0] == cls._symbolnode:
358 # "name = ...." style
359 # "name = ...." style
359 name = tree[1]
360 name = tree[1]
360 if name.startswith('$'):
361 if name.startswith('$'):
361 return (decl, None, _("invalid symbol '%s'") % name)
362 return (decl, None, _("invalid symbol '%s'") % name)
362 return (name, None, None)
363 return (name, None, None)
363
364
364 func = cls._trygetfunc(tree)
365 func = cls._trygetfunc(tree)
365 if func:
366 if func:
366 # "name(arg, ....) = ...." style
367 # "name(arg, ....) = ...." style
367 name, args = func
368 name, args = func
368 if name.startswith('$'):
369 if name.startswith('$'):
369 return (decl, None, _("invalid function '%s'") % name)
370 return (decl, None, _("invalid function '%s'") % name)
370 if any(t[0] != cls._symbolnode for t in args):
371 if any(t[0] != cls._symbolnode for t in args):
371 return (decl, None, _("invalid argument list"))
372 return (decl, None, _("invalid argument list"))
372 if len(args) != len(set(args)):
373 if len(args) != len(set(args)):
373 return (name, None, _("argument names collide with each other"))
374 return (name, None, _("argument names collide with each other"))
374 return (name, [t[1] for t in args], None)
375 return (name, [t[1] for t in args], None)
375
376
376 return (decl, None, _("invalid format"))
377 return (decl, None, _("invalid format"))
377
378
378 @classmethod
379 @classmethod
379 def _relabelargs(cls, tree, args):
380 def _relabelargs(cls, tree, args):
380 """Mark alias arguments as ``_aliasarg``"""
381 """Mark alias arguments as ``_aliasarg``"""
381 if not isinstance(tree, tuple):
382 if not isinstance(tree, tuple):
382 return tree
383 return tree
383 op = tree[0]
384 op = tree[0]
384 if op != cls._symbolnode:
385 if op != cls._symbolnode:
385 return (op,) + tuple(cls._relabelargs(x, args) for x in tree[1:])
386 return (op,) + tuple(cls._relabelargs(x, args) for x in tree[1:])
386
387
387 assert len(tree) == 2
388 assert len(tree) == 2
388 sym = tree[1]
389 sym = tree[1]
389 if sym in args:
390 if sym in args:
390 op = '_aliasarg'
391 op = '_aliasarg'
391 elif sym.startswith('$'):
392 elif sym.startswith('$'):
392 raise error.ParseError(_("invalid symbol '%s'") % sym)
393 raise error.ParseError(_("invalid symbol '%s'") % sym)
393 return (op, sym)
394 return (op, sym)
394
395
395 @classmethod
396 @classmethod
396 def _builddefn(cls, defn, args):
397 def _builddefn(cls, defn, args):
397 """Parse an alias definition into a tree and marks substitutions
398 """Parse an alias definition into a tree and marks substitutions
398
399
399 This function marks alias argument references as ``_aliasarg``. The
400 This function marks alias argument references as ``_aliasarg``. The
400 parsing rule is provided by ``_parse()``.
401 parsing rule is provided by ``_parse()``.
401
402
402 ``args`` is a list of alias argument names, or None if the alias
403 ``args`` is a list of alias argument names, or None if the alias
403 is declared as a symbol.
404 is declared as a symbol.
404
405
405 >>> parsemap = {
406 >>> parsemap = {
406 ... '$1 or foo': ('or', ('symbol', '$1'), ('symbol', 'foo')),
407 ... '$1 or foo': ('or', ('symbol', '$1'), ('symbol', 'foo')),
407 ... '$1 or $bar': ('or', ('symbol', '$1'), ('symbol', '$bar')),
408 ... '$1 or $bar': ('or', ('symbol', '$1'), ('symbol', '$bar')),
408 ... '$10 or baz': ('or', ('symbol', '$10'), ('symbol', 'baz')),
409 ... '$10 or baz': ('or', ('symbol', '$10'), ('symbol', 'baz')),
409 ... '"$1" or "foo"': ('or', ('string', '$1'), ('string', 'foo')),
410 ... '"$1" or "foo"': ('or', ('string', '$1'), ('string', 'foo')),
410 ... }
411 ... }
411 >>> class aliasrules(basealiasrules):
412 >>> class aliasrules(basealiasrules):
412 ... _parse = staticmethod(parsemap.__getitem__)
413 ... _parse = staticmethod(parsemap.__getitem__)
413 ... _trygetfunc = staticmethod(lambda x: None)
414 ... _trygetfunc = staticmethod(lambda x: None)
414 >>> builddefn = aliasrules._builddefn
415 >>> builddefn = aliasrules._builddefn
415 >>> def pprint(tree):
416 >>> def pprint(tree):
416 ... print prettyformat(tree, ('_aliasarg', 'string', 'symbol'))
417 ... print prettyformat(tree, ('_aliasarg', 'string', 'symbol'))
417 >>> args = ['$1', '$2', 'foo']
418 >>> args = ['$1', '$2', 'foo']
418 >>> pprint(builddefn('$1 or foo', args))
419 >>> pprint(builddefn('$1 or foo', args))
419 (or
420 (or
420 ('_aliasarg', '$1')
421 ('_aliasarg', '$1')
421 ('_aliasarg', 'foo'))
422 ('_aliasarg', 'foo'))
422 >>> try:
423 >>> try:
423 ... builddefn('$1 or $bar', args)
424 ... builddefn('$1 or $bar', args)
424 ... except error.ParseError as inst:
425 ... except error.ParseError as inst:
425 ... print parseerrordetail(inst)
426 ... print parseerrordetail(inst)
426 invalid symbol '$bar'
427 invalid symbol '$bar'
427 >>> args = ['$1', '$10', 'foo']
428 >>> args = ['$1', '$10', 'foo']
428 >>> pprint(builddefn('$10 or baz', args))
429 >>> pprint(builddefn('$10 or baz', args))
429 (or
430 (or
430 ('_aliasarg', '$10')
431 ('_aliasarg', '$10')
431 ('symbol', 'baz'))
432 ('symbol', 'baz'))
432 >>> pprint(builddefn('"$1" or "foo"', args))
433 >>> pprint(builddefn('"$1" or "foo"', args))
433 (or
434 (or
434 ('string', '$1')
435 ('string', '$1')
435 ('string', 'foo'))
436 ('string', 'foo'))
436 """
437 """
437 tree = cls._parse(defn)
438 tree = cls._parse(defn)
438 if args:
439 if args:
439 args = set(args)
440 args = set(args)
440 else:
441 else:
441 args = set()
442 args = set()
442 return cls._relabelargs(tree, args)
443 return cls._relabelargs(tree, args)
443
444
444 @classmethod
445 @classmethod
445 def build(cls, decl, defn):
446 def build(cls, decl, defn):
446 """Parse an alias declaration and definition into an alias object"""
447 """Parse an alias declaration and definition into an alias object"""
447 repl = efmt = None
448 repl = efmt = None
448 name, args, err = cls._builddecl(decl)
449 name, args, err = cls._builddecl(decl)
449 if err:
450 if err:
450 efmt = _('bad declaration of %(section)s "%(name)s": %(error)s')
451 efmt = _('bad declaration of %(section)s "%(name)s": %(error)s')
451 else:
452 else:
452 try:
453 try:
453 repl = cls._builddefn(defn, args)
454 repl = cls._builddefn(defn, args)
454 except error.ParseError as inst:
455 except error.ParseError as inst:
455 err = parseerrordetail(inst)
456 err = parseerrordetail(inst)
456 efmt = _('bad definition of %(section)s "%(name)s": %(error)s')
457 efmt = _('bad definition of %(section)s "%(name)s": %(error)s')
457 if err:
458 if err:
458 err = efmt % {'section': cls._section, 'name': name, 'error': err}
459 err = efmt % {'section': cls._section, 'name': name, 'error': err}
459 return alias(name, args, err, repl)
460 return alias(name, args, err, repl)
460
461
461 @classmethod
462 @classmethod
462 def buildmap(cls, items):
463 def buildmap(cls, items):
463 """Parse a list of alias (name, replacement) pairs into a dict of
464 """Parse a list of alias (name, replacement) pairs into a dict of
464 alias objects"""
465 alias objects"""
465 aliases = {}
466 aliases = {}
466 for decl, defn in items:
467 for decl, defn in items:
467 a = cls.build(decl, defn)
468 a = cls.build(decl, defn)
468 aliases[a.name] = a
469 aliases[a.name] = a
469 return aliases
470 return aliases
470
471
471 @classmethod
472 @classmethod
472 def _getalias(cls, aliases, tree):
473 def _getalias(cls, aliases, tree):
473 """If tree looks like an unexpanded alias, return (alias, pattern-args)
474 """If tree looks like an unexpanded alias, return (alias, pattern-args)
474 pair. Return None otherwise.
475 pair. Return None otherwise.
475 """
476 """
476 if not isinstance(tree, tuple):
477 if not isinstance(tree, tuple):
477 return None
478 return None
478 if tree[0] == cls._symbolnode:
479 if tree[0] == cls._symbolnode:
479 name = tree[1]
480 name = tree[1]
480 a = aliases.get(name)
481 a = aliases.get(name)
481 if a and a.args is None:
482 if a and a.args is None:
482 return a, None
483 return a, None
483 func = cls._trygetfunc(tree)
484 func = cls._trygetfunc(tree)
484 if func:
485 if func:
485 name, args = func
486 name, args = func
486 a = aliases.get(name)
487 a = aliases.get(name)
487 if a and a.args is not None:
488 if a and a.args is not None:
488 return a, args
489 return a, args
489 return None
490 return None
490
491
491 @classmethod
492 @classmethod
492 def _expandargs(cls, tree, args):
493 def _expandargs(cls, tree, args):
493 """Replace _aliasarg instances with the substitution value of the
494 """Replace _aliasarg instances with the substitution value of the
494 same name in args, recursively.
495 same name in args, recursively.
495 """
496 """
496 if not isinstance(tree, tuple):
497 if not isinstance(tree, tuple):
497 return tree
498 return tree
498 if tree[0] == '_aliasarg':
499 if tree[0] == '_aliasarg':
499 sym = tree[1]
500 sym = tree[1]
500 return args[sym]
501 return args[sym]
501 return tuple(cls._expandargs(t, args) for t in tree)
502 return tuple(cls._expandargs(t, args) for t in tree)
502
503
503 @classmethod
504 @classmethod
504 def _expand(cls, aliases, tree, expanding, cache):
505 def _expand(cls, aliases, tree, expanding, cache):
505 if not isinstance(tree, tuple):
506 if not isinstance(tree, tuple):
506 return tree
507 return tree
507 r = cls._getalias(aliases, tree)
508 r = cls._getalias(aliases, tree)
508 if r is None:
509 if r is None:
509 return tuple(cls._expand(aliases, t, expanding, cache)
510 return tuple(cls._expand(aliases, t, expanding, cache)
510 for t in tree)
511 for t in tree)
511 a, l = r
512 a, l = r
512 if a.error:
513 if a.error:
513 raise error.Abort(a.error)
514 raise error.Abort(a.error)
514 if a in expanding:
515 if a in expanding:
515 raise error.ParseError(_('infinite expansion of %(section)s '
516 raise error.ParseError(_('infinite expansion of %(section)s '
516 '"%(name)s" detected')
517 '"%(name)s" detected')
517 % {'section': cls._section, 'name': a.name})
518 % {'section': cls._section, 'name': a.name})
518 # get cacheable replacement tree by expanding aliases recursively
519 # get cacheable replacement tree by expanding aliases recursively
519 expanding.append(a)
520 expanding.append(a)
520 if a.name not in cache:
521 if a.name not in cache:
521 cache[a.name] = cls._expand(aliases, a.replacement, expanding,
522 cache[a.name] = cls._expand(aliases, a.replacement, expanding,
522 cache)
523 cache)
523 result = cache[a.name]
524 result = cache[a.name]
524 expanding.pop()
525 expanding.pop()
525 if a.args is None:
526 if a.args is None:
526 return result
527 return result
527 # substitute function arguments in replacement tree
528 # substitute function arguments in replacement tree
528 if len(l) != len(a.args):
529 if len(l) != len(a.args):
529 raise error.ParseError(_('invalid number of arguments: %d')
530 raise error.ParseError(_('invalid number of arguments: %d')
530 % len(l))
531 % len(l))
531 l = [cls._expand(aliases, t, [], cache) for t in l]
532 l = [cls._expand(aliases, t, [], cache) for t in l]
532 return cls._expandargs(result, dict(zip(a.args, l)))
533 return cls._expandargs(result, dict(zip(a.args, l)))
533
534
534 @classmethod
535 @classmethod
535 def expand(cls, aliases, tree):
536 def expand(cls, aliases, tree):
536 """Expand aliases in tree, recursively.
537 """Expand aliases in tree, recursively.
537
538
538 'aliases' is a dictionary mapping user defined aliases to alias objects.
539 'aliases' is a dictionary mapping user defined aliases to alias objects.
539 """
540 """
540 return cls._expand(aliases, tree, [], {})
541 return cls._expand(aliases, tree, [], {})
General Comments 0
You need to be logged in to leave comments. Login now