changeset 13569:1894412de0ed

Ruby: major upgrade in debugging support, mainly for navigation: step, next (passing over calls), return (from enclosing function), etc. Also a few bug fixes.
author Michael Van De Vanter <michael.van.de.vanter@oracle.com>
date Wed, 08 Jan 2014 14:03:36 -0800
parents f29a358cf3da
children d7af2296cebb
files graal/com.oracle.truffle.ruby.nodes/src/com/oracle/truffle/ruby/nodes/debug/RubyProxyNode.java graal/com.oracle.truffle.ruby.parser/src/com/oracle/truffle/ruby/parser/Translator.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakAfterLineProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakAfterLocalProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakAfterProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakBeforeLineProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakBeforeProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyCallProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyDebugManager.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyLineProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyLocalProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcAfterLineProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcAfterLocalProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcAfterProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcBeforeLineProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcBeforeProbe.java graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyTraceProbe.java
diffstat 18 files changed, 1106 insertions(+), 317 deletions(-) [+]
line wrap: on
line diff
--- a/graal/com.oracle.truffle.ruby.nodes/src/com/oracle/truffle/ruby/nodes/debug/RubyProxyNode.java	Wed Jan 08 14:00:21 2014 -0800
+++ b/graal/com.oracle.truffle.ruby.nodes/src/com/oracle/truffle/ruby/nodes/debug/RubyProxyNode.java	Wed Jan 08 14:03:36 2014 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
+ * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved. This
  * code is released under a tri EPL/GPL/LGPL license. You can use it,
  * redistribute it and/or modify it under the terms of the:
  *
@@ -10,6 +10,7 @@
 package com.oracle.truffle.ruby.nodes.debug;
 
 import java.math.*;
+import java.util.*;
 
 import com.oracle.truffle.api.*;
 import com.oracle.truffle.api.frame.*;
@@ -34,9 +35,16 @@
 
     public RubyProxyNode(RubyContext context, RubyNode child) {
         super(context, SourceSection.NULL);
-        this.child = adoptChild(child);
         assert !(child instanceof RubyProxyNode);
-        this.probeChain = new ProbeChain(child.getSourceSection(), null);
+        this.child = adoptChild(child);
+        this.probeChain = context.getDebugManager().getProbeChain(child.getSourceSection());
+    }
+
+    public RubyProxyNode(RubyContext context, RubyNode child, ProbeChain probeChain) {
+        super(context, SourceSection.NULL);
+        assert !(child instanceof RubyProxyNode);
+        this.child = adoptChild(child);
+        this.probeChain = probeChain;
     }
 
     @Override
@@ -189,4 +197,16 @@
         }
     }
 
+    public boolean isMarkedAs(NodePhylum phylum) {
+        return probeChain.isMarkedAs(phylum);
+    }
+
+    public Set<NodePhylum> getPhylumMarks() {
+        return probeChain.getPhylumMarks();
+    }
+
+    public void markAs(NodePhylum phylum) {
+        probeChain.markAs(phylum);
+    }
+
 }
--- a/graal/com.oracle.truffle.ruby.parser/src/com/oracle/truffle/ruby/parser/Translator.java	Wed Jan 08 14:00:21 2014 -0800
+++ b/graal/com.oracle.truffle.ruby.parser/src/com/oracle/truffle/ruby/parser/Translator.java	Wed Jan 08 14:03:36 2014 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
+ * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
  * code is released under a tri EPL/GPL/LGPL license. You can use it,
  * redistribute it and/or modify it under the terms of the:
  *
@@ -16,6 +16,8 @@
 import com.oracle.truffle.api.*;
 import com.oracle.truffle.api.frame.*;
 import com.oracle.truffle.api.impl.*;
+import com.oracle.truffle.api.nodes.instrument.*;
+import com.oracle.truffle.api.nodes.instrument.InstrumentationProbeNode.ProbeChain;
 import com.oracle.truffle.ruby.nodes.*;
 import com.oracle.truffle.ruby.nodes.call.*;
 import com.oracle.truffle.ruby.nodes.cast.*;
@@ -87,6 +89,15 @@
         nodeDefinedNames.put(org.jrubyparser.ast.DVarNode.class, "local-variable");
     }
 
+    private static final Set<String> debugIgnoredCalls = new HashSet<>();
+
+    static {
+        debugIgnoredCalls.add("downto");
+        debugIgnoredCalls.add("each");
+        debugIgnoredCalls.add("times");
+        debugIgnoredCalls.add("upto");
+    }
+
     /**
      * Global variables which in common usage have frame local semantics.
      */
@@ -289,7 +300,20 @@
 
         final ArgumentsAndBlockTranslation argumentsAndBlock = translateArgumentsAndBlock(sourceSection, block, args, extraArgument);
 
-        return new CallNode(context, sourceSection, node.getName(), receiverTranslated, argumentsAndBlock.getBlock(), argumentsAndBlock.isSplatted(), argumentsAndBlock.getArguments());
+        RubyNode translated = new CallNode(context, sourceSection, node.getName(), receiverTranslated, argumentsAndBlock.getBlock(), argumentsAndBlock.isSplatted(), argumentsAndBlock.getArguments());
+
+        if (context.getConfiguration().getDebug()) {
+            final CallNode callNode = (CallNode) translated;
+            if (!debugIgnoredCalls.contains(callNode.getName())) {
+
+                final RubyProxyNode proxy = new RubyProxyNode(context, translated);
+                proxy.markAs(NodePhylum.CALL);
+                proxy.getProbeChain().appendProbe(new RubyCallProbe(context, node.getName()));
+                translated = proxy;
+            }
+        }
+
+        return translated;
     }
 
     protected class ArgumentsAndBlockTranslation {
@@ -1147,6 +1171,7 @@
             } else {
                 proxy = new RubyProxyNode(context, translated);
             }
+            proxy.markAs(NodePhylum.ASSIGNMENT);
             context.getDebugManager().registerLocalDebugProxy(methodIdentifier, node.getName(), proxy.getProbeChain());
 
             translated = proxy;
@@ -1455,15 +1480,19 @@
         if (context.getConfiguration().getDebug()) {
 
             RubyProxyNode proxy;
-            SourceSection sourceSection;
             if (translated instanceof RubyProxyNode) {
                 proxy = (RubyProxyNode) translated;
-                sourceSection = proxy.getChild().getSourceSection();
+                if (proxy.getChild() instanceof CallNode) {
+                    // Special case; replace proxy with one registered by line, merge in information
+                    final CallNode callNode = (CallNode) proxy.getChild();
+                    final ProbeChain probeChain = proxy.getProbeChain();
+
+                    proxy = new RubyProxyNode(context, callNode, probeChain);
+                }
             } else {
                 proxy = new RubyProxyNode(context, translated);
-                sourceSection = translated.getSourceSection();
             }
-            context.getDebugManager().registerProbeChain(sourceSection, proxy.getProbeChain());
+            proxy.markAs(NodePhylum.STATEMENT);
             translated = proxy;
         }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakAfterLineProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.api.frame.*;
