Lichen

branching.py

26:f47c63967c59
2016-09-05 Paul Boddie Separated inspection-related naming methods from common module methods.
     1 #!/usr/bin/env python     2      3 """     4 Track attribute usage for names.     5      6 Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013,     7               2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>     8      9 This program is free software; you can redistribute it and/or modify it under    10 the terms of the GNU General Public License as published by the Free Software    11 Foundation; either version 3 of the License, or (at your option) any later    12 version.    13     14 This program is distributed in the hope that it will be useful, but WITHOUT    15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    16 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    17 details.    18     19 You should have received a copy of the GNU General Public License along with    20 this program.  If not, see <http://www.gnu.org/licenses/>.    21 """    22     23 from common import dict_for_keys, init_item    24     25 class Branch:    26     27     """    28     A control-flow branch capturing local attribute usage for names.    29     Branches typically begin with assignments or function parameters and are    30     connected to others introduced by conditional and loop nodes.    31     32     Branches hosting accesses, and thus providing usage information, are    33     contributors to preceding branches.    34     35     Branches also provide a route from accesses back to assignments which are    36     the ultimate suppliers of the names involved.    37     """    38     39     def __init__(self, names, assigning=False, values=None):    40     41         """    42         Capture attribute usage for the given 'names', with the accompanying    43         'values' indicating assigned values for each name, if indicated.    44         """    45     46         self.contributors = set()    47         self.suppliers = {}    48         self.assignments = set(assigning and names or [])    49         self.usage = {}    50         self.values = {}    51     52         # Initialise usage for each name.    53     54         for name in names:    55             self.usage[name] = set()    56     57         # Initialise assigned values if any were provided.    58     59         if values:    60             for name, value in zip(names, values):    61                 if value:    62                     self.values[name] = value    63     64         # Computed results.    65     66         self.combined_usage = None    67     68     def get_assignment_sources(self, name):    69     70         """    71         Return the sources of 'name' from this branch's assignment information,    72         returning a list containing only this branch itself if it is the source.    73         """    74     75         if name in self.assignments:    76             return [self]    77         else:    78             return [b for b in self.get_all_suppliers(name) if name in b.assignments]    79     80     def set_usage(self, name, attrname):    81     82         """    83         Record usage on the given 'name' of the attribute 'attrname'.    84         """    85     86         if self.usage.has_key(name):    87             self.usage[name].add(attrname)    88     89     def get_usage(self):    90     91         """    92         Obtain usage from this node, combined with usage observed by its    93         contributors. Unlike the local usage which involves only a single set of    94         attribute names for a given variable name, the returned usage is a set    95         of attribute name combinations for a given variable name. For example:    96     97         {'a': set([('p', 'q', 'r'), ('p', 'r')])}    98         """    99    100         if self.combined_usage is None:   101    102             # Accumulate usage observations from contributors.   103    104             all_usage = []   105    106             for contributor in self.contributors:   107    108                 # Record any usage that can be returned.   109    110                 all_usage.append(contributor.get_usage())   111    112             # Merge usage from the contributors.   113    114             merged_usage = merge_dicts(all_usage)   115    116             # Make the local usage compatible with the combined usage.   117    118             usage = deepen_dict(self.usage)   119    120             self.combined_usage = combine_dicts(usage, merged_usage, combine_sets)   121    122         return self.combined_usage   123    124     def get_all_suppliers(self, name, all_suppliers=None):   125    126         "Return all branches supplying this branch with definitions of 'name'."   127    128         all_suppliers = all_suppliers or set()   129         all_suppliers.add(self)   130    131         if self.suppliers.has_key(name):   132             for supplier in self.suppliers[name]:   133                 if supplier not in all_suppliers:   134                     supplier.get_all_suppliers(name, all_suppliers)   135    136         return all_suppliers   137    138     def __repr__(self):   139         return "Branch(%r, %r)" % (self.usage.keys(),   140             self.assignments and True or False)   141    142 class BranchTracker:   143    144     """   145     A tracker of attribute usage for names in a namespace. This tracker directs   146     usage observations to branches which are the ultimate repositories of   147     attribute usage information.   148    149     As a program unit is inspected, the branches associated with names may   150     change. Assignments reset the branches; control-flow operations cause   151     branches to be accumulated from different code paths.   152     """   153    154     def __init__(self):   155    156         # Track assignments.   157    158         self.assignments = {}   159    160         # Details of attributes at each active branch level.   161    162         self.attribute_branches = [{}]          # stack of branches for names   163         self.attribute_branch_shelves = []      # stack of shelved branches   164    165         # Suspended branch details plus loop details.   166    167         self.suspended_broken_branches = []     # stack of lists of dicts   168         self.suspended_continuing_branches = [] # stack of lists of dicts   169    170         # Abandoned usage, useful for reviving usage for exception handlers.   171    172         self.abandoned_branches = [[]]          # stack of lists of branches   173    174         # Returning branches are like abandoned branches but are only revived in   175         # finally clauses.   176    177         self.returning_branches = [[]]   178    179         # Branches active when starting loops.   180    181         self.loop_branches = []   182    183         # Inherited usage.   184    185         self.inherited = None   186    187     # Structure assembly methods.   188    189     def new_branchpoint(self, loop_node=False):   190    191         """   192         Indicate that branches diverge, initialising resources dependent on   193         any given 'loop_node'.   194         """   195    196         self.attribute_branch_shelves.append([])   197    198         if loop_node:   199             self.suspended_broken_branches.append([])   200             self.suspended_continuing_branches.append([])   201    202         # Retain a record of abandoned branches.   203    204         self.abandoned_branches.append([])   205         self.returning_branches.append([])   206    207     def new_branch(self, loop_node=False):   208    209         "Create a new branch."   210    211         attribute_branches = self.attribute_branches[-1]   212    213         branch, new_branches = self._new_branch(attribute_branches)   214    215         if branch and loop_node:   216             self.loop_branches.append(branch)   217    218         # Start using the branch for known names.   219    220         self.attribute_branches.append(new_branches)   221    222     def _new_branch(self, attribute_branches):   223    224         """   225         Define a new branch that will record attribute usage on known names from   226         'attribute_branches'.   227         """   228    229         # Detect abandoned branches.   230    231         if isinstance(attribute_branches, AbandonedDict):   232             return None, AbandonedDict()   233    234         # Otherwise, define a new branch.   235    236         names = attribute_branches.keys()   237    238         new_branches = {}   239         branch = Branch(names)   240    241         for name in names:   242             new_branches[name] = [branch]   243    244         # Add this new branch as a contributor to the previously active   245         # branches.   246    247         self._connect_branches(attribute_branches, branch)   248    249         return branch, new_branches   250    251     def shelve_branch(self, loop_node=False):   252    253         "Retain the current branch for later merging."   254    255         branches = self.attribute_branches.pop()   256         self.attribute_branch_shelves[-1].append(branches)   257    258         # Connect any loop branch to the active branches as contributors.   259    260         if loop_node:   261             branch = self.loop_branches.pop()   262             self._connect_branches(branches, branch, loop_node)   263    264     def abandon_branch(self):   265    266         "Abandon the current branch, retaining it for later."   267    268         attribute_branches = self.attribute_branches[-1]   269         self._abandon_branch()   270         self.abandoned_branches[-1].append(attribute_branches)   271    272     def abandon_returning_branch(self):   273    274         "Abandon the current branch, retaining it for later."   275    276         attribute_branches = self.attribute_branches[-1]   277         self._abandon_branch()   278         self.returning_branches[-1].append(attribute_branches)   279    280     def suspend_broken_branch(self):   281    282         "Suspend a branch for breaking out of a loop."   283    284         attribute_branches = self.attribute_branches[-1]   285    286         branches = self.suspended_broken_branches[-1]   287         branches.append(attribute_branches)   288         self._abandon_branch()   289    290     def suspend_continuing_branch(self):   291    292         "Suspend a branch for loop continuation."   293    294         attribute_branches = self.attribute_branches[-1]   295    296         branches = self.suspended_continuing_branches[-1]   297         branches.append(attribute_branches)   298         self._abandon_branch()   299    300     def _abandon_branch(self):   301    302         "Abandon the current branch."   303    304         self.attribute_branches[-1] = AbandonedDict()   305    306     def resume_abandoned_branches(self):   307    308         """   309         Resume branches previously abandoned.   310    311         Abandoned branches are not reset because they may not be handled by   312         exception handlers after all.   313         """   314    315         current_branches = self.attribute_branches[-1]   316         abandoned_branches = self.abandoned_branches[-1]   317         merged_branches = merge_dicts(abandoned_branches + [current_branches])   318    319         # Replace the combined branches with a new branch applying to all active   320         # names, connected to the supplying branches.   321    322         branch, new_branches = self._new_branch(merged_branches)   323         self.attribute_branches.append(new_branches)   324    325         # Although returning branches should not be considered as accumulating   326         # usage, they do provide sources of assignments.   327    328         if branch:   329             for returning_branches in self.returning_branches[-1]:   330                 self._connect_suppliers(returning_branches, branch)   331    332     def resume_all_abandoned_branches(self):   333    334         """   335         Resume branches previously abandoned including returning branches.   336    337         Abandoned branches are not reset because they may not be handled by   338         exception handlers after all.   339         """   340    341         current_branches = self.attribute_branches[-1]   342         abandoned_branches = self.abandoned_branches[-1]   343         returning_branches = self.returning_branches[-1]   344         merged_branches = merge_dicts(abandoned_branches + returning_branches + [current_branches])   345         self.replace_branches(merged_branches)   346    347         # Return the previously-active branches for later restoration.   348    349         return current_branches   350    351     def resume_broken_branches(self):   352    353         "Resume branches previously suspended for breaking out of a loop."   354    355         suspended_branches = self.suspended_broken_branches.pop()   356         current_branches = self.attribute_branches[-1]   357    358         # Merge suspended branches with the current branch.   359    360         merged_branches = merge_dicts(suspended_branches + [current_branches])   361         self.replace_branches(merged_branches)   362    363     def resume_continuing_branches(self):   364    365         "Resume branches previously suspended for loop continuation."   366    367         suspended_branches = self.suspended_continuing_branches.pop()   368         current_branches = self.attribute_branches[-1]   369    370         # Merge suspended branches with the current branch.   371    372         merged_branches = merge_dicts(suspended_branches + [current_branches])   373         self.replace_branches(merged_branches)   374    375     def replace_branches(self, merged_branches):   376    377         """   378         Replace the 'merged_branches' with a new branch applying to all active   379         names, connected to the supplying branches.   380         """   381    382         branch, new_branches = self._new_branch(merged_branches)   383         self.attribute_branches[-1] = new_branches   384    385     def restore_active_branches(self, branches):   386    387         "Restore the active 'branches'."   388    389         self.attribute_branches[-1] = branches   390    391     def merge_branches(self):   392    393         "Merge branches."   394    395         # Combine the attribute branches. This ensures that a list of branches   396         # affected by attribute usage is maintained for the current branch.   397    398         all_shelved_branches = self.attribute_branch_shelves.pop()   399         merged_branches = merge_dicts(all_shelved_branches, missing=make_missing)   400         self.replace_branches(merged_branches)   401    402         # Abandoned branches are retained for exception handling purposes.   403    404         all_abandoned_branches = self.abandoned_branches.pop()   405         new_abandoned_branches = merge_dicts(all_abandoned_branches)   406         self.abandoned_branches[-1].append(new_abandoned_branches)   407    408         # Returning branches are retained for finally clauses.   409    410         all_returning_branches = self.returning_branches.pop()   411         new_returning_branches = merge_dicts(all_returning_branches)   412         self.returning_branches[-1].append(new_returning_branches)   413    414     # Internal structure assembly methods.   415    416     def _connect_branches(self, attribute_branches, contributor, loop_node=False):   417    418         """   419         Given the 'attribute_branches' mapping, connect the branches referenced   420         in the mapping to the given 'contributor' branch. If 'loop_node' is   421         set to a true value, connect only the branches so that the 'contributor'   422         references the nodes supplying it with name information.   423         """   424    425         all_branches = self._connect_suppliers(attribute_branches, contributor)   426         if not loop_node:   427             self._connect_contributor(contributor, all_branches)   428    429     def _connect_suppliers(self, attribute_branches, contributor):   430    431         "Connect the 'attribute_branches' to the given 'contributor'."   432    433         # Gather branches involved with all known names into a single set.   434    435         all_branches = set()   436    437         for name, branches in attribute_branches.items():   438             all_branches.update(branches)   439    440             # Also note receiving branches on the contributor.   441    442             for branch in branches:   443                 init_item(contributor.suppliers, name, set)   444                 contributor.suppliers[name].add(branch)   445    446         return all_branches   447    448     def _connect_contributor(self, contributor, branches):   449    450         "Connect the given 'contributor' branch to the given 'branches'."   451    452         for branch in branches:   453             branch.contributors.add(contributor)   454    455     # Namespace methods.   456    457     def inherit_branches(self, tracker, names):   458    459         """   460         Propagate branches from the given 'tracker' excluding those associated   461         with 'names'.   462         """   463    464         # For each inherited name, create a branch connected to the inherited   465         # branches.   466    467         self.inherited = {}   468    469         for name, branches in tracker.attribute_branches[-1].items():   470    471             # Do not inherit any listed names (typically parameters) or any   472             # special names.   473    474             if name in names or name.startswith("$"):   475                 continue   476    477             # Make a tentative assignment for the name.   478    479             contributor = Branch([name], True)   480             init_item(self.assignments, name, list)   481             self.assignments[name].append(contributor)   482    483             # Connect the inherited branch to the new one.   484    485             for branch in branches:   486                 init_item(contributor.suppliers, name, set)   487                 contributor.suppliers[name].add(branch)   488                 branch.contributors.add(contributor)   489    490             # Record the inherited branch.   491    492             self.inherited[name] = [contributor]   493    494         self.attribute_branches[-1].update(self.inherited)   495    496     def disconnect_name(self, name):   497    498         "Disconnect inherited branches for 'name'."   499    500         if not self.inherited or not self.inherited.has_key(name):   501             return   502    503         # Remove the new branch from the inherited branches for the name.   504    505         for contributor in self.inherited[name]:   506             for supplier in contributor.suppliers[name]:   507                 supplier.contributors.remove(contributor)   508             del contributor.suppliers[name]   509    510         del self.inherited[name]   511    512     # Attribute usage methods.   513    514     def tracking_name(self, name):   515    516         """   517         Return whether 'name' is being tracked, returning all branches doing so   518         if it is.   519         """   520    521         return self.assignments.has_key(name) and \   522                (not self.inherited or not self.inherited.has_key(name)) and \   523                self.have_name(name)   524    525     def have_name(self, name):   526    527         "Return whether 'name' is known, perhaps having been inherited."   528    529         return self.attribute_branches[-1].get(name)   530    531     def assign_names(self, names, values=None):   532    533         """   534         Define the start of usage tracking for the given 'names', each being   535         assigned with the corresponding 'values' if indicated.   536         """   537    538         branches = self.attribute_branches[-1]   539         branch = Branch(names, True, values)   540    541         for name in names:   542             self.disconnect_name(name)   543    544             branches[name] = [branch]   545             init_item(self.assignments, name, list)   546             self.assignments[name].append(branch)   547    548         return branch   549    550     def use_attribute(self, name, attrname):   551    552         """   553         Indicate the use on the given 'name' of an attribute with the given   554         'attrname'.   555    556         Return all branches that support 'name'.   557         """   558    559         branches = self.attribute_branches[-1]   560    561         # Add the usage to all current branches.   562    563         if branches.has_key(name):   564             for branch in branches[name]:   565                 branch.set_usage(name, attrname)   566             return branches[name]   567         else:   568             return None   569    570     # Query methods.   571    572     def get_assignment_positions_for_branches(self, name, branches, missing=True):   573    574         """   575         Return the positions of assignments involving the given 'name' affected   576         by the given 'branches'. If 'missing' is set to a false value, branches   577         with missing name details will be excluded instead of contributing the   578         value None to the list of positions.   579         """   580    581         if not branches:   582             return [None]   583    584         positions = set()   585         assignments = self.assignments[name]   586    587         for assignment in self.get_assignments_for_branches(name, branches):   588    589             # Use None to indicate a branch without assignment information.   590    591             if missing and isinstance(assignment, MissingBranch):   592                 positions.add(None)   593             else:   594                 pos = assignments.index(assignment)   595                 positions.add(pos)   596    597         positions = list(positions)   598         positions.sort()   599         return positions   600    601     def get_assignments_for_branches(self, name, branches, missing=True):   602    603         """   604         Return the origins of assignments involving the given 'name' affected   605         by the given 'branches'. The origins are a list of branches where names   606         are defined using assignments. If 'missing' is set to a false value,   607         branches with missing name details are excluded.   608         """   609    610         all_branches = []   611         assignments = self.assignments[name]   612    613         # Obtain the assignments recorded for each branch.   614    615         for branch in branches:   616    617             # Find the branch representing the definition of some names in the   618             # scope's assignments, making sure that the given name is involved.   619    620             for assignment in branch.get_assignment_sources(name):   621    622                 # Capture branches without assignment information as well as   623                 # genuine assignment branches.   624    625                 if assignment in assignments or missing and isinstance(assignment, MissingBranch):   626                     all_branches.append(assignment)   627    628         return all_branches   629    630     def get_all_usage(self):   631    632         """   633         Convert usage observations from the tracker to a simple mapping of   634         names to sets of attribute names.   635         """   636    637         d = {}   638         for name, branches in self.assignments.items():   639             d[name] = self.get_usage_from_branches_for_name(branches, name)   640         return d   641    642     def get_usage_from_branches_for_name(self, branches, name):   643    644         """   645         Convert usage observations from the 'branches' to a simple list of   646         usage sets for the given 'name'.   647         """   648    649         l = []   650         for branch in branches:   651             l.append(branch.get_usage()[name])   652         return l   653    654     def get_all_values(self):   655    656         "Return a mapping from names to lists of assigned values."   657    658         d = {}   659         for name, branches in self.assignments.items():   660             d[name] = [branch.values.get(name) for branch in branches]   661         return d   662    663 # Special objects.   664    665 class AbandonedDict(dict):   666    667     "A dictionary representing mappings in an abandoned branch."   668    669     def __repr__(self):   670         return "AbandonedDict()"   671    672 class MissingBranch(Branch):   673    674     "A branch introduced during dictionary merging."   675    676     def __repr__(self):   677         return "MissingBranch(%r, %r)" % (self.usage.keys(),   678             self.assignments and True or False)   679    680 def make_missing(name):   681    682     "Make a special branch indicating missing name information."   683    684     return set([MissingBranch([name], True)])   685    686 # Dictionary utilities.   687    688 def merge_dicts(dicts, ignored=AbandonedDict, missing=None):   689    690     """   691     Merge the given 'dicts' mapping keys to sets of values.   692    693     Where 'ignored' is specified, any dictionary of the given type is ignored.   694     Where all dictionaries to be merged are of the given type, an instance of   695     the type is returned as the merged dictionary.   696    697     Where 'missing' is specified, it provides a callable that produces a set of   698     suitable values for a given name.   699     """   700    701     new_dict = {}   702     all_names = set()   703    704     # Determine all known names.   705    706     for old_dict in dicts:   707         all_names.update(old_dict.keys())   708    709     # Merge the dictionaries, looking for all known names in each one.   710    711     have_dicts = False   712    713     for old_dict in dicts:   714    715         # Abandoned dictionaries should not contribute information.   716    717         if isinstance(old_dict, ignored):   718             continue   719         else:   720             have_dicts = True   721    722         for name in all_names:   723    724             # Find branches providing each name.   725    726             if old_dict.has_key(name):   727                 values = old_dict[name]   728    729             # Branches not providing names may indicate usage before assignment.   730    731             elif missing:   732                 values = missing(name)   733             else:   734                 continue   735    736             # Initialise mappings in the resulting dictionary.   737    738             if not new_dict.has_key(name):   739                 new_dict[name] = set(values)   740             else:   741                 new_dict[name].update(values)   742    743     # Where no dictionaries contributed, all branches were abandoned.   744    745     if have_dicts:   746         return new_dict   747     else:   748         return ignored()   749    750 def deepen_dict(d):   751    752     """   753     Return a version of dictionary 'd' with its values converted to sets   754     containing each original value as a single element in each new value.   755     Original values are assumed to be sequences. Thus...   756    757     {"self" : ("x", "y")}   758    759     ...would become...   760    761     {"self" : set([("x", "y")])}   762    763     ...allowing other such values to be added to the set alongside the original   764     value.   765     """   766    767     l = []   768    769     for key, value in d.items():   770    771         # Sort the attribute name details for stable comparisons.   772    773         value = list(value)   774         value.sort()   775         l.append((key, set([tuple(value)])))   776    777     return dict(l)   778    779 def combine_sets(s1, s2):   780    781     "Combine elements from sets 's1' and 's2'."   782    783     if not s1:   784         return s2   785     elif not s2:   786         return s1   787    788     s = set()   789    790     for i1 in s1:   791         for i2 in s2:   792    793             # Sort the attribute name details for stable comparisons.   794    795             l = list(set(i1 + i2))   796             l.sort()   797             s.add(tuple(l))   798    799     return s   800    801 def combine_dicts(d1, d2, combine=combine_sets):   802    803     """   804     Combine dictionaries 'd1' and 'd2' such that the values for common keys   805     are themselves combined in the result.   806     """   807    808     d = {}   809    810     for key in d1.keys():   811         if d2.has_key(key):   812             d[key] = combine(d1[key], d2[key])   813         else:   814             d[key] = d1[key]   815    816     return d   817    818 # vim: tabstop=4 expandtab shiftwidth=4