1 #!/usr/bin/env python 2 3 """ 4 A really simple virtual processor employing a simple set of instructions which 5 ignore low-level operations and merely concentrate on variable access, structure 6 access, structure allocation and function invocations. 7 8 Copyright (C) 2007, 2008, 2009, 2010 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 The execution model of the virtual processor involves the following things: 26 27 * Memory contains constants, global variable 28 references and program code 29 30 * PC (program counter) stack contains the return address associated 31 with each function invocation 32 33 * Frame stack contains invocation frames in use and in 34 preparation plus temporary storage 35 36 * Local frame pointer stack refers to the frames in the frame stack 37 38 * Invocation frame pointer stack 39 40 * Exception handler stack 41 42 * Exception handler locals stack refers to the state of the local frame 43 pointer stack 44 45 * Exception handler PC stack refers to the state of the PC stack 46 47 * Registers: current value, 48 boolean status value, 49 source value, 50 current result, 51 current exception, 52 current callable 53 """ 54 55 from micropython.program import DataValue, ReplaceableContext, PlaceholderContext, FragmentObject 56 from rsvplib import Library 57 58 class IllegalInstruction(Exception): 59 pass 60 61 class IllegalAddress(Exception): 62 def __init__(self, address): 63 self.address = address 64 def __repr__(self): 65 return "IllegalAddress(%r)" % self.address 66 def __str__(self): 67 return repr(self) 68 69 class EmptyPCStack(Exception): 70 pass 71 72 class EmptyFrameStack(Exception): 73 pass 74 75 class BreakpointReached(Exception): 76 pass 77 78 class RSVPMachine: 79 80 "A really simple virtual processor." 81 82 def __init__(self, memory, objlist, paramlist, pc=None, debug=0, abort_upon_exception=0): 83 84 """ 85 Initialise the processor with a 'memory' (a list of values containing 86 instructions and data), the object and parameter lists 'objlist' and 87 'paramlist', and the optional program counter 'pc'. 88 """ 89 90 self.memory = memory 91 self._objlist = objlist 92 self._paramlist = paramlist 93 self.objlist = objlist.as_raw() 94 self.paramlist = paramlist.as_raw() 95 self.library = None 96 97 self.pc = pc or 0 98 self.debug = debug 99 self.abort_upon_exception = abort_upon_exception 100 101 # Profiling. 102 103 self.counter = 0 104 self.cost = 0 105 106 # Stacks. 107 108 self.pc_stack = [] 109 self.frame_stack = [] 110 self.local_sp_stack = [0] 111 self.invocation_sp_stack = [] 112 self.handler_stack = [len(self.memory) - 1] # final handler is the end of the code 113 self.handler_local_sp_stack = [] 114 self.handler_pc_stack = [] 115 116 # Registers. 117 118 self.instruction = None 119 self.operand = None 120 self.value = None 121 self.status = None 122 self.source = None 123 self.callable = None 124 self.result = None 125 self.exception = None 126 127 # Constants. 128 129 cls = self._get_class("__builtins__", "AttributeError") 130 self.attr_error = cls.location 131 self.attr_error_instance = cls.instance_template_location 132 cls = self._get_class("__builtins__", "TypeError") 133 self.type_error = cls.location 134 self.type_error_instance = cls.instance_template_location 135 cls = self._get_class("__builtins__", "tuple") 136 self.tuple_class = cls.location 137 self.tuple_instance = cls.instance_template_location 138 139 # Debugging attributes. 140 141 self.breakpoints = set() 142 143 def _get_class(self, module, name): 144 attr = self._objlist.access(module, name) 145 if attr is not None: 146 return attr.get_value() 147 else: 148 return None 149 150 # Debugging methods. 151 152 def dump(self): 153 print "PC", self.pc, "->", self.load(self.pc) 154 print "PC stack", self.pc_stack 155 print "Frame stack", self.frame_stack 156 print "Local stack pointers", self.local_sp_stack 157 print "Invocation stack pointers", self.invocation_sp_stack 158 print "Handler stack", self.handler_stack 159 print "Handler frame stack", self.handler_local_sp_stack 160 print "Handler PC stack", self.handler_pc_stack 161 print 162 print "Instruction", self.instruction 163 print "Operand", self.operand 164 print "Value", self.value 165 print "Status", self.status 166 print "Source", self.source 167 print "Callable", self.callable 168 print "Result", self.result 169 print "Exception", self.exception 170 171 def show(self): 172 self.show_memory(self.memory, 0) 173 174 def show_pc(self, run_in=10): 175 start = max(0, self.pc - run_in) 176 end = self.pc + run_in 177 memory = self.memory[start:end] 178 self.show_memory(memory, start) 179 180 def show_memory(self, memory, start): 181 for i, x in enumerate(memory): 182 location = start + i 183 if location == self.pc: 184 print "->", 185 else: 186 print " ", 187 print "%5d %r" % (location, x) 188 189 def step(self, dump=0): 190 self.execute() 191 self.show_pc() 192 if dump: 193 self.dump() 194 195 def set_break(self, location): 196 self.breakpoints.add(location) 197 198 # Internal operations. 199 200 def load(self, address): 201 202 "Return the value at the given 'address'." 203 204 try: 205 return self.memory[address] 206 except IndexError: 207 raise IllegalAddress(address) 208 except TypeError: 209 raise IllegalAddress(address) 210 211 def save(self, address, value): 212 213 "Save to the given 'address' the specified 'value'." 214 215 try: 216 self.memory[address] = value 217 except IndexError: 218 raise IllegalAddress(address) 219 except TypeError: 220 raise IllegalAddress(address) 221 222 def new(self, size): 223 224 """ 225 Allocate space of the given 'size', returning the address of the space. 226 """ 227 228 addr = len(self.memory) 229 for i in range(0, size): 230 self.memory.append(None) 231 return addr 232 233 def push_pc(self, data): 234 235 "Push 'data' onto the PC stack." 236 237 self.pc_stack.append(data) 238 239 def pull_pc(self): 240 241 "Pull a value from the PC stack and return it." 242 243 try: 244 return self.pc_stack.pop() 245 except IndexError: 246 raise EmptyPCStack 247 248 def run(self): 249 250 "Execute code in the memory, starting from the current PC address." 251 252 breakpoint = 0 253 254 try: 255 while 1: 256 self.execute() 257 except EmptyPCStack: 258 pass 259 except BreakpointReached: 260 breakpoint = 1 261 262 print "Execution terminated", 263 if self.exception is not None: 264 ref = self.exception 265 addr = self.load(ref + 1) 266 print "with exception:", self.load(ref) 267 print "At address %d: %r" % (addr, self.load(addr)) 268 elif breakpoint: 269 print "with breakpoint." 270 print "At address", self.pc 271 else: 272 print "successfully." 273 print "After", self.counter, "instructions at cost", self.cost, "units." 274 275 def test(self, module): 276 277 """ 278 Test the code in the memory by running the code and investigating the 279 contents of variables. Use 'module' to identify result variables. 280 """ 281 282 self.run() 283 success = 1 284 285 if self.exception is None: 286 for name in module.keys(): 287 if name.startswith("result"): 288 label, expected = name.split("_") 289 attr = module[name] 290 291 # NOTE: Assumptions about headers and content made. 292 293 attr_location = module.location + 1 + attr.position 294 value = self.load(attr_location) 295 296 if value is not None: 297 content = self.load(value.ref + 1) 298 print label, expected, content 299 success = success and (int(expected) == content) 300 else: 301 print label, expected, "missing" 302 success = 0 303 304 return success 305 else: 306 return 0 307 308 def execute(self): 309 310 "Execute code in the memory at the current PC address." 311 312 if self.pc in self.breakpoints: 313 self.breakpoints.remove(self.pc) 314 raise BreakpointReached 315 316 self.instruction = self.load(self.pc) 317 318 # Process any inputs of the instruction. 319 320 self.process_inputs() 321 322 # Perform the instruction itself. 323 324 next_pc = self.perform(self.instruction) 325 326 # Update the program counter. 327 328 if next_pc is None: 329 self.pc += 1 330 else: 331 self.pc = next_pc 332 333 def get_method(self, instruction): 334 335 "Return the handler method for the given 'instruction'." 336 337 instruction_name = instruction.__class__.__name__ 338 if self.debug: 339 print "%8d %s" % (self.pc, instruction_name) 340 method = getattr(self, instruction_name, None) 341 if method is None: 342 raise IllegalInstruction, (self.pc, instruction_name) 343 return method 344 345 def perform(self, instruction, is_input=0): 346 347 "Perform the 'instruction', returning the next PC value or None." 348 349 if not is_input: 350 self.counter += 1 351 self.cost += instruction.cost 352 self.operand = instruction.get_operand() 353 method = self.get_method(instruction) 354 return method() 355 356 def process_inputs(self): 357 358 """ 359 Process any inputs of the current instruction. This permits any directly 360 connected sub-instructions to produce the effects that separate 361 instructions would otherwise have. 362 """ 363 364 value = self.value 365 if self.instruction.source is not None: 366 self.perform(self.instruction.source, 1) 367 self.source = self.value 368 self.value = value 369 if self.instruction.input is not None: 370 self.perform(self.instruction.input, 1) 371 372 def jump(self, addr, next): 373 374 """ 375 Jump to the subroutine at (or identified by) 'addr'. If 'addr' 376 identifies a library function then invoke the library function and set 377 PC to 'next' afterwards; otherwise, set PC to 'addr'. 378 """ 379 380 # Trap library functions introduced through the use of strings instead 381 # of proper locations. 382 383 if isinstance(addr, str): 384 handler = self.library and self.library.native_functions[addr](self.library) 385 if handler is None: 386 return next 387 else: 388 return handler 389 else: 390 self.push_pc(self.pc + 1) 391 return addr 392 393 # Instructions. 394 395 def LoadConst(self): 396 self.value = DataValue(self.operand, self.operand) 397 398 def LoadClass(self): 399 self.value = DataValue(PlaceholderContext, self.operand) 400 401 def LoadFunction(self): 402 self.value = DataValue(ReplaceableContext, self.operand) 403 404 def LoadName(self): 405 frame = self.local_sp_stack[-1] 406 self.value = self.frame_stack[frame + self.operand] 407 408 def StoreName(self): 409 frame = self.local_sp_stack[-1] 410 self.frame_stack[frame + self.operand] = self.source # uses the source value 411 412 LoadTemp = LoadName 413 414 def StoreTemp(self): 415 frame = self.local_sp_stack[-1] 416 self.frame_stack[frame + self.operand] = self.value 417 418 def LoadAddress(self): 419 # Preserve context (potentially null). 420 self.value = self.load(self.operand) 421 422 def LoadAddressContext(self): 423 value = self.load(self.operand) 424 inst_value = self.value 425 self.value = DataValue(inst_value.ref, value.ref) 426 427 def LoadAddressContextCond(self): 428 value = self.load(self.operand) 429 inst_value = self.value 430 self.value = self._LoadAddressContextCond(value.context, value.ref, inst_value.ref) 431 432 def StoreAddress(self): 433 # Preserve context. 434 self.save(self.operand, self.source) 435 436 def StoreAddressContext(self): 437 # Overwrite context if null. 438 context_value = self.value 439 source_value = self.source 440 if source_value.context is ReplaceableContext: 441 context = context_value.ref 442 else: 443 context = source_value.context 444 self.save(self.operand, DataValue(context, source_value.ref)) 445 446 def MakeInstance(self): 447 size = self.operand 448 value = self.value 449 # NOTE: Referencing the instance template. 450 addr = self._MakeObject(size, value.ref - 1) 451 # Introduce object as context for the new object. 452 self.value = DataValue(addr, addr) 453 454 def MakeFragment(self): 455 size = self.operand 456 # Reserve twice the amount of space. 457 addr = self._MakeFragment(size, size * 2) 458 # NOTE: Context is not relevant for fragments. 459 self.value = DataValue(None, addr) 460 461 def LoadAttr(self): 462 value = self.value 463 # Retrieved context should already be appropriate for the instance. 464 # NOTE: Adding 1 to skip any header. 465 self.value = self.load(value.ref + self.operand + 1) 466 467 def StoreAttr(self): 468 value = self.value 469 # Target should already be an instance. 470 # NOTE: Adding 1 to skip any header. 471 self.save(value.ref + self.operand + 1, self.source) 472 473 def LoadAttrIndex(self): 474 value = self.value 475 data = self.load(value.ref) 476 element = self.objlist[data.classcode + self.operand] 477 478 if element is not None: 479 attr_index, static_attr, offset = element 480 if attr_index == self.operand: 481 if static_attr: 482 self.value = self.load(offset) # offset is address of class/module attribute 483 else: 484 self.value = self.load(value.ref + offset) 485 return 486 487 self.exception = self._MakeObject(2, self.attr_error_instance) 488 return self.RaiseException() 489 490 # LoadAttrIndexContext not defined. 491 492 def LoadAttrIndexContextCond(self): 493 inst_value = self.value 494 data = self.load(inst_value.ref) 495 element = self.objlist[data.classcode + self.operand] 496 497 if element is not None: 498 attr_index, static_attr, offset = element 499 if attr_index == self.operand: 500 if static_attr: 501 loaded_value = self.load(offset) # offset is address of class/module attribute 502 if data.attrcode is None: # absent attrcode == class/module 503 self.value = loaded_value 504 else: 505 self.value = self._LoadAddressContextCond(loaded_value.context, loaded_value.ref, inst_value.ref) 506 else: 507 self.value = self.load(inst_value.ref + offset) 508 return 509 510 self.exception = self._MakeObject(2, self.attr_error_instance) 511 return self.RaiseException() 512 513 def StoreAttrIndex(self): 514 value = self.value 515 data = self.load(value.ref) 516 element = self.objlist[data.classcode + self.operand] 517 518 if element is not None: 519 attr_index, static_attr, offset = element 520 if attr_index == self.operand: 521 if static_attr: 522 self.exception = self._MakeObject(2, self.type_error_instance) 523 return self.RaiseException() 524 else: 525 self.save(value.ref + offset, self.source) 526 return 527 528 self.exception = self._MakeObject(2, self.attr_error_instance) 529 return self.RaiseException() 530 531 # NOTE: LoadAttrIndexContext is a possibility if a particular attribute can always be overridden. 532 533 def MakeFrame(self): 534 self.invocation_sp_stack.append(len(self.frame_stack)) 535 self.frame_stack.extend([None] * self.operand) 536 537 def DropFrame(self): 538 self.local_sp_stack.pop() 539 frame = self.invocation_sp_stack.pop() 540 del self.frame_stack[frame:] # reset stack before call 541 542 def StoreFrame(self): 543 frame = self.invocation_sp_stack[-1] # different from the current frame after MakeFrame 544 self.frame_stack[frame + self.operand] = self.value 545 546 def StoreFrameIndex(self): 547 value = self.value 548 frame = self.invocation_sp_stack[-1] # different from the current frame after MakeFrame 549 data = self.load(value.ref) 550 element = self.paramlist[data.funccode + self.operand] 551 552 if element is not None: 553 # NOTE: Need to ensure correct positioning where a context has been generated. 554 param_index, offset = element 555 if param_index == self.operand: 556 self.frame_stack[frame + offset] = self.source 557 return 558 559 self.exception = self._MakeObject(2, self.type_error_instance) 560 return self.RaiseException() 561 562 def LoadCallable(self): 563 value = self.value 564 data = self.load(value.ref) 565 self.callable = data.codeaddr 566 567 def StoreCallable(self): 568 value = self.value 569 # NOTE: Should improve the representation and permit direct saving. 570 data = self.load(value.ref) 571 self.save(value.ref, data.with_callable(self.callable)) 572 573 def LoadContext(self): 574 value = self.value 575 # NOTE: Omission of the context of the context would make things like 576 # NOTE: self() inside methods impossible. 577 self.value = DataValue(value.context, value.context) 578 579 def CheckContext(self): 580 self.status = self.value.ref is not ReplaceableContext 581 582 def CheckClass(self): 583 value = self.value 584 if value.ref in (ReplaceableContext, PlaceholderContext): 585 self.status = 0 586 return 587 588 data = self.load(value.ref) 589 590 # Classes are not themselves usable as the self argument. 591 # NOTE: This may change at some point. 592 # However, where classes appear as the context, instance 593 # compatibility is required in the first argument. 594 595 self.status = data.attrcode is None # absent attrcode == class 596 597 def CheckFrame(self): 598 (nargs, ndefaults) = self.operand 599 600 # The frame is actually installed as the locals. 601 # Retrieve the context from the first local. 602 603 frame = self.local_sp_stack[-1] 604 nlocals = len(self.frame_stack[frame:]) 605 606 if not ((nargs - ndefaults) <= nlocals): 607 raise Exception, "CheckFrame %r (%r <= %r <= %r)" % (self.operand, nargs - ndefaults, nlocals, nargs) 608 self.exception = self._MakeObject(2, self.type_error_instance) 609 return self.RaiseException() 610 611 def CheckExtra(self): 612 nargs = self.operand 613 614 # The frame is actually installed as the locals. 615 # Retrieve the context from the first local. 616 617 frame = self.local_sp_stack[-1] 618 nlocals = len(self.frame_stack[frame:]) 619 620 # Provide the extra star parameter if necessary. 621 622 if nlocals == nargs: 623 self.frame_stack.extend([None]) # ExtendFrame(1) 624 625 def FillDefaults(self): 626 value = self.value 627 (nargs, ndefaults) = self.operand 628 629 # The frame is actually installed as the locals. 630 631 frame = self.local_sp_stack[-1] 632 nlocals = len(self.frame_stack[frame:]) 633 634 # Support population of defaults. 635 # This involves copying the "attributes" of a function into the frame. 636 637 default = nlocals - (nargs - ndefaults) 638 self.frame_stack.extend([None] * (nargs - nlocals)) 639 pos = nlocals 640 641 while pos < nargs: 642 self.frame_stack[frame + pos] = self.load(value.ref + default + 1) # skip header 643 default += 1 644 pos += 1 645 646 def CopyExtra(self): 647 start = self.operand 648 649 # The frame is the source of the extra arguments. 650 651 frame = self.local_sp_stack[-1] 652 nlocals = len(self.frame_stack[frame:]) 653 654 # Make a tuple to hold the arguments. 655 656 ref = self._MakeObject(nlocals - start + 1, self.tuple_instance) 657 658 extra = 0 659 pos = start 660 661 while pos < nlocals: 662 self.save(ref + extra + 1, self.frame_stack[frame + pos]) # skip header when storing 663 extra += 1 664 pos += 1 665 666 self.value = DataValue(ref, ref) 667 668 def CheckInstance(self): 669 value = self.value 670 target_value = self.source 671 672 # For the 'self' parameter in an invoked function, the proposed context 673 # ('self') is checked against the target's context. 674 675 self.status = self._CheckInstance(value.ref, target_value.ref) 676 677 def CheckType(self): 678 value = self.value 679 target_value = self.operand 680 self.status = self._CheckType(value.ref, target_value.ref) 681 682 def JumpInFrame(self): 683 codeaddr = self.callable 684 return self.jump(codeaddr, self.pc + 1) # return to the instruction after this one 685 686 def JumpWithFrame(self): 687 codeaddr = self.callable 688 self.local_sp_stack.append(self.invocation_sp_stack[-1]) # adopt the invocation frame 689 return self.jump(codeaddr, self.pc + 1) # return to the instruction after this one 690 691 def JumpWithFrameDirect(self): 692 operand = self.operand 693 self.local_sp_stack.append(self.invocation_sp_stack[-1]) # adopt the invocation frame 694 return self.jump(operand, self.pc + 1) # return to the instruction after this one 695 696 def ExtendFrame(self): 697 self.frame_stack.extend([None] * self.operand) 698 699 def AdjustFrame(self): 700 self.invocation_sp_stack[-1] += self.operand 701 702 def Return(self): 703 return self.pull_pc() 704 705 def LoadResult(self): 706 self.value = self.result 707 708 def StoreResult(self): 709 self.result = self.value 710 711 def Jump(self): 712 return self.operand 713 714 def JumpIfTrue(self): 715 if self.status: 716 return self.operand 717 718 def JumpIfFalse(self): 719 if not self.status: 720 return self.operand 721 722 def LoadException(self): 723 self.value = DataValue(self.exception, self.exception) 724 725 def StoreException(self): 726 self.exception = self.value.ref 727 728 def ClearException(self): 729 self.exception = None 730 731 def RaiseException(self): 732 # NOTE: Adding the program counter as the first attribute. 733 self.save(self.exception + 1, self.pc) 734 # Jumping to the current handler. 735 if self.abort_upon_exception: 736 raise Exception 737 return self.handler_stack[-1] 738 739 def PushHandler(self): 740 self.handler_stack.append(self.operand) 741 self.handler_local_sp_stack.append(len(self.local_sp_stack)) 742 self.handler_pc_stack.append(len(self.pc_stack)) 743 744 def PopHandler(self): 745 # Reduce the local frame pointer stack to refer to the handler's frame. 746 del self.local_sp_stack[self.handler_local_sp_stack.pop():] 747 # Reduce the PC stack to discard all superfluous return addresses. 748 self.pc_stack = self.pc_stack[:self.handler_pc_stack.pop()] 749 self.handler_stack.pop() 750 751 def CheckException(self): 752 self.status = self.exception is not None and self._CheckInstance(self.exception, self.value.ref) 753 754 def TestIdentity(self): 755 self.status = self.value.ref == self.source.ref 756 757 def TestIdentityAddress(self): 758 self.status = self.value.ref == self.operand 759 760 # LoadBoolean is implemented in the generated code. 761 # StoreBoolean is implemented by testing against the True value. 762 763 def InvertBoolean(self): 764 self.status = not self.status 765 766 # Common implementation details. 767 768 def _CheckInstance(self, ref, cls): 769 data = self.load(ref) 770 target_data = self.load(cls) 771 772 # Insist on instance vs. class. 773 774 if data.attrcode is None: # absent attrcode == class/module 775 return 0 776 777 if target_data.attrcode is not None: # present attrcode == instance 778 return 0 779 780 # Find the table entry for the descendant. 781 782 element = self.objlist[target_data.classcode + data.attrcode] 783 784 if element is not None: 785 attr_index, static_attr, offset = element 786 return attr_index == data.attrcode 787 else: 788 return 0 789 790 def _CheckType(self, ref, cls): 791 data = self.load(ref) 792 target_data = self.load(cls) 793 794 # Insist on instance vs. class. 795 796 if data.attrcode is None: # absent attrcode == class/module 797 return 0 798 799 if target_data.attrcode is not None: # present attrcode == instance 800 return 0 801 802 # Return whether the types match. 803 804 return data.classcode == target_data.classcode 805 806 def _MakeObject(self, size, ref): 807 # Load the template. 808 data = self.load(ref) 809 addr = self.new(size) 810 # Save the header, overriding the size. 811 self.save(addr, data.with_size(size)) 812 return addr 813 814 def _MakeFragment(self, occupied, size): 815 addr = self.new(size) 816 # Save the header, overriding the size. 817 self.save(addr, FragmentObject(occupied, size)) 818 return addr 819 820 def _LoadAddressContextCond(self, context, ref, inst_ref): 821 # Check the instance context against the target's context. 822 # This provides the context overriding for methods. 823 if context is ReplaceableContext or context is not PlaceholderContext and self._CheckInstance(inst_ref, context): 824 # Replace the context with the instance. 825 return DataValue(inst_ref, ref) 826 else: 827 return DataValue(context, ref) 828 829 # Convenience functions. 830 831 def machine(program, with_builtins=0, debug=0, abort_upon_exception=0): 832 print "Making the image..." 833 code = program.get_image(with_builtins) 834 print "Getting raw structures..." 835 ot = program.get_object_table() 836 pt = program.get_parameter_table() 837 objlist = ot.as_list() 838 paramlist = pt.as_list() 839 print "Getting raw image..." 840 rc = program.get_raw_image() 841 print "Initialising the machine..." 842 importer = program.get_importer() 843 constants = {} 844 for x in (True, False, NotImplemented): 845 constants[x] = importer.get_constant(x).location 846 rm = RSVPMachine(rc, objlist, paramlist, debug=debug, abort_upon_exception=abort_upon_exception) 847 library = Library(rm, constants) 848 rm.library = library 849 rm.pc = program.code_location 850 print "Returning program occupying %d locations." % len(rm.memory) 851 return rm 852 853 # vim: tabstop=4 expandtab shiftwidth=4