+import com.oracle.truffle.api.nodes.*;
+import com.oracle.truffle.api.source.*;
+import com.oracle.truffle.ruby.runtime.*;
+
+/**
+ * A Ruby probe for halting execution at a line after a child execution method completes.
+ */
+public final class RubyBreakAfterLineProbe extends RubyLineProbe {
+
+    /**
+     * Creates a probe that will cause a halt when child execution is complete; a {@code oneShot}
+     * probe will remove itself the first time it halts.
+     */
+    public RubyBreakAfterLineProbe(RubyContext context, SourceLineLocation location, boolean oneShot) {
+        super(context, location, oneShot);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame) {
+        if (oneShot) {
+            // One-shot breakpoints retire after one activation.
+            context.getDebugManager().retireLineProbe(location, this);
+        }
+        context.getDebugManager().haltedAt(astNode, frame.materialize());
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, boolean result) {
+        leave(astNode, frame);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, int result) {
+        leave(astNode, frame);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, double result) {
+        leave(astNode, frame);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, Object result) {
+        leave(astNode, frame);
+    }
+
+    @Override
+    public void leaveExceptional(Node astNode, VirtualFrame frame, Exception e) {
+        leave(astNode, frame);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakAfterLocalProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2014 Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.api.frame.*;
+import com.oracle.truffle.api.nodes.*;
+import com.oracle.truffle.ruby.runtime.*;
+
+/**
+ * A Ruby probe for halting execution after a local assignment.
+ */
+public final class RubyBreakAfterLocalProbe extends RubyLocalProbe {
+
+    public RubyBreakAfterLocalProbe(RubyContext context, MethodLocal local) {
+        super(context, local, false);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame) {
+        context.getDebugManager().haltedAt(astNode, frame.materialize());
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, boolean result) {
+        context.getDebugManager().haltedAt(astNode, frame.materialize());
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, int result) {
+        context.getDebugManager().haltedAt(astNode, frame.materialize());
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, double result) {
+        context.getDebugManager().haltedAt(astNode, frame.materialize());
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, Object result) {
+        context.getDebugManager().haltedAt(astNode, frame.materialize());
+    }
+
+    @Override
+    public void leaveExceptional(Node astNode, VirtualFrame frame, Exception e) {
+        context.getDebugManager().haltedAt(astNode, frame.materialize());
+    }
+
+}
--- a/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakAfterProbe.java	Wed Jan 08 14:00:21 2014 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-/*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
- * code is released under a tri EPL/GPL/LGPL license. You can use it,
- * redistribute it and/or modify it under the terms of the:
- *
- * Eclipse Public License version 1.0
- * GNU General Public License version 2
- * GNU Lesser General Public License version 2.1
- */
-package com.oracle.truffle.ruby.runtime.debug;
-
-import com.oracle.truffle.api.frame.*;
-import com.oracle.truffle.api.nodes.*;
-import com.oracle.truffle.ruby.runtime.*;
-
-/**
- * A Ruby probe for invoking a breakpoint shell after a child execution method completes.
- */
-public final class RubyBreakAfterProbe extends RubyProbe {
-
-    public RubyBreakAfterProbe(RubyContext context) {
-        super(context);
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame) {
-        context.getDebugManager().haltedAt(astNode, frame.materialize());
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame, boolean result) {
-        context.getDebugManager().haltedAt(astNode, frame.materialize());
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame, int result) {
-        context.getDebugManager().haltedAt(astNode, frame.materialize());
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame, double result) {
-        context.getDebugManager().haltedAt(astNode, frame.materialize());
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame, Object result) {
-        context.getDebugManager().haltedAt(astNode, frame.materialize());
-    }
-
-    @Override
-    public void leaveExceptional(Node astNode, VirtualFrame frame, Exception e) {
-        context.getDebugManager().haltedAt(astNode, frame.materialize());
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakBeforeLineProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.api.frame.*;
+import com.oracle.truffle.api.nodes.*;
+import com.oracle.truffle.api.source.*;
+import com.oracle.truffle.ruby.runtime.*;
+
+/**
+ * A probe for halting execution at a line before a child execution method.
+ */
+public final class RubyBreakBeforeLineProbe extends RubyLineProbe {
+
+    /**
+     * Creates a probe that will cause a halt just before child execution starts; a {@code oneShot}
+     * probe will remove itself the first time it halts.
+     */
+    public RubyBreakBeforeLineProbe(RubyContext context, SourceLineLocation location, boolean oneShot) {
+        super(context, location, oneShot);
+    }
+
+    @Override
+    public void enter(Node astNode, VirtualFrame frame) {
+
+        if (!isStepping()) {
+            // Ordinary line breakpoints ignored during stepping so no double halts.
+            if (oneShot) {
+                // One-shot breakpoints retire after one activation.
+                context.getDebugManager().retireLineProbe(location, this);
+            }
+            context.getDebugManager().haltedAt(astNode, frame.materialize());
+        }
+    }
+}
--- a/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyBreakBeforeProbe.java	Wed Jan 08 14:00:21 2014 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-/*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
- * code is released under a tri EPL/GPL/LGPL license. You can use it,
- * redistribute it and/or modify it under the terms of the:
- *
- * Eclipse Public License version 1.0
- * GNU General Public License version 2
- * GNU Lesser General Public License version 2.1
- */
-package com.oracle.truffle.ruby.runtime.debug;
-
-import com.oracle.truffle.api.frame.*;
-import com.oracle.truffle.api.nodes.*;
-import com.oracle.truffle.ruby.runtime.*;
-
-/**
- * A probe for invoking a breakpoint shell before a child execution method.
- */
-public final class RubyBreakBeforeProbe extends RubyProbe {
-
-    public RubyBreakBeforeProbe(RubyContext context) {
-        super(context);
-    }
-
-    @Override
-    public void enter(Node astNode, VirtualFrame frame) {
-        context.getDebugManager().haltedAt(astNode, frame.materialize());
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyCallProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2014 Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.api.frame.*;
+import com.oracle.truffle.api.nodes.*;
+import com.oracle.truffle.ruby.runtime.*;
+
+public final class RubyCallProbe extends RubyProbe {
+
+    private final String name;
+
+    public RubyCallProbe(RubyContext context, String name) {
+        super(context, false);
+        this.name = name;
+    }
+
+    @Override
+    public void enter(Node astNode, VirtualFrame frame) {
+        context.getDebugManager().notifyCallEntry(astNode, name);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame) {
+        context.getDebugManager().notifyCallExit(astNode, name);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, boolean result) {
+        context.getDebugManager().notifyCallExit(astNode, name);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, int result) {
+        context.getDebugManager().notifyCallExit(astNode, name);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, double result) {
+        context.getDebugManager().notifyCallExit(astNode, name);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, Object result) {
+        context.getDebugManager().notifyCallExit(astNode, name);
+    }
+
+    @Override
+    public void leaveExceptional(Node astNode, VirtualFrame frame, Exception e) {
+        context.getDebugManager().notifyCallExit(astNode, name);
+    }
+}
--- a/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyDebugManager.java	Wed Jan 08 14:00:21 2014 -0800
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyDebugManager.java	Wed Jan 08 14:03:36 2014 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
+ * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
  * code is released under a tri EPL/GPL/LGPL license. You can use it,
  * redistribute it and/or modify it under the terms of the:
  *
@@ -9,38 +9,113 @@
  */
 package com.oracle.truffle.ruby.runtime.debug;
 
+import java.io.*;
 import java.util.*;
+import java.util.Map.Entry;
 
 import com.oracle.truffle.api.*;
 import com.oracle.truffle.api.frame.*;
 import com.oracle.truffle.api.nodes.*;
+import com.oracle.truffle.api.nodes.instrument.InstrumentationProbeNode.ProbeChain;
 import com.oracle.truffle.api.nodes.instrument.*;
-import com.oracle.truffle.api.nodes.instrument.InstrumentationProbeNode.ProbeChain;
 import com.oracle.truffle.api.source.*;
 import com.oracle.truffle.ruby.runtime.*;
 import com.oracle.truffle.ruby.runtime.core.*;
 import com.oracle.truffle.ruby.runtime.methods.*;
 
 /**
- * Manager for Ruby AST execution.
+ * Manager for Ruby AST execution under debugging control.
  */
 public final class RubyDebugManager implements DebugManager {
 
-    // TODO (mlvdv) no REPL support yet for debugging "locals"; only lines
+    // TODO (mlvdv) no REPL support yet for debugging "locals" (assignment to local variables); only
+    // line-based step/next/return
+
+    private static final boolean TRACE = false;
+    private static final PrintStream OUT = System.out;
+
+    private static enum ExecutionMode {
+
+        /**
+         * Context: ordinary debugging execution, e.g. in response to a "Continue" request or a
+         * "Load-Run" request.
+         * <ul>
+         * <li>User breakpoints are enabled.</li>
+         * <li>Continue until either:
+         * <ol>
+         * <li>execution arrives at a node with attached user breakpoint, <strong>or:</strong></li>
+         * <li>execution completes.</li>
+         * </ol>
+         * </ul>
+         */
+        CONTINUE,
+
+        /**
+         * Context: per-statement stepping execution, e.g. in response to a "Step" request.
+         * <ul>
+         * <li>User breakpoints are disabled.</li>
+         * <li>Continue until either:
+         * <ol>
+         * <li>execution arrives at a "Statement" node, <strong>or:</strong></li>
+         * <li>execution completes.</li>
+         * </ol>
+         * </ul>
+         */
+        STEP,
+
+        /**
+         * Context: per-statement stepping in response to a "Next" request and when not nested in
+         * any function/method call.
+         * <ul>
+         * <li>User breakpoints are disabled.</li>
+         * <li>Continue until either:
+         * <ol>
+         * <li>execution arrives at a "Statement" node <strong>or:</strong></li>
+         * <li>the program completes <strong>or:</strong></li>
+         * <li>execution arrives at a function/method entry, in which case the mode changes to
+         * {@link #NEXT_NESTED} and execution continues.</li>
+         * </ol>
+         * </ul>
+         */
+        NEXT,
+
+        /**
+         * Context: ordinary debugging execution in response to a "Next" requested and when nested
+         * at least one deep in function/method calls.
+         * <ul>
+         * <li>User breakpoints are enabled.</li>
+         * <li>Execute until either:
+         * <ol>
+         * <li>execution arrives at a node with attached user breakpoint, <strong>or:</strong></li>
+         * <li>execution completes, <strong>or:</strong></li>
+         * <li>execution returns from all nested function/method calls, in which case the mode
+         * changes to {@link #NEXT} and execution continues.</li>
+         * </ol>
+         * </ul>
+         */
+        NEXT_NESTED;
+    }
 
     private static enum BreakpointStatus {
 
         /**
-         * Created for a source location but not yet attached for some legitimate reason: new and
-         * not yet attached; new and the source file hasn't been loaded yet; old and the source file
-         * is in the process of being reloaded.
+         * Created for a source location but not yet attached for some legitimate reason: perhaps
+         * newly created and not yet attached; perhaps newly created and the source file hasn't been
+         * loaded yet; perhaps old and the source file is in the process of being reloaded.
          */
         PENDING("Pending"),
 
         /**
-         * Has an active break probe in the AST.
+         * Has a {@link RubyProbe}, which is attached to a {@linkplain ProbeChain known location} in
+         * the AST.
          */
-        ATTACHED("Active"),
+        ACTIVE("Active"),
+
+        /**
+         * Has a {@link RubyProbe}, which is associated with a {@linkplain ProbeChain known
+         * location} in the AST, but which has been temporarily removed.
+         */
+        DISABLED("Disabled"),
 
         /**
          * Should be attached, but the line location cannot be found in the source.
@@ -64,19 +139,45 @@
         }
     }
 
-    /**
-     * Map: Source lines ==> source chains known to be at line locations in an AST.
-     */
-    private final Map<SourceLineLocation, ProbeChain> linesToProbeChains = new HashMap<>();
-
     private final Set<Source> loadedSources = new HashSet<>();
 
     private Source beingLoaded = null;
 
     /**
-     * Map: Source lines ==> attached Breakpoints/procs to be activated before execution at line.
+     * The current mode of execution.
+     */
+    private ExecutionMode executionMode = ExecutionMode.CONTINUE;
+
+    /**
+     * When running in "step" mode, this is the number of steps that haven't yet completed.
+     */
+    private int unfinishedStepCount = 0;
+
+    /**
+     * When running in "next" mode, this is the number of steps that haven't yet completed.
+     */
+    private int unfinishedNextCount = 0;
+    /**
+     * When running in "next" mode, this is non-null when running a function/method that must be
+     * continued across.
      */
-    private final Map<SourceLineLocation, RubyLineBreakpoint> linesToBreakpoints = new TreeMap<>();
+    private Node nextNestedInCallNode = null;
+
+    /**
+     * Map: SourceSection ==> probe chain associated with that source section in an AST.
+     */
+    private final Map<SourceSection, ProbeChain> srcToProbeChain = new HashMap<>();
+
+    /**
+     * Map: Source lines ==> probe chains associated with source sections starting on the line.
+     */
+    private final Map<SourceLineLocation, Set<ProbeChain>> lineToProbeChains = new HashMap<>();
+
+    /**
+     * Map: Source lines ==> attached Breakpoints/procs to be activated before execution at line.
+     * There should be no more than one line breakpoint associated with a line.
+     */
+    private final Map<SourceLineLocation, RubyLineBreakpoint> lineToBreakpoint = new TreeMap<>();
 
     /**
      * Map: Method locals in AST ==> Method local assignments where breakpoints can be attached.
@@ -94,25 +195,58 @@
         this.context = context;
     }
 
+    /**
+     * Gets the {@linkplain ProbeChain probe} associated with a particular {@link SourceSection
+     * source location}, creating a new one if needed. There should only be one probe associated
+     * with each {@linkplain SourceSection source location}.
+     */
+    public ProbeChain getProbeChain(SourceSection sourceSection) {
+        assert sourceSection != null;
+        assert sourceSection.getSource().equals(beingLoaded);
+
+        ProbeChain probeChain = srcToProbeChain.get(sourceSection);
+
+        if (probeChain != null) {
+            return probeChain;
+        }
+        probeChain = new ProbeChain(context, sourceSection, null);
+
+        // Register new ProbeChain by unique SourceSection
+        srcToProbeChain.put(sourceSection, probeChain);
+
+        // Register new ProbeChain by source line, there may be more than one
+        // Create line location for map key
+        final SourceLineLocation lineLocation = new SourceLineLocation(sourceSection.getSource(), sourceSection.getStartLine());
+
+        Set<ProbeChain> probeChains = lineToProbeChains.get(lineLocation);
+        if (probeChains == null) {
+            probeChains = new HashSet<>();
+            lineToProbeChains.put(lineLocation, probeChains);
+        }
+        probeChains.add(probeChain);
+
+        return probeChain;
+    }
+
     public void notifyStartLoading(Source source) {
 
         beingLoaded = source;
 
-        // Forget all the probe chains from previous loading
-        final List<SourceLineLocation> locations = new ArrayList<>();
-        for (SourceLineLocation lineLocation : linesToProbeChains.keySet()) {
-            if (lineLocation.getSource().equals(beingLoaded)) {
-                locations.add(lineLocation);
-            }
-        }
-        for (SourceLineLocation lineLocation : locations) {
-            linesToProbeChains.remove(lineLocation);
-        }
-
-        // Forget whatever we knew, and detach from old AST/ProbeChain if needed
-        for (RubyLineBreakpoint breakpoint : linesToBreakpoints.values()) {
+        /**
+         * We'd like to know when we're reloading a file if the old AST is completely dead, so that
+         * we can correctly identify the state of breakpoints related to it, but that doesn't seem
+         * possible.
+         * 
+         * Before we start, find any breakpoints that never got attached, which get reported as
+         * errors. Revert them to "pending", in case their lines are found this time around.
+         */
+        for (RubyLineBreakpoint breakpoint : lineToBreakpoint.values()) {
             if (breakpoint.getSourceLineLocation().getSource().equals(beingLoaded)) {
-                breakpoint.setPending();
+                if (breakpoint.status == BreakpointStatus.ERROR) {
+                    // It was an error, which means we have not yet found that line for this Source.
+                    // It might show up while loading this time, so make it pending.
+                    breakpoint.setPending();
+                }
             }
         }
     }
@@ -120,51 +254,66 @@
     public void notifyFinishedLoading(Source source) {
         assert source == beingLoaded;
 
-        // Any pending breakpoints are now erroneous, didn't find the line.
-        for (RubyLineBreakpoint breakpoint : linesToBreakpoints.values()) {
+        // Update any pending breakpoints associated with this source
+
+        for (RubyLineBreakpoint breakpoint : lineToBreakpoint.values()) {
             if (breakpoint.getSourceLineLocation().getSource().equals(beingLoaded)) {
                 if (breakpoint.status == BreakpointStatus.PENDING) {
-                    breakpoint.setError();
+                    final ProbeChain probeChain = findProbeChain(breakpoint.location);
+                    if (probeChain == null) {
+                        breakpoint.setError();
+                    } else {
+                        breakpoint.attach(probeChain);
+                    }
                 }
             }
         }
-
         loadedSources.add(source);
         beingLoaded = null;
-
     }
 
     /**
-     * Notifies the manager about creation of a newly created probe chain associated with a proxy
-     * for an AST node at a specific location in the source text.
+     * Returns a {@link ProbeChain} associated with source that starts on a specified line; if there
+     * are more than one, return the one with the first character location.
      */
-    public void registerProbeChain(SourceSection sourceSection, ProbeChain probeChain) {
-
-        assert sourceSection.getSource().equals(beingLoaded);
+    private ProbeChain findProbeChain(SourceLineLocation lineLocation) {
+        ProbeChain probeChain = null;
+        final Set<ProbeChain> probeChains = lineToProbeChains.get(lineLocation);
+        if (probeChains != null) {
+            assert probeChains.size() > 0;
+            for (ProbeChain chain : probeChains) {
+                if (probeChain == null) {
+                    probeChain = chain;
+                } else if (chain.getProbedSourceSection().getCharIndex() < probeChain.getProbedSourceSection().getCharIndex()) {
+                    probeChain = chain;
+                }
+            }
+        }
+        return probeChain;
+    }
 
-        // Remember this probe chain, indexed by line number
-        final SourceLineLocation lineLocation = new SourceLineLocation(sourceSection.getSource(), sourceSection.getStartLine());
-        linesToProbeChains.put(lineLocation, probeChain);
+    /**
+     * Remove a probe from a line location and retire it permanently.
+     */
+    public void retireLineProbe(SourceLineLocation location, RubyLineProbe probe) {
+        final RubyLineBreakpoint breakpoint = lineToBreakpoint.get(location);
+        lineToBreakpoint.remove(location);
+        breakpoint.retire(probe);
+    }
 
-        final RubyLineBreakpoint breakpoint = linesToBreakpoints.get(lineLocation);
-        if (breakpoint != null && breakpoint.location.equals(lineLocation)) {
-            // We only register while we're loading;
-            // While we're loading, there should only be pending breakpoints for this source
-            assert breakpoint.status == BreakpointStatus.PENDING;
-
-            // Found a line/probeChain where a pending breakpoint should be set
-            breakpoint.attach(probeChain);
-        }
+    @Override
+    public LineBreakpoint[] getBreakpoints() {
+        return lineToBreakpoint.values().toArray(new LineBreakpoint[0]);
     }
 
     @Override
     public RubyLineBreakpoint setBreakpoint(SourceLineLocation lineLocation) {
 
-        RubyLineBreakpoint breakpoint = linesToBreakpoints.get(lineLocation);
+        RubyLineBreakpoint breakpoint = lineToBreakpoint.get(lineLocation);
 
         if (breakpoint != null) {
             switch (breakpoint.status) {
-                case ATTACHED:
+                case ACTIVE:
                     throw new RuntimeException("Breakpoint already set at line " + lineLocation);
 
                 case PENDING:
@@ -175,10 +324,10 @@
                     assert false;
             }
         } else {
-            breakpoint = new RubyLineBreakpoint(lineLocation, new RubyBreakBeforeProbe(context));
-            linesToBreakpoints.put(lineLocation, breakpoint);
+            breakpoint = new RubyLineBreakpoint(lineLocation, new RubyBreakBeforeLineProbe(context, lineLocation, false));
+            lineToBreakpoint.put(lineLocation, breakpoint);
 
-            final ProbeChain probeChain = linesToProbeChains.get(lineLocation);
+            final ProbeChain probeChain = findProbeChain(lineLocation);
             if (probeChain != null) {
                 breakpoint.attach(probeChain);
             }
@@ -193,30 +342,12 @@
     }
 
     @Override
-    public LineBreakpoint[] getBreakpoints() {
-        return linesToBreakpoints.values().toArray(new LineBreakpoint[0]);
-    }
-
-    @Override
-    public void removeBreakpoint(SourceLineLocation lineLocation) {
-        final RubyLineBreakpoint breakpoint = linesToBreakpoints.get(lineLocation);
-        if (breakpoint == null) {
-            throw new RuntimeException("No break/proc located at line " + lineLocation);
-        }
-        linesToBreakpoints.remove(lineLocation);
-        breakpoint.retire();
-    }
-
-    /**
-     * Sets a Ruby proc of no arguments to be run before a specified line is executed.
-     */
-    public void setLineProc(SourceLineLocation lineLocation, RubyProc proc) {
-
-        RubyLineBreakpoint breakpoint = linesToBreakpoints.get(lineLocation);
+    public LineBreakpoint setOneShotBreakpoint(SourceLineLocation lineLocation) {
+        RubyLineBreakpoint breakpoint = lineToBreakpoint.get(lineLocation);
 
         if (breakpoint != null) {
             switch (breakpoint.status) {
-                case ATTACHED:
+                case ACTIVE:
                     throw new RuntimeException("Breakpoint already set at line " + lineLocation);
 
                 case PENDING:
@@ -227,18 +358,165 @@
                     assert false;
             }
         } else {
-            breakpoint = new RubyLineBreakpoint(lineLocation, new RubyProcBeforeProbe(context, proc));
-            linesToBreakpoints.put(lineLocation, breakpoint);
+            breakpoint = new RubyLineBreakpoint(lineLocation, new RubyBreakBeforeLineProbe(context, lineLocation, true));
+            lineToBreakpoint.put(lineLocation, breakpoint);
+
+            final ProbeChain probeChain = findProbeChain(lineLocation);
+            if (probeChain != null) {
+                breakpoint.attach(probeChain);
+            }
+        }
+
+        return breakpoint;
+    }
+
+    public boolean hasBreakpoint(SourceLineLocation lineLocation) {
+        return lineToBreakpoint.get(lineLocation) != null;
+    }
+
+    @Override
+    public void removeBreakpoint(SourceLineLocation lineLocation) {
+        final RubyLineBreakpoint breakpoint = lineToBreakpoint.get(lineLocation);
+        if (breakpoint == null) {
+            throw new RuntimeException("No break/proc located at line " + lineLocation);
+        }
+        lineToBreakpoint.remove(lineLocation);
+        breakpoint.retire();
+    }
+
+    private void removeOneShotBreakpoints() {
+        for (Entry<SourceLineLocation, RubyLineBreakpoint> entry : lineToBreakpoint.entrySet()) {
+            final RubyLineBreakpoint breakpoint = entry.getValue();
+            if (breakpoint.probe.isOneShot()) {
+                lineToBreakpoint.remove(entry.getKey());
+                breakpoint.retire();
+            }
+        }
+    }
+
+    /**
+     * Prepare to execute a "Continue":
+     * <ul>
+     * <li>Execution will continue until either:
+     * <ol>
+     * <li>execution arrives at a node to which a breakpoint is attached, <strong>or:</strong></li>
+     * <li>execution completes.</li>
+     * </ol>
+     * </ul>
+     */
+    public void setContinue() {
+        // Nothing to do here; "Continue" is the default, which should be restored after each halt.
+    }
+
+    /**
+     * Prepare to execute a "Step":
+     * <ul>
+     * <li>User breakpoints are disabled.</li>
+     * <li>Execution will continue until either:
+     * <ol>
+     * <li>execution arrives at a "Statement" node, <strong>or:</strong></li>
+     * <li>execution completes.</li>
+     * </ol>
+     * This status persists only through one execution, and reverts to
+     * {@link ExecutionMode#CONTINUE}.
+     * </ul>
+     */
+    public void setStep(int stepCount) {
+        assert executionMode == ExecutionMode.CONTINUE;
+        disableLineBreakpoints();
+        setStepping(true);
+        unfinishedStepCount = stepCount;
+        setMode(ExecutionMode.STEP);
+    }
 
-            final ProbeChain probeChain = linesToProbeChains.get(lineLocation);
+    /**
+     * Prepare to execute a "Next":
+     * <ul>
+     * <li>Execution will continue until either:
+     * <ol>
+     * <li>execution arrives at a "Statement" node when not nested in one or more function/method
+     * calls, <strong>or:</strong></li>
+     * <li>execution arrives at a node to which a breakpoint is attached and when nested in one or
+     * more function/method calls, <strong>or:</strong></li>
+     * <li>execution completes.</li>
+     * </ol>
+     * This status persists only through one execution, and reverts to
+     * {@link ExecutionMode#CONTINUE}.
+     * </ul>
+     */
+    public void setNext(int nextCount) {
+        assert executionMode == ExecutionMode.CONTINUE;
+        disableLineBreakpoints();
+        setStepping(true);
+        unfinishedNextCount = nextCount;
+        setMode(ExecutionMode.NEXT);
+    }
+
+    private void disableLineBreakpoints() {
+        for (RubyLineBreakpoint breakpoint : lineToBreakpoint.values()) {
+            if (breakpoint.status == BreakpointStatus.ACTIVE) {
+                breakpoint.disable();
+            }
+        }
+    }
+
+    private void enableLineBreakpoints() {
+        for (RubyLineBreakpoint breakpoint : lineToBreakpoint.values()) {
+            if (breakpoint.status == BreakpointStatus.DISABLED) {
+                breakpoint.enable();
+            }
+        }
+    }
+
+    private void setStepping(boolean isStepping) {
+        // Set the "stepping" flag on every statement probe.
+        for (ProbeChain probeChain : srcToProbeChain.values()) {
+            if (probeChain.isMarkedAs(NodePhylum.STATEMENT)) {
+                probeChain.setStepping(isStepping);
+            }
+        }
+    }
+
+    private void setMode(ExecutionMode mode) {
+        if (TRACE) {
+            OUT.println("DebugManager: " + executionMode.toString() + "-->" + mode.toString());
+        }
+        executionMode = mode;
+    }
+
+    /**
+     * Sets a Ruby proc of no arguments to be run before a specified line is executed.
+     */
+    public void setLineProc(SourceLineLocation lineLocation, RubyProc proc) {
+
+        RubyLineBreakpoint breakpoint = lineToBreakpoint.get(lineLocation);
+
+        if (breakpoint != null) {
+            switch (breakpoint.status) {
+                case ACTIVE:
+                    throw new RuntimeException("Breakpoint already set at line " + lineLocation);
+
+                case PENDING:
+                case ERROR:
+                    throw new RuntimeException("Breakpoint already pending at line " + lineLocation);
+
+                default:
+                    assert false;
+            }
+        } else {
+            breakpoint = new RubyLineBreakpoint(lineLocation, new RubyProcBeforeLineProbe(context, lineLocation, proc));
+            lineToBreakpoint.put(lineLocation, breakpoint);
+
+            final ProbeChain probeChain = findProbeChain(lineLocation);
             if (probeChain != null) {
                 breakpoint.attach(probeChain);
             }
         }
     }
 
+    // TODO (mlvdv) rework locals (watchpoints) to work like breaks; I doubt it is even correct now
     /**
-     * Registers the chain of probes associated with a method local variable in the AST.
+     * Registers the chain of probes associated with a method local variable assignment in the AST.
      */
     public void registerLocalDebugProxy(UniqueMethodIdentifier methodIdentifier, String localName, ProbeChain probeChain) {
         final MethodLocal methodLocal = new MethodLocal(methodIdentifier, localName);
@@ -258,7 +536,7 @@
         if (probe != null) {
             throw new RuntimeException("Breakpoint already set on method local " + methodLocal);
         }
-        probe = new RubyBreakAfterProbe(context);
+        probe = new RubyBreakAfterLocalProbe(context, methodLocal);
         localsToAttachedBreakpoints.put(methodLocal, probe);
         probeChain.appendProbe(probe);
     }
@@ -277,7 +555,7 @@
         if (probe != null) {
             throw new RuntimeException("Assignment proc already set on method local " + methodLocal);
         }
-        probe = new RubyProcAfterProbe(context, proc);
+        probe = new RubyProcAfterLocalProbe(context, methodLocal, proc);
         localsToAttachedBreakpoints.put(methodLocal, probe);
         probeChain.appendProbe(probe);
     }
@@ -295,24 +573,72 @@
         localsToAttachedBreakpoints.remove(methodLocal);
     }
 
-    /**
-     * Receives notification of a suspended execution context; execution resumes when this method
-     * returns.
-     * 
-     * @param astNode a guest language AST node that represents the current execution site, assumed
-     *            not to be any kind of {@link InstrumentationNode},
-     * @param frame execution frame at the site where execution suspended
-     */
     public void haltedAt(Node astNode, MaterializedFrame frame) {
+        switch (executionMode) {
+            case CONTINUE:
+            case NEXT_NESTED:
+                // User breakpoints should already be enabled
+                // Stepping should be false
+                nextNestedInCallNode = null;
+                break;
+            case STEP:
+                unfinishedStepCount--;
+                if (unfinishedStepCount > 0) {
+                    return;
+                }
+                // Revert to default mode.
+                enableLineBreakpoints();
+                setStepping(false);
+                break;
+            case NEXT:
+                unfinishedNextCount--;
+                if (unfinishedNextCount > 0) {
+                    return;
+                }
+                // Revert to default mode.
+                enableLineBreakpoints();
+                setStepping(false);
+                break;
+            default:
+                assert false;  // Should not happen
+                break;
+
+        }
+        // Clean up, just in cased the one-shot breakpoints got confused
+        removeOneShotBreakpoints();
+
+        setMode(ExecutionMode.CONTINUE);
+
+        // Return control to the debug client
         context.haltedAt(astNode, frame);
+
     }
 
-    private static final class RubyLineBreakpoint implements DebugManager.LineBreakpoint, Comparable {
+    private RubyProbe createReplacement(RubyProbe probe) {
+        // Should be a specialized replacement for any kind of probe created.
+        // Ugly, but there's no other way to reset the parent pointer and reuse a probe node.
+        if (probe instanceof RubyBreakBeforeLineProbe) {
+            final RubyBreakBeforeLineProbe oldProbe = (RubyBreakBeforeLineProbe) probe;
+            return new RubyBreakBeforeLineProbe(context, oldProbe.getLineLocation(), oldProbe.isOneShot());
+        }
+        if (probe instanceof RubyProcBeforeLineProbe) {
+            final RubyProcBeforeLineProbe oldProbe = (RubyProcBeforeLineProbe) probe;
+            return new RubyProcBeforeLineProbe(context, oldProbe.getLineLocation(), oldProbe.getProc());
+        }
+        assert false;
+        return null;
+    }
+
+    /**
+     * A breakpoint of the sort that would be created by a client, with a life-cycle represented by
+     * {@link BreakpointStatus}.
+     */
+    private final class RubyLineBreakpoint implements DebugManager.LineBreakpoint, Comparable {
 
         private final SourceLineLocation location;
 
         private RubyProbe probe;  // non-null until RETIRED, but may get replaced.
-        private ProbeChain probeChain = null;  // only non-null when ATTACHED
+        private ProbeChain probeChain = null;
         private BreakpointStatus status = BreakpointStatus.PENDING;
 
         public RubyLineBreakpoint(SourceLineLocation location, RubyProbe probe) {
@@ -332,7 +658,11 @@
 
         @Override
         public String getDebugStatus() {
-            return status == null ? "<none>" : status.name;
+            String result = status == null ? "<none>" : status.name;
+            if (probe.isOneShot()) {
+                result = result + ", " + "One-Shot";
+            }
+            return result;
         }
 
         private void attach(ProbeChain chain) {
@@ -341,27 +671,29 @@
             probeChain = chain;
             probeChain.appendProbe(probe);
 
-            status = BreakpointStatus.ATTACHED;
+            status = BreakpointStatus.ACTIVE;
+        }
+
+        private void disable() {
+            assert status == BreakpointStatus.ACTIVE;
+
+            probeChain.removeProbe(probe);
+            status = BreakpointStatus.DISABLED;
+        }
+
+        private void enable() {
+            assert status == BreakpointStatus.DISABLED;
+
+            // Can't re-attach to probe chain, because can't re-assign parent.
+            probe = createReplacement(probe);
+            probeChain.appendProbe(probe);
+            status = BreakpointStatus.ACTIVE;
         }
 
         private void setPending() {
-            switch (status) {
-                case ATTACHED:
-                    detach();
-                    // TODO (mlvdv) replace the probe
-                    status = BreakpointStatus.PENDING;
-                    break;
-                case ERROR:
-                    status = BreakpointStatus.PENDING;
-                    break;
-                case PENDING:
-                    break;
-                case RETIRED:
-                    assert false;
-                    break;
-                default:
-                    assert false;
-            }
+            assert status == BreakpointStatus.ERROR;
+
+            status = BreakpointStatus.PENDING;
         }
 
         public void setError() {
@@ -370,18 +702,9 @@
             status = BreakpointStatus.ERROR;
         }
 
-        private void detach() {
-            assert status == BreakpointStatus.ATTACHED;
-
-            probeChain.removeProbe(probe);
-            probeChain = null;
-
-            status = BreakpointStatus.PENDING;
-        }
-
         private void retire() {
 
-            if (probeChain != null) {
+            if (status == BreakpointStatus.ACTIVE) {
                 probeChain.removeProbe(probe);
             }
             probe = null;
@@ -389,6 +712,151 @@
 
             status = BreakpointStatus.RETIRED;
         }
+
+        private void retire(RubyProbe retiredProbe) {
+
+            assert this.probe == retiredProbe;
+            retire();
+        }
+    }
+
+    private static final class CallRecord {
+
+        final SourceSection section;
+        @SuppressWarnings("unused") final String name;
+        final CallRecord predecessor;
+
+        public CallRecord(SourceSection section, String name, CallRecord predecessor) {
+            this.section = section;
+            this.name = name;
+            this.predecessor = predecessor;
+        }
+    }
+
+    private CallRecord callStack = null;
+
+    public void notifyCallEntry(Node astNode, String name) {
+        if (TRACE) {
+            OUT.println("DebugManager: ENTER \"" + name + "\" " + nodeToString(astNode));
+        }
+        if (executionMode == ExecutionMode.NEXT && nextNestedInCallNode == null) {
+            // In "Next" mode, where we have been "stepping", but are about to enter a call.
+            // Switch modes to be like "Continue" until/if return from this call
+            nextNestedInCallNode = astNode;
+            enableLineBreakpoints();
+            setStepping(false);
+            setMode(ExecutionMode.NEXT_NESTED);
+        }
+
+        callStack = new CallRecord(astNode.getSourceSection(), name, callStack);
+    }
+
+    public void notifyCallExit(Node astNode, String name) {
+        if (TRACE) {
+            OUT.println("DebugManager: EXIT \"" + name + "\" " + nodeToString(astNode));
+        }
+
+        if (executionMode == ExecutionMode.NEXT_NESTED) {
+            assert nextNestedInCallNode != null;
+            if (nextNestedInCallNode == astNode) {
+                // In "Next" mode while nested in a function/method call, but about to return.
+                // Switch modes to be like "Step" until/if enter another function/method call.
+                nextNestedInCallNode = null;
+                disableLineBreakpoints();
+                setStepping(true);
+                setMode(ExecutionMode.NEXT);
+            }
+        }
+
+        final SourceSection section = astNode.getSourceSection();
+        if (section instanceof NullSourceSection) {
+            if (TRACE) {
+                OUT.println("Ignoring call exit \"" + name + "\" " + nodeToString(astNode));
+            }
+        }
+        callStack = callStack.predecessor;
+    }
+
+    /**
+     * Sets a one-shot breakpoint to halt just after the completion of the call site at the top of
+     * the current call stack.
+     */
+    public boolean setReturnBreakpoint() {
+        if (callStack == null) {
+            return false;
+        }
+        final SourceLineLocation lineLocation = new SourceLineLocation(callStack.section);
+        RubyLineBreakpoint breakpoint = lineToBreakpoint.get(lineLocation);
+        if (breakpoint != null) {
+            return true;
+        }
+        final ProbeChain probeChain = findProbeChain(lineLocation);
+        if (probeChain != null) {
+            breakpoint = new RubyLineBreakpoint(lineLocation, new RubyBreakAfterLineProbe(context, lineLocation, true));
+            lineToBreakpoint.put(lineLocation, breakpoint);
+            breakpoint.attach(probeChain);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Notifies that a new execution is about to start, i.e. running a program or an eval.
+     */
+    @SuppressWarnings("static-method")
+    public void startExecution(String name) {
+        if (TRACE) {
+            OUT.println("RubyDebugManager: START " + name);
+        }
+        // TODO (mlvdv) push the current call stack onto a stack; start new empty call stack
+    }
+
+    /**
+     * Notifies that the current execution has ended.
+     */
+    public void endExecution(String name) {
+        if (TRACE) {
+            OUT.println("RubyDebugManager: END " + name);
+        }
+
+        // TODO (mlvdv) pop the current call stack, restore previous
+
+        switch (executionMode) {
+            case CONTINUE:
+            case NEXT_NESTED:
+                // User breakpoints should already be enabled
+                // Stepping should be false
+                nextNestedInCallNode = null;
+                break;
+            case STEP:
+                // Revert to default mode.
+                enableLineBreakpoints();
+                setStepping(false);
+                unfinishedStepCount = 0;
+                break;
+            case NEXT:
+                // Revert to default mode.
+                enableLineBreakpoints();
+                setStepping(false);
+                unfinishedNextCount = 0;
+                break;
+            default:
+                assert false;  // Should not happen
+                break;
+        }
+        // Clean up, just in cased the one-shot breakpoints got confused
+        removeOneShotBreakpoints();
+
+        setMode(ExecutionMode.CONTINUE);
+    }
+
+    @SuppressWarnings("static-method")
+    private String nodeToString(Node astNode) {
+        final SourceSection sourceSection = astNode.getSourceSection();
+        if (sourceSection != null) {
+            return Integer.toString(sourceSection.getStartLine()) + ":" + astNode;
+        }
+        return astNode.toString();
     }
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyLineProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.api.source.*;
+import com.oracle.truffle.ruby.runtime.*;
+
+/**
+ * A Ruby probe situated at a Ruby "line".
+ */
+public abstract class RubyLineProbe extends RubyProbe {
+
+    protected final SourceLineLocation location;
+
+    /**
+     * Creates a probe that will cause a halt just before child execution starts; a {@code oneShot}
+     * probe will remove itself the first time it halts.
+     */
+    public RubyLineProbe(RubyContext context, SourceLineLocation location, boolean oneShot) {
+        super(context, oneShot);
+        this.location = location;
+    }
+
+    public SourceLineLocation getLineLocation() {
+        return location;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyLocalProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.ruby.runtime.*;
+
+/**
+ * A Ruby probe situated at a Ruby local assignment.
+ */
+public abstract class RubyLocalProbe extends RubyProbe {
+
+    protected final MethodLocal local;
+
+    public RubyLocalProbe(RubyContext context, MethodLocal local, boolean oneShot) {
+        super(context, oneShot);
+        this.local = local;
+    }
+
+}
--- a/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProbe.java	Wed Jan 08 14:00:21 2014 -0800
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
+ * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
  * code is released under a tri EPL/GPL/LGPL license. You can use it,
  * redistribute it and/or modify it under the terms of the:
  *
@@ -18,14 +18,24 @@
  */
 public abstract class RubyProbe extends InstrumentationProbeNode.DefaultProbeNode {
 
+    protected final boolean oneShot;
+
     protected final RubyContext context;
 
-    public RubyProbe(RubyContext context) {
+    /**
+     * OneShot is this a one-shot (self-removing) probe?
+     */
+    public RubyProbe(RubyContext context, boolean oneShot) {
+        super(context);
+        this.oneShot = oneShot;
         this.context = context;
-        assert context != null;
     }
 
-    public RubyContext getContext() {
-        return context;
+    /**
+     * Is this a one-shot (self-removing) probe? If so, it will remove itself the first time
+     * activated.
+     */
+    public boolean isOneShot() {
+        return oneShot;
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcAfterLineProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2014 Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.api.frame.*;
+import com.oracle.truffle.api.nodes.*;
+import com.oracle.truffle.api.source.*;
+import com.oracle.truffle.ruby.runtime.*;
+import com.oracle.truffle.ruby.runtime.core.*;
+
+/**
+ * A probe for instrumenting a Ruby program with a Ruby procedure to run on the return value from
+ * node execution at a line.
+ */
+public final class RubyProcAfterLineProbe extends RubyLineProbe {
+
+    private final RubyProc proc;
+
+    public RubyProcAfterLineProbe(RubyContext context, SourceLineLocation location, RubyProc proc) {
+        super(context, location, false);
+        this.proc = proc;
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame) {
+        proc.call(frame.pack());
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, boolean result) {
+        proc.call(frame.pack(), result);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, int result) {
+        proc.call(frame.pack(), result);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, double result) {
+        proc.call(frame.pack(), result);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, Object result) {
+        proc.call(frame.pack(), result);
+    }
+
+    @Override
+    public void leaveExceptional(Node astNode, VirtualFrame frame, Exception e) {
+        proc.call(frame.pack());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcAfterLocalProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.api.frame.*;
+import com.oracle.truffle.api.nodes.*;
+import com.oracle.truffle.ruby.runtime.*;
+import com.oracle.truffle.ruby.runtime.core.*;
+
+/**
+ * A probe for instrumenting a Ruby program with a Ruby procedure to run on the return value from a
+ * local assignment.
+ */
+public final class RubyProcAfterLocalProbe extends RubyLocalProbe {
+
+    private final RubyProc proc;
+
+    public RubyProcAfterLocalProbe(RubyContext context, MethodLocal local, RubyProc proc) {
+        super(context, local, false);
+        this.proc = proc;
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame) {
+        proc.call(frame.pack());
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, boolean result) {
+        proc.call(frame.pack(), result);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, int result) {
+        proc.call(frame.pack(), result);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, double result) {
+        proc.call(frame.pack(), result);
+    }
+
+    @Override
+    public void leave(Node astNode, VirtualFrame frame, Object result) {
+        proc.call(frame.pack(), result);
+    }
+
+    @Override
+    public void leaveExceptional(Node astNode, VirtualFrame frame, Exception e) {
+        proc.call(frame.pack());
+    }
+}
--- a/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcAfterProbe.java	Wed Jan 08 14:00:21 2014 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
- * code is released under a tri EPL/GPL/LGPL license. You can use it,
- * redistribute it and/or modify it under the terms of the:
- *
- * Eclipse Public License version 1.0
- * GNU General Public License version 2
- * GNU Lesser General Public License version 2.1
- */
-package com.oracle.truffle.ruby.runtime.debug;
-
-import com.oracle.truffle.api.frame.*;
-import com.oracle.truffle.api.nodes.*;
-import com.oracle.truffle.ruby.runtime.*;
-import com.oracle.truffle.ruby.runtime.core.*;
-
-/**
- * A probe for instrumenting a Ruby program with a Ruby procedure to run on the return value from
- * node execution.
- */
-public final class RubyProcAfterProbe extends RubyProbe {
-
-    private final RubyProc proc;
-
-    public RubyProcAfterProbe(RubyContext context, RubyProc proc) {
-        super(context);
-        this.proc = proc;
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame) {
-        proc.call(frame.pack());
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame, boolean result) {
-        proc.call(frame.pack(), result);
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame, int result) {
-        proc.call(frame.pack(), result);
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame, double result) {
-        proc.call(frame.pack(), result);
-    }
-
-    @Override
-    public void leave(Node astNode, VirtualFrame frame, Object result) {
-        proc.call(frame.pack(), result);
-    }
-
-    @Override
-    public void leaveExceptional(Node astNode, VirtualFrame frame, Exception e) {
-        proc.call(frame.pack());
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcBeforeLineProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
+ * code is released under a tri EPL/GPL/LGPL license. You can use it,
+ * redistribute it and/or modify it under the terms of the:
+ *
+ * Eclipse Public License version 1.0
+ * GNU General Public License version 2
+ * GNU Lesser General Public License version 2.1
+ */
+package com.oracle.truffle.ruby.runtime.debug;
+
+import com.oracle.truffle.api.frame.*;
+import com.oracle.truffle.api.nodes.*;
+import com.oracle.truffle.api.source.*;
+import com.oracle.truffle.ruby.runtime.*;
+import com.oracle.truffle.ruby.runtime.core.*;
+
+/**
+ * A probe for instrumenting a Ruby program with a Ruby procedure to run before a calling a child
+ * node at a line.
+ */
+public final class RubyProcBeforeLineProbe extends RubyLineProbe {
+
+    private final RubyProc proc;
+
+    public RubyProcBeforeLineProbe(RubyContext context, SourceLineLocation location, RubyProc proc) {
+        super(context, location, false);
+        this.proc = proc;
+    }
+
+    @Override
+    public void enter(Node astNode, VirtualFrame frame) {
+        proc.call(frame.pack());
+    }
+
+    public RubyProc getProc() {
+        return proc;
+    }
+
+}
--- a/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyProcBeforeProbe.java	Wed Jan 08 14:00:21 2014 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-/*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
- * code is released under a tri EPL/GPL/LGPL license. You can use it,
- * redistribute it and/or modify it under the terms of the:
- *
- * Eclipse Public License version 1.0
- * GNU General Public License version 2
- * GNU Lesser General Public License version 2.1
- */
-package com.oracle.truffle.ruby.runtime.debug;
-
-import com.oracle.truffle.api.frame.*;
-import com.oracle.truffle.api.nodes.*;
-import com.oracle.truffle.ruby.runtime.*;
-import com.oracle.truffle.ruby.runtime.core.*;
-
-/**
- * A probe for instrumenting a Ruby program with a Ruby procedure to run before a call.
- */
-public final class RubyProcBeforeProbe extends RubyProbe {
-
-    private final RubyProc proc;
-
-    public RubyProcBeforeProbe(RubyContext context, RubyProc proc) {
-        super(context);
-        this.proc = proc;
-    }
-
-    @Override
-    public void enter(Node astNode, VirtualFrame frame) {
-        proc.call(frame.pack());
-    }
-
-}
--- a/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyTraceProbe.java	Wed Jan 08 14:00:21 2014 -0800
+++ b/graal/com.oracle.truffle.ruby.runtime/src/com/oracle/truffle/ruby/runtime/debug/RubyTraceProbe.java	Wed Jan 08 14:03:36 2014 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. This
+ * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
  * code is released under a tri EPL/GPL/LGPL license. You can use it,
  * redistribute it and/or modify it under the terms of the:
  *
@@ -26,7 +26,7 @@
     @CompilerDirectives.CompilationFinal private boolean tracingEverEnabled = false;
 
     public RubyTraceProbe(RubyContext context) {
-        super(context);
+        super(context, false);
         this.notTracingAssumption = context.getTraceManager().getNotTracingAssumption();
     }