Author: Armin Rigo <[email protected]>
Branch: 
Changeset: r94673:c67b31661f36
Date: 2018-05-23 22:43 +0200
http://bitbucket.org/pypy/pypy/changeset/c67b31661f36/

Log:    hg merge reverse-debugger

diff too long, truncating to 2000 out of 7443 lines

diff --git a/.hgtags b/.hgtags
--- a/.hgtags
+++ b/.hgtags
@@ -33,7 +33,12 @@
 050d84dd78997f021acf0e133934275d63547cc0 release-pypy2.7-v5.4.1
 050d84dd78997f021acf0e133934275d63547cc0 release-pypy2.7-v5.4.1
 0e2d9a73f5a1818d0245d75daccdbe21b2d5c3ef release-pypy2.7-v5.4.1
+4909c06daf41ce88f87dc01c57959cadad4df4a8 RevDB-pypy2.7-v5.4.1
+4909c06daf41ce88f87dc01c57959cadad4df4a8 RevDB-pypy2.7-v5.4.1
+d7724c0a5700b895a47de44074cdf5fd659a988f RevDB-pypy2.7-v5.4.1
 aff251e543859ce4508159dd9f1a82a2f553de00 release-pypy2.7-v5.6.0
+e90317857d27917bf840caf675832292ee070510 RevDB-pypy2.7-v5.6.1
+a24d6c7000c8099c73d3660857f7e3cee5ac045c RevDB-pypy2.7-v5.6.2
 fa3249d55d15b9829e1be69cdf45b5a44cec902d release-pypy2.7-v5.7.0
 b16a4363e930f6401bceb499b9520955504c6cb0 release-pypy3.5-v5.7.0
 1aa2d8e03cdfab54b7121e93fda7e98ea88a30bf release-pypy2.7-v5.7.1
diff --git a/lib-python/2.7/opcode.py b/lib-python/2.7/opcode.py
--- a/lib-python/2.7/opcode.py
+++ b/lib-python/2.7/opcode.py
@@ -194,5 +194,6 @@
 def_op('CALL_METHOD', 202)            # #args not including 'self'
 def_op('BUILD_LIST_FROM_ARG', 203)
 jrel_op('JUMP_IF_NOT_DEBUG', 204)     # jump over assert statements
+def_op('LOAD_REVDB_VAR', 205)         # reverse debugger (syntax example: $5)
 
 del def_op, name_op, jrel_op, jabs_op
diff --git a/pypy/config/pypyoption.py b/pypy/config/pypyoption.py
--- a/pypy/config/pypyoption.py
+++ b/pypy/config/pypyoption.py
@@ -57,6 +57,11 @@
     "termios", "_minimal_curses",
 ])
 
+reverse_debugger_disable_modules = set([
+    "_continuation", "_vmprof", "_multiprocessing",
+    "micronumpy",
+    ])
+
 # XXX this should move somewhere else, maybe to platform ("is this posixish"
 #     check or something)
 if sys.platform == "win32":
@@ -292,6 +297,9 @@
     modules = working_modules.copy()
     if config.translation.sandbox:
         modules = default_modules
+    if config.translation.reverse_debugger:
+        for mod in reverse_debugger_disable_modules:
+            setattr(config.objspace.usemodules, mod, False)
     # ignore names from 'essential_modules', notably 'exceptions', which
     # may not be present in config.objspace.usemodules at all
     modules = [name for name in modules if name not in essential_modules]
diff --git a/pypy/doc/whatsnew-head.rst b/pypy/doc/whatsnew-head.rst
--- a/pypy/doc/whatsnew-head.rst
+++ b/pypy/doc/whatsnew-head.rst
@@ -22,3 +22,8 @@
 .. branch: gc-more-logging
 
 Log additional gc-minor and gc-collect-step info in the PYPYLOG
+
+.. branch: reverse-debugger
+
+The reverse-debugger branch has been merged.  For more information, see
+https://bitbucket.org/pypy/revdb
diff --git a/pypy/interpreter/app_main.py b/pypy/interpreter/app_main.py
--- a/pypy/interpreter/app_main.py
+++ b/pypy/interpreter/app_main.py
@@ -83,11 +83,24 @@
     sys.excepthook(), catching SystemExit, printing a newline after
     sys.stdout if needed, etc.
     """
+    # don't use try:except: here, otherwise the exception remains
+    # visible in user code.  Make sure revdb_stop is a callable, so
+    # that we can call it immediately after finally: below.  Doing
+    # so minimizes the number of "blind" lines that we need to go
+    # back from, with "bstep", after we do "continue" in revdb.
+    if '__pypy__' in sys.builtin_module_names:
+        from __pypy__ import revdb_stop
+    else:
+        revdb_stop = None
+    if revdb_stop is None:
+        revdb_stop = lambda: None
+
     try:
         # run it
         try:
             f(*fargs, **fkwds)
         finally:
+            revdb_stop()
             sys.settrace(None)
             sys.setprofile(None)
 
diff --git a/pypy/interpreter/astcompiler/assemble.py 
b/pypy/interpreter/astcompiler/assemble.py
--- a/pypy/interpreter/astcompiler/assemble.py
+++ b/pypy/interpreter/astcompiler/assemble.py
@@ -673,6 +673,7 @@
     ops.JUMP_IF_NOT_DEBUG: 0,
 
     ops.BUILD_LIST_FROM_ARG: 1,
+    ops.LOAD_REVDB_VAR: 1,
 }
 
 
diff --git a/pypy/interpreter/astcompiler/ast.py 
b/pypy/interpreter/astcompiler/ast.py
--- a/pypy/interpreter/astcompiler/ast.py
+++ b/pypy/interpreter/astcompiler/ast.py
@@ -1593,6 +1593,8 @@
             return Num.from_object(space, w_node)
         if space.isinstance_w(w_node, get(space).w_Str):
             return Str.from_object(space, w_node)
+        if space.isinstance_w(w_node, get(space).w_RevDBMetaVar):
+            return RevDBMetaVar.from_object(space, w_node)
         if space.isinstance_w(w_node, get(space).w_Attribute):
             return Attribute.from_object(space, w_node)
         if space.isinstance_w(w_node, get(space).w_Subscript):
@@ -2456,6 +2458,41 @@
 State.ast_type('Str', 'expr', ['s'])
 
 
+class RevDBMetaVar(expr):
+
+    def __init__(self, metavar, lineno, col_offset):
+        self.metavar = metavar
+        expr.__init__(self, lineno, col_offset)
+
+    def walkabout(self, visitor):
+        visitor.visit_RevDBMetaVar(self)
+
+    def mutate_over(self, visitor):
+        return visitor.visit_RevDBMetaVar(self)
+
+    def to_object(self, space):
+        w_node = space.call_function(get(space).w_RevDBMetaVar)
+        w_metavar = space.newint(self.metavar)  # int
+        space.setattr(w_node, space.newtext('metavar'), w_metavar)
+        w_lineno = space.newint(self.lineno)  # int
+        space.setattr(w_node, space.newtext('lineno'), w_lineno)
+        w_col_offset = space.newint(self.col_offset)  # int
+        space.setattr(w_node, space.newtext('col_offset'), w_col_offset)
+        return w_node
+
+    @staticmethod
+    def from_object(space, w_node):
+        w_metavar = get_field(space, w_node, 'metavar', False)
+        w_lineno = get_field(space, w_node, 'lineno', False)
+        w_col_offset = get_field(space, w_node, 'col_offset', False)
+        _metavar = space.int_w(w_metavar)
+        _lineno = space.int_w(w_lineno)
+        _col_offset = space.int_w(w_col_offset)
+        return RevDBMetaVar(_metavar, _lineno, _col_offset)
+
+State.ast_type('RevDBMetaVar', 'expr', ['metavar'])
+
+
 class Attribute(expr):
 
     def __init__(self, value, attr, ctx, lineno, col_offset):
@@ -3588,6 +3625,8 @@
         return self.default_visitor(node)
     def visit_Str(self, node):
         return self.default_visitor(node)
+    def visit_RevDBMetaVar(self, node):
+        return self.default_visitor(node)
     def visit_Attribute(self, node):
         return self.default_visitor(node)
     def visit_Subscript(self, node):
@@ -3804,6 +3843,9 @@
     def visit_Str(self, node):
         pass
 
+    def visit_RevDBMetaVar(self, node):
+        pass
+
     def visit_Attribute(self, node):
         node.value.walkabout(self)
 
diff --git a/pypy/interpreter/astcompiler/astbuilder.py 
b/pypy/interpreter/astcompiler/astbuilder.py
--- a/pypy/interpreter/astcompiler/astbuilder.py
+++ b/pypy/interpreter/astcompiler/astbuilder.py
@@ -1165,6 +1165,11 @@
         elif first_child_type == tokens.BACKQUOTE:
             expr = self.handle_testlist(atom_node.get_child(1))
             return ast.Repr(expr, atom_node.get_lineno(), 
atom_node.get_column())
+        elif first_child_type == tokens.REVDBMETAVAR:
+            string = atom_node.get_child(0).get_value()
+            return ast.RevDBMetaVar(int(string[1:]),
+                                    atom_node.get_lineno(),
+                                    atom_node.get_column())
         else:
             raise AssertionError("unknown atom")
 
diff --git a/pypy/interpreter/astcompiler/codegen.py 
b/pypy/interpreter/astcompiler/codegen.py
--- a/pypy/interpreter/astcompiler/codegen.py
+++ b/pypy/interpreter/astcompiler/codegen.py
@@ -1203,6 +1203,20 @@
             sub.value.walkabout(self)
         self._compile_slice(sub.slice, sub.ctx)
 
+    def _revdb_metavar(self, node):
+        # moved in its own function for the import statement
+        from pypy.interpreter.reverse_debugging import dbstate
+        if not dbstate.standard_code:
+            self.emit_op_arg(ops.LOAD_REVDB_VAR, node.metavar)
+            return True
+        return False
+
+    def visit_RevDBMetaVar(self, node):
+        if self.space.reverse_debugging and self._revdb_metavar(node):
+            return
+        self.error("Unknown character ('$NUM' is only valid in the "
+                   "reverse-debugger)", node)
+
 
 class TopLevelCodeGenerator(PythonCodeGenerator):
 
diff --git a/pypy/interpreter/astcompiler/test/test_compiler.py 
b/pypy/interpreter/astcompiler/test/test_compiler.py
--- a/pypy/interpreter/astcompiler/test/test_compiler.py
+++ b/pypy/interpreter/astcompiler/test/test_compiler.py
@@ -953,6 +953,20 @@
         yield (self.st, "x=(lambda: (-0.0, 0.0), lambda: (0.0, -0.0))[1]()",
                         'repr(x)', '(0.0, -0.0)')
 
+    def test_revdb_metavar(self):
+        from pypy.interpreter.reverse_debugging import dbstate, setup_revdb
+        self.space.config.translation.reverse_debugger = True
+        self.space.reverse_debugging = True
+        try:
+            setup_revdb(self.space)
+            dbstate.standard_code = False
+            dbstate.metavars = [self.space.wrap(6)]
+            self.simple_test("x = 7*$0", "x", 42)
+            dbstate.standard_code = True
+            self.error_test("x = 7*$0", SyntaxError)
+        finally:
+            self.space.reverse_debugging = False
+
 
 class AppTestCompiler:
 
diff --git a/pypy/interpreter/astcompiler/tools/Python.asdl 
b/pypy/interpreter/astcompiler/tools/Python.asdl
--- a/pypy/interpreter/astcompiler/tools/Python.asdl
+++ b/pypy/interpreter/astcompiler/tools/Python.asdl
@@ -71,6 +71,7 @@
             | Repr(expr value)
             | Num(object n) -- a number as a PyObject.
             | Str(string s) -- need to specify raw, unicode, etc?
+             | RevDBMetaVar(int metavar)
             -- other literals? bools?
 
             -- the following expression can appear in assignment context
diff --git a/pypy/interpreter/baseobjspace.py b/pypy/interpreter/baseobjspace.py
--- a/pypy/interpreter/baseobjspace.py
+++ b/pypy/interpreter/baseobjspace.py
@@ -430,6 +430,8 @@
     """Base class for the interpreter-level implementations of object spaces.
     http://pypy.readthedocs.org/en/latest/objspace.html""";
 
