##// END OF EJS Templates
revsetlang: fix _quote on int on python3...
Augie Fackler -
r31636:f3b15127 default
parent child Browse files
Show More
@@ -1,702 +1,702 b''
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 getlist(x):
202 def getlist(x):
203 if not x:
203 if not x:
204 return []
204 return []
205 if x[0] == 'list':
205 if x[0] == 'list':
206 return list(x[1:])
206 return list(x[1:])
207 return [x]
207 return [x]
208
208
209 def getrange(x, err):
209 def getrange(x, err):
210 if not x:
210 if not x:
211 raise error.ParseError(err)
211 raise error.ParseError(err)
212 op = x[0]
212 op = x[0]
213 if op == 'range':
213 if op == 'range':
214 return x[1], x[2]
214 return x[1], x[2]
215 elif op == 'rangepre':
215 elif op == 'rangepre':
216 return None, x[1]
216 return None, x[1]
217 elif op == 'rangepost':
217 elif op == 'rangepost':
218 return x[1], None
218 return x[1], None
219 elif op == 'rangeall':
219 elif op == 'rangeall':
220 return None, None
220 return None, None
221 raise error.ParseError(err)
221 raise error.ParseError(err)
222
222
223 def getargs(x, min, max, err):
223 def getargs(x, min, max, err):
224 l = getlist(x)
224 l = getlist(x)
225 if len(l) < min or (max >= 0 and len(l) > max):
225 if len(l) < min or (max >= 0 and len(l) > max):
226 raise error.ParseError(err)
226 raise error.ParseError(err)
227 return l
227 return l
228
228
229 def getargsdict(x, funcname, keys):
229 def getargsdict(x, funcname, keys):
230 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
230 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
231 keyvaluenode='keyvalue', keynode='symbol')
231 keyvaluenode='keyvalue', keynode='symbol')
232
232
233 # Constants for ordering requirement, used in _analyze():
233 # Constants for ordering requirement, used in _analyze():
234 #
234 #
235 # If 'define', any nested functions and operations can change the ordering of
235 # 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
236 # 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.
237 # should take the ordering specified by the first operand to the '&' operator.
238 #
238 #
239 # For instance,
239 # For instance,
240 #
240 #
241 # X & (Y | Z)
241 # X & (Y | Z)
242 # ^ ^^^^^^^
242 # ^ ^^^^^^^
243 # | follow
243 # | follow
244 # define
244 # define
245 #
245 #
246 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
246 # 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.
247 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
248 #
248 #
249 # 'any' means the order doesn't matter. For instance,
249 # 'any' means the order doesn't matter. For instance,
250 #
250 #
251 # X & !Y
251 # X & !Y
252 # ^
252 # ^
253 # any
253 # any
254 #
254 #
255 # 'y()' can either enforce its ordering requirement or take the ordering
255 # 'y()' can either enforce its ordering requirement or take the ordering
256 # specified by 'x()' because 'not()' doesn't care the order.
256 # specified by 'x()' because 'not()' doesn't care the order.
257 #
257 #
258 # Transition of ordering requirement:
258 # Transition of ordering requirement:
259 #
259 #
260 # 1. starts with 'define'
260 # 1. starts with 'define'
261 # 2. shifts to 'follow' by 'x & y'
261 # 2. shifts to 'follow' by 'x & y'
262 # 3. changes back to 'define' on function call 'f(x)' or function-like
262 # 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
263 # operation 'x (f) y' because 'f' may have its own ordering requirement
264 # for 'x' and 'y' (e.g. 'first(x)')
264 # for 'x' and 'y' (e.g. 'first(x)')
265 #
265 #
266 anyorder = 'any' # don't care the order
266 anyorder = 'any' # don't care the order
267 defineorder = 'define' # should define the order
267 defineorder = 'define' # should define the order
268 followorder = 'follow' # must follow the current order
268 followorder = 'follow' # must follow the current order
269
269
270 # transition table for 'x & y', from the current expression 'x' to 'y'
270 # transition table for 'x & y', from the current expression 'x' to 'y'
271 _tofolloworder = {
271 _tofolloworder = {
272 anyorder: anyorder,
272 anyorder: anyorder,
273 defineorder: followorder,
273 defineorder: followorder,
274 followorder: followorder,
274 followorder: followorder,
275 }
275 }
276
276
277 def _matchonly(revs, bases):
277 def _matchonly(revs, bases):
278 """
278 """
279 >>> f = lambda *args: _matchonly(*map(parse, args))
279 >>> f = lambda *args: _matchonly(*map(parse, args))
280 >>> f('ancestors(A)', 'not ancestors(B)')
280 >>> f('ancestors(A)', 'not ancestors(B)')
281 ('list', ('symbol', 'A'), ('symbol', 'B'))
281 ('list', ('symbol', 'A'), ('symbol', 'B'))
282 """
282 """
283 if (revs is not None
283 if (revs is not None
284 and revs[0] == 'func'
284 and revs[0] == 'func'
285 and getsymbol(revs[1]) == 'ancestors'
285 and getsymbol(revs[1]) == 'ancestors'
286 and bases is not None
286 and bases is not None
287 and bases[0] == 'not'
287 and bases[0] == 'not'
288 and bases[1][0] == 'func'
288 and bases[1][0] == 'func'
289 and getsymbol(bases[1][1]) == 'ancestors'):
289 and getsymbol(bases[1][1]) == 'ancestors'):
290 return ('list', revs[2], bases[1][2])
290 return ('list', revs[2], bases[1][2])
291
291
292 def _fixops(x):
292 def _fixops(x):
293 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
293 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
294 handled well by our simple top-down parser"""
294 handled well by our simple top-down parser"""
295 if not isinstance(x, tuple):
295 if not isinstance(x, tuple):
296 return x
296 return x
297
297
298 op = x[0]
298 op = x[0]
299 if op == 'parent':
299 if op == 'parent':
300 # x^:y means (x^) : y, not x ^ (:y)
300 # x^:y means (x^) : y, not x ^ (:y)
301 # x^: means (x^) :, not x ^ (:)
301 # x^: means (x^) :, not x ^ (:)
302 post = ('parentpost', x[1])
302 post = ('parentpost', x[1])
303 if x[2][0] == 'dagrangepre':
303 if x[2][0] == 'dagrangepre':
304 return _fixops(('dagrange', post, x[2][1]))
304 return _fixops(('dagrange', post, x[2][1]))
305 elif x[2][0] == 'rangepre':
305 elif x[2][0] == 'rangepre':
306 return _fixops(('range', post, x[2][1]))
306 return _fixops(('range', post, x[2][1]))
307 elif x[2][0] == 'rangeall':
307 elif x[2][0] == 'rangeall':
308 return _fixops(('rangepost', post))
308 return _fixops(('rangepost', post))
309 elif op == 'or':
309 elif op == 'or':
310 # make number of arguments deterministic:
310 # make number of arguments deterministic:
311 # x + y + z -> (or x y z) -> (or (list x y z))
311 # x + y + z -> (or x y z) -> (or (list x y z))
312 return (op, _fixops(('list',) + x[1:]))
312 return (op, _fixops(('list',) + x[1:]))
313
313
314 return (op,) + tuple(_fixops(y) for y in x[1:])
314 return (op,) + tuple(_fixops(y) for y in x[1:])
315
315
316 def _analyze(x, order):
316 def _analyze(x, order):
317 if x is None:
317 if x is None:
318 return x
318 return x
319
319
320 op = x[0]
320 op = x[0]
321 if op == 'minus':
321 if op == 'minus':
322 return _analyze(('and', x[1], ('not', x[2])), order)
322 return _analyze(('and', x[1], ('not', x[2])), order)
323 elif op == 'only':
323 elif op == 'only':
324 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
324 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
325 return _analyze(t, order)
325 return _analyze(t, order)
326 elif op == 'onlypost':
326 elif op == 'onlypost':
327 return _analyze(('func', ('symbol', 'only'), x[1]), order)
327 return _analyze(('func', ('symbol', 'only'), x[1]), order)
328 elif op == 'dagrangepre':
328 elif op == 'dagrangepre':
329 return _analyze(('func', ('symbol', 'ancestors'), x[1]), order)
329 return _analyze(('func', ('symbol', 'ancestors'), x[1]), order)
330 elif op == 'dagrangepost':
330 elif op == 'dagrangepost':
331 return _analyze(('func', ('symbol', 'descendants'), x[1]), order)
331 return _analyze(('func', ('symbol', 'descendants'), x[1]), order)
332 elif op == 'negate':
332 elif op == 'negate':
333 s = getstring(x[1], _("can't negate that"))
333 s = getstring(x[1], _("can't negate that"))
334 return _analyze(('string', '-' + s), order)
334 return _analyze(('string', '-' + s), order)
335 elif op in ('string', 'symbol'):
335 elif op in ('string', 'symbol'):
336 return x
336 return x
337 elif op == 'and':
337 elif op == 'and':
338 ta = _analyze(x[1], order)
338 ta = _analyze(x[1], order)
339 tb = _analyze(x[2], _tofolloworder[order])
339 tb = _analyze(x[2], _tofolloworder[order])
340 return (op, ta, tb, order)
340 return (op, ta, tb, order)
341 elif op == 'or':
341 elif op == 'or':
342 return (op, _analyze(x[1], order), order)
342 return (op, _analyze(x[1], order), order)
343 elif op == 'not':
343 elif op == 'not':
344 return (op, _analyze(x[1], anyorder), order)
344 return (op, _analyze(x[1], anyorder), order)
345 elif op == 'rangeall':
345 elif op == 'rangeall':
346 return (op, None, order)
346 return (op, None, order)
347 elif op in ('rangepre', 'rangepost', 'parentpost'):
347 elif op in ('rangepre', 'rangepost', 'parentpost'):
348 return (op, _analyze(x[1], defineorder), order)
348 return (op, _analyze(x[1], defineorder), order)
349 elif op == 'group':
349 elif op == 'group':
350 return _analyze(x[1], order)
350 return _analyze(x[1], order)
351 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
351 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
352 ta = _analyze(x[1], defineorder)
352 ta = _analyze(x[1], defineorder)
353 tb = _analyze(x[2], defineorder)
353 tb = _analyze(x[2], defineorder)
354 return (op, ta, tb, order)
354 return (op, ta, tb, order)
355 elif op == 'list':
355 elif op == 'list':
356 return (op,) + tuple(_analyze(y, order) for y in x[1:])
356 return (op,) + tuple(_analyze(y, order) for y in x[1:])
357 elif op == 'keyvalue':
357 elif op == 'keyvalue':
358 return (op, x[1], _analyze(x[2], order))
358 return (op, x[1], _analyze(x[2], order))
359 elif op == 'func':
359 elif op == 'func':
360 f = getsymbol(x[1])
360 f = getsymbol(x[1])
361 d = defineorder
361 d = defineorder
362 if f == 'present':
362 if f == 'present':
363 # 'present(set)' is known to return the argument set with no
363 # 'present(set)' is known to return the argument set with no
364 # modification, so forward the current order to its argument
364 # modification, so forward the current order to its argument
365 d = order
365 d = order
366 return (op, x[1], _analyze(x[2], d), order)
366 return (op, x[1], _analyze(x[2], d), order)
367 raise ValueError('invalid operator %r' % op)
367 raise ValueError('invalid operator %r' % op)
368
368
369 def analyze(x, order=defineorder):
369 def analyze(x, order=defineorder):
370 """Transform raw parsed tree to evaluatable tree which can be fed to
370 """Transform raw parsed tree to evaluatable tree which can be fed to
371 optimize() or getset()
371 optimize() or getset()
372
372
373 All pseudo operations should be mapped to real operations or functions
373 All pseudo operations should be mapped to real operations or functions
374 defined in methods or symbols table respectively.
374 defined in methods or symbols table respectively.
375
375
376 'order' specifies how the current expression 'x' is ordered (see the
376 'order' specifies how the current expression 'x' is ordered (see the
377 constants defined above.)
377 constants defined above.)
378 """
378 """
379 return _analyze(x, order)
379 return _analyze(x, order)
380
380
381 def _optimize(x, small):
381 def _optimize(x, small):
382 if x is None:
382 if x is None:
383 return 0, x
383 return 0, x
384
384
385 smallbonus = 1
385 smallbonus = 1
386 if small:
386 if small:
387 smallbonus = .5
387 smallbonus = .5
388
388
389 op = x[0]
389 op = x[0]
390 if op in ('string', 'symbol'):
390 if op in ('string', 'symbol'):
391 return smallbonus, x # single revisions are small
391 return smallbonus, x # single revisions are small
392 elif op == 'and':
392 elif op == 'and':
393 wa, ta = _optimize(x[1], True)
393 wa, ta = _optimize(x[1], True)
394 wb, tb = _optimize(x[2], True)
394 wb, tb = _optimize(x[2], True)
395 order = x[3]
395 order = x[3]
396 w = min(wa, wb)
396 w = min(wa, wb)
397
397
398 # (::x and not ::y)/(not ::y and ::x) have a fast path
398 # (::x and not ::y)/(not ::y and ::x) have a fast path
399 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
399 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
400 if tm:
400 if tm:
401 return w, ('func', ('symbol', 'only'), tm, order)
401 return w, ('func', ('symbol', 'only'), tm, order)
402
402
403 if tb is not None and tb[0] == 'not':
403 if tb is not None and tb[0] == 'not':
404 return wa, ('difference', ta, tb[1], order)
404 return wa, ('difference', ta, tb[1], order)
405
405
406 if wa > wb:
406 if wa > wb:
407 return w, (op, tb, ta, order)
407 return w, (op, tb, ta, order)
408 return w, (op, ta, tb, order)
408 return w, (op, ta, tb, order)
409 elif op == 'or':
409 elif op == 'or':
410 # fast path for machine-generated expression, that is likely to have
410 # fast path for machine-generated expression, that is likely to have
411 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
411 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
412 order = x[2]
412 order = x[2]
413 ws, ts, ss = [], [], []
413 ws, ts, ss = [], [], []
414 def flushss():
414 def flushss():
415 if not ss:
415 if not ss:
416 return
416 return
417 if len(ss) == 1:
417 if len(ss) == 1:
418 w, t = ss[0]
418 w, t = ss[0]
419 else:
419 else:
420 s = '\0'.join(t[1] for w, t in ss)
420 s = '\0'.join(t[1] for w, t in ss)
421 y = ('func', ('symbol', '_list'), ('string', s), order)
421 y = ('func', ('symbol', '_list'), ('string', s), order)
422 w, t = _optimize(y, False)
422 w, t = _optimize(y, False)
423 ws.append(w)
423 ws.append(w)
424 ts.append(t)
424 ts.append(t)
425 del ss[:]
425 del ss[:]
426 for y in getlist(x[1]):
426 for y in getlist(x[1]):
427 w, t = _optimize(y, False)
427 w, t = _optimize(y, False)
428 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
428 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
429 ss.append((w, t))
429 ss.append((w, t))
430 continue
430 continue
431 flushss()
431 flushss()
432 ws.append(w)
432 ws.append(w)
433 ts.append(t)
433 ts.append(t)
434 flushss()
434 flushss()
435 if len(ts) == 1:
435 if len(ts) == 1:
436 return ws[0], ts[0] # 'or' operation is fully optimized out
436 return ws[0], ts[0] # 'or' operation is fully optimized out
437 # we can't reorder trees by weight because it would change the order.
437 # we can't reorder trees by weight because it would change the order.
438 # ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a")
438 # ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a")
439 # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0]))
439 # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0]))
440 return max(ws), (op, ('list',) + tuple(ts), order)
440 return max(ws), (op, ('list',) + tuple(ts), order)
441 elif op == 'not':
441 elif op == 'not':
442 # Optimize not public() to _notpublic() because we have a fast version
442 # Optimize not public() to _notpublic() because we have a fast version
443 if x[1][:3] == ('func', ('symbol', 'public'), None):
443 if x[1][:3] == ('func', ('symbol', 'public'), None):
444 order = x[1][3]
444 order = x[1][3]
445 newsym = ('func', ('symbol', '_notpublic'), None, order)
445 newsym = ('func', ('symbol', '_notpublic'), None, order)
446 o = _optimize(newsym, not small)
446 o = _optimize(newsym, not small)
447 return o[0], o[1]
447 return o[0], o[1]
448 else:
448 else:
449 o = _optimize(x[1], not small)
449 o = _optimize(x[1], not small)
450 order = x[2]
450 order = x[2]
451 return o[0], (op, o[1], order)
451 return o[0], (op, o[1], order)
452 elif op == 'rangeall':
452 elif op == 'rangeall':
453 return smallbonus, x
453 return smallbonus, x
454 elif op in ('rangepre', 'rangepost', 'parentpost'):
454 elif op in ('rangepre', 'rangepost', 'parentpost'):
455 o = _optimize(x[1], small)
455 o = _optimize(x[1], small)
456 order = x[2]
456 order = x[2]
457 return o[0], (op, o[1], order)
457 return o[0], (op, o[1], order)
458 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
458 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
459 wa, ta = _optimize(x[1], small)
459 wa, ta = _optimize(x[1], small)
460 wb, tb = _optimize(x[2], small)
460 wb, tb = _optimize(x[2], small)
461 order = x[3]
461 order = x[3]
462 return wa + wb, (op, ta, tb, order)
462 return wa + wb, (op, ta, tb, order)
463 elif op == 'list':
463 elif op == 'list':
464 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
464 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
465 return sum(ws), (op,) + ts
465 return sum(ws), (op,) + ts
466 elif op == 'keyvalue':
466 elif op == 'keyvalue':
467 w, t = _optimize(x[2], small)
467 w, t = _optimize(x[2], small)
468 return w, (op, x[1], t)
468 return w, (op, x[1], t)
469 elif op == 'func':
469 elif op == 'func':
470 f = getsymbol(x[1])
470 f = getsymbol(x[1])
471 wa, ta = _optimize(x[2], small)
471 wa, ta = _optimize(x[2], small)
472 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
472 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
473 'keyword', 'outgoing', 'user', 'destination'):
473 'keyword', 'outgoing', 'user', 'destination'):
474 w = 10 # slow
474 w = 10 # slow
475 elif f in ('modifies', 'adds', 'removes'):
475 elif f in ('modifies', 'adds', 'removes'):
476 w = 30 # slower
476 w = 30 # slower
477 elif f == "contains":
477 elif f == "contains":
478 w = 100 # very slow
478 w = 100 # very slow
479 elif f == "ancestor":
479 elif f == "ancestor":
480 w = 1 * smallbonus
480 w = 1 * smallbonus
481 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
481 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
482 w = 0
482 w = 0
483 elif f == "sort":
483 elif f == "sort":
484 w = 10 # assume most sorts look at changelog
484 w = 10 # assume most sorts look at changelog
485 else:
485 else:
486 w = 1
486 w = 1
487 order = x[3]
487 order = x[3]
488 return w + wa, (op, x[1], ta, order)
488 return w + wa, (op, x[1], ta, order)
489 raise ValueError('invalid operator %r' % op)
489 raise ValueError('invalid operator %r' % op)
490
490
491 def optimize(tree):
491 def optimize(tree):
492 """Optimize evaluatable tree
492 """Optimize evaluatable tree
493
493
494 All pseudo operations should be transformed beforehand.
494 All pseudo operations should be transformed beforehand.
495 """
495 """
496 _weight, newtree = _optimize(tree, small=True)
496 _weight, newtree = _optimize(tree, small=True)
497 return newtree
497 return newtree
498
498
499 # the set of valid characters for the initial letter of symbols in
499 # the set of valid characters for the initial letter of symbols in
500 # alias declarations and definitions
500 # alias declarations and definitions
501 _aliassyminitletters = _syminitletters | set(pycompat.sysstr('$'))
501 _aliassyminitletters = _syminitletters | set(pycompat.sysstr('$'))
502
502
503 def _parsewith(spec, lookup=None, syminitletters=None):
503 def _parsewith(spec, lookup=None, syminitletters=None):
504 """Generate a parse tree of given spec with given tokenizing options
504 """Generate a parse tree of given spec with given tokenizing options
505
505
506 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
506 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
507 ('func', ('symbol', 'foo'), ('symbol', '$1'))
507 ('func', ('symbol', 'foo'), ('symbol', '$1'))
508 >>> _parsewith('$1')
508 >>> _parsewith('$1')
509 Traceback (most recent call last):
509 Traceback (most recent call last):
510 ...
510 ...
511 ParseError: ("syntax error in revset '$1'", 0)
511 ParseError: ("syntax error in revset '$1'", 0)
512 >>> _parsewith('foo bar')
512 >>> _parsewith('foo bar')
513 Traceback (most recent call last):
513 Traceback (most recent call last):
514 ...
514 ...
515 ParseError: ('invalid token', 4)
515 ParseError: ('invalid token', 4)
516 """
516 """
517 p = parser.parser(elements)
517 p = parser.parser(elements)
518 tree, pos = p.parse(tokenize(spec, lookup=lookup,
518 tree, pos = p.parse(tokenize(spec, lookup=lookup,
519 syminitletters=syminitletters))
519 syminitletters=syminitletters))
520 if pos != len(spec):
520 if pos != len(spec):
521 raise error.ParseError(_('invalid token'), pos)
521 raise error.ParseError(_('invalid token'), pos)
522 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
522 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
523
523
524 class _aliasrules(parser.basealiasrules):
524 class _aliasrules(parser.basealiasrules):
525 """Parsing and expansion rule set of revset aliases"""
525 """Parsing and expansion rule set of revset aliases"""
526 _section = _('revset alias')
526 _section = _('revset alias')
527
527
528 @staticmethod
528 @staticmethod
529 def _parse(spec):
529 def _parse(spec):
530 """Parse alias declaration/definition ``spec``
530 """Parse alias declaration/definition ``spec``
531
531
532 This allows symbol names to use also ``$`` as an initial letter
532 This allows symbol names to use also ``$`` as an initial letter
533 (for backward compatibility), and callers of this function should
533 (for backward compatibility), and callers of this function should
534 examine whether ``$`` is used also for unexpected symbols or not.
534 examine whether ``$`` is used also for unexpected symbols or not.
535 """
535 """
536 return _parsewith(spec, syminitletters=_aliassyminitletters)
536 return _parsewith(spec, syminitletters=_aliassyminitletters)
537
537
538 @staticmethod
538 @staticmethod
539 def _trygetfunc(tree):
539 def _trygetfunc(tree):
540 if tree[0] == 'func' and tree[1][0] == 'symbol':
540 if tree[0] == 'func' and tree[1][0] == 'symbol':
541 return tree[1][1], getlist(tree[2])
541 return tree[1][1], getlist(tree[2])
542
542
543 def expandaliases(ui, tree):
543 def expandaliases(ui, tree):
544 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
544 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
545 tree = _aliasrules.expand(aliases, tree)
545 tree = _aliasrules.expand(aliases, tree)
546 # warn about problematic (but not referred) aliases
546 # warn about problematic (but not referred) aliases
547 for name, alias in sorted(aliases.iteritems()):
547 for name, alias in sorted(aliases.iteritems()):
548 if alias.error and not alias.warned:
548 if alias.error and not alias.warned:
549 ui.warn(_('warning: %s\n') % (alias.error))
549 ui.warn(_('warning: %s\n') % (alias.error))
550 alias.warned = True
550 alias.warned = True
551 return tree
551 return tree
552
552
553 def foldconcat(tree):
553 def foldconcat(tree):
554 """Fold elements to be concatenated by `##`
554 """Fold elements to be concatenated by `##`
555 """
555 """
556 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
556 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
557 return tree
557 return tree
558 if tree[0] == '_concat':
558 if tree[0] == '_concat':
559 pending = [tree]
559 pending = [tree]
560 l = []
560 l = []
561 while pending:
561 while pending:
562 e = pending.pop()
562 e = pending.pop()
563 if e[0] == '_concat':
563 if e[0] == '_concat':
564 pending.extend(reversed(e[1:]))
564 pending.extend(reversed(e[1:]))
565 elif e[0] in ('string', 'symbol'):
565 elif e[0] in ('string', 'symbol'):
566 l.append(e[1])
566 l.append(e[1])
567 else:
567 else:
568 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
568 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
569 raise error.ParseError(msg)
569 raise error.ParseError(msg)
570 return ('string', ''.join(l))
570 return ('string', ''.join(l))
571 else:
571 else:
572 return tuple(foldconcat(t) for t in tree)
572 return tuple(foldconcat(t) for t in tree)
573
573
574 def parse(spec, lookup=None):
574 def parse(spec, lookup=None):
575 return _parsewith(spec, lookup=lookup)
575 return _parsewith(spec, lookup=lookup)
576
576
577 def _quote(s):
577 def _quote(s):
578 r"""Quote a value in order to make it safe for the revset engine.
578 r"""Quote a value in order to make it safe for the revset engine.
579
579
580 >>> _quote('asdf')
580 >>> _quote('asdf')
581 "'asdf'"
581 "'asdf'"
582 >>> _quote("asdf'\"")
582 >>> _quote("asdf'\"")
583 '\'asdf\\\'"\''
583 '\'asdf\\\'"\''
584 >>> _quote('asdf\'')
584 >>> _quote('asdf\'')
585 "'asdf\\''"
585 "'asdf\\''"
586 >>> _quote(1)
586 >>> _quote(1)
587 "'1'"
587 "'1'"
588 """
588 """
589 return "'%s'" % util.escapestr('%s' % s)
589 return "'%s'" % util.escapestr(pycompat.bytestr(s))
590
590
591 def formatspec(expr, *args):
591 def formatspec(expr, *args):
592 '''
592 '''
593 This is a convenience function for using revsets internally, and
593 This is a convenience function for using revsets internally, and
594 escapes arguments appropriately. Aliases are intentionally ignored
594 escapes arguments appropriately. Aliases are intentionally ignored
595 so that intended expression behavior isn't accidentally subverted.
595 so that intended expression behavior isn't accidentally subverted.
596
596
597 Supported arguments:
597 Supported arguments:
598
598
599 %r = revset expression, parenthesized
599 %r = revset expression, parenthesized
600 %d = int(arg), no quoting
600 %d = int(arg), no quoting
601 %s = string(arg), escaped and single-quoted
601 %s = string(arg), escaped and single-quoted
602 %b = arg.branch(), escaped and single-quoted
602 %b = arg.branch(), escaped and single-quoted
603 %n = hex(arg), single-quoted
603 %n = hex(arg), single-quoted
604 %% = a literal '%'
604 %% = a literal '%'
605
605
606 Prefixing the type with 'l' specifies a parenthesized list of that type.
606 Prefixing the type with 'l' specifies a parenthesized list of that type.
607
607
608 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
608 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
609 '(10 or 11):: and ((this()) or (that()))'
609 '(10 or 11):: and ((this()) or (that()))'
610 >>> formatspec('%d:: and not %d::', 10, 20)
610 >>> formatspec('%d:: and not %d::', 10, 20)
611 '10:: and not 20::'
611 '10:: and not 20::'
612 >>> formatspec('%ld or %ld', [], [1])
612 >>> formatspec('%ld or %ld', [], [1])
613 "_list('') or 1"
613 "_list('') or 1"
614 >>> formatspec('keyword(%s)', 'foo\\xe9')
614 >>> formatspec('keyword(%s)', 'foo\\xe9')
615 "keyword('foo\\\\xe9')"
615 "keyword('foo\\\\xe9')"
616 >>> b = lambda: 'default'
616 >>> b = lambda: 'default'
617 >>> b.branch = b
617 >>> b.branch = b
618 >>> formatspec('branch(%b)', b)
618 >>> formatspec('branch(%b)', b)
619 "branch('default')"
619 "branch('default')"
620 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
620 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
621 "root(_list('a\\x00b\\x00c\\x00d'))"
621 "root(_list('a\\x00b\\x00c\\x00d'))"
622 '''
622 '''
623
623
624 def argtype(c, arg):
624 def argtype(c, arg):
625 if c == 'd':
625 if c == 'd':
626 return '%d' % int(arg)
626 return '%d' % int(arg)
627 elif c == 's':
627 elif c == 's':
628 return _quote(arg)
628 return _quote(arg)
629 elif c == 'r':
629 elif c == 'r':
630 parse(arg) # make sure syntax errors are confined
630 parse(arg) # make sure syntax errors are confined
631 return '(%s)' % arg
631 return '(%s)' % arg
632 elif c == 'n':
632 elif c == 'n':
633 return _quote(node.hex(arg))
633 return _quote(node.hex(arg))
634 elif c == 'b':
634 elif c == 'b':
635 return _quote(arg.branch())
635 return _quote(arg.branch())
636
636
637 def listexp(s, t):
637 def listexp(s, t):
638 l = len(s)
638 l = len(s)
639 if l == 0:
639 if l == 0:
640 return "_list('')"
640 return "_list('')"
641 elif l == 1:
641 elif l == 1:
642 return argtype(t, s[0])
642 return argtype(t, s[0])
643 elif t == 'd':
643 elif t == 'd':
644 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
644 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
645 elif t == 's':
645 elif t == 's':
646 return "_list('%s')" % "\0".join(s)
646 return "_list('%s')" % "\0".join(s)
647 elif t == 'n':
647 elif t == 'n':
648 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
648 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
649 elif t == 'b':
649 elif t == 'b':
650 return "_list('%s')" % "\0".join(a.branch() for a in s)
650 return "_list('%s')" % "\0".join(a.branch() for a in s)
651
651
652 m = l // 2
652 m = l // 2
653 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
653 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
654
654
655 expr = pycompat.bytestr(expr)
655 expr = pycompat.bytestr(expr)
656 ret = ''
656 ret = ''
657 pos = 0
657 pos = 0
658 arg = 0
658 arg = 0
659 while pos < len(expr):
659 while pos < len(expr):
660 c = expr[pos]
660 c = expr[pos]
661 if c == '%':
661 if c == '%':
662 pos += 1
662 pos += 1
663 d = expr[pos]
663 d = expr[pos]
664 if d == '%':
664 if d == '%':
665 ret += d
665 ret += d
666 elif d in 'dsnbr':
666 elif d in 'dsnbr':
667 ret += argtype(d, args[arg])
667 ret += argtype(d, args[arg])
668 arg += 1
668 arg += 1
669 elif d == 'l':
669 elif d == 'l':
670 # a list of some type
670 # a list of some type
671 pos += 1
671 pos += 1
672 d = expr[pos]
672 d = expr[pos]
673 ret += listexp(list(args[arg]), d)
673 ret += listexp(list(args[arg]), d)
674 arg += 1
674 arg += 1
675 else:
675 else:
676 raise error.Abort(_('unexpected revspec format character %s')
676 raise error.Abort(_('unexpected revspec format character %s')
677 % d)
677 % d)
678 else:
678 else:
679 ret += c
679 ret += c
680 pos += 1
680 pos += 1
681
681
682 return ret
682 return ret
683
683
684 def prettyformat(tree):
684 def prettyformat(tree):
685 return parser.prettyformat(tree, ('string', 'symbol'))
685 return parser.prettyformat(tree, ('string', 'symbol'))
686
686
687 def depth(tree):
687 def depth(tree):
688 if isinstance(tree, tuple):
688 if isinstance(tree, tuple):
689 return max(map(depth, tree)) + 1
689 return max(map(depth, tree)) + 1
690 else:
690 else:
691 return 0
691 return 0
692
692
693 def funcsused(tree):
693 def funcsused(tree):
694 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
694 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
695 return set()
695 return set()
696 else:
696 else:
697 funcs = set()
697 funcs = set()
698 for s in tree[1:]:
698 for s in tree[1:]:
699 funcs |= funcsused(s)
699 funcs |= funcsused(s)
700 if tree[0] == 'func':
700 if tree[0] == 'func':
701 funcs.add(tree[1][1])
701 funcs.add(tree[1][1])
702 return funcs
702 return funcs
General Comments 0
You need to be logged in to leave comments. Login now