1 #!/usr/bin/env python 2 3 """ 4 Optimise object layouts and generate access instruction plans. 5 6 Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from common import add_counter_item, get_attrname_from_location, init_item, \ 23 sorted_output 24 from encoders import digest, encode_access_location, encode_instruction, get_kinds 25 from os.path import exists, join 26 from os import makedirs 27 from referencing import Reference 28 29 class Optimiser: 30 31 "Optimise objects in a program." 32 33 def __init__(self, importer, deducer, output): 34 35 """ 36 Initialise an instance using the given 'importer' and 'deducer' that 37 will perform the arrangement of attributes for program objects, writing 38 the results to the given 'output' directory. 39 """ 40 41 self.importer = importer 42 self.deducer = deducer 43 self.output = output 44 45 # Locations/offsets of attributes in objects. 46 47 self.locations = None 48 self.attr_locations = None 49 self.all_attrnames = None 50 51 # Locations of parameters in parameter tables. 52 53 self.arg_locations = None 54 self.param_locations = None 55 self.all_paramnames = None 56 57 # Specific attribute access information. 58 59 self.access_instructions = {} 60 self.accessor_kinds = {} 61 62 # Object structure information. 63 64 self.structures = {} 65 self.attr_table = {} 66 67 # Parameter list information. 68 69 self.parameters = {} 70 self.param_table = {} 71 72 # Constant literal information. 73 74 self.constants = [] 75 self.constant_numbers = {} 76 77 # Optimiser activities. 78 79 self.populate_objects() 80 self.position_attributes() 81 self.populate_parameters() 82 self.position_parameters() 83 self.populate_tables() 84 self.populate_constants() 85 self.initialise_access_instructions() 86 87 def to_output(self): 88 89 "Write the output files using optimisation information." 90 91 if not exists(self.output): 92 makedirs(self.output) 93 94 self.write_objects() 95 96 def write_objects(self): 97 98 """ 99 Write object-related output. 100 101 The locations are a list of positions indicating the attributes residing 102 at each position in the different structures in a program. 103 104 ---- 105 106 The parameter locations are a list of positions indicating the parameters 107 residing at each position in the different parameter lists in a program. 108 109 ---- 110 111 Each attribute plan provides attribute details in the following format: 112 113 location " " name " " test " " test type " " base 114 " " traversed attributes " " traversed attribute ambiguity 115 " " traversal access modes 116 " " attributes to traverse " " attribute ambiguity 117 " " context " " access method " " static attribute 118 119 Locations have the following format: 120 121 qualified name of scope "." local name ":" name version 122 123 Traversal access modes are either "class" (obtain accessor class to 124 access attribute) or "object" (obtain attribute directly from accessor). 125 126 ---- 127 128 The structures are presented as a table in the following format: 129 130 qualified name " " attribute names 131 132 The attribute names are separated by ", " characters and indicate the 133 attribute provided at each position in the structure associated with the 134 given type. Where no attribute is provided at a particular location 135 within a structure, "-" is given. 136 137 ---- 138 139 The parameters are presented as a table in the following format: 140 141 qualified name " " parameter details 142 143 The parameter details are separated by ", " characters and indicate 144 the parameter name and list position for each parameter described at 145 each location in the parameter table associated with the given 146 function. Where no parameter details are provided at a particular 147 location within a parameter table, "-" is given. The name and list 148 position are separated by a colon (":"). 149 150 ---- 151 152 The attribute table is presented as a table in the following format: 153 154 qualified name " " attribute identifiers 155 156 Instead of attribute names, identifiers defined according to the order 157 given in the "attrnames" file are employed to denote the attributes 158 featured in each type's structure. Where no attribute is provided at a 159 particular location within a structure, "-" is given. 160 161 ---- 162 163 The parameter table is presented as a table in the following format: 164 165 qualified name " " parameter details 166 167 Instead of parameter names, identifiers defined according to the order 168 given in the "paramnames" file are employed to denote the parameters 169 featured in each function's parameter table. Where no parameter is 170 provided at a particular location within a table, "-" is given. 171 172 ---- 173 174 The ordered list of attribute names is given in the "attrnames" file. 175 176 ---- 177 178 The ordered list of parameter names is given in the "paramnames" file. 179 180 ---- 181 182 The ordered list of constant literals is given in the "constants" file. 183 """ 184 185 f = open(join(self.output, "locations"), "w") 186 try: 187 for attrnames in self.locations: 188 print >>f, sorted_output(attrnames) 189 190 finally: 191 f.close() 192 193 f = open(join(self.output, "parameter_locations"), "w") 194 try: 195 for argnames in self.arg_locations: 196 print >>f, sorted_output(argnames) 197 198 finally: 199 f.close() 200 201 f = open(join(self.output, "instruction_plans"), "w") 202 try: 203 access_instructions = self.access_instructions.items() 204 access_instructions.sort() 205 206 for location, instructions in access_instructions: 207 print >>f, encode_access_location(location), "..." 208 for instruction in instructions: 209 print >>f, encode_instruction(instruction) 210 print >>f 211 212 finally: 213 f.close() 214 215 f = open(join(self.output, "structures"), "w") 216 try: 217 structures = self.structures.items() 218 structures.sort() 219 220 for name, attrnames in structures: 221 print >>f, name, ", ".join([s or "-" for s in attrnames]) 222 223 finally: 224 f.close() 225 226 f = open(join(self.output, "parameters"), "w") 227 try: 228 parameters = self.parameters.items() 229 parameters.sort() 230 231 for name, argnames in parameters: 232 print >>f, name, ", ".join([s and ("%s:%d" % s) or "-" for s in argnames]) 233 234 finally: 235 f.close() 236 237 f = open(join(self.output, "attrtable"), "w") 238 try: 239 attr_table = self.attr_table.items() 240 attr_table.sort() 241 242 for name, attrcodes in attr_table: 243 print >>f, name, ", ".join([i is not None and str(i) or "-" for i in attrcodes]) 244 245 finally: 246 f.close() 247 248 f = open(join(self.output, "paramtable"), "w") 249 try: 250 param_table = self.param_table.items() 251 param_table.sort() 252 253 for name, paramcodes in param_table: 254 print >>f, name, ", ".join([s and ("%d:%d" % s) or "-" for s in paramcodes]) 255 256 finally: 257 f.close() 258 259 f = open(join(self.output, "attrnames"), "w") 260 try: 261 for name in self.all_attrnames: 262 print >>f, name 263 264 finally: 265 f.close() 266 267 f = open(join(self.output, "paramnames"), "w") 268 try: 269 for name in self.all_paramnames: 270 print >>f, name 271 272 finally: 273 f.close() 274 275 f = open(join(self.output, "constants"), "w") 276 try: 277 constants = [] 278 for (value, value_type, encoding), n in self.constants.items(): 279 constants.append((n, value_type, encoding, value)) 280 constants.sort() 281 for n, value_type, encoding, value in constants: 282 print >>f, value_type, encoding or "{}", repr(value) 283 284 finally: 285 f.close() 286 287 def populate_objects(self): 288 289 "Populate objects using attribute and usage information." 290 291 self.all_attrs = {} 292 293 # Partition attributes into separate sections so that class and instance 294 # attributes are treated separately. 295 296 for source, objkind in [ 297 (self.importer.all_class_attrs, "<class>"), 298 (self.importer.all_instance_attrs, "<instance>"), 299 (self.importer.all_module_attrs, "<module>") 300 ]: 301 302 for name, attrnames in source.items(): 303 304 # Remove temporary names from structures. 305 306 attrnames = filter(lambda x: not x.startswith("$t"), attrnames) 307 self.all_attrs[(objkind, name)] = attrnames 308 309 self.locations = get_allocated_locations(self.all_attrs, get_attributes_and_sizes) 310 311 def populate_parameters(self): 312 313 "Populate parameter tables using parameter information." 314 315 self.arg_locations = [set()] + get_allocated_locations(self.importer.function_parameters, get_parameters_and_sizes) 316 317 def position_attributes(self): 318 319 "Position specific attribute references." 320 321 # Reverse the location mappings. 322 323 attr_locations = self.attr_locations = {} 324 325 for i, attrnames in enumerate(self.locations): 326 for attrname in attrnames: 327 attr_locations[attrname] = i 328 329 # Record the structures. 330 331 for (objkind, name), attrnames in self.all_attrs.items(): 332 key = Reference(objkind, name) 333 l = self.structures[key] = [None] * len(attrnames) 334 for attrname in attrnames: 335 position = attr_locations[attrname] 336 if position >= len(l): 337 l.extend([None] * (position - len(l) + 1)) 338 l[position] = attrname 339 340 def initialise_access_instructions(self): 341 342 "Expand access plans into instruction sequences." 343 344 for access_location, access_plan in self.deducer.access_plans.items(): 345 346 # Obtain the access details. 347 348 name, test, test_type, base, \ 349 traversed, traversal_modes, attrnames, \ 350 context, context_test, \ 351 first_method, final_method, \ 352 origin, accessor_kinds = access_plan 353 354 # Emit instructions by appending them to a list. 355 356 instructions = [] 357 emit = instructions.append 358 359 # Identify any static original accessor. 360 361 if base: 362 original_accessor = base 363 else: 364 original_accessor = "<expr>" # use a generic placeholder 365 366 # Prepare for any first attribute access. 367 368 if traversed: 369 attrname = traversed[0] 370 del traversed[0] 371 elif attrnames: 372 attrname = attrnames[0] 373 del attrnames[0] 374 375 # Perform the first access explicitly if at least one operation 376 # requires it. 377 378 access_first_attribute = final_method in ("access", "access-invoke", "assign") or traversed or attrnames 379 380 # Determine whether the first access involves assignment. 381 382 assigning = not traversed and not attrnames and final_method == "assign" 383 set_accessor = assigning and "<set_target_accessor>" or "<set_accessor>" 384 stored_accessor = assigning and "<target_accessor>" or "<accessor>" 385 386 # Set the context if already available. 387 388 if context == "base": 389 accessor = context_var = (base,) 390 elif context == "original-accessor": 391 392 # Prevent re-evaluation of any dynamic expression by storing it. 393 394 if original_accessor == "<expr>": 395 if final_method in ("access-invoke", "static-invoke"): 396 emit(("<set_context>", original_accessor)) 397 accessor = context_var = ("<context>",) 398 else: 399 emit((set_accessor, original_accessor)) 400 accessor = context_var = (stored_accessor,) 401 else: 402 accessor = context_var = (original_accessor,) 403 404 # Assigning does not set the context. 405 406 elif context in ("final-accessor", "unset") and access_first_attribute: 407 408 # Prevent re-evaluation of any dynamic expression by storing it. 409 410 if original_accessor == "<expr>": 411 emit((set_accessor, original_accessor)) 412 accessor = (stored_accessor,) 413 else: 414 accessor = (original_accessor,) 415 416 # Apply any test. 417 418 if test[0] == "test": 419 accessor = ("__%s_%s_%s" % test, accessor, test_type) 420 421 # Perform the first or final access. 422 # The access only needs performing if the resulting accessor is used. 423 424 remaining = len(traversed + attrnames) 425 426 if access_first_attribute: 427 428 if first_method == "relative-class": 429 if assigning: 430 emit(("__store_via_class", accessor, attrname, "<assexpr>")) 431 else: 432 accessor = ("__load_via_class", accessor, attrname) 433 434 elif first_method == "relative-object": 435 if assigning: 436 emit(("__store_via_object", accessor, attrname, "<assexpr>")) 437 else: 438 accessor = ("__load_via_object", accessor, attrname) 439 440 elif first_method == "relative-object-class": 441 if assigning: 442 emit(("__get_class_and_store", accessor, attrname, "<assexpr>")) 443 else: 444 accessor = ("__get_class_and_load", accessor, attrname) 445 446 elif first_method == "check-class": 447 if assigning: 448 emit(("__check_and_store_via_class", accessor, attrname, "<assexpr>")) 449 else: 450 accessor = ("__check_and_load_via_class", accessor, attrname) 451 452 elif first_method == "check-object": 453 if assigning: 454 emit(("__check_and_store_via_object", accessor, attrname, "<assexpr>")) 455 else: 456 accessor = ("__check_and_load_via_object", accessor, attrname) 457 458 elif first_method == "check-object-class": 459 if assigning: 460 emit(("__check_and_store_via_any", accessor, attrname, "<assexpr>")) 461 else: 462 accessor = ("__check_and_load_via_any", accessor, attrname) 463 464 # Traverse attributes using the accessor. 465 466 if traversed: 467 for attrname, traversal_mode in zip(traversed, traversal_modes): 468 assigning = remaining == 1 and final_method == "assign" 469 470 # Set the context, if appropriate. 471 472 if remaining == 1 and final_method != "assign" and context == "final-accessor": 473 474 # Invoked attributes employ a separate context accessed 475 # during invocation. 476 477 if final_method in ("access-invoke", "static-invoke"): 478 emit(("<set_context>", accessor)) 479 accessor = context_var = "<context>" 480 481 # A private context within the access is otherwise 482 # retained. 483 484 else: 485 emit(("<set_private_context>", accessor)) 486 accessor = context_var = "<private_context>" 487 488 # Perform the access only if not achieved directly. 489 490 if remaining > 1 or final_method in ("access", "access-invoke", "assign"): 491 492 if traversal_mode == "class": 493 if assigning: 494 emit(("__store_via_class", accessor, attrname, "<assexpr>")) 495 else: 496 accessor = ("__load_via_class", accessor, attrname) 497 else: 498 if assigning: 499 emit(("__store_via_object", accessor, attrname, "<assexpr>")) 500 else: 501 accessor = ("__load_via_object", accessor, attrname) 502 503 remaining -= 1 504 505 if attrnames: 506 for attrname in attrnames: 507 assigning = remaining == 1 and final_method == "assign" 508 509 # Set the context, if appropriate. 510 511 if remaining == 1 and final_method != "assign" and context == "final-accessor": 512 513 # Invoked attributes employ a separate context accessed 514 # during invocation. 515 516 if final_method in ("access-invoke", "static-invoke"): 517 emit(("<set_context>", accessor)) 518 accessor = context_var = "<context>" 519 520 # A private context within the access is otherwise 521 # retained. 522 523 else: 524 emit(("<set_private_context>", accessor)) 525 accessor = context_var = "<private_context>" 526 527 # Perform the access only if not achieved directly. 528 529 if remaining > 1 or final_method in ("access", "access-invoke", "assign"): 530 531 if assigning: 532 emit(("__check_and_store_via_any", accessor, attrname, "<assexpr>")) 533 else: 534 accessor = ("__check_and_load_via_any", accessor, attrname) 535 536 remaining -= 1 537 538 # Define or emit the means of accessing the actual target. 539 540 # Assignments to known attributes. 541 542 if final_method == "static-assign": 543 parent, attrname = origin.rsplit(".", 1) 544 emit(("__store_via_object", parent, attrname, "<assexpr>")) 545 546 # Invoked attributes employ a separate context. 547 548 elif final_method in ("static", "static-invoke"): 549 accessor = ("__load_static_ignore", origin) 550 551 # Wrap accesses in context operations. 552 553 if context_test == "test": 554 555 # Test and combine the context with static attribute details. 556 557 if final_method == "static": 558 emit(("__load_static_test", context_var, origin)) 559 560 # Test the context, storing it separately if required for the 561 # immediately invoked static attribute. 562 563 elif final_method == "static-invoke": 564 emit(("<test_context_static>", context_var, origin)) 565 566 # Test the context, storing it separately if required for an 567 # immediately invoked attribute. 568 569 elif final_method == "access-invoke": 570 emit(("<test_context_revert>", context_var, accessor)) 571 572 # Test the context and update the attribute details if 573 # appropriate. 574 575 else: 576 emit(("__test_context", context_var, accessor)) 577 578 elif context_test == "replace": 579 580 # Produce an object with updated context. 581 582 if final_method == "static": 583 emit(("__load_static_replace", context_var, origin)) 584 585 # Omit the context update operation where the target is static 586 # and the context is recorded separately. 587 588 elif final_method == "static-invoke": 589 pass 590 591 # If a separate context is used for an immediate invocation, 592 # produce the attribute details unchanged. 593 594 elif final_method == "access-invoke": 595 emit(accessor) 596 597 # Update the context in the attribute details. 598 599 else: 600 emit(("__update_context", context_var, accessor)) 601 602 # Omit the accessor for assignments and for invocations of static 603 # targets. 604 605 elif final_method not in ("assign", "static-assign", "static-invoke"): 606 emit(accessor) 607 608 self.access_instructions[access_location] = instructions 609 self.accessor_kinds[access_location] = accessor_kinds 610 611 def get_ambiguity_for_attributes(self, attrnames): 612 613 """ 614 Return a list of attribute position alternatives corresponding to each 615 of the given 'attrnames'. 616 """ 617 618 ambiguity = [] 619 620 for attrname in attrnames: 621 position = self.attr_locations[attrname] 622 ambiguity.append(len(self.locations[position])) 623 624 return ambiguity 625 626 def position_parameters(self): 627 628 "Position the parameters for each function's parameter table." 629 630 # Reverse the location mappings. 631 632 param_locations = self.param_locations = {} 633 634 for i, argnames in enumerate(self.arg_locations): 635 636 # Position the arguments. 637 638 for argname in argnames: 639 param_locations[argname] = i 640 641 for name, argnames in self.importer.function_parameters.items(): 642 643 # Allocate an extra context parameter in the table. 644 645 l = self.parameters[name] = [None] + [None] * len(argnames) 646 647 # Store an entry for the name along with the name's position in the 648 # parameter list. 649 650 for pos, argname in enumerate(argnames): 651 652 # Position the argument in the table. 653 654 position = param_locations[argname] 655 if position >= len(l): 656 l.extend([None] * (position - len(l) + 1)) 657 658 # Indicate an argument list position starting from 1 (after the 659 # initial context argument). 660 661 l[position] = (argname, pos + 1) 662 663 def populate_tables(self): 664 665 """ 666 Assign identifiers to attributes and encode structure information using 667 these identifiers. 668 """ 669 670 self.all_attrnames, d = self._get_name_mapping(self.attr_locations) 671 672 # Record the numbers indicating the locations of the names. 673 674 for key, attrnames in self.structures.items(): 675 l = self.attr_table[key] = [] 676 for attrname in attrnames: 677 if attrname is None: 678 l.append(None) 679 else: 680 l.append(d[attrname]) 681 682 self.all_paramnames, d = self._get_name_mapping(self.param_locations) 683 684 # Record the numbers indicating the locations of the names. 685 686 for key, values in self.parameters.items(): 687 l = self.param_table[key] = [] 688 for value in values: 689 if value is None: 690 l.append(None) 691 else: 692 name, pos = value 693 l.append((d[name], pos)) 694 695 def _get_name_mapping(self, locations): 696 697 """ 698 Get a sorted list of names from 'locations', then map them to 699 identifying numbers. Return the list and the mapping. 700 """ 701 702 all_names = locations.keys() 703 all_names.sort() 704 d = {} 705 for i, name in enumerate(all_names): 706 d[name] = i 707 return all_names, d 708 709 def populate_constants(self): 710 711 """ 712 Obtain a collection of distinct constant literals, building a mapping 713 from constant references to those in this collection. 714 """ 715 716 # Obtain mappings from constant values to identifiers. 717 718 self.constants = {} 719 720 for path, constants in self.importer.all_constants.items(): 721 722 # Record constants and obtain a number for them. 723 # Each constant is actually (value, value_type, encoding). 724 725 for constant, n in constants.items(): 726 self.constants[constant] = digest(constant) 727 728 self.constant_numbers = {} 729 730 for name, constant in self.importer.all_constant_values.items(): 731 self.constant_numbers[name] = self.constants[constant] 732 733 def combine_rows(a, b): 734 c = [] 735 for i, j in zip(a, b): 736 if i is None or j is None: 737 c.append(i or j) 738 else: 739 return None 740 return c 741 742 def get_attributes_and_sizes(d): 743 744 """ 745 Return a matrix of attributes, a list of type names corresponding to columns 746 in the matrix, and a list of ranked sizes each indicating... 747 748 * a weighted size depending on the kind of object 749 * the minimum size of an object employing an attribute 750 * the number of free columns in the matrix for the attribute 751 * the attribute name itself 752 """ 753 754 attrs = {} 755 sizes = {} 756 objkinds = {} 757 758 for name, attrnames in d.items(): 759 objkind, _name = name 760 761 for attrname in attrnames: 762 763 # Record each type supporting the attribute. 764 765 init_item(attrs, attrname, set) 766 attrs[attrname].add(name) 767 768 # Maintain a record of the smallest object size supporting the given 769 # attribute. 770 771 if not sizes.has_key(attrname): 772 sizes[attrname] = len(attrnames) 773 else: 774 sizes[attrname] = min(sizes[attrname], len(attrnames)) 775 776 # Record the object types/kinds supporting the attribute. 777 778 init_item(objkinds, attrname, set) 779 objkinds[attrname].add(objkind) 780 781 # Obtain attribute details in order of size and occupancy. 782 783 names = d.keys() 784 785 rsizes = [] 786 for attrname, size in sizes.items(): 787 priority = "<instance>" in objkinds[attrname] and 0.5 or 1 788 occupied = len(attrs[attrname]) 789 key = (priority * size, size, len(names) - occupied, attrname) 790 rsizes.append(key) 791 792 rsizes.sort() 793 794 # Make a matrix of attributes. 795 796 matrix = {} 797 for attrname, types in attrs.items(): 798 row = [] 799 for name in names: 800 if name in types: 801 row.append(attrname) 802 else: 803 row.append(None) 804 matrix[attrname] = row 805 806 return matrix, names, rsizes 807 808 def get_parameters_and_sizes(d): 809 810 """ 811 Return a matrix of parameters, a list of functions corresponding to columns 812 in the matrix, and a list of ranked sizes each indicating... 813 814 * a weighted size depending on the kind of object 815 * the minimum size of a parameter list employing a parameter 816 * the number of free columns in the matrix for the parameter 817 * the parameter name itself 818 819 This is a slightly simpler version of the above 'get_attributes_and_sizes' 820 function. 821 """ 822 823 params = {} 824 sizes = {} 825 826 for name, argnames in d.items(): 827 for argname in argnames: 828 829 # Record each function supporting the parameter. 830 831 init_item(params, argname, set) 832 params[argname].add(name) 833 834 # Maintain a record of the smallest parameter list supporting the 835 # given parameter. 836 837 if not sizes.has_key(argname): 838 sizes[argname] = len(argnames) 839 else: 840 sizes[argname] = min(sizes[argname], len(argnames)) 841 842 # Obtain attribute details in order of size and occupancy. 843 844 names = d.keys() 845 846 rsizes = [] 847 for argname, size in sizes.items(): 848 occupied = len(params[argname]) 849 key = (size, size, len(names) - occupied, argname) 850 rsizes.append(key) 851 852 rsizes.sort() 853 854 # Make a matrix of parameters. 855 856 matrix = {} 857 for argname, types in params.items(): 858 row = [] 859 for name in names: 860 if name in types: 861 row.append(argname) 862 else: 863 row.append(None) 864 matrix[argname] = row 865 866 return matrix, names, rsizes 867 868 def get_allocated_locations(d, fn): 869 870 """ 871 Return a list where each element corresponds to a structure location and 872 contains a set of attribute names that may be stored at that location, given 873 a mapping 'd' whose keys are (object kind, object name) tuples and whose 874 values are collections of attributes. 875 """ 876 877 matrix, names, rsizes = fn(d) 878 allocated = [] 879 880 x = 0 881 while x < len(rsizes): 882 weight, size, free, attrname = rsizes[x] 883 base = matrix[attrname] 884 y = x + 1 885 while y < len(rsizes): 886 _weight, _size, _free, _attrname = rsizes[y] 887 occupied = len(names) - _free 888 if occupied > free: 889 break 890 new = combine_rows(base, matrix[_attrname]) 891 if new: 892 del matrix[_attrname] 893 del rsizes[y] 894 base = new 895 free -= occupied 896 else: 897 y += 1 898 allocated.append(base) 899 x += 1 900 901 # Return the list of attribute names from each row of the allocated 902 # attributes table. 903 904 locations = [] 905 for attrnames in allocated: 906 l = set() 907 for attrname in attrnames: 908 if attrname: 909 l.add(attrname) 910 locations.append(l) 911 return locations 912 913 # vim: tabstop=4 expandtab shiftwidth=4