+    reverse_debugging = False
+
     @not_rpython
     def __init__(self, config=None):
         "Basic initialization of objects."
@@ -441,6 +443,7 @@
             from pypy.config.pypyoption import get_pypy_config
             config = get_pypy_config(translating=False)
         self.config = config
+        self.reverse_debugging = config.translation.reverse_debugger
 
         self.builtin_modules = {}
         self.reloading_modules = {}
@@ -458,6 +461,9 @@
 
     def startup(self):
         # To be called before using the space
+        if self.reverse_debugging:
+            self._revdb_startup()
+
         self.threadlocals.enter_thread(self)
 
         # Initialize already imported builtin modules
@@ -868,7 +874,8 @@
         w_s1 = self.interned_strings.get(s)
         if w_s1 is None:
             w_s1 = w_s
-            self.interned_strings.set(s, w_s1)
+            if self._side_effects_ok():
+                self.interned_strings.set(s, w_s1)
         return w_s1
 
     def new_interned_str(self, s):
@@ -878,9 +885,39 @@
         w_s1 = self.interned_strings.get(s)
         if w_s1 is None:
             w_s1 = self.newtext(s)
-            self.interned_strings.set(s, w_s1)
+            if self._side_effects_ok():
+                self.interned_strings.set(s, w_s1)
         return w_s1
 
+    def _revdb_startup(self):
+        # moved in its own function for the import statement
+        from pypy.interpreter.reverse_debugging import setup_revdb
+        setup_revdb(self)
+
+    def _revdb_standard_code(self):
+        # moved in its own function for the import statement
+        from pypy.interpreter.reverse_debugging import dbstate
+        return dbstate.standard_code
+
+    def _side_effects_ok(self):
+        # For the reverse debugger: we run compiled watchpoint
+        # expressions in a fast way that will crash if they have
+        # side-effects.  The obvious Python code with side-effects is
+        # documented "don't do that"; but some non-obvious side
+        # effects are also common, like interning strings (from
+        # unmarshalling the code object containing the watchpoint
+        # expression) to the two attribute caches in mapdict.py and
+        # typeobject.py.  For now, we have to identify such places
+        # that are not acceptable for "reasonable" read-only
+        # watchpoint expressions, and write:
+        #
+        #     if not space._side_effects_ok():
+        #         don't cache.
+        #
+        if self.reverse_debugging:
+            return self._revdb_standard_code()
+        return True
+
     def is_interned_str(self, s):
         # interface for marshal_impl
         if not we_are_translated():
diff --git a/pypy/interpreter/executioncontext.py 
b/pypy/interpreter/executioncontext.py
--- a/pypy/interpreter/executioncontext.py
+++ b/pypy/interpreter/executioncontext.py
@@ -64,6 +64,8 @@
         return frame
 
     def enter(self, frame):
+        if self.space.reverse_debugging:
+            self._revdb_enter(frame)
         frame.f_backref = self.topframeref
         self.topframeref = jit.virtual_ref(frame)
 
@@ -84,6 +86,8 @@
                 # be accessed also later
                 frame_vref()
             jit.virtual_ref_finish(frame_vref, frame)
+            if self.space.reverse_debugging:
+                self._revdb_leave(got_exception)
 
     # ________________________________________________________________
 
