pry-byebug: binding.pry doesn't fire when it's on the last line of a program

Failing case:

gem install pry-byebug

# test.rb
require 'pry'
binding.pry

Test: ruby test.rb. No pry shell opens.

Passing case: gem uninstall pry-byebug

Or, add a line with assignment. Oddly enough, ending lines of comments or simply an ending line of 1 doesn’t workaround this.

# test.rb
require 'pry'
binding.pry
a = 1

Test: ruby test.rb. The pry shell opens.

The workaround is pretty simple. Ever since moving to ruby 2 and getting rid of pry-nav, I’ve just got in the habit of doing this. Logging an issue in case this is unexpected. 🍰

About this issue

  • Original URL
  • State: open
  • Created 10 years ago
  • Reactions: 7
  • Comments: 34 (20 by maintainers)

Commits related to this issue

Most upvoted comments

(╯°□°)╯︵ ┻━┻

Hi @JoshCheek, thanks for your feedback. I still feel the best resolution for this issue would be to make pry-byebug stop being a pry plugin and make it into an independent tool with a separate entry point other than binding.pry.

But I’m of course I’m open to smarter, better thought-out implementations of byebug that make it a better tool, regardless of this issue with pry-byebug.

Yeah, that makes sense. An alternative would be to leave it as a plugin, but one which you explicitly enter, eg a debug command to turn it on, and until then, it just delegates down to pry. Another option would be a different method, eg pry.debug so that the user could opt into the functionality of their preference.

Really, the pain I’m trying to solve is that one of my projects has it as a dependency, so it gets installed with that project, and I didn’t really realize that. So, then when pry autoloads it, and it changes pry’s behaviour, the line numbers are off, so I thought pry was buggy for ~6 months, and finally sat down last night to report it / try to fix it. Anything which addresses this unintentional loading and changing is sufficient, IMO.

Do note that I may be an exception as I often work without bundler, allowing this situation to occur.

FWIW, I got it to behave the way I would expect (didn’t test much, just playing around):

diff --git a/lib/byebug/processors/pry_processor.rb b/lib/byebug/processors/pry_processor.rb
index 57a4ee1..2d3ae78 100644
--- a/lib/byebug/processors/pry_processor.rb
+++ b/lib/byebug/processors/pry_processor.rb
@@ -11,11 +11,23 @@ module Byebug
     def_delegators :@pry, :output
     def_delegators Pry::Helpers::Text, :bold

-    def self.start
+    def self.start(target=nil)
       Byebug.start
       Setting[:autolist] = false
-      Context.processor = self
-      Byebug.current_context.step_out(4, true)
+
+      # hack because byebug instantiates our class, so this lets us hang onto
+      # the binding until we have an instance to use it on
+      initializer = lambda do |*args, &b|
+        processor = new(*args, &b)
+        processor.enq_binding target if target.kind_of? ::Binding
+        processor
+      end
+      class << initializer
+        alias new call
+      end
+      Context.processor = initializer
+
+      Byebug.current_context.step_out(3, true)
     end

     #
@@ -37,6 +49,22 @@ module Byebug
       return_value
     end

+    def enq_binding(bnd)
+      enqueued_bindings << bnd
+    end
+
+    private def enqueued_bindings
+      @enqueued_bindings ||= []
+    end
+
+    private def new_binding
+      if enqueued_bindings.any?
+        enqueued_bindings.shift
+      else
+        frame._binding
+      end
+    end
+
     #
     # Set up a number of navigational commands to be performed by Byebug.
     #
@@ -106,8 +134,6 @@ module Byebug
     # Resume an existing Pry REPL at the paused point.
     #
     def resume_pry
-      new_binding = frame._binding
-
       run do
         if defined?(@pry) && @pry
           @pry.repl(new_binding)
diff --git a/lib/pry-byebug/pry_ext.rb b/lib/pry-byebug/pry_ext.rb
index abfdd96..583eefe 100644
--- a/lib/pry-byebug/pry_ext.rb
+++ b/lib/pry-byebug/pry_ext.rb
@@ -5,7 +5,7 @@ class << Pry

   def start_with_pry_byebug(target = TOPLEVEL_BINDING, options = {})
     if target.is_a?(Binding) && PryByebug.file_context?(target)
-      Byebug::PryProcessor.start unless ENV["DISABLE_PRY"]
+      Byebug::PryProcessor.start target unless ENV["DISABLE_PRY"]
     else
       # No need for the tracer unless we have a file context to step through
       start_without_pry_byebug(target, options)

After looking at it for a while and reading a lot of the byebug source, I finally realized that we mostly just needed to pass the supplied binding to pry on the first invocation, and then after that, it can behave the way it currently does.


It turned out to be a lot harder of a problem than I was would expecting, because byebug seems to mostly just be a wrapper around the tracepoint API. So all it can really do is hook into tracepoint events. Because we start pry-byebug after the call to binding.pry, there are no more events on that line.

Eg consider this program:

def a
  set_trace_func proc { |event, _file, lineno, methodname, binding, classname|
    desc = sprintf "%8s ", event
    desc << methodname.to_s if methodname
    printf "%-25s %2d: %s", desc, lineno, File.readlines(__FILE__)[lineno-1]
  }
end

def b
  123
end

a
b

When we run it, we will see the tracepoint events. It prints:

c-return set_trace_func    2:   set_trace_func proc { |event, _file, lineno, methodname, binding, classname|
  return a                 7: end
    line                  14: b
    call b                 9: def b
    line b                10:   123
  return b                11: end

So, we start on line 2, with a return from the C-function set_trace_func, then move to the end on line 7, where we return from a. If we then moved to line 13, the location we are returning to, this would be easy, but tracepoint skips that and goes to the next event: moving to line 14.

Frankly, it doesn’t seem very thought-out. Eg replace the line that says b with b;b;b, since both the call and the return pass the method’s binding, there doesn’t seem to be a way to get access to the caller between the invocations. Whenever I’ve played w/ this API in the past, I always had to track everything and re-construct the missing events/data to present a coherent picture. I’m not sure why byebug doesn’t do that.


Also, here’s another example that I hit often, where the current implementation doesn’t work (I use big case statements when iterating over things like ASTs):

object = 123

case object
when Integer
  puts 'its a number!'
  require "pry"
  binding.pry
when String
  puts 'its a string!'
when Regexp
  puts 'its a regex!'
end

I’ve been recently considering to actually change the command to enter pry-byebug instead of reusing binding.pry. That way the user would be able to choose whether (s)he wants step-by-step debugging or just a plain REPL (like in this case, since step-by-step debugging doesn’t make a lot of sense from the last line of a program).

Would that be a satisfactory resolution for this issue?

@squarism Thanks for the feedback. I have that vim snippet too 😃