##// END OF EJS Templates
revsetlang: add a getboolean helper function...
Denis Laxalde -
r31997:11f501f0 default
parent child Browse files
Show More
@@ -1,702 +1,708
1 # revsetlang.py - parser, tokenizer and utility for revision set language
1 # revsetlang.py - parser, tokenizer and utility for revision set language
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import string
10 import string
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 node,
15 node,
16 parser,
16 parser,
17 pycompat,
17 pycompat,
18 util,
18 util,
19 )
19 )
20
20
21 elements = {
21 elements = {
22 # token-type: binding-strength, primary, prefix, infix, suffix
22 # token-type: binding-strength, primary, prefix, infix, suffix
23 "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
23 "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
24 "##": (20, None, None, ("_concat", 20), None),
24 "##": (20, None, None, ("_concat", 20), None),
25 "~": (18, None, None, ("ancestor", 18), None),
25 "~": (18, None, None, ("ancestor", 18), None),
26 "^": (18, None, None, ("parent", 18), "parentpost"),
26 "^": (18, None, None, ("parent", 18), "parentpost"),
27 "-": (5, None, ("negate", 19), ("minus", 5), None),
27 "-": (5, None, ("negate", 19), ("minus", 5), None),
28 "::": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
28 "::": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
29 "..": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
29 "..": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
30 ":": (15, "rangeall", ("rangepre", 15), ("range", 15), "rangepost"),
30 ":": (15, "rangeall", ("rangepre", 15), ("range", 15), "rangepost"),
31 "not": (10, None, ("not", 10), None, None),
31 "not": (10, None, ("not", 10), None, None),
32 "!": (10, None, ("not", 10), None, None),
32 "!": (10, None, ("not", 10), None, None),
33 "and": (5, None, None, ("and", 5), None),
33 "and": (5, None, None, ("and", 5), None),
34 "&": (5, None, None, ("and", 5), None),
34 "&": (5, None, None, ("and", 5), None),
35 "%": (5, None, None, ("only", 5), "onlypost"),
35 "%": (5, None, None, ("only", 5), "onlypost"),
36 "or": (4, None, None, ("or", 4), None),
36 "or": (4, None, None, ("or", 4), None),
37 "|": (4, None, None, ("or", 4), None),
37 "|": (4, None, None, ("or", 4), None),
38 "+": (4, None, None, ("or", 4), None),
38 "+": (4, None, None, ("or", 4), None),
39 "=": (3, None, None, ("keyvalue", 3), None),
39 "=": (3, None, None, ("keyvalue", 3), None),
40 ",": (2, None, None, ("list", 2), None),
40 ",": (2, None, None, ("list", 2), None),
41 ")": (0, None, None, None, None),
41 ")": (0, None, None, None, None),
42 "symbol": (0, "symbol", None, None, None),
42 "symbol": (0, "symbol", None, None, None),
43 "string": (0, "string", None, None, None),
43 "string": (0, "string", None, None, None),
44 "end": (0, None, None, None, None),
44 "end": (0, None, None, None, None),
45 }
45 }
46
46
47 keywords = set(['and', 'or', 'not'])
47 keywords = set(['and', 'or', 'not'])
48
48
49 _quoteletters = set(['"', "'"])
49 _quoteletters = set(['"', "'"])
50 _simpleopletters = set(pycompat.iterbytestr("():=,-|&+!~^%"))
50 _simpleopletters = set(pycompat.iterbytestr("():=,-|&+!~^%"))
51
51
52 # default set of valid characters for the initial letter of symbols
52 # default set of valid characters for the initial letter of symbols
53 _syminitletters = set(pycompat.iterbytestr(
53 _syminitletters = set(pycompat.iterbytestr(
54 string.ascii_letters.encode('ascii') +
54 string.ascii_letters.encode('ascii') +
55 string.digits.encode('ascii') +
55 string.digits.encode('ascii') +
56 '._@')) | set(map(pycompat.bytechr, xrange(128, 256)))
56 '._@')) | set(map(pycompat.bytechr, xrange(128, 256)))
57
57
58 # default set of valid characters for non-initial letters of symbols
58 # default set of valid characters for non-initial letters of symbols
59 _symletters = _syminitletters | set(pycompat.iterbytestr('-/'))
59 _symletters = _syminitletters | set(pycompat.iterbytestr('-/'))
60
60
61 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
61 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
62 '''
62 '''
63 Parse a revset statement into a stream of tokens
63 Parse a revset statement into a stream of tokens
64
64
65 ``syminitletters`` is the set of valid characters for the initial
65 ``syminitletters`` is the set of valid characters for the initial
66 letter of symbols.
66 letter of symbols.
67
67
68 By default, character ``c`` is recognized as valid for initial
68 By default, character ``c`` is recognized as valid for initial
69 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
69 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
70
70
71 ``symletters`` is the set of valid characters for non-initial
71 ``symletters`` is the set of valid characters for non-initial
72 letters of symbols.
72 letters of symbols.
73
73
74 By default, character ``c`` is recognized as valid for non-initial
74 By default, character ``c`` is recognized as valid for non-initial
75 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
75 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
76
76
77 Check that @ is a valid unquoted token character (issue3686):
77 Check that @ is a valid unquoted token character (issue3686):
78 >>> list(tokenize("@::"))
78 >>> list(tokenize("@::"))
79 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
79 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
80
80
81 '''
81 '''
82 program = pycompat.bytestr(program)
82 program = pycompat.bytestr(program)
83 if syminitletters is None:
83 if syminitletters is None:
84 syminitletters = _syminitletters
84 syminitletters = _syminitletters
85 if symletters is None:
85 if symletters is None:
86 symletters = _symletters
86 symletters = _symletters
87
87
88 if program and lookup:
88 if program and lookup:
89 # attempt to parse old-style ranges first to deal with
89 # attempt to parse old-style ranges first to deal with
90 # things like old-tag which contain query metacharacters
90 # things like old-tag which contain query metacharacters
91 parts = program.split(':', 1)
91 parts = program.split(':', 1)
92 if all(lookup(sym) for sym in parts if sym):
92 if all(lookup(sym) for sym in parts if sym):
93 if parts[0]:
93 if parts[0]:
94 yield ('symbol', parts[0], 0)
94 yield ('symbol', parts[0], 0)
95 if len(parts) > 1:
95 if len(parts) > 1:
96 s = len(parts[0])
96 s = len(parts[0])
97 yield (':', None, s)
97 yield (':', None, s)
98 if parts[1]:
98 if parts[1]:
99 yield ('symbol', parts[1], s + 1)
99 yield ('symbol', parts[1], s + 1)
100 yield ('end', None, len(program))
100 yield ('end', None, len(program))
101 return
101 return
102
102
103 pos, l = 0, len(program)
103 pos, l = 0, len(program)
104 while pos < l:
104 while pos < l:
105 c = program[pos]
105 c = program[pos]
106 if c.isspace(): # skip inter-token whitespace
106 if c.isspace(): # skip inter-token whitespace
107 pass
107 pass
108 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
108 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
109 yield ('::', None, pos)
109 yield ('::', None, pos)
110 pos += 1 # skip ahead
110 pos += 1 # skip ahead
111 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
111 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
112 yield ('..', None, pos)
112 yield ('..', None, pos)
113 pos += 1 # skip ahead
113 pos += 1 # skip ahead
114 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
114 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
115 yield ('##', None, pos)
115 yield ('##', None, pos)
116 pos += 1 # skip ahead
116 pos += 1 # skip ahead
117 elif c in _simpleopletters: # handle simple operators
117 elif c in _simpleopletters: # handle simple operators
118 yield (c, None, pos)
118 yield (c, None, pos)
119 elif (c in _quoteletters or c == 'r' and
119 elif (c in _quoteletters or c == 'r' and
120 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
120 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
121 if c == 'r':
121 if c == 'r':
122 pos += 1
122 pos += 1
123 c = program[pos]
123 c = program[pos]
124 decode = lambda x: x
124 decode = lambda x: x
125 else:
125 else:
126 decode = parser.unescapestr
126 decode = parser.unescapestr
127 pos += 1
127 pos += 1
128 s = pos
128 s = pos
129 while pos < l: # find closing quote
129 while pos < l: # find closing quote
130 d = program[pos]
130 d = program[pos]
131 if d == '\\': # skip over escaped characters
131 if d == '\\': # skip over escaped characters
132 pos += 2
132 pos += 2
133 continue
133 continue
134 if d == c:
134 if d == c:
135 yield ('string', decode(program[s:pos]), s)
135 yield ('string', decode(program[s:pos]), s)
136 break
136 break
137 pos += 1
137 pos += 1
138 else:
138 else:
139 raise error.ParseError(_("unterminated string"), s)
139 raise error.ParseError(_("unterminated string"), s)
140 # gather up a symbol/keyword
140 # gather up a symbol/keyword
141 elif c in syminitletters:
141 elif c in syminitletters:
142 s = pos
142 s = pos
143 pos += 1
143 pos += 1
144 while pos < l: # find end of symbol
144 while pos < l: # find end of symbol
145 d = program[pos]
145 d = program[pos]
146 if d not in symletters:
146 if d not in symletters:
147 break
147 break
148 if d == '.' and program[pos - 1] == '.': # special case for ..
148 if d == '.' and program[pos - 1] == '.': # special case for ..
149 pos -= 1
149 pos -= 1
150 break
150 break
151 pos += 1
151 pos += 1
152 sym = program[s:pos]
152 sym = program[s:pos]
153 if sym in keywords: # operator keywords
153 if sym in keywords: # operator keywords
154 yield (sym, None, s)
154 yield (sym, None, s)
155 elif '-' in sym:
155 elif '-' in sym:
156 # some jerk gave us foo-bar-baz, try to check if it's a symbol
156 # some jerk gave us foo-bar-baz, try to check if it's a symbol
157 if lookup and lookup(sym):
157 if lookup and lookup(sym):
158 # looks like a real symbol
158 # looks like a real symbol
159 yield ('symbol', sym, s)
159 yield ('symbol', sym, s)
160 else:
160 else:
161 # looks like an expression
161 # looks like an expression
162 parts = sym.split('-')
162 parts = sym.split('-')
163 for p in parts[:-1]:
163 for p in parts[:-1]:
164 if p: # possible consecutive -
164 if p: # possible consecutive -
165 yield ('symbol', p, s)
165 yield ('symbol', p, s)
166 s += len(p)
166 s += len(p)
167 yield ('-', None, pos)
167 yield ('-', None, pos)
168 s += 1
168 s += 1
169 if parts[-1]: # possible trailing -
169 if parts[-1]: # possible trailing -
170 yield ('symbol', parts[-1], s)
170 yield ('symbol', parts[-1], s)
171 else:
171 else:
172 yield ('symbol', sym, s)
172 yield ('symbol', sym, s)
173 pos -= 1
173 pos -= 1
174 else:
174 else:
175 raise error.ParseError(_("syntax error in revset '%s'") %
175 raise error.ParseError(_("syntax error in revset '%s'") %
176 program, pos)
176 program, pos)
177 pos += 1
177 pos += 1
178 yield ('end', None, pos)
178 yield ('end', None, pos)
179
179
180 # helpers
180 # helpers
181
181
182 _notset = object()
182 _notset = object()
183
183
184 def getsymbol(x):
184 def getsymbol(x):
185 if x and x[0] == 'symbol':
185 if x and x[0] == 'symbol':
186 return x[1]
186 return x[1]
187 raise error.ParseError(_('not a symbol'))
187 raise error.ParseError(_('not a symbol'))
188
188
189 def getstring(x, err):
189 def getstring(x, err):
190 if x and (x[0] == 'string' or x[0] == 'symbol'):
190 if x and (x[0] == 'string' or x[0] == 'symbol'):
191 return x[1]
191 return x[1]
192 raise error.ParseError(err)
192 raise error.ParseError(err)
193
193
194 def getinteger(x, err, default=_notset):
194 def getinteger(x, err, default=_notset):
195 if not x and default is not _notset:
195 if not x and default is not _notset:
196 return default
196 return default
197 try:
197 try:
198 return int(getstring(x, err))
198 return int(getstring(x, err))
199 except ValueError:
199 except ValueError:
200 raise error.ParseError(err)
200 raise error.ParseError(err)
201
201
202 def getboolean(x, err):
203 value = util.parsebool(getsymbol(x))
204 if value is not None:
205 return value
206 raise error.ParseError(err)
207
202 def getlist(x):
208 def getlist(x):
203 if not x:
209 if not x:
204 return []
210 return []
205 if x[0] == 'list':
211 if x[0] == 'list':
206 return list(x[1:])
212 return list(x[1:])
207 return [x]
213 return [x]
208
214
209 def getrange(x, err):
215 def getrange(x, err):
210 if not x:
216 if not x:
211 raise error.ParseError(err)
217 raise error.ParseError(err)
212 op = x[0]
218 op = x[0]
213 if op == 'range':
219 if op == 'range':
214 return x[1], x[2]
220 return x[1], x[2]
215 elif op == 'rangepre':
221 elif op == 'rangepre':
216 return None, x[1]
222 return None, x[1]
217 elif op == 'rangepost':
223 elif op == 'rangepost':
218 return x[1], None
224 return x[1], None
219 elif op == 'rangeall':
225 elif op == 'rangeall':
220 return None, None
226 return None, None
221 raise error.ParseError(err)
227 raise error.ParseError(err)
222
228
223 def getargs(x, min, max, err):
229 def getargs(x, min, max, err):
224 l = getlist(x)
230 l = getlist(x)
225 if len(l) < min or (max >= 0 and len(l) > max):
231 if len(l) < min or (max >= 0 and len(l) > max):
226 raise error.ParseError(err)
232 raise error.ParseError(err)
227 return l
233 return l
228
234
229 def getargsdict(x, funcname, keys):
235 def getargsdict(x, funcname, keys):
230 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
236 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
231 keyvaluenode='keyvalue', keynode='symbol')
237 keyvaluenode='keyvalue', keynode='symbol')
232
238
233 # Constants for ordering requirement, used in _analyze():
239 # Constants for ordering requirement, used in _analyze():
234 #
240 #
235 # If 'define', any nested functions and operations can change the ordering of
241 # If 'define', any nested functions and operations can change the ordering of
236 # the entries in the set. If 'follow', any nested functions and operations
242 # the entries in the set. If 'follow', any nested functions and operations
237 # should take the ordering specified by the first operand to the '&' operator.
243 # should take the ordering specified by the first operand to the '&' operator.
238 #
244 #
239 # For instance,
245 # For instance,
240 #
246 #
241 # X & (Y | Z)
247 # X & (Y | Z)
242 # ^ ^^^^^^^
248 # ^ ^^^^^^^
243 # | follow
249 # | follow
244 # define
250 # define
245 #
251 #
246 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
252 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
247 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
253 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
248 #
254 #
249 # 'any' means the order doesn't matter. For instance,
255 # 'any' means the order doesn't matter. For instance,
250 #
256 #
251 # X & !Y
257 # X & !Y
252 # ^
258 # ^
253 # any
259 # any
254 #
260 #
255 # 'y()' can either enforce its ordering requirement or take the ordering
261 # 'y()' can either enforce its ordering requirement or take the ordering
256 # specified by 'x()' because 'not()' doesn't care the order.
262 # specified by 'x()' because 'not()' doesn't care the order.
257 #
263 #
258 # Transition of ordering requirement:
264 # Transition of ordering requirement:
259 #
265 #
260 # 1. starts with 'define'
266 # 1. starts with 'define'
261 # 2. shifts to 'follow' by 'x & y'
267 # 2. shifts to 'follow' by 'x & y'
262 # 3. changes back to 'define' on function call 'f(x)' or function-like
268 # 3. changes back to 'define' on function call 'f(x)' or function-like
263 # operation 'x (f) y' because 'f' may have its own ordering requirement
269 # operation 'x (f) y' because 'f' may have its own ordering requirement
264 # for 'x' and 'y' (e.g. 'first(x)')
270 # for 'x' and 'y' (e.g. 'first(x)')
265 #
271 #
266 anyorder = 'any' # don't care the order
272 anyorder = 'any' # don't care the order
267 defineorder = 'define' # should define the order
273 defineorder = 'define' # should define the order
268 followorder = 'follow' # must follow the current order
274 followorder = 'follow' # must follow the current order
269
275
270 # transition table for 'x & y', from the current expression 'x' to 'y'
276 # transition table for 'x & y', from the current expression 'x' to 'y'
271 _tofolloworder = {
277 _tofolloworder = {
272 anyorder: anyorder,
278 anyorder: anyorder,
273 defineorder: followorder,
279 defineorder: followorder,
274 followorder: followorder,
280 followorder: followorder,
275 }
281 }
276
282
277 def _matchonly(revs, bases):
283 def _matchonly(revs, bases):
278 """
284 """
279 >>> f = lambda *args: _matchonly(*map(parse, args))
285 >>> f = lambda *args: _matchonly(*map(parse, args))
280 >>> f('ancestors(A)', 'not ancestors(B)')
286 >>> f('ancestors(A)', 'not ancestors(B)')
281 ('list', ('symbol', 'A'), ('symbol', 'B'))
287 ('list', ('symbol', 'A'), ('symbol', 'B'))
282 """
288 """
283 if (revs is not None
289 if (revs is not None
284 and revs[0] == 'func'
290 and revs[0] == 'func'
285 and getsymbol(revs[1]) == 'ancestors'
291 and getsymbol(revs[1]) == 'ancestors'
286 and bases is not None
292 and bases is not None
287 and bases[0] == 'not'
293 and bases[0] == 'not'
288 and bases[1][0] == 'func'
294 and bases[1][0] == 'func'
289 and getsymbol(bases[1][1]) == 'ancestors'):
295 and getsymbol(bases[1][1]) == 'ancestors'):
290 return ('list', revs[2], bases[1][2])
296 return ('list', revs[2], bases[1][2])
291
297
292 def _fixops(x):
298 def _fixops(x):
293 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
299 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
294 handled well by our simple top-down parser"""
300 handled well by our simple top-down parser"""
295 if not isinstance(x, tuple):
301 if not isinstance(x, tuple):
296 return x
302 return x
297
303
298 op = x[0]
304 op = x[0]
299 if op == 'parent':
305 if op == 'parent':
300 # x^:y means (x^) : y, not x ^ (:y)
306 # x^:y means (x^) : y, not x ^ (:y)
301 # x^: means (x^) :, not x ^ (:)
307 # x^: means (x^) :, not x ^ (:)
302 post = ('parentpost', x[1])
308 post = ('parentpost', x[1])
303 if x[2][0] == 'dagrangepre':
309 if x[2][0] == 'dagrangepre':
304 return _fixops(('dagrange', post, x[2][1]))
310 return _fixops(('dagrange', post, x[2][1]))
305 elif x[2][0] == 'rangepre':
311 elif x[2][0] == 'rangepre':
306 return _fixops(('range', post, x[2][1]))
312 return _fixops(('range', post, x[2][1]))
307 elif x[2][0] == 'rangeall':
313 elif x[2][0] == 'rangeall':
308 return _fixops(('rangepost', post))
314 return _fixops(('rangepost', post))
309 elif op == 'or':
315 elif op == 'or':
310 # make number of arguments deterministic:
316 # make number of arguments deterministic:
311 # x + y + z -> (or x y z) -> (or (list x y z))
317 # x + y + z -> (or x y z) -> (or (list x y z))
312 return (op, _fixops(('list',) + x[1:]))
318 return (op, _fixops(('list',) + x[1:]))
313
319
314 return (op,) + tuple(_fixops(y) for y in x[1:])
320 return (op,) + tuple(_fixops(y) for y in x[1:])
315
321
316 def _analyze(x, order):
322 def _analyze(x, order):
317 if x is None:
323 if x is None:
318 return x
324 return x
319
325
320 op = x[0]
326 op = x[0]
321 if op == 'minus':
327 if op == 'minus':
322 return _analyze(('and', x[1], ('not', x[2])), order)
328 return _analyze(('and', x[1], ('not', x[2])), order)
323 elif op == 'only':
329 elif op == 'only':
324 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
330 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
325 return _analyze(t, order)
331 return _analyze(t, order)
326 elif op == 'onlypost':
332 elif op == 'onlypost':
327 return _analyze(('func', ('symbol', 'only'), x[1]), order)
333 return _analyze(('func', ('symbol', 'only'), x[1]), order)
328 elif op == 'dagrangepre':
334 elif op == 'dagrangepre':
329 return _analyze(('func', ('symbol', 'ancestors'), x[1]), order)
335 return _analyze(('func', ('symbol', 'ancestors'), x[1]), order)
330 elif op == 'dagrangepost':
336 elif op == 'dagrangepost':
331 return _analyze(('func', ('symbol', 'descendants'), x[1]), order)
337 return _analyze(('func', ('symbol', 'descendants'), x[1]), order)
332 elif op == 'negate':
338 elif op == 'negate':
333 s = getstring(x[1], _("can't negate that"))
339 s = getstring(x[1], _("can't negate that"))
334 return _analyze(('string', '-' + s), order)
340 return _analyze(('string', '-' + s), order)
335 elif op in ('string', 'symbol'):
341 elif op in ('string', 'symbol'):
336 return x
342 return x
337 elif op == 'and':
343 elif op == 'and':
338 ta = _analyze(x[1], order)
344 ta = _analyze(x[1], order)
339 tb = _analyze(x[2], _tofolloworder[order])
345 tb = _analyze(x[2], _tofolloworder[order])
340 return (op, ta, tb, order)
346 return (op, ta, tb, order)
341 elif op == 'or':
347 elif op == 'or':
342 return (op, _analyze(x[1], order), order)
348 return (op, _analyze(x[1], order), order)
343 elif op == 'not':
349 elif op == 'not':
344 return (op, _analyze(x[1], anyorder), order)
350 return (op, _analyze(x[1], anyorder), order)
345 elif op == 'rangeall':
351 elif op == 'rangeall':
346 return (op, None, order)
352 return (op, None, order)
347 elif op in ('rangepre', 'rangepost', 'parentpost'):
353 elif op in ('rangepre', 'rangepost', 'parentpost'):
348 return (op, _analyze(x[1], defineorder), order)
354 return (op, _analyze(x[1], defineorder), order)
349 elif op == 'group':
355 elif op == 'group':
350 return _analyze(x[1], order)
356 return _analyze(x[1], order)
351 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
357 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
352 ta = _analyze(x[1], defineorder)
358 ta = _analyze(x[1], defineorder)
353 tb = _analyze(x[2], defineorder)
359 tb = _analyze(x[2], defineorder)
354 return (op, ta, tb, order)
360 return (op, ta, tb, order)
355 elif op == 'list':
361 elif op == 'list':
356 return (op,) + tuple(_analyze(y, order) for y in x[1:])
362 return (op,) + tuple(_analyze(y, order) for y in x[1:])
357 elif op == 'keyvalue':
363 elif op == 'keyvalue':
358 return (op, x[1], _analyze(x[2], order))
364 return (op, x[1], _analyze(x[2], order))
359 elif op == 'func':
365 elif op == 'func':
360 f = getsymbol(x[1])
366 f = getsymbol(x[1])
361 d = defineorder
367 d = defineorder
362 if f == 'present':
368 if f == 'present':
363 # 'present(set)' is known to return the argument set with no
369 # 'present(set)' is known to return the argument set with no
364 # modification, so forward the current order to its argument
370 # modification, so forward the current order to its argument
365 d = order
371 d = order
366 return (op, x[1], _analyze(x[2], d), order)
372 return (op, x[1], _analyze(x[2], d), order)
367 raise ValueError('invalid operator %r' % op)
373 raise ValueError('invalid operator %r' % op)
368
374
369 def analyze(x, order=defineorder):
375 def analyze(x, order=defineorder):
370 """Transform raw parsed tree to evaluatable tree which can be fed to
376 """Transform raw parsed tree to evaluatable tree which can be fed to
371 optimize() or getset()
377 optimize() or getset()
372
378
373 All pseudo operations should be mapped to real operations or functions
379 All pseudo operations should be mapped to real operations or functions
374 defined in methods or symbols table respectively.
380 defined in methods or symbols table respectively.
375
381
376 'order' specifies how the current expression 'x' is ordered (see the
382 'order' specifies how the current expression 'x' is ordered (see the
377 constants defined above.)
383 constants defined above.)
378 """
384 """
379 return _analyze(x, order)
385 return _analyze(x, order)
380
386
381 def _optimize(x, small):
387 def _optimize(x, small):
382 if x is None:
388 if x is None:
383 return 0, x
389 return 0, x
384
390
385 smallbonus = 1
391 smallbonus = 1
386 if small:
392 if small:
387 smallbonus = .5
393 smallbonus = .5
388
394
389 op = x[0]
395 op = x[0]
390 if op in ('string', 'symbol'):
396 if op in ('string', 'symbol'):
391 return smallbonus, x # single revisions are small
397 return smallbonus, x # single revisions are small
392 elif op == 'and':
398 elif op == 'and':
393 wa, ta = _optimize(x[1], True)
399 wa, ta = _optimize(x[1], True)
394 wb, tb = _optimize(x[2], True)
400 wb, tb = _optimize(x[2], True)
395 order = x[3]
401 order = x[3]
396 w = min(wa, wb)
402 w = min(wa, wb)
397
403
398 # (::x and not ::y)/(not ::y and ::x) have a fast path
404 # (::x and not ::y)/(not ::y and ::x) have a fast path
399 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
405 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
400 if tm:
406 if tm:
401 return w, ('func', ('symbol', 'only'), tm, order)
407 return w, ('func', ('symbol', 'only'), tm, order)
402
408
403 if tb is not None and tb[0] == 'not':
409 if tb is not None and tb[0] == 'not':
404 return wa, ('difference', ta, tb[1], order)
410 return wa, ('difference', ta, tb[1], order)
405
411
406 if wa > wb:
412 if wa > wb:
407 return w, (op, tb, ta, order)
413 return w, (op, tb, ta, order)
408 return w, (op, ta, tb, order)
414 return w, (op, ta, tb, order)
409 elif op == 'or':
415 elif op == 'or':
410 # fast path for machine-generated expression, that is likely to have
416 # fast path for machine-generated expression, that is likely to have
411 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
417 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
412 order = x[2]
418 order = x[2]
413 ws, ts, ss = [], [], []
419 ws, ts, ss = [], [], []
414 def flushss():
420 def flushss():
415 if not ss:
421 if not ss:
416 return
422 return
417 if len(ss) == 1:
423 if len(ss) == 1:
418 w, t = ss[0]
424 w, t = ss[0]
419 else:
425 else:
420 s = '\0'.join(t[1] for w, t in ss)
426 s = '\0'.join(t[1] for w, t in ss)
421 y = ('func', ('symbol', '_list'), ('string', s), order)
427 y = ('func', ('symbol', '_list'), ('string', s), order)
422 w, t = _optimize(y, False)
428 w, t = _optimize(y, False)
423 ws.append(w)
429 ws.append(w)
424 ts.append(t)
430 ts.append(t)
425 del ss[:]
431 del ss[:]
426 for y in getlist(x[1]):
432 for y in getlist(x[1]):
427 w, t = _optimize(y, False)
433 w, t = _optimize(y, False)
428 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
434 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
429 ss.append((w, t))
435 ss.append((w, t))
430 continue
436 continue
431 flushss()
437 flushss()
432 ws.append(w)
438 ws.append(w)
433 ts.append(t)
439 ts.append(t)
434 flushss()
440 flushss()
435 if len(ts) == 1:
441 if len(ts) == 1:
436 return ws[0], ts[0] # 'or' operation is fully optimized out
442 return ws[0], ts[0] # 'or' operation is fully optimized out
437 if order != defineorder:
443 if order != defineorder:
438 # reorder by weight only when f(a + b) == f(b + a)
444 # reorder by weight only when f(a + b) == f(b + a)
439 ts = [wt[1] for wt in sorted(zip(ws, ts), key=lambda wt: wt[0])]
445 ts = [wt[1] for wt in sorted(zip(ws, ts), key=lambda wt: wt[0])]
440 return max(ws), (op, ('list',) + tuple(ts), order)
446 return max(ws), (op, ('list',) + tuple(ts), order)
441 elif op == 'not':
447 elif op == 'not':
442 # Optimize not public() to _notpublic() because we have a fast version
448 # Optimize not public() to _notpublic() because we have a fast version
443 if x[1][:3] == ('func', ('symbol', 'public'), None):
449 if x[1][:3] == ('func', ('symbol', 'public'), None):
444 order = x[1][3]
450 order = x[1][3]
445 newsym = ('func', ('symbol', '_notpublic'), None, order)
451 newsym = ('func', ('symbol', '_notpublic'), None, order)
446 o = _optimize(newsym, not small)
452 o = _optimize(newsym, not small)
447 return o[0], o[1]
453 return o[0], o[1]
448 else:
454 else:
449 o = _optimize(x[1], not small)
455 o = _optimize(x[1], not small)
450 order = x[2]
456 order = x[2]
451 return o[0], (op, o[1], order)
457 return o[0], (op, o[1], order)
452 elif op == 'rangeall':
458 elif op == 'rangeall':
453 return smallbonus, x
459 return smallbonus, x
454 elif op in ('rangepre', 'rangepost', 'parentpost'):
460 elif op in ('rangepre', 'rangepost', 'parentpost'):
455 o = _optimize(x[1], small)
461 o = _optimize(x[1], small)
456 order = x[2]
462 order = x[2]
457 return o[0], (op, o[1], order)
463 return o[0], (op, o[1], order)
458 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
464 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
459 wa, ta = _optimize(x[1], small)
465 wa, ta = _optimize(x[1], small)
460 wb, tb = _optimize(x[2], small)
466 wb, tb = _optimize(x[2], small)
461 order = x[3]
467 order = x[3]
462 return wa + wb, (op, ta, tb, order)
468 return wa + wb, (op, ta, tb, order)
463 elif op == 'list':
469 elif op == 'list':
464 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
470 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
465 return sum(ws), (op,) + ts
471 return sum(ws), (op,) + ts
466 elif op == 'keyvalue':
472 elif op == 'keyvalue':
467 w, t = _optimize(x[2], small)
473 w, t = _optimize(x[2], small)
468 return w, (op, x[1], t)
474 return w, (op, x[1], t)
469 elif op == 'func':
475 elif op == 'func':
470 f = getsymbol(x[1])
476 f = getsymbol(x[1])
471 wa, ta = _optimize(x[2], small)
477 wa, ta = _optimize(x[2], small)
472 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
478 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
473 'keyword', 'outgoing', 'user', 'destination'):
479 'keyword', 'outgoing', 'user', 'destination'):
474 w = 10 # slow
480 w = 10 # slow
475 elif f in ('modifies', 'adds', 'removes'):
481 elif f in ('modifies', 'adds', 'removes'):
476 w = 30 # slower
482 w = 30 # slower
477 elif f == "contains":
483 elif f == "contains":
478 w = 100 # very slow
484 w = 100 # very slow
479 elif f == "ancestor":
485 elif f == "ancestor":
480 w = 1 * smallbonus
486 w = 1 * smallbonus
481 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
487 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
482 w = 0
488 w = 0
483 elif f == "sort":
489 elif f == "sort":
484 w = 10 # assume most sorts look at changelog
490 w = 10 # assume most sorts look at changelog
485 else:
491 else:
486 w = 1
492 w = 1
487 order = x[3]
493 order = x[3]
488 return w + wa, (op, x[1], ta, order)
494 return w + wa, (op, x[1], ta, order)
489 raise ValueError('invalid operator %r' % op)
495 raise ValueError('invalid operator %r' % op)
490
496
491 def optimize(tree):
497 def optimize(tree):
492 """Optimize evaluatable tree
498 """Optimize evaluatable tree
493
499
494 All pseudo operations should be transformed beforehand.
500 All pseudo operations should be transformed beforehand.
495 """
501 """
496 _weight, newtree = _optimize(tree, small=True)
502 _weight, newtree = _optimize(tree, small=True)
497 return newtree
503 return newtree
498
504
499 # the set of valid characters for the initial letter of symbols in
505 # the set of valid characters for the initial letter of symbols in
500 # alias declarations and definitions
506 # alias declarations and definitions
501 _aliassyminitletters = _syminitletters | set(pycompat.sysstr('$'))
507 _aliassyminitletters = _syminitletters | set(pycompat.sysstr('$'))
502
508
503 def _parsewith(spec, lookup=None, syminitletters=None):
509 def _parsewith(spec, lookup=None, syminitletters=None):
504 """Generate a parse tree of given spec with given tokenizing options
510 """Generate a parse tree of given spec with given tokenizing options
505
511
506 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
512 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
507 ('func', ('symbol', 'foo'), ('symbol', '$1'))
513 ('func', ('symbol', 'foo'), ('symbol', '$1'))
508 >>> _parsewith('$1')
514 >>> _parsewith('$1')
509 Traceback (most recent call last):
515 Traceback (most recent call last):
510 ...
516 ...
511 ParseError: ("syntax error in revset '$1'", 0)
517 ParseError: ("syntax error in revset '$1'", 0)
512 >>> _parsewith('foo bar')
518 >>> _parsewith('foo bar')
513 Traceback (most recent call last):
519 Traceback (most recent call last):
514 ...
520 ...
515 ParseError: ('invalid token', 4)
521 ParseError: ('invalid token', 4)
516 """
522 """
517 p = parser.parser(elements)
523 p = parser.parser(elements)
518 tree, pos = p.parse(tokenize(spec, lookup=lookup,
524 tree, pos = p.parse(tokenize(spec, lookup=lookup,
519 syminitletters=syminitletters))
525 syminitletters=syminitletters))
520 if pos != len(spec):
526 if pos != len(spec):
521 raise error.ParseError(_('invalid token'), pos)
527 raise error.ParseError(_('invalid token'), pos)
522 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
528 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
523
529
524 class _aliasrules(parser.basealiasrules):
530 class _aliasrules(parser.basealiasrules):
525 """Parsing and expansion rule set of revset aliases"""
531 """Parsing and expansion rule set of revset aliases"""
526 _section = _('revset alias')
532 _section = _('revset alias')
527
533
528 @staticmethod
534 @staticmethod
529 def _parse(spec):
535 def _parse(spec):
530 """Parse alias declaration/definition ``spec``
536 """Parse alias declaration/definition ``spec``
531
537
532 This allows symbol names to use also ``$`` as an initial letter
538 This allows symbol names to use also ``$`` as an initial letter
533 (for backward compatibility), and callers of this function should
539 (for backward compatibility), and callers of this function should
534 examine whether ``$`` is used also for unexpected symbols or not.
540 examine whether ``$`` is used also for unexpected symbols or not.
535 """
541 """
536 return _parsewith(spec, syminitletters=_aliassyminitletters)
542 return _parsewith(spec, syminitletters=_aliassyminitletters)
537
543
538 @staticmethod
544 @staticmethod
539 def _trygetfunc(tree):
545 def _trygetfunc(tree):
540 if tree[0] == 'func' and tree[1][0] == 'symbol':
546 if tree[0] == 'func' and tree[1][0] == 'symbol':
541 return tree[1][1], getlist(tree[2])
547 return tree[1][1], getlist(tree[2])
542
548
543 def expandaliases(ui, tree):
549 def expandaliases(ui, tree):
544 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
550 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
545 tree = _aliasrules.expand(aliases, tree)
551 tree = _aliasrules.expand(aliases, tree)
546 # warn about problematic (but not referred) aliases
552 # warn about problematic (but not referred) aliases
547 for name, alias in sorted(aliases.iteritems()):
553 for name, alias in sorted(aliases.iteritems()):
548 if alias.error and not alias.warned:
554 if alias.error and not alias.warned:
549 ui.warn(_('warning: %s\n') % (alias.error))
555 ui.warn(_('warning: %s\n') % (alias.error))
550 alias.warned = True
556 alias.warned = True
551 return tree
557 return tree
552
558
553 def foldconcat(tree):
559 def foldconcat(tree):
554 """Fold elements to be concatenated by `##`
560 """Fold elements to be concatenated by `##`
555 """
561 """
556 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
562 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
557 return tree
563 return tree
558 if tree[0] == '_concat':
564 if tree[0] == '_concat':
559 pending = [tree]
565 pending = [tree]
560 l = []
566 l = []
561 while pending:
567 while pending:
562 e = pending.pop()
568 e = pending.pop()
563 if e[0] == '_concat':
569 if e[0] == '_concat':
564 pending.extend(reversed(e[1:]))
570 pending.extend(reversed(e[1:]))
565 elif e[0] in ('string', 'symbol'):
571 elif e[0] in ('string', 'symbol'):
566 l.append(e[1])
572 l.append(e[1])
567 else:
573 else:
568 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
574 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
569 raise error.ParseError(msg)
575 raise error.ParseError(msg)
570 return ('string', ''.join(l))
576 return ('string', ''.join(l))
571 else:
577 else:
572 return tuple(foldconcat(t) for t in tree)
578 return tuple(foldconcat(t) for t in tree)
573
579
574 def parse(spec, lookup=None):
580 def parse(spec, lookup=None):
575 return _parsewith(spec, lookup=lookup)
581 return _parsewith(spec, lookup=lookup)
576
582
577 def _quote(s):
583 def _quote(s):
578 r"""Quote a value in order to make it safe for the revset engine.
584 r"""Quote a value in order to make it safe for the revset engine.
579
585
580 >>> _quote('asdf')
586 >>> _quote('asdf')
581 "'asdf'"
587 "'asdf'"
582 >>> _quote("asdf'\"")
588 >>> _quote("asdf'\"")
583 '\'asdf\\\'"\''
589 '\'asdf\\\'"\''
584 >>> _quote('asdf\'')
590 >>> _quote('asdf\'')
585 "'asdf\\''"
591 "'asdf\\''"
586 >>> _quote(1)
592 >>> _quote(1)
587 "'1'"
593 "'1'"
588 """
594 """
589 return "'%s'" % util.escapestr(pycompat.bytestr(s))
595 return "'%s'" % util.escapestr(pycompat.bytestr(s))
590
596
591 def formatspec(expr, *args):
597 def formatspec(expr, *args):
592 '''
598 '''
593 This is a convenience function for using revsets internally, and
599 This is a convenience function for using revsets internally, and
594 escapes arguments appropriately. Aliases are intentionally ignored
600 escapes arguments appropriately. Aliases are intentionally ignored
595 so that intended expression behavior isn't accidentally subverted.
601 so that intended expression behavior isn't accidentally subverted.
596
602
597 Supported arguments:
603 Supported arguments:
598
604
599 %r = revset expression, parenthesized
605 %r = revset expression, parenthesized
600 %d = int(arg), no quoting
606 %d = int(arg), no quoting
601 %s = string(arg), escaped and single-quoted
607 %s = string(arg), escaped and single-quoted
602 %b = arg.branch(), escaped and single-quoted
608 %b = arg.branch(), escaped and single-quoted
603 %n = hex(arg), single-quoted
609 %n = hex(arg), single-quoted
604 %% = a literal '%'
610 %% = a literal '%'
605
611
606 Prefixing the type with 'l' specifies a parenthesized list of that type.
612 Prefixing the type with 'l' specifies a parenthesized list of that type.
607
613
608 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
614 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
609 '(10 or 11):: and ((this()) or (that()))'
615 '(10 or 11):: and ((this()) or (that()))'
610 >>> formatspec('%d:: and not %d::', 10, 20)
616 >>> formatspec('%d:: and not %d::', 10, 20)
611 '10:: and not 20::'
617 '10:: and not 20::'
612 >>> formatspec('%ld or %ld', [], [1])
618 >>> formatspec('%ld or %ld', [], [1])
613 "_list('') or 1"
619 "_list('') or 1"
614 >>> formatspec('keyword(%s)', 'foo\\xe9')
620 >>> formatspec('keyword(%s)', 'foo\\xe9')
615 "keyword('foo\\\\xe9')"
621 "keyword('foo\\\\xe9')"
616 >>> b = lambda: 'default'
622 >>> b = lambda: 'default'
617 >>> b.branch = b
623 >>> b.branch = b
618 >>> formatspec('branch(%b)', b)
624 >>> formatspec('branch(%b)', b)
619 "branch('default')"
625 "branch('default')"
620 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
626 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
621 "root(_list('a\\x00b\\x00c\\x00d'))"
627 "root(_list('a\\x00b\\x00c\\x00d'))"
622 '''
628 '''
623
629
624 def argtype(c, arg):
630 def argtype(c, arg):
625 if c == 'd':
631 if c == 'd':
626 return '%d' % int(arg)
632 return '%d' % int(arg)
627 elif c == 's':
633 elif c == 's':
628 return _quote(arg)
634 return _quote(arg)
629 elif c == 'r':
635 elif c == 'r':
630 parse(arg) # make sure syntax errors are confined
636 parse(arg) # make sure syntax errors are confined
631 return '(%s)' % arg
637 return '(%s)' % arg
632 elif c == 'n':
638 elif c == 'n':
633 return _quote(node.hex(arg))
639 return _quote(node.hex(arg))
634 elif c == 'b':
640 elif c == 'b':
635 return _quote(arg.branch())
641 return _quote(arg.branch())
636
642
637 def listexp(s, t):
643 def listexp(s, t):
638 l = len(s)
644 l = len(s)
639 if l == 0:
645 if l == 0:
640 return "_list('')"
646 return "_list('')"
641 elif l == 1:
647 elif l == 1:
642 return argtype(t, s[0])
648 return argtype(t, s[0])
643 elif t == 'd':
649 elif t == 'd':
644 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
650 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
645 elif t == 's':
651 elif t == 's':
646 return "_list('%s')" % "\0".join(s)
652 return "_list('%s')" % "\0".join(s)
647 elif t == 'n':
653 elif t == 'n':
648 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
654 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
649 elif t == 'b':
655 elif t == 'b':
650 return "_list('%s')" % "\0".join(a.branch() for a in s)
656 return "_list('%s')" % "\0".join(a.branch() for a in s)
651
657
652 m = l // 2
658 m = l // 2
653 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
659 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
654
660
655 expr = pycompat.bytestr(expr)
661 expr = pycompat.bytestr(expr)
656 ret = ''
662 ret = ''
657 pos = 0
663 pos = 0
658 arg = 0
664 arg = 0
659 while pos < len(expr):
665 while pos < len(expr):
660 c = expr[pos]
666 c = expr[pos]
661 if c == '%':
667 if c == '%':
662 pos += 1
668 pos += 1
663 d = expr[pos]
669 d = expr[pos]
664 if d == '%':
670 if d == '%':
665 ret += d
671 ret += d
666 elif d in 'dsnbr':
672 elif d in 'dsnbr':
667 ret += argtype(d, args[arg])
673 ret += argtype(d, args[arg])
668 arg += 1
674 arg += 1
669 elif d == 'l':
675 elif d == 'l':
670 # a list of some type
676 # a list of some type
671 pos += 1
677 pos += 1
672 d = expr[pos]
678 d = expr[pos]
673 ret += listexp(list(args[arg]), d)
679 ret += listexp(list(args[arg]), d)
674 arg += 1
680 arg += 1
675 else:
681 else:
676 raise error.Abort(_('unexpected revspec format character %s')
682 raise error.Abort(_('unexpected revspec format character %s')
677 % d)
683 % d)
678 else:
684 else:
679 ret += c
685 ret += c
680 pos += 1
686 pos += 1
681
687
682 return ret
688 return ret
683
689
684 def prettyformat(tree):
690 def prettyformat(tree):
685 return parser.prettyformat(tree, ('string', 'symbol'))
691 return parser.prettyformat(tree, ('string', 'symbol'))
686
692
687 def depth(tree):
693 def depth(tree):
688 if isinstance(tree, tuple):
694 if isinstance(tree, tuple):
689 return max(map(depth, tree)) + 1
695 return max(map(depth, tree)) + 1
690 else:
696 else:
691 return 0
697 return 0
692
698
693 def funcsused(tree):
699 def funcsused(tree):
694 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
700 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
695 return set()
701 return set()
696 else:
702 else:
697 funcs = set()
703 funcs = set()
698 for s in tree[1:]:
704 for s in tree[1:]:
699 funcs |= funcsused(s)
705 funcs |= funcsused(s)
700 if tree[0] == 'func':
706 if tree[0] == 'func':
701 funcs.add(tree[1][1])
707 funcs.add(tree[1][1])
702 return funcs
708 return funcs
General Comments 0
You need to be logged in to leave comments. Login now