@@ -153,6 +157,8 @@
         Like bytecode_trace() but doesn't invoke any other events besides the
         trace function.
         """
+        if self.space.reverse_debugging:
+            self._revdb_potential_stop_point(frame)
         if (frame.get_w_f_trace() is None or self.is_tracing or
             self.gettrace() is None):
             return
@@ -385,6 +391,21 @@
         if self.space.check_signal_action is not None:
             self.space.check_signal_action.perform(self, None)
 
+    def _revdb_enter(self, frame):
+        # moved in its own function for the import statement
+        from pypy.interpreter.reverse_debugging import enter_call
+        enter_call(self.topframeref(), frame)
+
+    def _revdb_leave(self, got_exception):
+        # moved in its own function for the import statement
+        from pypy.interpreter.reverse_debugging import leave_call
+        leave_call(self.topframeref(), got_exception)
+
+    def _revdb_potential_stop_point(self, frame):
+        # moved in its own function for the import statement
+        from pypy.interpreter.reverse_debugging import potential_stop_point
+        potential_stop_point(frame)
+
     def _freeze_(self):
         raise Exception("ExecutionContext instances should not be seen during"
                         " translation.  Now is a good time to inspect the"
diff --git a/pypy/interpreter/pyframe.py b/pypy/interpreter/pyframe.py
--- a/pypy/interpreter/pyframe.py
+++ b/pypy/interpreter/pyframe.py
@@ -832,9 +832,11 @@
     def fget_f_builtins(self, space):
         return self.get_builtin().getdict(space)
 
+    def get_f_back(self):
+        return ExecutionContext.getnextframe_nohidden(self)
+
     def fget_f_back(self, space):
-        f_back = ExecutionContext.getnextframe_nohidden(self)
-        return f_back
+        return self.get_f_back()
 
     def fget_f_lasti(self, space):
         return self.space.newint(self.last_instr)
diff --git a/pypy/interpreter/pyopcode.py b/pypy/interpreter/pyopcode.py
--- a/pypy/interpreter/pyopcode.py
+++ b/pypy/interpreter/pyopcode.py
@@ -437,6 +437,8 @@
                 self.WITH_CLEANUP(oparg, next_instr)
             elif opcode == opcodedesc.YIELD_VALUE.index:
                 self.YIELD_VALUE(oparg, next_instr)
+            elif opcode == opcodedesc.LOAD_REVDB_VAR.index:
+                self.LOAD_REVDB_VAR(oparg, next_instr)
             else:
                 self.MISSING_OPCODE(oparg, next_instr)
 
@@ -1047,9 +1049,16 @@
     def YIELD_VALUE(self, oparg, next_instr):
         raise Yield
 
+    def _revdb_jump_backward(self, jumpto):
+        # moved in its own function for the import statement
+        from pypy.interpreter.reverse_debugging import jump_backward
+        jump_backward(self, jumpto)
+
     def jump_absolute(self, jumpto, ec):
         # this function is overridden by pypy.module.pypyjit.interp_jit
         check_nonneg(jumpto)
+        if self.space.reverse_debugging:
+            self._revdb_jump_backward(jumpto)
         return jumpto
 
     def JUMP_FORWARD(self, jumpby, next_instr):
@@ -1303,6 +1312,18 @@
         w_dict = self.peekvalue()
         self.space.setitem(w_dict, w_key, w_value)
 
+    def _revdb_load_var(self, oparg):
+        # moved in its own function for the import statement
+        from pypy.interpreter.reverse_debugging import load_metavar
+        w_var = load_metavar(oparg)
+        self.pushvalue(w_var)
+
+    def LOAD_REVDB_VAR(self, oparg, next_instr):
+        if self.space.reverse_debugging:
+            self._revdb_load_var(oparg)
+        else:
+            self.MISSING_OPCODE(oparg, next_instr)
+
 
 ### ____________________________________________________________ ###
 
diff --git a/pypy/interpreter/pyparser/data/Grammar2.7 
b/pypy/interpreter/pyparser/data/Grammar2.7
--- a/pypy/interpreter/pyparser/data/Grammar2.7
+++ b/pypy/interpreter/pyparser/data/Grammar2.7
@@ -104,7 +104,7 @@
        '[' [listmaker] ']' |
        '{' [dictorsetmaker] '}' |
        '`' testlist1 '`' |
-       NAME | NUMBER | STRING+)
+       NAME | NUMBER | STRING+ | '$NUM')
 listmaker: test ( list_for | (',' test)* [','] )
 testlist_comp: test ( comp_for | (',' test)* [','] )
 lambdef: 'lambda' [varargslist] ':' test
diff --git a/pypy/interpreter/pyparser/dfa_generated.py 
b/pypy/interpreter/pyparser/dfa_generated.py
--- a/pypy/interpreter/pyparser/dfa_generated.py
+++ b/pypy/interpreter/pyparser/dfa_generated.py
@@ -6,37 +6,38 @@
 from pypy.interpreter.pyparser import automata
 accepts = [True, True, True, True, True, True, True, True,
            True, True, False, True, True, True, True, False,
-           False, False, True, False, False, True, False,
-           False, True, False, True, False, True, False,
-           False, True, False, False, True, True, True,
-           False, False, True, False, False, False, True]
+           False, False, False, True, False, False, True,
+           False, False, True, False, True, True, False,
+           True, False, False, True, False, False, True,
+           True, True, False, False, True, False, False,
+           False, True]
 states = [
     # 0
     {'\t': 0, '\n': 13, '\x0c': 0,
-     '\r': 14, ' ': 0, '!': 10, '"': 16,
-     '#': 18, '%': 12, '&': 12, "'": 15,
-     '(': 13, ')': 13, '*': 7, '+': 12,
-     ',': 13, '-': 12, '.': 6, '/': 11,
-     '0': 4, '1': 5, '2': 5, '3': 5,
-     '4': 5, '5': 5, '6': 5, '7': 5,
-     '8': 5, '9': 5, ':': 13, ';': 13,
-     '<': 9, '=': 12, '>': 8, '@': 13,
-     'A': 1, 'B': 2, 'C': 1, 'D': 1,
-     'E': 1, 'F': 1, 'G': 1, 'H': 1,
-     'I': 1, 'J': 1, 'K': 1, 'L': 1,
-     'M': 1, 'N': 1, 'O': 1, 'P': 1,
-     'Q': 1, 'R': 3, 'S': 1, 'T': 1,
-     'U': 2, 'V': 1, 'W': 1, 'X': 1,
-     'Y': 1, 'Z': 1, '[': 13, '\\': 17,
-     ']': 13, '^': 12, '_': 1, '`': 13,
-     'a': 1, 'b': 2, 'c': 1, 'd': 1,
-     'e': 1, 'f': 1, 'g': 1, 'h': 1,
-     'i': 1, 'j': 1, 'k': 1, 'l': 1,
-     'm': 1, 'n': 1, 'o': 1, 'p': 1,
-     'q': 1, 'r': 3, 's': 1, 't': 1,
-     'u': 2, 'v': 1, 'w': 1, 'x': 1,
-     'y': 1, 'z': 1, '{': 13, '|': 12,
-     '}': 13, '~': 13},
+     '\r': 14, ' ': 0, '!': 10, '"': 17,
+     '#': 19, '$': 15, '%': 12, '&': 12,
+     "'": 16, '(': 13, ')': 13, '*': 7,
+     '+': 12, ',': 13, '-': 12, '.': 6,
+     '/': 11, '0': 4, '1': 5, '2': 5,
+     '3': 5, '4': 5, '5': 5, '6': 5,
+     '7': 5, '8': 5, '9': 5, ':': 13,
+     ';': 13, '<': 9, '=': 12, '>': 8,
+     '@': 13, 'A': 1, 'B': 2, 'C': 1,
+     'D': 1, 'E': 1, 'F': 1, 'G': 1,
+     'H': 1, 'I': 1, 'J': 1, 'K': 1,
+     'L': 1, 'M': 1, 'N': 1, 'O': 1,
+     'P': 1, 'Q': 1, 'R': 3, 'S': 1,
+     'T': 1, 'U': 2, 'V': 1, 'W': 1,
+     'X': 1, 'Y': 1, 'Z': 1, '[': 13,
+     '\\': 18, ']': 13, '^': 12, '_': 1,
+     '`': 13, 'a': 1, 'b': 2, 'c': 1,
+     'd': 1, 'e': 1, 'f': 1, 'g': 1,
+     'h': 1, 'i': 1, 'j': 1, 'k': 1,
+     'l': 1, 'm': 1, 'n': 1, 'o': 1,
+     'p': 1, 'q': 1, 'r': 3, 's': 1,
+     't': 1, 'u': 2, 'v': 1, 'w': 1,
+     'x': 1, 'y': 1, 'z': 1, '{': 13,
+     '|': 12, '}': 13, '~': 13},
     # 1
     {'0': 1, '1': 1, '2': 1, '3': 1,
      '4': 1, '5': 1, '6': 1, '7': 1,
@@ -55,7 +56,7 @@
      't': 1, 'u': 1, 'v': 1, 'w': 1,
      'x': 1, 'y': 1, 'z': 1},
     # 2
-    {'"': 16, "'": 15, '0': 1, '1': 1,
+    {'"': 17, "'": 16, '0': 1, '1': 1,
      '2': 1, '3': 1, '4': 1, '5': 1,
      '6': 1, '7': 1, '8': 1, '9': 1,
      'A': 1, 'B': 1, 'C': 1, 'D': 1,
@@ -73,7 +74,7 @@
      'v': 1, 'w': 1, 'x': 1, 'y': 1,
      'z': 1},
     # 3
-    {'"': 16, "'": 15, '0': 1, '1': 1,
+    {'"': 17, "'": 16, '0': 1, '1': 1,
      '2': 1, '3': 1, '4': 1, '5': 1,
      '6': 1, '7': 1, '8': 1, '9': 1,
      'A': 1, 'B': 1, 'C': 1, 'D': 1,
@@ -91,22 +92,22 @@
      'v': 1, 'w': 1, 'x': 1, 'y': 1,
      'z': 1},
     # 4
-    {'.': 24, '0': 21, '1': 21, '2': 21,
-     '3': 21, '4': 21, '5': 21, '6': 21,
-     '7': 21, '8': 23, '9': 23, 'B': 22,
-     'E': 25, 'J': 13, 'L': 13, 'O': 20,
-     'X': 19, 'b': 22, 'e': 25, 'j': 13,
-     'l': 13, 'o': 20, 'x': 19},
+    {'.': 25, '0': 22, '1': 22, '2': 22,
+     '3': 22, '4': 22, '5': 22, '6': 22,
+     '7': 22, '8': 24, '9': 24, 'B': 23,
+     'E': 26, 'J': 13, 'L': 13, 'O': 21,
+     'X': 20, 'b': 23, 'e': 26, 'j': 13,
+     'l': 13, 'o': 21, 'x': 20},
     # 5
-    {'.': 24, '0': 5, '1': 5, '2': 5,
+    {'.': 25, '0': 5, '1': 5, '2': 5,
      '3': 5, '4': 5, '5': 5, '6': 5,
-     '7': 5, '8': 5, '9': 5, 'E': 25,
-     'J': 13, 'L': 13, 'e': 25, 'j': 13,
+     '7': 5, '8': 5, '9': 5, 'E': 26,
+     'J': 13, 'L': 13, 'e': 26, 'j': 13,
      'l': 13},
     # 6
-    {'0': 26, '1': 26, '2': 26, '3': 26,
-     '4': 26, '5': 26, '6': 26, '7': 26,
-     '8': 26, '9': 26},
+    {'0': 27, '1': 27, '2': 27, '3': 27,
+     '4': 27, '5': 27, '6': 27, '7': 27,
+     '8': 27, '9': 27},
     # 7
     {'*': 12, '=': 13},
     # 8
@@ -124,107 +125,115 @@
     # 14
     {'\n': 13},
     # 15
-    {automata.DEFAULT: 30, '\n': 27,
-     '\r': 27, "'": 28, '\\': 29},
+    {'0': 28, '1': 28, '2': 28, '3': 28,
+     '4': 28, '5': 28, '6': 28, '7': 28,
+     '8': 28, '9': 28},
     # 16
-    {automata.DEFAULT: 33, '\n': 27,
-     '\r': 27, '"': 31, '\\': 32},
+    {automata.DEFAULT: 32, '\n': 29,
+     '\r': 29, "'": 30, '\\': 31},
     # 17
+    {automata.DEFAULT: 35, '\n': 29,
+     '\r': 29, '"': 33, '\\': 34},
+    # 18
     {'\n': 13, '\r': 14},
-    # 18
-    {automata.DEFAULT: 18, '\n': 27, '\r': 27},
     # 19
-    {'0': 34, '1': 34, '2': 34, '3': 34,
-     '4': 34, '5': 34, '6': 34, '7': 34,
-     '8': 34, '9': 34, 'A': 34, 'B': 34,
-     'C': 34, 'D': 34, 'E': 34, 'F': 34,
-     'a': 34, 'b': 34, 'c': 34, 'd': 34,
-     'e': 34, 'f': 34},
+    {automata.DEFAULT: 19, '\n': 29, '\r': 29},
     # 20
-    {'0': 35, '1': 35, '2': 35, '3': 35,
-     '4': 35, '5': 35, '6': 35, '7': 35},
+    {'0': 36, '1': 36, '2': 36, '3': 36,
+     '4': 36, '5': 36, '6': 36, '7': 36,
+     '8': 36, '9': 36, 'A': 36, 'B': 36,
+     'C': 36, 'D': 36, 'E': 36, 'F': 36,
+     'a': 36, 'b': 36, 'c': 36, 'd': 36,
+     'e': 36, 'f': 36},
     # 21
-    {'.': 24, '0': 21, '1': 21, '2': 21,
-     '3': 21, '4': 21, '5': 21, '6': 21,
-     '7': 21, '8': 23, '9': 23, 'E': 25,
-     'J': 13, 'L': 13, 'e': 25, 'j': 13,
+    {'0': 37, '1': 37, '2': 37, '3': 37,
+     '4': 37, '5': 37, '6': 37, '7': 37},
+    # 22
+    {'.': 25, '0': 22, '1': 22, '2': 22,
+     '3': 22, '4': 22, '5': 22, '6': 22,
+     '7': 22, '8': 24, '9': 24, 'E': 26,
+     'J': 13, 'L': 13, 'e': 26, 'j': 13,
      'l': 13},
-    # 22
-    {'0': 36, '1': 36},
     # 23
-    {'.': 24, '0': 23, '1': 23, '2': 23,
-     '3': 23, '4': 23, '5': 23, '6': 23,
-     '7': 23, '8': 23, '9': 23, 'E': 25,
-     'J': 13, 'e': 25, 'j': 13},
+    {'0': 38, '1': 38},
     # 24
-    {'0': 24, '1': 24, '2': 24, '3': 24,
-     '4': 24, '5': 24, '6': 24, '7': 24,
-     '8': 24, '9': 24, 'E': 37, 'J': 13,
-     'e': 37, 'j': 13},
+    {'.': 25, '0': 24, '1': 24, '2': 24,
+     '3': 24, '4': 24, '5': 24, '6': 24,
+     '7': 24, '8': 24, '9': 24, 'E': 26,
+     'J': 13, 'e': 26, 'j': 13},
     # 25
-    {'+': 38, '-': 38, '0': 39, '1': 39,
-     '2': 39, '3': 39, '4': 39, '5': 39,
-     '6': 39, '7': 39, '8': 39, '9': 39},
+    {'0': 25, '1': 25, '2': 25, '3': 25,
+     '4': 25, '5': 25, '6': 25, '7': 25,
+     '8': 25, '9': 25, 'E': 39, 'J': 13,
+     'e': 39, 'j': 13},
     # 26
-    {'0': 26, '1': 26, '2': 26, '3': 26,
-     '4': 26, '5': 26, '6': 26, '7': 26,
-     '8': 26, '9': 26, 'E': 37, 'J': 13,
-     'e': 37, 'j': 13},
+    {'+': 40, '-': 40, '0': 41, '1': 41,
+     '2': 41, '3': 41, '4': 41, '5': 41,
+     '6': 41, '7': 41, '8': 41, '9': 41},
     # 27
+    {'0': 27, '1': 27, '2': 27, '3': 27,
+     '4': 27, '5': 27, '6': 27, '7': 27,
+     '8': 27, '9': 27, 'E': 39, 'J': 13,
+     'e': 39, 'j': 13},
+    # 28
+    {'0': 28, '1': 28, '2': 28, '3': 28,
+     '4': 28, '5': 28, '6': 28, '7': 28,
+     '8': 28, '9': 28},
+    # 29
     {},
-    # 28
+    # 30
     {"'": 13},
-    # 29
-    {automata.DEFAULT: 40, '\n': 13, '\r': 14},
-    # 30
-    {automata.DEFAULT: 30, '\n': 27,
-     '\r': 27, "'": 13, '\\': 29},
     # 31
+    {automata.DEFAULT: 42, '\n': 13, '\r': 14},
+    # 32
+    {automata.DEFAULT: 32, '\n': 29,
+     '\r': 29, "'": 13, '\\': 31},
+    # 33
     {'"': 13},
-    # 32
-    {automata.DEFAULT: 41, '\n': 13, '\r': 14},
-    # 33
-    {automata.DEFAULT: 33, '\n': 27,
-     '\r': 27, '"': 13, '\\': 32},
     # 34
-    {'0': 34, '1': 34, '2': 34, '3': 34,
-     '4': 34, '5': 34, '6': 34, '7': 34,
-     '8': 34, '9': 34, 'A': 34, 'B': 34,
-     'C': 34, 'D': 34, 'E': 34, 'F': 34,
-     'L': 13, 'a': 34, 'b': 34, 'c': 34,
-     'd': 34, 'e': 34, 'f': 34, 'l': 13},
+    {automata.DEFAULT: 43, '\n': 13, '\r': 14},
     # 35
-    {'0': 35, '1': 35, '2': 35, '3': 35,
-     '4': 35, '5': 35, '6': 35, '7': 35,
+    {automata.DEFAULT: 35, '\n': 29,
+     '\r': 29, '"': 13, '\\': 34},
+    # 36
+    {'0': 36, '1': 36, '2': 36, '3': 36,
+     '4': 36, '5': 36, '6': 36, '7': 36,
+     '8': 36, '9': 36, 'A': 36, 'B': 36,
+     'C': 36, 'D': 36, 'E': 36, 'F': 36,
+     'L': 13, 'a': 36, 'b': 36, 'c': 36,
+     'd': 36, 'e': 36, 'f': 36, 'l': 13},
+    # 37
+    {'0': 37, '1': 37, '2': 37, '3': 37,
+     '4': 37, '5': 37, '6': 37, '7': 37,
      'L': 13, 'l': 13},
-    # 36
-    {'0': 36, '1': 36, 'L': 13, 'l': 13},
-    # 37
-    {'+': 42, '-': 42, '0': 43, '1': 43,
-     '2': 43, '3': 43, '4': 43, '5': 43,
-     '6': 43, '7': 43, '8': 43, '9': 43},
     # 38
-    {'0': 39, '1': 39, '2': 39, '3': 39,
-     '4': 39, '5': 39, '6': 39, '7': 39,
-     '8': 39, '9': 39},
+    {'0': 38, '1': 38, 'L': 13, 'l': 13},
     # 39
-    {'0': 39, '1': 39, '2': 39, '3': 39,
-     '4': 39, '5': 39, '6': 39, '7': 39,
-     '8': 39, '9': 39, 'J': 13, 'j': 13},
+    {'+': 44, '-': 44, '0': 45, '1': 45,
+     '2': 45, '3': 45, '4': 45, '5': 45,
+     '6': 45, '7': 45, '8': 45, '9': 45},
     # 40
-    {automata.DEFAULT: 40, '\n': 27,
-     '\r': 27, "'": 13, '\\': 29},
+    {'0': 41, '1': 41, '2': 41, '3': 41,
+     '4': 41, '5': 41, '6': 41, '7': 41,
+     '8': 41, '9': 41},
     # 41
-    {automata.DEFAULT: 41, '\n': 27,
-     '\r': 27, '"': 13, '\\': 32},
+    {'0': 41, '1': 41, '2': 41, '3': 41,
+     '4': 41, '5': 41, '6': 41, '7': 41,
+     '8': 41, '9': 41, 'J': 13, 'j': 13},
     # 42
-    {'0': 43, '1': 43, '2': 43, '3': 43,
-     '4': 43, '5': 43, '6': 43, '7': 43,
-     '8': 43, '9': 43},
+    {automata.DEFAULT: 42, '\n': 29,
+     '\r': 29, "'": 13, '\\': 31},
     # 43
-    {'0': 43, '1': 43, '2': 43, '3': 43,
-     '4': 43, '5': 43, '6': 43, '7': 43,
-     '8': 43, '9': 43, 'J': 13, 'j': 13},
+    {automata.DEFAULT: 43, '\n': 29,
+     '\r': 29, '"': 13, '\\': 34},
+    # 44
+    {'0': 45, '1': 45, '2': 45, '3': 45,
+     '4': 45, '5': 45, '6': 45, '7': 45,
+     '8': 45, '9': 45},
+    # 45
+    {'0': 45, '1': 45, '2': 45, '3': 45,
+     '4': 45, '5': 45, '6': 45, '7': 45,
+     '8': 45, '9': 45, 'J': 13, 'j': 13},
     ]
 pseudoDFA = automata.DFA(states, accepts)
 
diff --git a/pypy/interpreter/pyparser/gendfa.py 
b/pypy/interpreter/pyparser/gendfa.py
old mode 100755
new mode 100644
--- a/pypy/interpreter/pyparser/gendfa.py
+++ b/pypy/interpreter/pyparser/gendfa.py
@@ -144,7 +144,10 @@
     special = group(states,
                     makeEOL(),
                     groupStr(states, "@:;.,`"))
-    funny = group(states, operator, bracket, special)
+    revdb_metavar = chain(states,
+                          groupStr(states, "$"),
+                          atleastonce(states, makeDigits()))
+    funny = group(states, operator, bracket, special, revdb_metavar)
     # ____________________________________________________________
     def makeStrPrefix ():
         return chain(states,
diff --git a/pypy/interpreter/pyparser/pytoken.py 
b/pypy/interpreter/pyparser/pytoken.py
--- a/pypy/interpreter/pyparser/pytoken.py
+++ b/pypy/interpreter/pyparser/pytoken.py
@@ -67,5 +67,6 @@
 # extra PyPy-specific tokens
 _add_tok("COMMENT")
 _add_tok("NL")
+_add_tok("REVDBMETAVAR", "$NUM")
 
 del _add_tok
diff --git a/pypy/interpreter/pyparser/pytokenizer.py 
b/pypy/interpreter/pyparser/pytokenizer.py
--- a/pypy/interpreter/pyparser/pytokenizer.py
+++ b/pypy/interpreter/pyparser/pytokenizer.py
@@ -220,6 +220,10 @@
                     last_comment = ''
                 elif initial == '\\':                      # continued stmt
                     continued = 1
+                elif initial == '$':
+                    token_list.append((tokens.REVDBMETAVAR, token,
+                                       lnum, start, line))
+                    last_comment = ''
                 else:
                     if initial in '([{':
                         parenstack.append((initial, lnum, start, line))
diff --git a/pypy/interpreter/pyparser/test/test_pyparse.py 
b/pypy/interpreter/pyparser/test/test_pyparse.py
--- a/pypy/interpreter/pyparser/test/test_pyparse.py
+++ b/pypy/interpreter/pyparser/test/test_pyparse.py
@@ -167,6 +167,15 @@
             tree = self.parse(fmt % linefeed)
             assert expected_tree == tree
 
+    def test_revdb_dollar_num(self):
+        self.parse('$0')
+        self.parse('$5')
+        self.parse('$42')
+        self.parse('2+$42.attrname')
+        py.test.raises(SyntaxError, self.parse, '$')
+        py.test.raises(SyntaxError, self.parse, '$a')
+        py.test.raises(SyntaxError, self.parse, '$.5')
+
     def test_error_forgotten_chars(self):
         info = py.test.raises(SyntaxError, self.parse, "if 1\n    print 4")
         assert "(expected ':')" in info.value.msg
diff --git a/pypy/interpreter/reverse_debugging.py 
b/pypy/interpreter/reverse_debugging.py
new file mode 100644
--- /dev/null
+++ b/pypy/interpreter/reverse_debugging.py
@@ -0,0 +1,852 @@
+import sys
+from rpython.rlib import revdb
+from rpython.rlib.debug import make_sure_not_resized
+from rpython.rlib.objectmodel import specialize, we_are_translated
+from rpython.rtyper.annlowlevel import cast_gcref_to_instance
+from pypy.interpreter.error import OperationError, oefmt
+from pypy.interpreter.baseobjspace import W_Root
+from pypy.interpreter import gateway, typedef, pycode, pytraceback, pyframe
+from pypy.module.marshal import interp_marshal
+from pypy.interpreter.executioncontext import AbstractActionFlag, ActionFlag
+
+
+class DBState:
+    standard_code = True
+    breakpoint_stack_id = 0
+    breakpoint_funcnames = None
+    breakpoint_filelines = None
+    breakpoint_by_file = None
+    breakpoint_version = 0
+    printed_objects = {}
+    metavars = []
+    watch_progs = []
+    watch_futures = {}
+
+dbstate = DBState()
+
+
+pycode.PyCode.co_revdb_linestarts = None   # or a string: see below
+pycode.PyCode.co_revdb_bkpt_version = 0    # see check_and_trigger_bkpt()
+pycode.PyCode.co_revdb_bkpt_cache = None   # see check_and_trigger_bkpt()
+
+# invariant: "f_revdb_nextline_instr" is the bytecode offset of
+# the start of the line that follows "last_instr".
+pyframe.PyFrame.f_revdb_nextline_instr = -1
+
+
+# ____________________________________________________________
+
+
+def setup_revdb(space):
+    """Called at run-time, before the space is set up.
+
+    The various register_debug_command() lines attach functions
+    to some commands that 'revdb.py' can call, if we are running
+    in replay mode.
+    """
+    assert space.config.translation.reverse_debugger
+    dbstate.space = space
+
+    make_sure_not_resized(dbstate.watch_progs)
+    make_sure_not_resized(dbstate.metavars)
+
+    revdb.register_debug_command(revdb.CMD_PRINT, lambda_print)
+    revdb.register_debug_command(revdb.CMD_BACKTRACE, lambda_backtrace)
+    revdb.register_debug_command(revdb.CMD_LOCALS, lambda_locals)
+    revdb.register_debug_command(revdb.CMD_BREAKPOINTS, lambda_breakpoints)
+    revdb.register_debug_command(revdb.CMD_STACKID, lambda_stackid)
+    revdb.register_debug_command("ALLOCATING", lambda_allocating)
+    revdb.register_debug_command(revdb.CMD_ATTACHID, lambda_attachid)
+    revdb.register_debug_command(revdb.CMD_COMPILEWATCH, lambda_compilewatch)
+    revdb.register_debug_command(revdb.CMD_CHECKWATCH, lambda_checkwatch)
+    revdb.register_debug_command(revdb.CMD_WATCHVALUES, lambda_watchvalues)
+
+
+# ____________________________________________________________
+
+
+def enter_call(caller_frame, callee_frame):
+    if dbstate.breakpoint_funcnames is not None:
+        name = callee_frame.getcode().co_name
+        if name in dbstate.breakpoint_funcnames:
+            revdb.breakpoint(dbstate.breakpoint_funcnames[name])
+    if dbstate.breakpoint_stack_id != 0 and caller_frame is not None:
+        if dbstate.breakpoint_stack_id == revdb.get_unique_id(caller_frame):
+            revdb.breakpoint(-1)
+    #
+    code = callee_frame.pycode
+    if code.co_revdb_linestarts is None:
+        build_co_revdb_linestarts(code)
+
+def leave_call(caller_frame, got_exception):
+    if dbstate.breakpoint_stack_id != 0 and caller_frame is not None:
+        if dbstate.breakpoint_stack_id == revdb.get_unique_id(caller_frame):
+            revdb.breakpoint(-2)
+    if we_are_translated():
+        stop_point_activate(-2 + got_exception)
+
+def stop_point():
+    if we_are_translated():
+        revdb.breakpoint(-3)
+
+
+def jump_backward(frame, jumpto):
+    # When we see a jump backward, we set 'f_revdb_nextline_instr' in
+    # such a way that the next instruction, at 'jumpto', will trigger
+    # stop_point_activate().  We have to trigger it even if
+    # 'jumpto' is not actually a start of line.  For example, after a
+    # 'while foo:', the body ends with a JUMP_ABSOLUTE which
+    # jumps back to the *second* opcode of the while.
+    frame.f_revdb_nextline_instr = jumpto
+
+
+def potential_stop_point(frame):
+    if not we_are_translated():
+        return
+    #
+    # We only record a stop_point at every line, not every bytecode.
+    # Uses roughly the same algo as ExecutionContext.run_trace_func()
+    # to know where the line starts are, but tweaked for speed,
+    # avoiding the quadratic complexity when run N times with a large
+    # code object.
+    #
+    cur = frame.last_instr
+    if cur < frame.f_revdb_nextline_instr:
+        return    # fast path: we're still inside the same line as before
+    #
+    call_stop_point_at_line = True
+    co_revdb_linestarts = frame.pycode.co_revdb_linestarts
+    if cur > frame.f_revdb_nextline_instr:
+        #
+        # We jumped forward over the start of the next line.  We're
+        # inside a different line, but we will only trigger a stop
+        # point if we're at the starting bytecode of that line.  Fetch
+        # from co_revdb_linestarts the start of the line that is at or
+        # follows 'cur'.
+        ch = ord(co_revdb_linestarts[cur])
+        if ch == 0:
+            pass   # we are at the start of a line now
+        else:
+            # We are not, so don't call stop_point_activate().
+            # We still have to fill f_revdb_nextline_instr.
+            call_stop_point_at_line = False
+    #
+    if call_stop_point_at_line:
+        stop_point_activate(pycode=frame.pycode, opindex=cur)
+        cur += 1
+        ch = ord(co_revdb_linestarts[cur])
+    #
+    # Update f_revdb_nextline_instr.  Check if 'ch' was greater than
+    # 255, in which case it was rounded down to 255 and we have to
+    # continue looking
+    nextline_instr = cur + ch
+    while ch == 255:
+        ch = ord(co_revdb_linestarts[nextline_instr])
+        nextline_instr += ch
+    frame.f_revdb_nextline_instr = nextline_instr
+
+
+def build_co_revdb_linestarts(code):
+    # Inspired by findlinestarts() in the 'dis' standard module.
+    # Set up 'bits' so that it contains \x00 at line starts and \xff
+    # in-between.
+    bits = ['\xff'] * (len(code.co_code) + 1)
+    if not code.hidden_applevel:
+        lnotab = code.co_lnotab
+        addr = 0
+        p = 0
+        newline = 1
+        while p + 1 < len(lnotab):
+            byte_incr = ord(lnotab[p])
+            line_incr = ord(lnotab[p+1])
+            if byte_incr:
+                if newline != 0:
+                    bits[addr] = '\x00'
+                    newline = 0
+                addr += byte_incr
+            newline |= line_incr
+            p += 2
+        if newline:
+            bits[addr] = '\x00'
+    bits[len(code.co_code)] = '\x00'
+    #
+    # Change 'bits' so that the character at 'i', if not \x00, measures
+    # how far the next \x00 is
+    next_null = len(code.co_code)
+    p = next_null - 1
+    while p >= 0:
+        if bits[p] == '\x00':
+            next_null = p
+        else:
+            ch = next_null - p
+            if ch > 255: ch = 255
+            bits[p] = chr(ch)
+        p -= 1
+    lstart = ''.join(bits)
+    code.co_revdb_linestarts = lstart
+    return lstart
+
+def get_final_lineno(code):
+    lineno = code.co_firstlineno
+    lnotab = code.co_lnotab
+    p = 1
+    while p < len(lnotab):
+        line_incr = ord(lnotab[p])
+        lineno += line_incr
+        p += 2
+    return lineno
+
+def find_line_starts(code):
+    # RPython version of dis.findlinestarts()
+    lnotab = code.co_lnotab
+    lastlineno = -1
+    lineno = code.co_firstlineno
+    addr = 0
+    p = 0
+    result = []
+    while p < len(lnotab) - 1:
+        byte_incr = ord(lnotab[p + 0])
+        line_incr = ord(lnotab[p + 1])
+        if byte_incr:
+            if lineno != lastlineno:
+                result.append((addr, lineno))
+                lastlineno = lineno
+            addr += byte_incr
+        lineno += line_incr
+        p += 2
+    if lineno != lastlineno:
+        result.append((addr, lineno))
+    return result
+
+class NonStandardCode(object):
+    def __enter__(self):
+        dbstate.standard_code = False
+        self.t = dbstate.space.actionflag._ticker
+        self.c = dbstate.space.actionflag._ticker_revdb_count
+    def __exit__(self, *args):
+        dbstate.space.actionflag._ticker = self.t
+        dbstate.space.actionflag._ticker_revdb_count = self.c
+        dbstate.standard_code = True
+non_standard_code = NonStandardCode()
+
+
+def stop_point_activate(place=0, pycode=None, opindex=-1):
+    if revdb.watch_save_state():
+        any_watch_point = False
+        # ^^ this flag is set to True if we must continue to enter this
+        # block of code.  If it is still False for watch_restore_state()
+        # below, then future watch_save_state() will return False too---
+        # until the next time revdb.c:set_revdb_breakpoints() is called.
+        space = dbstate.space
+        with non_standard_code:
+            watch_id = -1
+            if dbstate.breakpoint_by_file is not None:
+                any_watch_point = True
+                if pycode is not None:
+                    watch_id = check_and_trigger_bkpt(pycode, opindex)
+            if watch_id == -1:
+                for prog, watch_id, expected in dbstate.watch_progs:
+                    any_watch_point = True
+                    try:
+                        got = _run_watch(space, prog)
+                    except OperationError as e:
+                        got = e.errorstr(space)
+                    except Exception:
+                        break
+                    if got != expected:
+                        break
+                else:
+                    watch_id = -1
+        revdb.watch_restore_state(any_watch_point)
+        if watch_id != -1:
+            revdb.breakpoint(watch_id)
+    revdb.stop_point(place)
+
+
+def future_object(space):
+    return space.w_Ellipsis    # a random prebuilt object
+
+def load_metavar(index):
+    assert index >= 0
+    space = dbstate.space
+    metavars = dbstate.metavars
+    w_var = metavars[index] if index < len(metavars) else None
+    if w_var is None:
+        raise oefmt(space.w_NameError, "no constant object '$%d'",
+                    index)
+    if w_var is future_object(space):
+        raise oefmt(space.w_RuntimeError,
+                    "'$%d' refers to an object created later in time",
+                    index)
+    return w_var
+
+def set_metavar(index, w_obj):
+    assert index >= 0
+    if index >= len(dbstate.metavars):
+        missing = index + 1 - len(dbstate.metavars)
+        dbstate.metavars = dbstate.metavars + [None] * missing
+    dbstate.metavars[index] = w_obj
+
+
+# ____________________________________________________________
+
+
+def fetch_cur_frame(silent=False):
+    ec = dbstate.space.threadlocals.get_ec()
+    if ec is None:
+        frame = None
+    else:
+        frame = ec.topframeref()
+    if frame is None and not silent:
+        revdb.send_output("No stack.\n")
+    return frame
+
+def compile(source, mode):
+    assert not dbstate.standard_code
+    space = dbstate.space
+    compiler = space.createcompiler()
+    code = compiler.compile(source, '<revdb>', mode, 0,
+                            hidden_applevel=True)
+    return code
+
+
+class W_RevDBOutput(W_Root):
+    softspace = 0
+
+    def __init__(self, space):
+        self.space = space
+
+    def descr_write(self, w_buffer):
+        assert not dbstate.standard_code
+        space = self.space
+        if space.isinstance_w(w_buffer, space.w_unicode):
+            w_buffer = space.call_method(w_buffer, 'encode',
+                                         space.newtext('utf-8'))   # safe?
+        revdb.send_output(space.bytes_w(w_buffer))
+
+def descr_get_softspace(space, revdb):
+    return space.newint(revdb.softspace)
+def descr_set_softspace(space, revdb, w_newvalue):
+    revdb.softspace = space.int_w(w_newvalue)
+
+W_RevDBOutput.typedef = typedef.TypeDef(
+    "revdb_output",
+    write = gateway.interp2app(W_RevDBOutput.descr_write),
+    softspace = typedef.GetSetProperty(descr_get_softspace,
+                                       descr_set_softspace,
+                                       cls=W_RevDBOutput),
+    )
+
+def revdb_displayhook(space, w_obj):
+    """Modified sys.displayhook() that also outputs '$NUM = ',
+    for non-prebuilt objects.  Such objects are then recorded in
+    'printed_objects'.
+    """
+    assert not dbstate.standard_code
+    if space.is_w(w_obj, space.w_None):
+        return
+    uid = revdb.get_unique_id(w_obj)
+    if uid > 0:
+        dbstate.printed_objects[uid] = w_obj
+        revdb.send_nextnid(uid)   # outputs '$NUM = '
+    space.setitem(space.builtin.w_dict, space.newtext('_'), w_obj)
+    # do repr() after setitem: if w_obj was produced successfully,
+    # but its repr crashes because it tries to do I/O, then we already
+    # have it recorded in '_' and in '$NUM ='.
+    w_repr = space.repr(w_obj)
+    if space.isinstance_w(w_repr, space.w_unicode):
+        w_repr = space.call_method(w_repr, 'encode',
+                                   space.newtext('utf-8'))   # safe?
+    revdb.send_output(space.bytes_w(w_repr))
+    revdb.send_output("\n")
+
[email protected]_spec(name='text0', level=int)
+def revdb_importhook(space, name, w_globals=None,
+                     w_locals=None, w_fromlist=None, level=-1):
+    # Incredibly simplified version of __import__, which only returns
+    # already-imported modules and doesn't call any custom import
+    # hooks.  Recognizes only absolute imports.  With a 'fromlist'
+    # argument that is a non-empty list, returns the module 'name3' if
+    # the 'name' argument is 'name1.name2.name3'.  With an empty or
+    # None 'fromlist' argument, returns the module 'name1' instead.
+    return space.appexec([space.newtext(name), w_fromlist or space.w_None,
+                          space.newint(level), space.sys],
+    """(name, fromlist, level, sys):
+        if level > 0:
+            raise ImportError("only absolute imports are "
+                               "supported in the debugger")
+        basename = name.split('.')[0]
+        try:
+            basemod = sys.modules[basename]
+            mod = sys.modules[name]
+        except KeyError:
+            raise ImportError("'%s' not found or not imported yet "
+                    "(the debugger can't import new modules, "
+                    "and only supports absolute imports)" % (name,))
+        if fromlist:
+            return mod
+        return basemod
+    """)
+
[email protected]()
+def get_revdb_displayhook(space):
+    return gateway.interp2app(revdb_displayhook).spacebind(space)
+
[email protected]()
+def get_revdb_importhook(space):
+    return gateway.interp2app(revdb_importhook).spacebind(space)
+
+
+def prepare_print_environment(space):
+    assert not dbstate.standard_code
+    w_revdb_output = W_RevDBOutput(space)
+    w_displayhook = get_revdb_displayhook(space)
+    w_import = get_revdb_importhook(space)
+    space.sys.setdictvalue(space, 'stdout', w_revdb_output)
+    space.sys.setdictvalue(space, 'stderr', w_revdb_output)
+    space.sys.setdictvalue(space, 'displayhook', w_displayhook)
+    space.builtin.setdictvalue(space, '__import__', w_import)
+
+def command_print(cmd, expression):
+    frame = fetch_cur_frame()
+    if frame is None:
+        return
+    space = dbstate.space
+    with non_standard_code:
+        try:
+            prepare_print_environment(space)
+            code = compile(expression, 'single')
+            try:
+                code.exec_code(space,
+                               frame.get_w_globals(),
+                               frame.getdictscope())
+
+            except OperationError as operationerr:
+                # can't use sys.excepthook: it will likely try to do 'import
+                # traceback', which might not be doable without using I/O
+                tb = operationerr.get_traceback()
+                if tb is not None:
+                    revdb.send_output("Traceback (most recent call last):\n")
+                    while tb is not None:
+                        if not isinstance(tb, pytraceback.PyTraceback):
+                            revdb.send_output("  ??? %s\n" % tb)
+                            break
+                        show_frame(tb.frame, tb.get_lineno(), indent='  ')
+                        tb = tb.next
+                revdb.send_output('%s\n' % operationerr.errorstr(space))
+
+                # set the sys.last_xxx attributes
+                w_type = operationerr.w_type
+                w_value = operationerr.get_w_value(space)
+                w_tb = operationerr.get_traceback()
+                w_dict = space.sys.w_dict
+                space.setitem(w_dict, space.newtext('last_type'), w_type)
+                space.setitem(w_dict, space.newtext('last_value'), w_value)
+                space.setitem(w_dict, space.newtext('last_traceback'), w_tb)
+
+        except OperationError as e:
+            revdb.send_output('%s\n' % e.errorstr(space, use_repr=True))
+lambda_print = lambda: command_print
+
+
+def file_and_lineno(frame, lineno):
+    code = frame.getcode()
+    return 'File "%s", line %d in %s' % (
+        code.co_filename, lineno, code.co_name)
+
+def show_frame(frame, lineno=0, indent=''):
+    if lineno == 0:
+        lineno = frame.get_last_lineno()
+    revdb.send_output("%s%s\n%s  " % (
+        indent,
+        file_and_lineno(frame, lineno),
+        indent))
+    revdb.send_linecache(frame.getcode().co_filename, lineno)
+
+def display_function_part(frame, max_lines_before, max_lines_after):
+    if frame is None:
+        return
+    code = frame.getcode()
+    if code.co_filename.startswith('<builtin>'):
+        return
+    first_lineno = code.co_firstlineno
+    current_lineno = frame.get_last_lineno()
+    final_lineno = get_final_lineno(code)
+    #
+    ellipsis_after = False
+    if first_lineno < current_lineno - max_lines_before - 1:
+        first_lineno = current_lineno - max_lines_before
+        revdb.send_output("...\n")
+    if final_lineno > current_lineno + max_lines_after + 1:
+        final_lineno = current_lineno + max_lines_after
+        ellipsis_after = True
+    #
+    for i in range(first_lineno, final_lineno + 1):
+        if i == current_lineno:
+            if revdb.current_place() == -2: # <= this is the arg to 
stop_point()
+                prompt = "<< "     # return
+            elif revdb.current_place() == -1:
+                prompt = "!! "     # exceptional return
+            else:
+                prompt = " > "     # plain line
+            revdb.send_output(prompt)
+        else:
+            revdb.send_output("   ")
+        revdb.send_linecache(code.co_filename, i, strip=False)
+    #
+    if ellipsis_after:
+        revdb.send_output("...\n")
+
+def command_backtrace(cmd, extra):
+    frame = fetch_cur_frame(silent=True)
+    if cmd.c_arg1 == 0:
+        if frame is not None:
+            revdb.send_output("%s:\n" % (
+                file_and_lineno(frame, frame.get_last_lineno()),))
+        display_function_part(frame, max_lines_before=8, max_lines_after=5)
+    elif cmd.c_arg1 == 2:
+        display_function_part(frame, 
max_lines_before=1000,max_lines_after=1000)
+    else:
+        revdb.send_output("Current call stack (most recent call last):\n")
+        if frame is None:
+            revdb.send_output("  (empty)\n")
+        frames = []
+        while frame is not None:
+            frames.append(frame)
+            if len(frames) == 200:
+                revdb.send_output("  ...\n")
+                break
+            frame = frame.get_f_back()
+        while len(frames) > 0:
+            show_frame(frames.pop(), indent='  ')
+lambda_backtrace = lambda: command_backtrace
+
+
+def command_locals(cmd, extra):
+    frame = fetch_cur_frame()
+    if frame is None:
+        return
+    space = dbstate.space
+    with non_standard_code:
+        try:
+            prepare_print_environment(space)
+            space.appexec([space.sys,
+                           frame.getdictscope()], """(sys, locals):
+                lst = locals.keys()
+                lst.sort()
+                print 'Locals:'
+                for key in lst:
+                    try:
+                        print '    %s =' % key,
+                        s = '%r' % locals[key]
+                        if len(s) > 140:
+                            s = s[:100] + '...' + s[-30:]
+                        print s
+                    except:
+                        exc, val, tb = sys.exc_info()
+                        print '!<%s: %r>' % (exc, val)
+            """)
+        except OperationError as e:
+            revdb.send_output('%s\n' % e.errorstr(space, use_repr=True))
+lambda_locals = lambda: command_locals
+
+# ____________________________________________________________
+
+
+def check_and_trigger_bkpt(pycode, opindex):
+    # We cache on 'pycode.co_revdb_bkpt_cache' either None or a dict
+    # mapping {opindex: bkpt_num}.  This cache is updated when the
+    # version in 'pycode.co_revdb_bkpt_version' does not match
+    # 'dbstate.breakpoint_version' any more.
+    if pycode.co_revdb_bkpt_version != dbstate.breakpoint_version:
+        update_bkpt_cache(pycode)
+    cache = pycode.co_revdb_bkpt_cache
+    if cache is not None and opindex in cache:
+        return cache[opindex]
+    else:
+        return -1
+
+def update_bkpt_cache(pycode):
+    # initialized by command_breakpoints():
+    #     dbstate.breakpoint_filelines == [('FILENAME', lineno, bkpt_num)]
+    # computed lazily (here, first half of the logic):
+    #     dbstate.breakpoint_by_file == {'co_filename': {lineno: bkpt_num}}
+    # the goal is to set:
+    #     pycode.co_revdb_bkpt_cache == {opindex: bkpt_num}
+
+    co_filename = pycode.co_filename
+    try:
+        linenos = dbstate.breakpoint_by_file[co_filename]
+    except KeyError:
+        linenos = None
+        match = co_filename.upper()    # ignore cAsE in filename matching
+        for filename, lineno, bkpt_num in dbstate.breakpoint_filelines:
+            if match.endswith(filename) and (
+                    len(match) == len(filename) or
+                    match[-len(filename)-1] in '/\\'):    # a valid prefix
+                if linenos is None:
+                    linenos = {}
+                linenos[lineno] = bkpt_num
+        dbstate.breakpoint_by_file[co_filename] = linenos
+
+    newcache = None
+    if linenos is not None:
+        # parse co_lnotab to figure out the opindexes that correspond
+        # to the marked line numbers.  here, linenos == {lineno: bkpt_num}
+        for addr, lineno in find_line_starts(pycode):
+            if lineno in linenos:
+                if newcache is None:
+                    newcache = {}
+                newcache[addr] = linenos[lineno]
+
+    pycode.co_revdb_bkpt_cache = newcache
+    pycode.co_revdb_bkpt_version = dbstate.breakpoint_version
+
+
+def valid_identifier(s):
+    if not s:
+        return False
+    if s[0].isdigit():
+        return False
+    for c in s:
+        if not (c.isalnum() or c == '_'):
+            return False
+    return True
+
+def add_breakpoint_funcname(name, i):
+    if dbstate.breakpoint_funcnames is None:
+        dbstate.breakpoint_funcnames = {}
+    dbstate.breakpoint_funcnames[name] = i
+
+def add_breakpoint_fileline(filename, lineno, i):
+    # dbstate.breakpoint_filelines is just a list of (FILENAME, lineno, i).
+    # dbstate.breakpoint_by_file is {co_filename: {lineno: i}}, but
+    # computed lazily when we encounter a code object with the given
+    # co_filename.  Any suffix 'filename' matches 'co_filename'.
+    if dbstate.breakpoint_filelines is None:
+        dbstate.breakpoint_filelines = []
+        dbstate.breakpoint_by_file = {}
+    dbstate.breakpoint_filelines.append((filename.upper(), lineno, i))
+
+def add_breakpoint(name, i):
+    # if it is empty, complain
+    if not name:
+        revdb.send_output("Empty breakpoint name\n")
+        revdb.send_change_breakpoint(i)
+        return
+    # if it is surrounded by < >, it is the name of a code object
+    if name.startswith('<') and name.endswith('>'):
+        add_breakpoint_funcname(name, i)
+        return
+    # if it has no ':', it can be a valid identifier (which we
+    # register as a function name), or a lineno
+    original_name = name
+    j = name.rfind(':')
+    if j < 0:
+        try:
+            lineno = int(name)
+        except ValueError:
+            if name.endswith('()'):
+                n = len(name) - 2
+                assert n >= 0
+                name = name[:n]
+            if not valid_identifier(name):
+                revdb.send_output(
+                    'Note: "%s()" doesn''t look like a function name. '
+                    'Setting breakpoint anyway\n' % name)
+            add_breakpoint_funcname(name, i)
+            name += '()'
+            if name != original_name:
+                revdb.send_change_breakpoint(i, name)
+            return
+        # "number" does the same as ":number"
+        filename = ''
+    else:
+        # if it has a ':', it must end in ':lineno'
+        try:
+            lineno = int(name[j+1:])
+        except ValueError:
+            revdb.send_output('expected a line number after colon\n')
+            revdb.send_change_breakpoint(i)
+            return
+        filename = name[:j]
+
+    # the text before must be a pathname, possibly a relative one,
+    # or be escaped by < >.  if it isn't, make it absolute and normalized
+    # and warn if it doesn't end in '.py'.
+    if filename == '':
+        frame = fetch_cur_frame()
+        if frame is None:
+            revdb.send_change_breakpoint(i)
+            return
+        filename = frame.getcode().co_filename
+    elif filename.startswith('<') and filename.endswith('>'):
+        pass    # use unmodified
+    elif not filename.lower().endswith('.py'):
+        # use unmodified, but warn
+        revdb.send_output(
+            'Note: "%s" doesn''t look like a Python filename. '
+            'Setting breakpoint anyway\n' % (filename,))
+
+    add_breakpoint_fileline(filename, lineno, i)
+    name = '%s:%d' % (filename, lineno)
+    if name != original_name:
+        revdb.send_change_breakpoint(i, name)
+
+def command_breakpoints(cmd, extra):
+    space = dbstate.space
+    dbstate.breakpoint_stack_id = cmd.c_arg1
+    revdb.set_thread_breakpoint(cmd.c_arg2)
+    dbstate.breakpoint_funcnames = None
+    dbstate.breakpoint_filelines = None
+    dbstate.breakpoint_by_file = None
+    dbstate.breakpoint_version += 1
+    watch_progs = []
+    with non_standard_code:
+        for i, kind, name in revdb.split_breakpoints_arg(extra):
+            if kind == 'B':
+                add_breakpoint(name, i)
+            elif kind == 'W':
+                code = interp_marshal.loads(space, space.newbytes(name))
+                watch_progs.append((code, i, ''))
+    dbstate.watch_progs = watch_progs[:]
+lambda_breakpoints = lambda: command_breakpoints
+
+
+def command_watchvalues(cmd, extra):
+    expected = extra.split('\x00')
+    for j in range(len(dbstate.watch_progs)):
+        prog, i, _ = dbstate.watch_progs[j]
+        if i >= len(expected):
+            raise IndexError
+        dbstate.watch_progs[j] = prog, i, expected[i]
+lambda_watchvalues = lambda: command_watchvalues
+
+
+def command_stackid(cmd, extra):
+    frame = fetch_cur_frame(silent=True)
+    if frame is not None and cmd.c_arg1 != 0:     # parent_flag
+        frame = 
dbstate.space.getexecutioncontext().getnextframe_nohidden(frame)
+    if frame is None:
+        uid = 0
+    else:
+        uid = revdb.get_unique_id(frame)
+    if revdb.current_place() == -2:
+        hidden_level = 1      # hide the "<<" events from next/bnext commands
+    else:
+        hidden_level = 0
+    revdb.send_answer(revdb.ANSWER_STACKID, uid, hidden_level)
+lambda_stackid = lambda: command_stackid
+
+
+def command_allocating(uid, gcref):
+    w_obj = cast_gcref_to_instance(W_Root, gcref)
+    dbstate.printed_objects[uid] = w_obj
+    try:
+        index_metavar = dbstate.watch_futures.pop(uid)
+    except KeyError:
+        pass
+    else:
+        set_metavar(index_metavar, w_obj)
+lambda_allocating = lambda: command_allocating
+
+
+def command_attachid(cmd, extra):
+    space = dbstate.space
+    index_metavar = cmd.c_arg1
+    uid = cmd.c_arg2
+    try:
+        w_obj = dbstate.printed_objects[uid]
+    except KeyError:
+        # uid not found, probably a future object
+        dbstate.watch_futures[uid] = index_metavar
+        w_obj = future_object(space)
+    set_metavar(index_metavar, w_obj)
+lambda_attachid = lambda: command_attachid
+
+
+def command_compilewatch(cmd, expression):
+    space = dbstate.space
+    with non_standard_code:
+        try:
+            code = compile(expression, 'eval')
+            marshalled_code = space.bytes_w(interp_marshal.dumps(
+                space, code,
+                space.newint(interp_marshal.Py_MARSHAL_VERSION)))
+        except OperationError as e:
+            revdb.send_watch(e.errorstr(space), ok_flag=0)
+        else:
+            revdb.send_watch(marshalled_code, ok_flag=1)
+lambda_compilewatch = lambda: command_compilewatch
+
+def command_checkwatch(cmd, marshalled_code):
+    space = dbstate.space
+    with non_standard_code:
+        try:
+            code = interp_marshal.loads(space, space.newbytes(marshalled_code))
+            text = _run_watch(space, code)
+        except OperationError as e:
+            revdb.send_watch(e.errorstr(space), ok_flag=0)
+        else:
+            revdb.send_watch(text, ok_flag=1)
+lambda_checkwatch = lambda: command_checkwatch
+
+
+def _run_watch(space, prog):
+    # must be called from non_standard_code!
+    w_dict = space.builtin.w_dict
+    w_res = prog.exec_code(space, w_dict, w_dict)
+    return space.text_w(space.repr(w_res))
+
+
+# ____________________________________________________________
+
+
+ActionFlag._ticker_revdb_count = -1
+
+class RDBSignalActionFlag(AbstractActionFlag):
+    # Used instead of pypy.module.signal.interp_signal.SignalActionFlag
+    # when we have reverse-debugging.  That other class would work too,
+    # but inefficiently: it would generate two words of data per bytecode.
+    # This class is tweaked to generate one byte per _SIG_TICKER_COUNT
+    # bytecodes, at the expense of not reacting to signals instantly.
+
+    # Threads: after 10'000 calls to decrement_ticker(), it should
+    # return -1.  It should also return -1 if there was a signal.
+    # This is done by calling _update_ticker_from_signals() every 100
+    # calls, and invoking rsignal.pypysig_check_and_reset(); this in
+    # turn returns -1 if there was a signal or if it was called 100
+    # times.
+
+    _SIG_TICKER_COUNT = 100
+    _ticker = 0
+    _ticker_revdb_count = _SIG_TICKER_COUNT * 10
+
+    def get_ticker(self):
+        return self._ticker
+
+    def reset_ticker(self, value):
+        self._ticker = value
+
+    def rearm_ticker(self):
+        self._ticker = -1
+
+    def decrement_ticker(self, by):
+        if we_are_translated():
+            c = self._ticker_revdb_count - 1
+            if c < 0:
+                c = self._update_ticker_from_signals()
+            self._ticker_revdb_count = c
+        #if self.has_bytecode_counter:    # this 'if' is constant-folded
+        #    print ("RDBSignalActionFlag: has_bytecode_counter: "
+        #           "not supported for now")
+        #    raise NotImplementedError
+        return self._ticker
+
+    def _update_ticker_from_signals(self):
+        from rpython.rlib import rsignal
+        if dbstate.standard_code:
+            if rsignal.pypysig_check_and_reset():
+                self.rearm_ticker()
+        return self._SIG_TICKER_COUNT
+    _update_ticker_from_signals._dont_inline_ = True
diff --git a/pypy/interpreter/test/test_reverse_debugging.py 
b/pypy/interpreter/test/test_reverse_debugging.py
new file mode 100644
--- /dev/null
+++ b/pypy/interpreter/test/test_reverse_debugging.py
@@ -0,0 +1,121 @@
+import dis
+from pypy.interpreter.reverse_debugging import *
+from pypy.interpreter import reverse_debugging
+from rpython.rlib import revdb
+
+
+class FakeCode:
+    hidden_applevel = False
+    def __init__(self, co_code='', co_lnotab='', co_filename='?'):
+        self.co_firstlineno = 43
+        self.co_code = co_code
+        self.co_lnotab = co_lnotab
+        self.co_revdb_linestarts = None
+        self.co_filename = co_filename
+
+
+try:
+    from hypothesis import given, strategies, example
+except ImportError:
+    pass
+else:
+    @given(strategies.binary())
+    @example("\x01\x02\x03\x04"
+             "\x00\xFF\x20\x30\x00\xFF\x00\x40"
+             "\xFF\x00\x0A\x0B\xFF\x00\x0C\x00")
+    def test_build_co_revdb_linestarts(lnotab):
+        if len(lnotab) & 1:
+            lnotab = lnotab + '\x00'   # make the length even
+        code = FakeCode("?" * sum(map(ord, lnotab[0::2])), lnotab)
+        lstart = build_co_revdb_linestarts(code)
+        assert lstart is code.co_revdb_linestarts
+
+        expected_starts = set()
+        for addr, lineno in dis.findlinestarts(code):
+            expected_starts.add(addr)
+
+        next_start = len(code.co_code)
+        for index in range(len(code.co_code), -1, -1):
+            if index in expected_starts:
+                next_start = index
+            assert lstart[index] == chr(next_start - index
+                                        if next_start - index <= 255
+                                        else 255)
+
+
+class FakeFrame:
+    def __init__(self, code):
+        self.__code = code
+    def getcode(self):
+        return self.__code
+
+def check_add_breakpoint(input, curfilename=None,
+                         expected_funcname=None,
+                         expected_fileline=None,
+                         expected_output=None,
+                         expected_chbkpt=None):
+    dbstate.__dict__.clear()
+    prev = revdb.send_answer, reverse_debugging.fetch_cur_frame
+    try:
+        messages = []
+        def got_message(cmd, arg1=0, arg2=0, arg3=0, extra=""):
+            messages.append((cmd, arg1, arg2, arg3, extra))
+        def my_cur_frame():
+            assert curfilename is not None
+            return FakeFrame(FakeCode(co_filename=curfilename))
+        revdb.send_answer = got_message
+        reverse_debugging.fetch_cur_frame = my_cur_frame
+        add_breakpoint(input, 5)
+    finally:
+        revdb.send_answer, reverse_debugging.fetch_cur_frame = prev
+
+    if expected_funcname is None:
+        assert dbstate.breakpoint_funcnames is None
+    else:
+        assert dbstate.breakpoint_funcnames == {expected_funcname: 5}
+
+    if expected_fileline is None:
+        assert dbstate.breakpoint_filelines is None
+    else:
+        filename, lineno = expected_fileline
+        assert dbstate.breakpoint_filelines == [(filename.upper(), lineno, 5)]
+
+    got_output = None
+    got_chbkpt = None
+    for msg in messages:
+        if msg[0] == revdb.ANSWER_TEXT:
+            assert got_output is None
+            got_output = msg[-1]
+        elif msg[0] == revdb.ANSWER_CHBKPT:
+            assert got_chbkpt is None
+            assert msg[1] == 5
+            got_chbkpt = msg[-1]
+
+    assert got_output == expected_output
+    assert got_chbkpt == expected_chbkpt
+
+def test_add_breakpoint():
+    check_add_breakpoint('', expected_output="Empty breakpoint name\n",
+                         expected_chbkpt='')
+    check_add_breakpoint('foo42', expected_funcname="foo42",
+                         expected_chbkpt="foo42()")
+    check_add_breakpoint('foo42()', expected_funcname="foo42")
+    check_add_breakpoint('foo.bar', expected_funcname="foo.bar",
+        expected_output='Note: "foo.bar()" doesn''t look like a function name.'
+                        ' Setting breakpoint anyway\n',
+        expected_chbkpt="foo.bar()")
+    check_add_breakpoint('<foo.bar>', expected_funcname="<foo.bar>")
+    check_add_breakpoint('42', curfilename='abcd',
+                         expected_fileline=('abcd', 42),
+                         expected_chbkpt='abcd:42')
+    check_add_breakpoint(':42', curfilename='abcd',
+                         expected_fileline=('abcd', 42),
+                         expected_chbkpt='abcd:42')
+    check_add_breakpoint('abcd:42', expected_fileline=('abcd', 42),
+        expected_output='Note: "abcd" doesnt look like a Python filename.'
+                        ' Setting breakpoint anyway\n')
+    check_add_breakpoint('abcd.py:42',
+                         expected_fileline=('abcd.py', 42))
+    check_add_breakpoint('42:abc',
+        expected_output='expected a line number after colon\n',
+        expected_chbkpt='')
diff --git a/pypy/module/__pypy__/__init__.py b/pypy/module/__pypy__/__init__.py
--- a/pypy/module/__pypy__/__init__.py
+++ b/pypy/module/__pypy__/__init__.py
@@ -106,6 +106,7 @@
                           'interp_magic.save_module_content_for_future_reload',
         'decode_long'               : 'interp_magic.decode_long',
         '_promote'                   : 'interp_magic._promote',
+        'side_effects_ok'           : 'interp_magic.side_effects_ok',
         'stack_almost_full'         : 'interp_magic.stack_almost_full',
     }
     if sys.platform == 'win32':
@@ -145,3 +146,7 @@
             features = detect_cpu.getcpufeatures(model)
             self.extra_interpdef('jit_backend_features',
                                     'space.wrap(%r)' % features)
+        if self.space.config.translation.reverse_debugger:
+            self.extra_interpdef('revdb_stop', 'interp_magic.revdb_stop')
+        else:
+            self.extra_interpdef('revdb_stop', 'space.w_None')
diff --git a/pypy/module/__pypy__/interp_magic.py 
b/pypy/module/__pypy__/interp_magic.py
--- a/pypy/module/__pypy__/interp_magic.py
+++ b/pypy/module/__pypy__/interp_magic.py
@@ -191,3 +191,23 @@
 def stack_almost_full(space):
     """Return True if the stack is more than 15/16th full."""
     return space.newbool(rstack.stack_almost_full())
+
+def side_effects_ok(space):
+    """For use with the reverse-debugger: this function normally returns
+    True, but will return False if we are evaluating a debugging command
+    like a watchpoint.  You are responsible for not doing any side effect
+    at all (including no caching) when evaluating watchpoints.  This
+    function is meant to help a bit---you can write:
+
+        if not __pypy__.side_effects_ok():
+            skip the caching logic
+
+    inside getter methods or properties, to make them usable from
+    watchpoints.  Note that you need to re-run ``REVDB=.. pypy''
+    after changing the Python code.
+    """
+    return space.newbool(space._side_effects_ok())
+
+def revdb_stop(space):
+    from pypy.interpreter.reverse_debugging import stop_point
+    stop_point()
diff --git a/pypy/module/_cffi_backend/ccallback.py 
b/pypy/module/_cffi_backend/ccallback.py
--- a/pypy/module/_cffi_backend/ccallback.py
+++ b/pypy/module/_cffi_backend/ccallback.py
@@ -3,9 +3,9 @@
 """
 import sys, os, py
 
