1 #!/usr/bin/env python 2 3 """ 4 The micropython package for processing Python source code. The code originates 5 from the simplify package but has had various details related to that package 6 removed. 7 8 Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Paul Boddie <paul@boddie.org.uk> 9 10 This program is free software; you can redistribute it and/or modify it under 11 the terms of the GNU General Public License as published by the Free Software 12 Foundation; either version 3 of the License, or (at your option) any later 13 version. 14 15 This program is distributed in the hope that it will be useful, but WITHOUT 16 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 17 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 18 details. 19 20 You should have received a copy of the GNU General Public License along with 21 this program. If not, see <http://www.gnu.org/licenses/>. 22 23 -------- 24 25 To use this module, an importer should be constructed. Here, the standard path 26 for module searching is employed: 27 28 importer = Importer(sys.path) 29 30 To generate programs, the above importer should be supplied in the 31 initialisation of a program instance: 32 33 program = Program(importer) 34 35 Such importer and program objects are the most convenient mechanism through 36 which the functionality of the micropython package may be accessed. 37 """ 38 39 from micropython.data import * 40 from micropython.errors import * 41 from micropython.objectset import ObjectSet 42 from micropython.types import * 43 import micropython.inspect 44 import micropython.table 45 import os 46 import sys 47 48 try: 49 set 50 except NameError: 51 from sets import Set as set 52 53 class Program: 54 55 "This class supports the generation of a program image." 56 57 def __init__(self, importer): 58 59 """ 60 Initialise the program representation with an 'importer' which is able 61 to locate and load Python modules. 62 """ 63 64 self.importer = importer 65 66 # Remember the tables once generated. 67 68 self.objtable = None 69 70 # A record of nodes for which no attribute target could be found. 71 72 self.unknown_target_nodes = [] 73 self.independent_target_nodes = [] 74 75 def get_importer(self): 76 return self.importer 77 78 # Access to finalised program information. 79 80 def finalise(self): 81 82 "Finalise the program." 83 84 self.importer.complete_modules() 85 86 # Need the tables to finalise. 87 88 objtable = self.get_object_table() 89 90 self.importer.vacuum(objtable) 91 92 # Now remove unneeded things from the tables. 93 94 objtable = self.get_object_table(reset=1) 95 96 self.importer.finalise(objtable) 97 98 def get_object_table(self, reset=0): 99 100 "Return a table with details of attributes for classes and modules." 101 102 if self.objtable is None or reset: 103 104 t = self.objtable = micropython.table.ObjectTable() 105 106 # First, get all active modules and classes. 107 108 all_objects = set() 109 110 for module in self.importer.get_modules(): 111 all_objects.add(module) 112 for obj in module.all_objects: 113 if isinstance(obj, Class): 114 all_objects.add(obj) 115 116 # Then, visit the modules and classes. 117 118 for obj in all_objects: 119 120 # Add module attributes and module identity information. 121 122 if isinstance(obj, Module): 123 full_name = obj.full_name() 124 attributes = {"#" + full_name : obj} 125 attributes.update(obj.module_attributes()) 126 t.add(full_name, attributes) 127 128 # Add class and instance attributes for all classes, together 129 # with descendant information. 130 131 elif isinstance(obj, Class): 132 133 full_name = obj.full_name() 134 135 # Prevent ambiguous classes. 136 137 #if obj.module.has_key(obj.name) and obj.module[obj.name].defines_ambiguous_class(): 138 # raise TableGenerationError, "Class %r in module %r is ambiguously defined." % (obj.name, obj.module.full_name()) 139 140 # Define a table entry for the class. 141 142 attributes = {"#" + full_name : obj} 143 attributes.update(obj.all_attributes()) 144 145 # Filter out unused classes. 146 147 for name, descendant in obj.all_descendants().items(): 148 if descendant in all_objects: 149 attributes["#" + name] = descendant 150 151 # Merge ambiguous classes. 152 153 if t.has(full_name): 154 old_attributes = t.get(full_name) 155 old_attributes.update(attributes) 156 attributes = old_attributes 157 158 t.add(full_name, attributes) 159 160 return self.objtable 161 162 class Importer: 163 164 "An import machine, searching for and loading modules." 165 166 predefined_constants = { 167 "None" : None, 168 "True" : True, 169 "False" : False, 170 #"Ellipsis" : Ellipsis, 171 "NotImplemented" : NotImplemented 172 } 173 174 names_always_used = [ 175 "__call__" 176 ] 177 178 def __init__(self, path=None, verbose=0): 179 180 """ 181 Initialise the importer with the given search 'path' - a list of 182 directories to search for Python modules. 183 184 The optional 'verbose' parameter causes output concerning the activities 185 of the object to be produced if set to a true value (not the default). 186 """ 187 188 self.path = path or [os.getcwd()] 189 self.verbose = verbose 190 191 self.modules = {} 192 self.modules_ordered = [] 193 self.loading = set() 194 195 # Importers responsible for initially importing modules. 196 197 self.importers = {} 198 199 # Modules involved in circular imports. 200 201 self.circular_imports = set() 202 203 # Constant records. 204 205 self.constant_values = {} 206 self.constants_used = set() 207 self.all_constants_used = set() 208 self.constant_references = {} 209 self.init_predefined_constants() 210 211 # Attribute usage. 212 213 self.attributes_used = set() 214 self.name_references = {} 215 self.specific_name_references = {} 216 217 # Attribute coverage calculated during collection. 218 219 self.inferred_name_references = {} 220 221 # Attribute coverage status during collection. 222 223 self.attribute_users_visited = set() 224 self.attributes_to_visit = {} 225 self.collecting_dynamic = False 226 227 # Attribute usage type deduction failures. 228 229 self.attribute_usage_failures = set() 230 231 # Status information. 232 233 self.completed = False 234 self.vacuumed = False 235 self.finalised = False 236 237 def get_modules(self): 238 239 "Return all modules known to the importer." 240 241 return self.modules.values() 242 243 def get_module(self, name): 244 245 "Return the module with the given 'name'." 246 247 return self.modules[name] 248 249 def complete_modules(self): 250 251 "Complete the processing of modules." 252 253 if self.completed: 254 return 255 256 self.get_module("__builtins__").complete() 257 self.get_module("__main__").complete() 258 259 self.completed = True 260 261 # General maintenance. 262 263 def vacuum(self, objtable): 264 265 "Tidy up the modules." 266 267 if self.vacuumed: 268 return 269 270 # Complete the list of attribute names used in the program. 271 272 self.collect_attributes(objtable) 273 274 for name, module in self.modules.items(): 275 if module.loaded: 276 module.vacuum() 277 else: 278 del self.modules[name] 279 280 self.vacuumed = True 281 282 def finalise(self, objtable): 283 284 "Finalise the program (which should have been vacuumed first)." 285 286 if self.finalised: 287 return 288 289 # Reset any previously compiled information. 290 291 for module in self.get_modules(): 292 module.unfinalise() 293 294 # Prepare module information again. 295 296 for module in self.get_modules(): 297 module.finalise(objtable) 298 299 self.finalised = True 300 301 # Name accounting. 302 303 def use_name(self, name, from_name, value=None): 304 305 """ 306 Register the given 'name' as being used in the program from within an 307 object with the specified 'from_name'. If the optional 'value' is given, 308 note an assignment. 309 """ 310 311 if not self.name_references.has_key(from_name): 312 self.name_references[from_name] = set() 313 314 attrnames = ObjectSet([name]) 315 316 # Note the assignment in association with the given attribute name. 317 318 if value is not None: 319 attrnames[name].add(value) 320 321 # Only a single set of usage is recorded here, but other situations 322 # may involve multiple usage observations. 323 324 usage = (attrnames,) 325 self.name_references[from_name].add((None, None, usage)) 326 327 def use_names(self, user, name, usage, from_name): 328 329 """ 330 For the given attribute 'user' (which may be None if no specific user is 331 given), register for the given 'name' the given attribute 'usage' 332 (combinations of attribute names), noting the scope of this usage as 333 being the program object with the specified 'from_name'. 334 """ 335 336 if not self.name_references.has_key(from_name): 337 self.name_references[from_name] = set() 338 339 self.name_references[from_name].add((user, name, usage)) 340 341 def use_specific_name(self, objname, attrname, from_name): 342 343 """ 344 Register the given 'objname' (for an object) whose 'attrname' is being 345 used in the program from within an object with the specified 346 'from_name'. 347 """ 348 349 if not self.specific_name_references.has_key(from_name): 350 self.specific_name_references[from_name] = set() 351 self.specific_name_references[from_name].add((objname, attrname)) 352 353 # Name accounting products. 354 355 def uses_attribute(self, objname, name): 356 357 """ 358 Return whether the attribute of the object with the given 'objname' 359 having the given 'name' is used as an attribute in the program. 360 """ 361 362 return (objname + "." + name) in self.attributes_used 363 364 def use_attribute(self, objname, name): 365 366 """ 367 Indicate that in the object with the given 'objname', the attribute of 368 the given 'name' is used. 369 """ 370 371 self.attributes_used.add(objname + "." + name) 372 373 def use_object(self, objname): 374 375 "Indicate that the object with the given 'objname' is used." 376 377 self.attributes_used.add(objname) 378 379 def collect_attributes(self, objtable): 380 381 "Collect attribute references for the entire program." 382 383 # Include names which may not be explicitly used in programs. 384 # NOTE: These would potentially be declared when inspecting, but the 385 # NOTE: only name involved currently (__call__) is implicit in 386 # NOTE: invocations and cannot be detected. 387 388 for attrname in self.names_always_used: 389 for objname in objtable.all_possible_objects([attrname]): 390 391 # Record attributes of objects for potential visiting. 392 393 self.add_attribute_to_visit(objname, attrname) 394 395 # Visit all modules, since some may employ initialisation code which has 396 # some kind of side-effect. 397 398 for name in self.modules.keys(): 399 self._collect_attributes(name, objtable) 400 401 # Even after all modules have been visited, there may be a need to 402 # re-evaluate getattr invocations in the context of constants now known 403 # to be used. 404 405 if "__builtins__.getattr" in self.attribute_users_visited: 406 self.collecting_dynamic = True 407 self._collect_attributes_from_getattr(objtable) 408 409 def add_attribute_to_visit(self, objname, attrname): 410 411 """ 412 Queue an attribute of the object with the given 'objname', having the 413 given 'attrname', to the list for potential visiting if the specified 414 object is actually referenced. 415 """ 416 417 if not self.attributes_to_visit.has_key(objname): 418 self.attributes_to_visit[objname] = set() 419 self.attributes_to_visit[objname].add(attrname) 420 421 def _collect_attributes_from(self, from_name, objname, attrname, objtable): 422 423 """ 424 Record the association between 'from_name' and the attribute of 425 'objname' with the given 'attrname'. Then collect attributes for the 426 referenced attribute using 'objtable'. 427 """ 428 429 if not self.inferred_name_references.has_key(from_name): 430 self.inferred_name_references[from_name] = set() 431 432 self.inferred_name_references[from_name].add((objname, attrname)) 433 self._collect_attributes(objname + "." + attrname, objtable) 434 435 def _collect_attributes_from_getattr(self, objtable): 436 437 """ 438 The getattr function is a special case: it can potentially reference 439 any known attribute. Since accessor attributes must be known 440 constants, the intersection of known constants and attributes is used 441 to build a set of objects that might be referenced by getattr. 442 """ 443 444 all_attributes = set(objtable.attribute_names()) 445 all_string_constants = set([const.get_value() for const in self.constants_used 446 if const.value_type_name() == "__builtins__.str"]) 447 all_attribute_constants = all_attributes.intersection(all_string_constants) 448 449 self.all_constants_used.update(self.constants_used) 450 self.constants_used = set() 451 452 # Get the types supporting each attribute and visit the referenced 453 # objects. 454 455 all_objtypes = set() 456 457 for attrname in all_attribute_constants: 458 objtypes = objtable.any_possible_objects_plus_status([attrname]) 459 all_objtypes.update(objtypes) 460 461 # Attribute assignment does not take place, so an empty list of 462 # values is given. 463 464 self._collect_attributes_for_types("__builtins__.getattr", objtable, all_objtypes, 465 [{attrname : []} for attrname in all_attribute_constants]) 466 467 def _collect_attributes(self, from_name, objtable): 468 469 """ 470 Given an object called 'from_name', find all names referenced from such 471 an object according to the register of names, using 'objtable' to infer 472 types. 473 """ 474 475 if from_name != "__builtins__.getattr" and from_name in self.attribute_users_visited: 476 return 477 478 self.attribute_users_visited.add(from_name) 479 480 if from_name == "__builtins__.getattr": 481 if self.collecting_dynamic: 482 self._collect_attributes_from_getattr(objtable) 483 else: 484 return 485 486 # Get constant references. 487 488 for const in self.constant_references.get(from_name, []): 489 self.constants_used.add(const) 490 491 # Get name references and find possible objects which support such 492 # combinations of attribute names. 493 494 for user, name, usage in self.name_references.get(from_name, []): 495 496 # Using all attribute names for a particular name, attempt to get 497 # specific object types. 498 499 all_objtypes = get_object_types_for_usage(usage, objtable, name, from_name, True, self) 500 if not all_objtypes: 501 all_objtypes = get_object_types_for_usage(usage, objtable, name, from_name, False, self) 502 503 # Where the name through which the attributes are accessed is the 504 # special "self" name, restrict the possibilities to types 505 # appropriate for the method involved. 506 507 if name == "self" and user and user.unit and user.unit.is_method(): 508 cls = user.unit.parent 509 valid_objtypes = filter_using_self(all_objtypes, cls) 510 else: 511 valid_objtypes = all_objtypes 512 513 # Investigate the object types. 514 515 self._collect_attributes_for_types(from_name, objtable, valid_objtypes, usage) 516 517 # Get specific name references and visit the referenced objects. 518 519 for objname, attrname in self.specific_name_references.get(from_name, []): 520 self.use_attribute(objname, attrname) 521 self._collect_attributes_from(from_name, objname, attrname, objtable) 522 523 # Where the object has an __init__ attribute, assume that it is an 524 # initialiser which is called at some point, and collect attributes used 525 # in this initialiser. 526 527 if "__init__" in objtable.table.get(from_name, []): 528 self.use_attribute(from_name, "__init__") 529 self._collect_attributes_from(from_name, from_name, "__init__", objtable) 530 531 # Visit attributes on this object that were queued in case of the object 532 # being referenced. 533 534 attributes_to_visit = self.attributes_to_visit.get(from_name, []) 535 536 if attributes_to_visit: 537 del self.attributes_to_visit[from_name] 538 539 for attrname in attributes_to_visit: 540 self.use_attribute(from_name, attrname) 541 self._collect_attributes_from(from_name, from_name, attrname, objtable) 542 543 def _collect_attributes_for_types(self, from_name, objtable, objtypes, usage): 544 545 """ 546 For the unit known as 'from_name' and using the 'objtable' to validate 547 each attribute, identify and attempt to visit attributes found for each 548 of the suggested object types given by 'objtypes' and the 'usage' 549 provided. 550 """ 551 552 for objname, is_static in objtypes: 553 for attrnames in usage: 554 for attrname, attrvalues in attrnames.items(): 555 556 # Test for the presence of an attribute on the suggested 557 # object type. 558 559 try: 560 attr = objtable.access(objname, attrname) 561 except TableError: 562 #print >>sys.stderr, "Warning: object type %r does not support attribute %r" % (objname, attrname) 563 continue 564 565 # Get the real identity of the attribute in order to 566 # properly collect usage from it. 567 568 parent = attr.parent 569 570 # NOTE: At this point, parent should never be None. 571 572 if parent is None: 573 continue 574 575 # Instances provide the same identity as the object name. 576 577 elif isinstance(parent, Instance): 578 parentname = objname 579 580 # In general, the fully qualified name is obtained. 581 582 else: 583 parentname = parent.full_name() 584 585 # Test for assignment. 586 587 if attrvalues: 588 589 # NOTE: Here, an instance can actually represent any 590 # NOTE: kind of object. 591 592 if isinstance(parent, Instance): 593 594 # Get the parent object using the special 595 # table entry. 596 597 parent = objtable.get_object(objname) 598 599 # Permit assignment to known instance attributes 600 # only. 601 602 if not (isinstance(parent, Class) and 603 parent.instance_attributes().has_key(attrname)): 604 605 print >>sys.stderr, "Warning: potential assignment to instance attribute %s of %s not permitted" % ( 606 attrname, parent.full_name()) 607 608 # Assignment to a known attribute is permitted. 609 610 elif parent.has_key(attrname): 611 for attrvalue in attrvalues: 612 parent.set(attrname, attrvalue, 0) 613 614 # Assignment to an unknown attribute is not permitted. 615 616 else: 617 print >>sys.stderr, "Warning: potential assignment to static attribute %s of %s not permitted" % ( 618 attrname, parent.full_name()) 619 620 # Visit attributes of objects known to be used. 621 622 if parentname in self.attributes_used: 623 self.use_attribute(parentname, attrname) 624 self._collect_attributes_from(from_name, parentname, attrname, objtable) 625 626 # Record attributes of other objects for potential visiting. 627 628 else: 629 self.add_attribute_to_visit(parentname, attrname) 630 631 def add_usage_failure(self, all_attributes, unit_name, name, attrnames): 632 633 """ 634 Record a type deduction failure based on 'all_attributes' (where true 635 indicates that all attribute names were required; false indicating that 636 any were required) within the given 'unit_name' for the variable of the 637 given 'name' and for the specified 'attrnames'. 638 """ 639 640 attrnames = tuple(attrnames) 641 self.attribute_usage_failures.add((unit_name, name, attrnames, all_attributes)) 642 643 # Constant accounting. 644 645 def use_constant(self, const, from_name): 646 647 """ 648 Register the given 'const' as being used in the program from within an 649 object with the specified 'from_name'. 650 """ 651 652 if not self.constant_references.has_key(from_name): 653 self.constant_references[from_name] = set() 654 655 self.constant_references[from_name].add(const) 656 657 def init_predefined_constants(self): 658 659 "Ensure the predefined constants." 660 661 for name, value in self.predefined_constants.items(): 662 self.constants_used.add(self.make_constant(value)) 663 664 def get_predefined_constant(self, name): 665 666 "Return the predefined constant for the given 'name'." 667 668 return self.make_constant(self.predefined_constants[name]) 669 670 def get_constant(self, value): 671 672 "Return a constant for the given 'value'." 673 674 const = Const(value) 675 return self.constant_values[const] 676 677 def get_constant_type_name(self, value): 678 679 "Return the type name for the given constant 'value'." 680 681 return value.__class__.__name__ 682 683 def make_constant(self, value): 684 685 "Make and return a constant for the given 'value'." 686 687 # Make a constant object and return it. 688 689 const = Const(value) 690 if not self.constant_values.has_key(const): 691 self.constant_values[const] = const 692 return self.constant_values[const] 693 694 def constants(self): 695 696 "Return a list of constants." 697 698 return self.all_constants_used 699 700 # Import methods. 701 702 def find_in_path(self, name): 703 704 """ 705 Find the given module 'name' in the search path, returning None where no 706 such module could be found, or a 2-tuple from the 'find' method 707 otherwise. 708 """ 709 710 for d in self.path: 711 m = self.find(d, name) 712 if m: return m 713 return None 714 715 def find(self, d, name): 716 717 """ 718 In the directory 'd', find the given module 'name', where 'name' can 719 either refer to a single file module or to a package. Return None if the 720 'name' cannot be associated with either a file or a package directory, 721 or a 2-tuple from '_find_package' or '_find_module' otherwise. 722 """ 723 724 m = self._find_package(d, name) 725 if m: return m 726 m = self._find_module(d, name) 727 if m: return m 728 return None 729 730 def _find_module(self, d, name): 731 732 """ 733 In the directory 'd', find the given module 'name', returning None where 734 no suitable file exists in the directory, or a 2-tuple consisting of 735 None (indicating that no package directory is involved) and a filename 736 indicating the location of the module. 737 """ 738 739 name_py = name + os.extsep + "py" 740 filename = self._find_file(d, name_py) 741 if filename: 742 return None, filename 743 return None 744 745 def _find_package(self, d, name): 746 747 """ 748 In the directory 'd', find the given package 'name', returning None 749 where no suitable package directory exists, or a 2-tuple consisting of 750 a directory (indicating the location of the package directory itself) 751 and a filename indicating the location of the __init__.py module which 752 declares the package's top-level contents. 753 """ 754 755 filename = self._find_file(d, name) 756 if filename: 757 init_py = "__init__" + os.path.extsep + "py" 758 init_py_filename = self._find_file(filename, init_py) 759 if init_py_filename: 760 return filename, init_py_filename 761 return None 762 763 def _find_file(self, d, filename): 764 765 """ 766 Return the filename obtained when searching the directory 'd' for the 767 given 'filename', or None if no actual file exists for the filename. 768 """ 769 770 filename = os.path.join(d, filename) 771 if os.path.exists(filename): 772 return filename 773 else: 774 return None 775 776 def load(self, name, return_leaf=0, importer=None): 777 778 """ 779 Load the module or package with the given 'name'. Return an object 780 referencing the loaded module or package, or None if no such module or 781 package exists. 782 783 If the given 'importer' is specified, it will be associated with the 784 imported module if it is responsible for importing the module for the 785 first time. 786 """ 787 788 if return_leaf: 789 name_for_return = name 790 else: 791 name_for_return = name.split(".")[0] 792 793 # Loaded modules are returned immediately. 794 # Modules may be known but not yet loading (having been registered as 795 # submodules), loading, loaded, or completely unknown. 796 797 if self.modules.has_key(name) and self.modules[name].loaded: 798 if self.verbose: 799 print >>sys.stderr, "Cached (%s)" % name 800 return self.modules[name_for_return] 801 802 if self.verbose: 803 print >>sys.stderr, "Loading", name 804 805 # Split the name into path components, and try to find the uppermost in 806 # the search path. 807 808 path = name.split(".") 809 path_so_far = [] 810 top = module = None 811 812 for p in path: 813 814 # Get the module's filesystem details. 815 816 if not path_so_far: 817 m = self.find_in_path(p) 818 else: 819 m = self.find(d, p) 820 821 if not m: 822 if self.verbose: 823 print >>sys.stderr, "Not found (%s)" % p 824 return None # NOTE: Import error. 825 826 # Get the module itself. 827 828 d, filename = m 829 path_so_far.append(p) 830 module_name = ".".join(path_so_far) 831 submodule = self.load_from_file(filename, module_name, importer) 832 833 if module is None: 834 top = submodule 835 else: 836 # Store the submodule within its parent module. 837 838 module.set_module(p, submodule) 839 840 module = submodule 841 842 # Stop descending if no package was found. 843 844 if not d: 845 break 846 847 # Return either the deepest or the uppermost module. 848 849 if return_leaf: 850 return module 851 else: 852 return top 853 854 def load_from_file(self, name, module_name=None, importer=None): 855 856 """ 857 Load the module with the given 'name' (which may be a full module path). 858 859 If the given 'importer' is specified, it will be associated with the 860 imported module if it is responsible for importing the module for the 861 first time. 862 """ 863 864 if module_name is None: 865 module_name = "__main__" 866 867 module = self.add_module(module_name) 868 if not module.loaded and module not in self.loading: 869 self.loading.add(module) 870 if self.verbose: 871 print >>sys.stderr, "Parsing", name 872 module.parse(name) 873 if self.verbose: 874 print >>sys.stderr, "Loaded", name 875 self.loading.remove(module) 876 module.loaded = True 877 878 # Record each module as imported by any importer. 879 880 if importer: 881 if not self.importers.has_key(importer): 882 self.importers[importer] = [] 883 self.importers[importer].append(module) 884 885 # Record the module. 886 887 self.use_object(module.full_name()) 888 #print >>sys.stderr, "Loaded", module_name, "with namespace", module.namespace.keys() 889 return module 890 891 def add_module(self, module_name): 892 893 """ 894 Return the module with the given 'module_name', adding a new module 895 object if one does not already exist. 896 """ 897 898 if not self.modules.has_key(module_name): 899 self.modules[module_name] = module = micropython.inspect.InspectedModule(module_name, self) 900 self.modules_ordered.append(module) 901 else: 902 module = self.modules[module_name] 903 return module 904 905 def add_submodules(self, pathname, module): 906 907 """ 908 Work around insufficient __all__ declarations and examine the directory 909 with the given 'pathname', adding submodules to the given 'module'. 910 """ 911 912 for filename in os.listdir(pathname): 913 submodule, ext = os.path.splitext(filename) 914 if submodule == "__init__" or ext not in ("", ".py"): 915 continue 916 module.set_module(submodule, self.add_module(module.name + "." + submodule)) 917 918 # vim: tabstop=4 expandtab shiftwidth=4