xref: /aosp_15_r20/external/tensorflow/tensorflow/python/autograph/pyct/static_analysis/activity.py (revision b6fb3261f9314811a0f4371741dbb8839866f948)
1# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ==============================================================================
15"""Activity analysis.
16
17Requires qualified name annotations (see qual_names.py).
18"""
19
20import copy
21import weakref
22
23import gast
24
25from tensorflow.python.autograph.pyct import anno
26from tensorflow.python.autograph.pyct import qual_names
27from tensorflow.python.autograph.pyct import transformer
28from tensorflow.python.autograph.pyct.static_analysis.annos import NodeAnno
29
30
31class Scope(object):
32  """Encloses local symbol definition and usage information.
33
34  This can track for instance whether a symbol is modified in the current scope.
35  Note that scopes do not necessarily align with Python's scopes. For example,
36  the body of an if statement may be considered a separate scope.
37
38  Caution - the AST references held by this object are weak.
39
40  Scope objects are mutable during construction only, and must be frozen using
41  `Scope.finalize()` before use. Furthermore, a scope is consistent only after
42  all its children have been frozen. While analysing code blocks, scopes are
43  being gradually built, from the innermost scope outward. Freezing indicates
44  that the analysis of a code block is complete. Once frozen, mutation is no
45  longer allowed. `is_final` tracks whether the scope is frozen or not. Certain
46  properties, like `referenced`, are only accurate when called on frozen scopes.
47
48  Attributes:
49    parent: Optional[Scope], the parent scope, if any.
50    isolated: bool, whether the scope is a true Python scope (e.g. the scope of
51      a function), or just a surrogate tracking an ordinary code block. Using
52      the terminology of the Python 3 reference documentation, True roughly
53      represents an actual scope, whereas False represents an ordinary code
54      block.
55    function_name: Optional[str], name of the function owning this scope.
56    isolated_names: Set[qual_names.QN], identifiers that are isolated to this
57      scope (even if the scope is not isolated).
58    annotations: Set[qual_names.QN], identifiers used as type annotations
59      in this scope.
60    read: Set[qual_names.QN], identifiers read in this scope.
61    modified: Set[qual_names.QN], identifiers modified in this scope.
62    deleted: Set[qual_names.QN], identifiers deleted in this scope.
63    bound: Set[qual_names.QN], names that are bound to this scope. See
64      https://docs.python.org/3/reference/executionmodel.html#binding-of-names
65      for a precise definition.
66    globals: Set[qual_names.QN], names that are explicitly marked as global in
67      this scope. Note that this doesn't include free read-only vars bound to
68      global symbols.
69    nonlocals: Set[qual_names.QN], names that are explicitly marked as nonlocal
70      in this scope. Note that this doesn't include free read-only vars bound to
71      global symbols.
72    free_vars: Set[qual_names.QN], the free variables in this scope. See
73      https://docs.python.org/3/reference/executionmodel.html for a precise
74      definition.
75    params: WeakValueDictionary[qual_names.QN, ast.Node], function arguments
76      visible in this scope, mapped to the function node that defines them.
77    enclosing_scope: Scope, the innermost isolated scope that is a transitive
78      parent of this scope. May be the scope itself.
79    referenced: Set[qual_names.QN], the totality of the symbols used by this
80      scope and its parents.
81    is_final: bool, whether the scope is frozen or not.
82
83  Note - simple statements may never delete and modify a symbol at the same
84  time. However, compound ones like if statements can. In that latter case, it's
85  undefined whether the symbol is actually modified or deleted upon statement
86  exit. Certain analyses like reaching definitions need to be careful about
87  this.
88  """
89
90  # Note: this mutable-immutable pattern is used because using a builder would
91  # have taken a lot more boilerplate.
92
93  def __init__(self, parent, isolated=True, function_name=None):
94    """Create a new scope.
95
96    Args:
97      parent: A Scope or None.
98      isolated: Whether the scope is isolated, that is, whether variables
99        modified in this scope should be considered modified in the parent
100        scope.
101      function_name: Name of the function owning this scope.
102    """
103    self.parent = parent
104    self.isolated = isolated
105    self.function_name = function_name
106
107    self.isolated_names = set()
108
109    self.read = set()
110    self.modified = set()
111    self.deleted = set()
112
113    self.bound = set()
114    self.globals = set()
115    self.nonlocals = set()
116    self.annotations = set()
117
118    self.params = weakref.WeakValueDictionary()
119
120    # Certain fields can only be accessed after the scope and all its parent
121    # scopes have been fully built. This field guards that.
122    self.is_final = False
123
124  @property
125  def enclosing_scope(self):
126    assert self.is_final
127    if self.parent is not None and not self.isolated:
128      return self.parent
129    return self
130
131  @property
132  def referenced(self):
133    if self.parent is not None:
134      return self.read | self.parent.referenced
135    return self.read
136
137  @property
138  def free_vars(self):
139    enclosing_scope = self.enclosing_scope
140    return enclosing_scope.read - enclosing_scope.bound
141
142  def copy_from(self, other):
143    """Recursively copies the contents of this scope from another scope."""
144    assert not self.is_final
145    if self.parent is not None:
146      assert other.parent is not None
147      self.parent.copy_from(other.parent)
148    self.isolated_names = copy.copy(other.isolated_names)
149    self.modified = copy.copy(other.modified)
150    self.read = copy.copy(other.read)
151    self.deleted = copy.copy(other.deleted)
152    self.bound = copy.copy(other.bound)
153    self.annotations = copy.copy(other.annotations)
154    self.params = copy.copy(other.params)
155
156  @classmethod
157  def copy_of(cls, other):
158    if other.parent is not None:
159      assert other.parent is not None
160      parent = cls.copy_of(other.parent)
161    else:
162      parent = None
163    new_copy = cls(parent)
164    new_copy.copy_from(other)
165    return new_copy
166
167  def merge_from(self, other):
168    """Adds all activity from another scope to this scope."""
169    assert not self.is_final
170    if self.parent is not None:
171      assert other.parent is not None
172      self.parent.merge_from(other.parent)
173    self.isolated_names.update(other.isolated_names)
174    self.read.update(other.read)
175    self.modified.update(other.modified)
176    self.bound.update(other.bound)
177    self.deleted.update(other.deleted)
178    self.annotations.update(other.annotations)
179    self.params.update(other.params)
180
181  def finalize(self):
182    """Freezes this scope."""
183    assert not self.is_final
184    # TODO(mdan): freeze read, modified, bound.
185    if self.parent is not None:
186      assert not self.parent.is_final
187      if not self.isolated:
188        self.parent.read.update(self.read - self.isolated_names)
189        self.parent.modified.update(self.modified - self.isolated_names)
190        self.parent.bound.update(self.bound - self.isolated_names)
191        self.parent.globals.update(self.globals)
192        self.parent.nonlocals.update(self.nonlocals)
193        self.parent.annotations.update(self.annotations)
194      else:
195        # TODO(mdan): This is not accurate.
196        self.parent.read.update(self.read - self.bound)
197        self.parent.annotations.update(self.annotations - self.bound)
198    self.is_final = True
199
200  def __repr__(self):
201    return 'Scope{r=%s, w=%s}' % (tuple(self.read), tuple(self.modified))
202
203  def mark_param(self, name, owner):
204    # Assumption: all AST nodes have the same life span. This lets us use
205    # a weak reference to mark the connection between a symbol node and the
206    # function node whose argument that symbol is.
207    self.params[name] = owner
208
209
210class _Comprehension(object):
211
212  no_root = True
213
214  def __init__(self):
215    # TODO(mdan): Consider using an enum.
216    self.is_list_comp = False
217    self.targets = set()
218
219
220class _FunctionOrClass(object):
221
222  def __init__(self):
223    self.node = None
224
225
226class ActivityAnalyzer(transformer.Base):
227  """Annotates nodes with local scope information.
228
229  See Scope.
230
231  The use of this class requires that qual_names.resolve() has been called on
232  the node. This class will ignore nodes have not been
233  annotated with their qualified names.
234  """
235
236  def __init__(self, context, parent_scope=None):
237    super(ActivityAnalyzer, self).__init__(context)
238    self.allow_skips = False
239    self.scope = Scope(parent_scope, isolated=True)
240
241    # Note: all these flags crucially rely on the respective nodes are
242    # leaves in the AST, that is, they cannot contain other statements.
243    self._in_aug_assign = False
244    self._in_annotation = False
245    self._track_annotations_only = False
246
247  @property
248  def _in_constructor(self):
249    context = self.state[_FunctionOrClass]
250    if context.level > 2:
251      innermost = context.stack[-1].node
252      parent = context.stack[-2].node
253      return (isinstance(parent, gast.ClassDef) and
254              (isinstance(innermost, gast.FunctionDef) and
255               innermost.name == '__init__'))
256    return False
257
258  def _node_sets_self_attribute(self, node):
259    if anno.hasanno(node, anno.Basic.QN):
260      qn = anno.getanno(node, anno.Basic.QN)
261      # TODO(mdan): The 'self' argument is not guaranteed to be called 'self'.
262      if qn.has_attr and qn.parent.qn == ('self',):
263        return True
264    return False
265
266  def _track_symbol(self, node, composite_writes_alter_parent=False):
267    if self._track_annotations_only and not self._in_annotation:
268      return
269
270    # A QN may be missing when we have an attribute (or subscript) on a function
271    # call. Example: a().b
272    if not anno.hasanno(node, anno.Basic.QN):
273      return
274    qn = anno.getanno(node, anno.Basic.QN)
275
276    # When inside a comprehension, ignore reads to any of the comprehensions's
277    # targets. This includes attributes or slices of those arguments.
278    for l in self.state[_Comprehension]:
279      if qn in l.targets:
280        return
281      if qn.owner_set & set(l.targets):
282        return
283
284    if isinstance(node.ctx, gast.Store):
285      # In comprehensions, modified symbols are the comprehension targets.
286      if self.state[_Comprehension].level > 0:
287        self.state[_Comprehension].targets.add(qn)
288        return
289
290      self.scope.modified.add(qn)
291      self.scope.bound.add(qn)
292      if qn.is_composite and composite_writes_alter_parent:
293        self.scope.modified.add(qn.parent)
294      if self._in_aug_assign:
295        self.scope.read.add(qn)
296
297    elif isinstance(node.ctx, gast.Load):
298      self.scope.read.add(qn)
299      if self._in_annotation:
300        self.scope.annotations.add(qn)
301
302    elif isinstance(node.ctx, gast.Param):
303      self.scope.bound.add(qn)
304      self.scope.mark_param(qn, self.state[_FunctionOrClass].node)
305
306    elif isinstance(node.ctx, gast.Del):
307      # The read matches the Python semantics - attempting to delete an
308      # undefined symbol is illegal.
309      self.scope.read.add(qn)
310      # Targets of del are considered bound:
311      # https://docs.python.org/3/reference/executionmodel.html#binding-of-names
312      self.scope.bound.add(qn)
313      self.scope.deleted.add(qn)
314
315    else:
316      raise ValueError('Unknown context {} for node "{}".'.format(
317          type(node.ctx), qn))
318
319  def _enter_scope(self, isolated, f_name=None):
320    self.scope = Scope(self.scope, isolated=isolated, function_name=f_name)
321
322  def _exit_scope(self):
323    exited_scope = self.scope
324    exited_scope.finalize()
325    self.scope = exited_scope.parent
326    return exited_scope
327
328  def _exit_and_record_scope(self, node, tag=anno.Static.SCOPE):
329    node_scope = self._exit_scope()
330    anno.setanno(node, tag, node_scope)
331    return node_scope
332
333  def _process_statement(self, node):
334    self._enter_scope(False)
335    node = self.generic_visit(node)
336    self._exit_and_record_scope(node)
337    return node
338
339  def _process_annotation(self, node):
340    self._in_annotation = True
341    node = self.visit(node)
342    self._in_annotation = False
343    return node
344
345  def visit_Import(self, node):
346    return self._process_statement(node)
347
348  def visit_ImportFrom(self, node):
349    return self._process_statement(node)
350
351  def visit_Global(self, node):
352    self._enter_scope(False)
353    for name in node.names:
354      qn = qual_names.QN(name)
355      self.scope.read.add(qn)
356      self.scope.globals.add(qn)
357    self._exit_and_record_scope(node)
358    return node
359
360  def visit_Nonlocal(self, node):
361    self._enter_scope(False)
362    for name in node.names:
363      qn = qual_names.QN(name)
364      self.scope.read.add(qn)
365      self.scope.bound.add(qn)
366      self.scope.nonlocals.add(qn)
367    self._exit_and_record_scope(node)
368    return node
369
370  def visit_Expr(self, node):
371    return self._process_statement(node)
372
373  def visit_Raise(self, node):
374    return self._process_statement(node)
375
376  def visit_Return(self, node):
377    return self._process_statement(node)
378
379  def visit_Assign(self, node):
380    return self._process_statement(node)
381
382  def visit_AnnAssign(self, node):
383    self._enter_scope(False)
384    node.target = self.visit(node.target)
385    if node.value is not None:
386      # Can be None for pure declarations, e.g. `n: int`. This is a new thing
387      # enabled by type annotations, but does not influence static analysis
388      # (declarations are not definitions).
389      node.value = self.visit(node.value)
390    if node.annotation:
391      node.annotation = self._process_annotation(node.annotation)
392    self._exit_and_record_scope(node)
393    return node
394
395  def visit_AugAssign(self, node):
396    # Special rules for AugAssign. Here, the AST only shows the target as
397    # written, when it is in fact also read.
398    self._enter_scope(False)
399
400    self._in_aug_assign = True
401    node.target = self.visit(node.target)
402    self._in_aug_assign = False
403
404    node.op = self.visit(node.op)
405    node.value = self.visit(node.value)
406    self._exit_and_record_scope(node)
407    return node
408
409  def visit_Delete(self, node):
410    return self._process_statement(node)
411
412  def visit_Name(self, node):
413    if node.annotation:
414      node.annotation = self._process_annotation(node.annotation)
415    self._track_symbol(node)
416    return node
417
418  def visit_alias(self, node):
419    node = self.generic_visit(node)
420
421    if node.asname is None:
422      # Only the root name is a real symbol operation.
423      qn = qual_names.QN(node.name.split('.')[0])
424    else:
425      qn = qual_names.QN(node.asname)
426
427    self.scope.modified.add(qn)
428    self.scope.bound.add(qn)
429    return node
430
431  def visit_Attribute(self, node):
432    node = self.generic_visit(node)
433    if self._in_constructor and self._node_sets_self_attribute(node):
434      self._track_symbol(node, composite_writes_alter_parent=True)
435    else:
436      self._track_symbol(node)
437    return node
438
439  def visit_Subscript(self, node):
440    node = self.generic_visit(node)
441    # Subscript writes (e.g. a[b] = "value") are considered to modify
442    # both the element itself (a[b]) and its parent (a).
443    self._track_symbol(node)
444    return node
445
446  def visit_Print(self, node):
447    self._enter_scope(False)
448    node.values = self.visit_block(node.values)
449    node_scope = self._exit_and_record_scope(node)
450    anno.setanno(node, NodeAnno.ARGS_SCOPE, node_scope)
451    return node
452
453  def visit_Assert(self, node):
454    return self._process_statement(node)
455
456  def visit_Call(self, node):
457    self._enter_scope(False)
458    node.args = self.visit_block(node.args)
459    node.keywords = self.visit_block(node.keywords)
460    # TODO(mdan): Account starargs, kwargs
461    self._exit_and_record_scope(node, tag=NodeAnno.ARGS_SCOPE)
462
463    node.func = self.visit(node.func)
464    return node
465
466  def _process_block_node(self, node, block, scope_name):
467    self._enter_scope(False)
468    block = self.visit_block(block)
469    self._exit_and_record_scope(node, tag=scope_name)
470    return node
471
472  def _process_parallel_blocks(self, parent, children):
473    # Because the scopes are not isolated, processing any child block
474    # modifies the parent state causing the other child blocks to be
475    # processed incorrectly. So we need to checkpoint the parent scope so that
476    # each child sees the same context.
477    before_parent = Scope.copy_of(self.scope)
478    after_children = []
479    for child, scope_name in children:
480      self.scope.copy_from(before_parent)
481      parent = self._process_block_node(parent, child, scope_name)
482      after_child = Scope.copy_of(self.scope)
483      after_children.append(after_child)
484    for after_child in after_children:
485      self.scope.merge_from(after_child)
486    return parent
487
488  def _process_comprehension(self,
489                             node,
490                             is_list_comp=False,
491                             is_dict_comp=False):
492    with self.state[_Comprehension] as comprehension_:
493      comprehension_.is_list_comp = is_list_comp
494      # Note: it's important to visit the generators first to properly account
495      # for the variables local to these generators. Example: `x` is local to
496      # the expression `z for x in y for z in x`.
497      node.generators = self.visit_block(node.generators)
498      if is_dict_comp:
499        node.key = self.visit(node.key)
500        node.value = self.visit(node.value)
501      else:
502        node.elt = self.visit(node.elt)
503      return node
504
505  def visit_comprehension(self, node):
506    # It is important to visit children in this order so that the reads to
507    # the target name are appropriately ignored.
508    node.iter = self.visit(node.iter)
509    node.target = self.visit(node.target)
510    return self.generic_visit(node)
511
512  def visit_DictComp(self, node):
513    return self._process_comprehension(node, is_dict_comp=True)
514
515  def visit_ListComp(self, node):
516    return self._process_comprehension(node, is_list_comp=True)
517
518  def visit_SetComp(self, node):
519    return self._process_comprehension(node)
520
521  def visit_GeneratorExp(self, node):
522    return self._process_comprehension(node)
523
524  def visit_ClassDef(self, node):
525    with self.state[_FunctionOrClass] as fn:
526      fn.node = node
527      # The ClassDef node itself has a Scope object that tracks the creation
528      # of its name, along with the usage of any decorator accompanying it.
529      self._enter_scope(False)
530      node.decorator_list = self.visit_block(node.decorator_list)
531      self.scope.modified.add(qual_names.QN(node.name))
532      self.scope.bound.add(qual_names.QN(node.name))
533      node.bases = self.visit_block(node.bases)
534      node.keywords = self.visit_block(node.keywords)
535      self._exit_and_record_scope(node)
536
537      # A separate Scope tracks the actual class definition.
538      self._enter_scope(True)
539      node = self.generic_visit(node)
540      self._exit_scope()
541      return node
542
543  def _visit_node_list(self, nodes):
544    return [(None if n is None else self.visit(n)) for n in nodes]
545
546  def _visit_arg_annotations(self, node):
547    node.args.kw_defaults = self._visit_node_list(node.args.kw_defaults)
548    node.args.defaults = self._visit_node_list(node.args.defaults)
549    self._track_annotations_only = True
550    node = self._visit_arg_declarations(node)
551    self._track_annotations_only = False
552    return node
553
554  def _visit_arg_declarations(self, node):
555    node.args.posonlyargs = self._visit_node_list(node.args.posonlyargs)
556    node.args.args = self._visit_node_list(node.args.args)
557    if node.args.vararg is not None:
558      node.args.vararg = self.visit(node.args.vararg)
559    node.args.kwonlyargs = self._visit_node_list(node.args.kwonlyargs)
560    if node.args.kwarg is not None:
561      node.args.kwarg = self.visit(node.args.kwarg)
562    return node
563
564  def visit_FunctionDef(self, node):
565    with self.state[_FunctionOrClass] as fn:
566      fn.node = node
567      # The FunctionDef node itself has a Scope object that tracks the creation
568      # of its name, along with the usage of any decorator accompanying it.
569      self._enter_scope(False)
570      node.decorator_list = self.visit_block(node.decorator_list)
571      if node.returns:
572        node.returns = self._process_annotation(node.returns)
573      # Argument annotartions (includeing defaults) affect the defining context.
574      node = self._visit_arg_annotations(node)
575
576      function_name = qual_names.QN(node.name)
577      self.scope.modified.add(function_name)
578      self.scope.bound.add(function_name)
579      self._exit_and_record_scope(node)
580
581      # A separate Scope tracks the actual function definition.
582      self._enter_scope(True, node.name)
583
584      # Keep a separate scope for the arguments node, which is used in the CFG.
585      self._enter_scope(False, node.name)
586
587      # Arg declarations only affect the function itself, and have no effect
588      # in the defining context whatsoever.
589      node = self._visit_arg_declarations(node)
590
591      self._exit_and_record_scope(node.args)
592
593      # Track the body separately. This is for compatibility reasons, it may not
594      # be strictly needed.
595      self._enter_scope(False, node.name)
596      node.body = self.visit_block(node.body)
597      self._exit_and_record_scope(node, NodeAnno.BODY_SCOPE)
598
599      self._exit_and_record_scope(node, NodeAnno.ARGS_AND_BODY_SCOPE)
600      return node
601
602  def visit_Lambda(self, node):
603    # Lambda nodes are treated in roughly the same way as FunctionDef nodes.
604    with self.state[_FunctionOrClass] as fn:
605      fn.node = node
606      # The Lambda node itself has a Scope object that tracks the creation
607      # of its name, along with the usage of any decorator accompanying it.
608      self._enter_scope(False)
609      node = self._visit_arg_annotations(node)
610      self._exit_and_record_scope(node)
611
612      # A separate Scope tracks the actual function definition.
613      self._enter_scope(True)
614
615      # Keep a separate scope for the arguments node, which is used in the CFG.
616      self._enter_scope(False)
617      node = self._visit_arg_declarations(node)
618      self._exit_and_record_scope(node.args)
619
620      # Track the body separately. This is for compatibility reasons, it may not
621      # be strictly needed.
622      # TODO(mdan): Do remove it, it's confusing.
623      self._enter_scope(False)
624      node.body = self.visit(node.body)
625
626      # The lambda body can contain nodes of types normally not found as
627      # statements, and may not have the SCOPE annotation needed by the CFG.
628      # So we attach one if necessary.
629      if not anno.hasanno(node.body, anno.Static.SCOPE):
630        anno.setanno(node.body, anno.Static.SCOPE, self.scope)
631
632      self._exit_and_record_scope(node, NodeAnno.BODY_SCOPE)
633
634      lambda_scope = self.scope
635      self._exit_and_record_scope(node, NodeAnno.ARGS_AND_BODY_SCOPE)
636
637      # Exception: lambdas are assumed to be used in the place where
638      # they are defined. Therefore, their activity is passed on to the
639      # calling statement.
640      self.scope.read.update(lambda_scope.read - lambda_scope.bound)
641
642      return node
643
644  def visit_With(self, node):
645    self._enter_scope(False)
646    node = self.generic_visit(node)
647    self._exit_and_record_scope(node, NodeAnno.BODY_SCOPE)
648    return node
649
650  def visit_withitem(self, node):
651    return self._process_statement(node)
652
653  def visit_If(self, node):
654    self._enter_scope(False)
655    node.test = self.visit(node.test)
656    node_scope = self._exit_and_record_scope(node.test)
657    anno.setanno(node, NodeAnno.COND_SCOPE, node_scope)
658
659    node = self._process_parallel_blocks(node,
660                                         ((node.body, NodeAnno.BODY_SCOPE),
661                                          (node.orelse, NodeAnno.ORELSE_SCOPE)))
662    return node
663
664  def visit_For(self, node):
665    self._enter_scope(False)
666    node.target = self.visit(node.target)
667    node.iter = self.visit(node.iter)
668    self._exit_and_record_scope(node.iter)
669
670    self._enter_scope(False)
671    self.visit(node.target)
672    if anno.hasanno(node, anno.Basic.EXTRA_LOOP_TEST):
673      self._process_statement(anno.getanno(node, anno.Basic.EXTRA_LOOP_TEST))
674    self._exit_and_record_scope(node, tag=NodeAnno.ITERATE_SCOPE)
675
676    node = self._process_parallel_blocks(node,
677                                         ((node.body, NodeAnno.BODY_SCOPE),
678                                          (node.orelse, NodeAnno.ORELSE_SCOPE)))
679    return node
680
681  def visit_While(self, node):
682    self._enter_scope(False)
683    node.test = self.visit(node.test)
684    node_scope = self._exit_and_record_scope(node.test)
685    anno.setanno(node, NodeAnno.COND_SCOPE, node_scope)
686
687    node = self._process_parallel_blocks(node,
688                                         ((node.body, NodeAnno.BODY_SCOPE),
689                                          (node.orelse, NodeAnno.ORELSE_SCOPE)))
690    return node
691
692  def visit_ExceptHandler(self, node):
693    self._enter_scope(False)
694    # try/except oddity: as expected, it leaks any names you defined inside the
695    # except block, but not the name of the exception variable.
696    if node.name is not None:
697      self.scope.isolated_names.add(anno.getanno(node.name, anno.Basic.QN))
698    node = self.generic_visit(node)
699    self._exit_scope()
700    return node
701
702
703def resolve(node, context, parent_scope=None):
704  return ActivityAnalyzer(context, parent_scope).visit(node)
705