-from rpython.rlib import clibffi, jit, rgc, objectmodel
+from rpython.rlib import clibffi, jit, objectmodel
 from rpython.rlib.objectmodel import keepalive_until_here
-from rpython.rtyper.lltypesystem import lltype, llmemory, rffi
+from rpython.rtyper.lltypesystem import lltype, rffi
 
 from pypy.interpreter.error import OperationError, oefmt
 from pypy.module._cffi_backend import cerrno, misc, parse_c_type
@@ -13,6 +13,7 @@
 from pypy.module._cffi_backend.ctypefunc import SIZE_OF_FFI_ARG, W_CTypeFunc
 from pypy.module._cffi_backend.ctypeprim import W_CTypePrimitiveSigned
 from pypy.module._cffi_backend.ctypevoid import W_CTypeVoid
+from pypy.module._cffi_backend.hide_reveal import hide_reveal1
 
 BIG_ENDIAN = sys.byteorder == 'big'
 
@@ -30,9 +31,7 @@
     return cdata
 
 def reveal_callback(raw_ptr):
-    addr = rffi.cast(llmemory.Address, raw_ptr)
-    gcref = rgc.reveal_gcref(addr)
-    return rgc.try_cast_gcref_to_instance(W_ExternPython, gcref)
+    return hide_reveal1().reveal_object(W_ExternPython, raw_ptr)
 
 
 class Closure(object):
