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