1 #!/usr/bin/env python 2 3 """ 4 Common functions. 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 errors import * 24 from os import listdir, makedirs, remove 25 from os.path import exists, isdir, join, split 26 import compiler 27 28 class CommonOutput: 29 30 "Common output functionality." 31 32 def check_output(self): 33 34 "Check the existing output and remove it if irrelevant." 35 36 if not exists(self.output): 37 makedirs(self.output) 38 39 details = self.importer.get_cache_details() 40 recorded_details = self.get_output_details() 41 42 if recorded_details != details: 43 self.remove_output() 44 45 writefile(self.get_output_details_filename(), details) 46 47 def get_output_details_filename(self): 48 49 "Return the output details filename." 50 51 return join(self.output, "$details") 52 53 def get_output_details(self): 54 55 "Return details of the existing output." 56 57 details_filename = self.get_output_details_filename() 58 59 if not exists(details_filename): 60 return None 61 else: 62 return readfile(details_filename) 63 64 def remove_output(self, dirname=None): 65 66 "Remove the output." 67 68 dirname = dirname or self.output 69 70 for filename in listdir(dirname): 71 path = join(dirname, filename) 72 if isdir(path): 73 self.remove_output(path) 74 else: 75 remove(path) 76 77 class CommonModule: 78 79 "A common module representation." 80 81 def __init__(self, name, importer): 82 83 """ 84 Initialise this module with the given 'name' and an 'importer' which is 85 used to provide access to other modules when required. 86 """ 87 88 self.name = name 89 self.importer = importer 90 self.filename = None 91 92 # Inspection-related attributes. 93 94 self.astnode = None 95 self.iterators = {} 96 self.temp = {} 97 self.lambdas = {} 98 99 # Constants, literals and values. 100 101 self.constants = {} 102 self.constant_values = {} 103 self.literals = {} 104 self.literal_types = {} 105 106 # Nested namespaces. 107 108 self.namespace_path = [] 109 self.in_function = False 110 111 # Attribute chains. 112 113 self.attrs = [] 114 115 def __repr__(self): 116 return "CommonModule(%r, %r)" % (self.name, self.importer) 117 118 def parse_file(self, filename): 119 120 "Parse the file with the given 'filename', initialising attributes." 121 122 self.filename = filename 123 self.astnode = compiler.parseFile(filename) 124 125 # Module-relative naming. 126 127 def get_global_path(self, name): 128 return "%s.%s" % (self.name, name) 129 130 def get_namespace_path(self): 131 return ".".join([self.name] + self.namespace_path) 132 133 def get_object_path(self, name): 134 return ".".join([self.name] + self.namespace_path + [name]) 135 136 def get_parent_path(self): 137 return ".".join([self.name] + self.namespace_path[:-1]) 138 139 # Namespace management. 140 141 def enter_namespace(self, name): 142 143 "Enter the namespace having the given 'name'." 144 145 self.namespace_path.append(name) 146 147 def exit_namespace(self): 148 149 "Exit the current namespace." 150 151 self.namespace_path.pop() 152 153 # Constant reference naming. 154 155 def get_constant_name(self, value): 156 157 "Add a new constant to the current namespace for 'value'." 158 159 path = self.get_namespace_path() 160 init_item(self.constants, path, dict) 161 return "$c%d" % add_counter_item(self.constants[path], value) 162 163 # Literal reference naming. 164 165 def get_literal_name(self): 166 167 "Add a new literal to the current namespace." 168 169 path = self.get_namespace_path() 170 init_item(self.literals, path, lambda: 0) 171 return "$C%d" % self.literals[path] 172 173 def next_literal(self): 174 self.literals[self.get_namespace_path()] += 1 175 176 # Temporary iterator naming. 177 178 def get_iterator_path(self): 179 return self.in_function and self.get_namespace_path() or self.name 180 181 def get_iterator_name(self): 182 path = self.get_iterator_path() 183 init_item(self.iterators, path, lambda: 0) 184 return "$i%d" % self.iterators[path] 185 186 def next_iterator(self): 187 self.iterators[self.get_iterator_path()] += 1 188 189 # Temporary variable naming. 190 191 def get_temporary_name(self): 192 path = self.get_namespace_path() 193 init_item(self.temp, path, lambda: 0) 194 return "$t%d" % self.temp[path] 195 196 def next_temporary(self): 197 self.temp[self.get_namespace_path()] += 1 198 199 # Arbitrary function naming. 200 201 def get_lambda_name(self): 202 path = self.get_namespace_path() 203 init_item(self.lambdas, path, lambda: 0) 204 name = "$l%d" % self.lambdas[path] 205 self.lambdas[path] += 1 206 return name 207 208 def reset_lambdas(self): 209 self.lambdas = {} 210 211 # Constant and literal recording. 212 213 def get_constant_reference(self, ref, value): 214 215 "Return a constant reference for the given 'ref' type and 'value'." 216 217 constant_name = self.get_constant_name(value) 218 219 # Return a reference for the constant. 220 221 objpath = self.get_object_path(constant_name) 222 name_ref = ConstantValueRef(constant_name, ref.instance_of(), value) 223 224 # Record the value and type for the constant. 225 226 self.constant_values[objpath] = name_ref.value, name_ref.get_origin() 227 return name_ref 228 229 def get_literal_reference(self, name, ref, items, cls): 230 231 # Construct an invocation using the items as arguments. 232 233 typename = "$L%s" % name 234 235 invocation = compiler.ast.CallFunc( 236 compiler.ast.Name(typename), 237 items 238 ) 239 240 # Get a name for the actual literal. 241 242 instname = self.get_literal_name() 243 self.next_literal() 244 245 # Record the type for the literal. 246 247 objpath = self.get_object_path(instname) 248 self.literal_types[objpath] = ref.get_origin() 249 250 # Return a wrapper for the invocation exposing the items. 251 252 return cls( 253 instname, 254 ref.instance_of(), 255 self.process_structure_node(invocation), 256 invocation.args 257 ) 258 259 # Node handling. 260 261 def process_structure(self, node): 262 263 """ 264 Within the given 'node', process the program structure. 265 266 During inspection, this will process global declarations, adjusting the 267 module namespace, and import statements, building a module dependency 268 hierarchy. 269 270 During translation, this will consult deduced program information and 271 output translated code. 272 """ 273 274 l = [] 275 for n in node.getChildNodes(): 276 l.append(self.process_structure_node(n)) 277 return l 278 279 def process_augassign_node(self, n): 280 281 "Process the given augmented assignment node 'n'." 282 283 op = operator_functions[n.op] 284 285 if isinstance(n.node, compiler.ast.Getattr): 286 target = compiler.ast.AssAttr(n.node.expr, n.node.attrname, "OP_ASSIGN") 287 elif isinstance(n.node, compiler.ast.Name): 288 target = compiler.ast.AssName(n.node.name, "OP_ASSIGN") 289 else: 290 target = n.node 291 292 assignment = compiler.ast.Assign( 293 [target], 294 compiler.ast.CallFunc( 295 compiler.ast.Name("$op%s" % op), 296 [n.node, n.expr])) 297 298 return self.process_structure_node(assignment) 299 300 def process_assignment_for_function(self, original_name, name): 301 302 """ 303 Return an assignment operation making 'original_name' refer to the given 304 'name'. 305 """ 306 307 assignment = compiler.ast.Assign( 308 [compiler.ast.AssName(original_name, "OP_ASSIGN")], 309 compiler.ast.Name(name) 310 ) 311 312 return self.process_structure_node(assignment) 313 314 def process_assignment_node_items(self, n, expr): 315 316 """ 317 Process the given assignment node 'n' whose children are to be assigned 318 items of 'expr'. 319 """ 320 321 name_ref = self.process_structure_node(expr) 322 323 # Either unpack the items and present them directly to each assignment 324 # node. 325 326 if isinstance(name_ref, LiteralSequenceRef): 327 self.process_literal_sequence_items(n, name_ref) 328 329 # Or have the assignment nodes access each item via the sequence API. 330 331 else: 332 self.process_assignment_node_items_by_position(n, expr, name_ref) 333 334 def process_assignment_node_items_by_position(self, n, expr, name_ref): 335 336 """ 337 Process the given sequence assignment node 'n', converting the node to 338 the separate assignment of each target using positional access on a 339 temporary variable representing the sequence. Use 'expr' as the assigned 340 value and 'name_ref' as the reference providing any existing temporary 341 variable. 342 """ 343 344 assignments = [] 345 346 if isinstance(name_ref, NameRef): 347 temp = name_ref.name 348 else: 349 temp = self.get_temporary_name() 350 self.next_temporary() 351 352 assignments.append( 353 compiler.ast.Assign([compiler.ast.AssName(temp, "OP_ASSIGN")], expr) 354 ) 355 356 for i, node in enumerate(n.nodes): 357 assignments.append( 358 compiler.ast.Assign([node], compiler.ast.Subscript( 359 compiler.ast.Name(temp), "OP_APPLY", [compiler.ast.Const(i)])) 360 ) 361 362 return self.process_structure_node(compiler.ast.Stmt(assignments)) 363 364 def process_literal_sequence_items(self, n, name_ref): 365 366 """ 367 Process the given assignment node 'n', obtaining from the given 368 'name_ref' the items to be assigned to the assignment targets. 369 """ 370 371 if len(n.nodes) == len(name_ref.items): 372 for node, item in zip(n.nodes, name_ref.items): 373 self.process_assignment_node(node, item) 374 else: 375 raise InspectError("In %s, item assignment needing %d items is given %d items." % ( 376 self.get_namespace_path(), len(n.nodes), len(name_ref.items))) 377 378 def process_compare_node(self, n): 379 380 """ 381 Process the given comparison node 'n', converting an operator sequence 382 from... 383 384 <expr1> <op1> <expr2> <op2> <expr3> 385 386 ...to... 387 388 <op1>(<expr1>, <expr2>) and <op2>(<expr2>, <expr3>) 389 """ 390 391 invocations = [] 392 last = n.expr 393 394 for op, op_node in n.ops: 395 op = operator_functions.get(op) 396 397 invocations.append(compiler.ast.CallFunc( 398 compiler.ast.Name("$op%s" % op), 399 [last, op_node])) 400 401 last = op_node 402 403 if len(invocations) > 1: 404 result = compiler.ast.And(invocations) 405 else: 406 result = invocations[0] 407 408 return self.process_structure_node(result) 409 410 def process_dict_node(self, node): 411 412 """ 413 Process the given dictionary 'node', returning a list of (key, value) 414 tuples. 415 """ 416 417 l = [] 418 for key, value in node.items: 419 l.append(( 420 self.process_structure_node(key), 421 self.process_structure_node(value))) 422 return l 423 424 def process_for_node(self, n): 425 426 """ 427 Generate attribute accesses for {n.list}.__iter__ and the next method on 428 the iterator, producing a replacement node for the original. 429 """ 430 431 node = compiler.ast.Stmt([ 432 433 # <iterator> = {n.list}.__iter__ 434 435 compiler.ast.Assign( 436 [compiler.ast.AssName(self.get_iterator_name(), "OP_ASSIGN")], 437 compiler.ast.CallFunc( 438 compiler.ast.Getattr(n.list, "__iter__"), 439 [] 440 )), 441 442 # try: 443 # while True: 444 # <var>... = <iterator>.next() 445 # ... 446 # except StopIteration: 447 # pass 448 449 compiler.ast.TryExcept( 450 compiler.ast.While( 451 compiler.ast.Name("True"), 452 compiler.ast.Stmt([ 453 compiler.ast.Assign( 454 [n.assign], 455 compiler.ast.CallFunc( 456 compiler.ast.Getattr(compiler.ast.Name(self.get_iterator_name()), "next"), 457 [] 458 )), 459 n.body]), 460 None), 461 [(compiler.ast.Name("StopIteration"), None, compiler.ast.Stmt([compiler.ast.Pass()]))], 462 None) 463 ]) 464 465 self.next_iterator() 466 self.process_structure_node(node) 467 468 def process_literal_sequence_node(self, n, name, ref, cls): 469 470 """ 471 Process the given literal sequence node 'n' as a function invocation, 472 with 'name' indicating the type of the sequence, and 'ref' being a 473 reference to the type. The 'cls' is used to instantiate a suitable name 474 reference. 475 """ 476 477 if name == "dict": 478 items = [] 479 for key, value in n.items: 480 items.append(compiler.ast.Tuple([key, value])) 481 else: # name in ("list", "tuple"): 482 items = n.nodes 483 484 return self.get_literal_reference(name, ref, items, cls) 485 486 def process_operator_node(self, n): 487 488 """ 489 Process the given operator node 'n' as an operator function invocation. 490 """ 491 492 op = operator_functions[n.__class__.__name__] 493 invocation = compiler.ast.CallFunc( 494 compiler.ast.Name("$op%s" % op), 495 list(n.getChildNodes()) 496 ) 497 return self.process_structure_node(invocation) 498 499 def process_slice_node(self, n, expr=None): 500 501 """ 502 Process the given slice node 'n' as an operator function invocation. 503 """ 504 505 op = n.flags == "OP_ASSIGN" and "setslice" or "getslice" 506 invocation = compiler.ast.CallFunc( 507 compiler.ast.Name("$op%s" % op), 508 [n.expr, n.lower or compiler.ast.Name("None"), n.upper or compiler.ast.Name("None")] + 509 (expr and [expr] or []) 510 ) 511 return self.process_structure_node(invocation) 512 513 def process_sliceobj_node(self, n): 514 515 """ 516 Process the given slice object node 'n' as a slice constructor. 517 """ 518 519 op = "slice" 520 invocation = compiler.ast.CallFunc( 521 compiler.ast.Name("$op%s" % op), 522 n.nodes 523 ) 524 return self.process_structure_node(invocation) 525 526 def process_subscript_node(self, n, expr=None): 527 528 """ 529 Process the given subscript node 'n' as an operator function invocation. 530 """ 531 532 op = n.flags == "OP_ASSIGN" and "setitem" or "getitem" 533 invocation = compiler.ast.CallFunc( 534 compiler.ast.Name("$op%s" % op), 535 [n.expr] + list(n.subs) + (expr and [expr] or []) 536 ) 537 return self.process_structure_node(invocation) 538 539 def process_attribute_chain(self, n): 540 541 """ 542 Process the given attribute access node 'n'. Return a reference 543 describing the expression. 544 """ 545 546 # AssAttr/Getattr are nested with the outermost access being the last 547 # access in any chain. 548 549 self.attrs.insert(0, n.attrname) 550 attrs = self.attrs 551 552 # Break attribute chains where non-access nodes are found. 553 554 if not self.have_access_expression(n): 555 self.attrs = [] 556 557 # Descend into the expression, extending backwards any existing chain, 558 # or building another for the expression. 559 560 name_ref = self.process_structure_node(n.expr) 561 562 # Restore chain information applying to this node. 563 564 self.attrs = attrs 565 566 # Return immediately if the expression was another access and thus a 567 # continuation backwards along the chain. The above processing will 568 # have followed the chain all the way to its conclusion. 569 570 if self.have_access_expression(n): 571 del self.attrs[0] 572 573 return name_ref 574 575 def have_access_expression(self, node): 576 577 "Return whether the expression associated with 'node' is Getattr." 578 579 return isinstance(node.expr, compiler.ast.Getattr) 580 581 def get_name_for_tracking(self, name, path=None): 582 583 """ 584 Return the name to be used for attribute usage observations involving 585 the given 'name' in the current namespace. If 'path' is indicated and 586 the name is being used outside a function, return the path value; 587 otherwise, return a path computed using the current namespace and the 588 given name. 589 590 The intention of this method is to provide a suitably-qualified name 591 that can be tracked across namespaces. Where globals are being 592 referenced in class namespaces, they should be referenced using their 593 path within the module, not using a path within each class. 594 595 It may not be possible to identify a global within a function at the 596 time of inspection (since a global may appear later in a file). 597 Consequently, globals are identified by their local name rather than 598 their module-qualified path. 599 """ 600 601 # For functions, use the appropriate local names. 602 603 if self.in_function: 604 return name 605 606 # For static namespaces, use the given qualified name. 607 608 elif path: 609 return path 610 611 # Otherwise, establish a name in the current (module) namespace. 612 613 else: 614 return self.get_object_path(name) 615 616 def get_path_for_access(self): 617 618 "Outside functions, register accesses at the module level." 619 620 if not self.in_function: 621 return self.name 622 else: 623 return self.get_namespace_path() 624 625 def get_module_name(self, node): 626 627 """ 628 Using the given From 'node' in this module, calculate any relative import 629 information, returning a tuple containing a module to import along with any 630 names to import based on the node's name information. 631 632 Where the returned module is given as None, whole module imports should 633 be performed for the returned modules using the returned names. 634 """ 635 636 # Absolute import. 637 638 if node.level == 0: 639 return node.modname, node.names 640 641 # Relative to an ancestor of this module. 642 643 else: 644 path = self.name.split(".") 645 level = node.level 646 647 # Relative imports treat package roots as submodules. 648 649 if split(self.filename)[-1] == "__init__.py": 650 level -= 1 651 652 if level > len(path): 653 raise InspectError("Relative import %r involves too many levels up from module %r" % ( 654 ("%s%s" % ("." * node.level, node.modname or "")), self.name)) 655 656 basename = ".".join(path[:len(path)-level]) 657 658 # Name imports from a module. 659 660 if node.modname: 661 return "%s.%s" % (basename, node.modname), node.names 662 663 # Relative whole module imports. 664 665 else: 666 return basename, node.names 667 668 def get_argnames(args): 669 670 """ 671 Return a list of all names provided by 'args'. Since tuples may be 672 employed, the arguments are traversed depth-first. 673 """ 674 675 l = [] 676 for arg in args: 677 if isinstance(arg, tuple): 678 l += get_argnames(arg) 679 else: 680 l.append(arg) 681 return l 682 683 # Classes representing inspection and translation observations. 684 685 class Result: 686 687 "An abstract expression result." 688 689 def is_name(self): 690 return False 691 def get_origin(self): 692 return None 693 694 class NameRef(Result): 695 696 "A reference to a name." 697 698 def __init__(self, name, expr=None): 699 self.name = name 700 self.expr = expr 701 702 def is_name(self): 703 return True 704 705 def reference(self): 706 return None 707 708 def final(self): 709 return None 710 711 def __repr__(self): 712 return "NameRef(%r, %r)" % (self.name, self.expr) 713 714 class LocalNameRef(NameRef): 715 716 "A reference to a local name." 717 718 def __init__(self, name, number): 719 NameRef.__init__(self, name) 720 self.number = number 721 722 def __repr__(self): 723 return "LocalNameRef(%r, %r)" % (self.name, self.number) 724 725 class ResolvedNameRef(NameRef): 726 727 "A resolved name-based reference." 728 729 def __init__(self, name, ref, expr=None): 730 NameRef.__init__(self, name, expr) 731 self.ref = ref 732 733 def reference(self): 734 return self.ref 735 736 def get_name(self): 737 return self.ref and self.ref.get_name() or None 738 739 def get_origin(self): 740 return self.ref and self.ref.get_origin() or None 741 742 def static(self): 743 return self.ref and self.ref.static() or None 744 745 def final(self): 746 return self.ref and self.ref.final() or None 747 748 def has_kind(self, kinds): 749 return self.ref and self.ref.has_kind(kinds) 750 751 def __repr__(self): 752 return "ResolvedNameRef(%r, %r, %r)" % (self.name, self.ref, self.expr) 753 754 class ConstantValueRef(ResolvedNameRef): 755 756 "A constant reference representing a single literal value." 757 758 def __init__(self, name, ref, value, number=None): 759 ResolvedNameRef.__init__(self, name, ref) 760 self.value = value 761 self.number = number 762 763 def __repr__(self): 764 return "ConstantValueRef(%r, %r, %r, %r)" % (self.name, self.ref, self.value, self.number) 765 766 class InstanceRef(Result): 767 768 "An instance reference." 769 770 def __init__(self, ref): 771 self.ref = ref 772 773 def reference(self): 774 return self.ref 775 776 def __repr__(self): 777 return "InstanceRef(%r)" % self.ref 778 779 class LiteralSequenceRef(ResolvedNameRef): 780 781 "A reference representing a sequence of values." 782 783 def __init__(self, name, ref, node, items=None): 784 ResolvedNameRef.__init__(self, name, ref) 785 self.node = node 786 self.items = items 787 788 def __repr__(self): 789 return "LiteralSequenceRef(%r, %r, %r, %r)" % (self.name, self.ref, self.node, self.items) 790 791 # Dictionary utilities. 792 793 def init_item(d, key, fn): 794 795 """ 796 Add to 'd' an entry for 'key' using the callable 'fn' to make an initial 797 value where no entry already exists. 798 """ 799 800 if not d.has_key(key): 801 d[key] = fn() 802 return d[key] 803 804 def dict_for_keys(d, keys): 805 806 "Return a new dictionary containing entries from 'd' for the given 'keys'." 807 808 nd = {} 809 for key in keys: 810 if d.has_key(key): 811 nd[key] = d[key] 812 return nd 813 814 def make_key(s): 815 816 "Make sequence 's' into a tuple-based key, first sorting its contents." 817 818 l = list(s) 819 l.sort() 820 return tuple(l) 821 822 def add_counter_item(d, key): 823 824 """ 825 Make a mapping in 'd' for 'key' to the number of keys added before it, thus 826 maintaining a mapping of keys to their order of insertion. 827 """ 828 829 if not d.has_key(key): 830 d[key] = len(d.keys()) 831 return d[key] 832 833 def remove_items(d1, d2): 834 835 "Remove from 'd1' all items from 'd2'." 836 837 for key in d2.keys(): 838 if d1.has_key(key): 839 del d1[key] 840 841 # Set utilities. 842 843 def first(s): 844 return list(s)[0] 845 846 def same(s1, s2): 847 return set(s1) == set(s2) 848 849 # General input/output. 850 851 def readfile(filename): 852 853 "Return the contents of 'filename'." 854 855 f = open(filename) 856 try: 857 return f.read() 858 finally: 859 f.close() 860 861 def writefile(filename, s): 862 863 "Write to 'filename' the string 's'." 864 865 f = open(filename, "w") 866 try: 867 f.write(s) 868 finally: 869 f.close() 870 871 # General encoding. 872 873 def sorted_output(x): 874 875 "Sort sequence 'x' and return a string with commas separating the values." 876 877 x = map(str, x) 878 x.sort() 879 return ", ".join(x) 880 881 # Attribute chain decoding. 882 883 def get_attrnames(attrnames): 884 if attrnames.startswith("#"): 885 return [attrnames] 886 else: 887 return attrnames.split(".") 888 889 def get_attrname_from_location(location): 890 path, name, attrnames, access = location 891 return get_attrnames(attrnames)[0] 892 893 # Useful data. 894 895 predefined_constants = "Ellipsis", "False", "None", "NotImplemented", "True" 896 897 operator_functions = { 898 899 # Fundamental operations. 900 901 "is" : "is_", 902 "is not" : "is_not", 903 904 # Binary operations. 905 906 "in" : "in_", 907 "not in" : "not_in", 908 "Add" : "add", 909 "Bitand" : "and_", 910 "Bitor" : "or_", 911 "Bitxor" : "xor", 912 "Div" : "div", 913 "FloorDiv" : "floordiv", 914 "LeftShift" : "lshift", 915 "Mod" : "mod", 916 "Mul" : "mul", 917 "Power" : "pow", 918 "RightShift" : "rshift", 919 "Sub" : "sub", 920 921 # Unary operations. 922 923 "Invert" : "invert", 924 "UnaryAdd" : "pos", 925 "UnarySub" : "neg", 926 927 # Augmented assignment. 928 929 "+=" : "iadd", 930 "-=" : "isub", 931 "*=" : "imul", 932 "/=" : "idiv", 933 "//=" : "ifloordiv", 934 "%=" : "imod", 935 "**=" : "ipow", 936 "<<=" : "ilshift", 937 ">>=" : "irshift", 938 "&=" : "iand", 939 "^=" : "ixor", 940 "|=" : "ior", 941 942 # Comparisons. 943 944 "==" : "eq", 945 "!=" : "ne", 946 "<" : "lt", 947 "<=" : "le", 948 ">=" : "ge", 949 ">" : "gt", 950 } 951 952 # vim: tabstop=4 expandtab shiftwidth=4