@@ -92,9 +91,7 @@
         return ctype
 
     def hide_object(self):
-        gcref = rgc.cast_instance_to_gcref(self)
-        raw = rgc.hide_nonmovable_gcref(gcref)
-        return rffi.cast(rffi.VOIDP, raw)
+        return hide_reveal1().hide_object(rffi.VOIDP, self)
 
     def _repr_extra(self):
         space = self.space
diff --git a/pypy/module/_cffi_backend/func.py 
b/pypy/module/_cffi_backend/func.py
--- a/pypy/module/_cffi_backend/func.py
+++ b/pypy/module/_cffi_backend/func.py
@@ -168,7 +168,8 @@
     rawbytes = cache.wdict.get(w_x)
     if rawbytes is None:
         data = space.bytes_w(w_x)
-        if we_are_translated() and not rgc.can_move(data):
+        if (we_are_translated() and not rgc.can_move(data)
+                                and not rgc.must_split_gc_address_space()):
             lldata = llstr(data)
             data_start = (llmemory.cast_ptr_to_adr(lldata) +
                           rffi.offsetof(STR, 'chars') +
diff --git a/pypy/module/_cffi_backend/handle.py 
b/pypy/module/_cffi_backend/handle.py
--- a/pypy/module/_cffi_backend/handle.py
+++ b/pypy/module/_cffi_backend/handle.py
@@ -3,8 +3,9 @@
 from pypy.interpreter.gateway import unwrap_spec
 from pypy.interpreter.baseobjspace import W_Root
 from pypy.module._cffi_backend import ctypeobj, ctypeptr, cdataobj
+from pypy.module._cffi_backend.hide_reveal import hide_reveal2
 from rpython.rtyper.lltypesystem import lltype, llmemory, rffi
-from rpython.rlib import rgc, objectmodel, jit
+from rpython.rlib import objectmodel, jit
 
 # ____________________________________________________________
 
@@ -15,9 +16,7 @@
     # we can cast the CCHARP back to a W_CDataHandle with reveal_gcref().
     new_cdataobj = objectmodel.instantiate(cdataobj.W_CDataHandle,
                                            nonmovable=True)
-    gcref = rgc.cast_instance_to_gcref(new_cdataobj)
-    _cdata = rgc.hide_nonmovable_gcref(gcref)
-    _cdata = rffi.cast(rffi.CCHARP, _cdata)
+    _cdata = hide_reveal2().hide_object(rffi.CCHARP, new_cdataobj)
     cdataobj.W_CDataHandle.__init__(new_cdataobj, space, _cdata, w_ctype, w_x)
     return new_cdataobj
 
@@ -43,11 +42,10 @@
 @jit.dont_look_inside
 def _reveal(space, ptr):
     addr = rffi.cast(llmemory.Address, ptr)
-    gcref = rgc.reveal_gcref(addr)
-    if not gcref:
+    if not addr:
         raise oefmt(space.w_RuntimeError,
                     "cannot use from_handle() on NULL pointer")
-    cd = rgc.try_cast_gcref_to_instance(cdataobj.W_CDataHandle, gcref)
+    cd = hide_reveal2().reveal_object(cdataobj.W_CDataHandle, addr)
     if cd is None:
         raise oefmt(space.w_SystemError,
                     "ffi.from_handle(): dead or bogus object handle")
diff --git a/pypy/module/_cffi_backend/hide_reveal.py 
b/pypy/module/_cffi_backend/hide_reveal.py
new file mode 100644
--- /dev/null
+++ b/pypy/module/_cffi_backend/hide_reveal.py
@@ -0,0 +1,66 @@
+from rpython.rlib import rgc
+from rpython.rlib.rweaklist import RWeakListMixin
+from rpython.rtyper.lltypesystem import lltype, llmemory, rffi
+
+
+class HideRevealRWeakList:
+    """Slow implementation of HideReveal: uses a RWeakListMixin."""
+
+    def __init__(self):
+        class GlobGcrefs(RWeakListMixin):
+            pass
+        glob_gcrefs = GlobGcrefs()
+        glob_gcrefs.initialize()
+
+        def hide_object(PTR, obj):
+            # XXX leaks if we call this function often on the same object
+            index = glob_gcrefs.add_handle(obj)
+            return rffi.cast(PTR, index + 1)
+
+        def reveal_object(Class, addr):
+            index = rffi.cast(lltype.Signed, addr) - 1
+            return glob_gcrefs.fetch_handle(index)
+
+        self.hide_object = hide_object
+        self.reveal_object = reveal_object
+
+    def _freeze_(self):
+        return True
+
+
+class HideRevealCast:
+    """Fast implementation of HideReveal: just a cast."""
+
+    def __init__(self):
+
_______________________________________________
pypy-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pypy-commit

Reply via email to