Repository: ruby/debug (Ruby)

Issue:
Existing from nested breakpoint doesn't resume to the correct frame
**Your environment**

* `ruby -v`: `3.1.0`
* `rdbg -v`: `dc93ef3ad7863a1bcfb34eb1bde78199fce9c504`

**Describe the bug**

#558 allows users to enter nested breakpoints. But after exiting the inner sub session, the debugger still stay at the inner frame.

**To Reproduce**

Script:

```rb
class Foo
  def self.bar(num)
    num
  end
end

binding.b
```

Command:

```
$ exe/rdbg -e 'b Foo.bar ;; c ;; bt ;; Foo.bar(10) ;; bt ;; c ;; bt' target.rb
```

Output:

```
❯ exe/rdbg -e 'b Foo.bar ;; c ;; bt ;; Foo.bar(10) ;; bt ;; c ;; bt' target.rb
[1, 7] in target.rb
=>   1| class Foo
     2|   def self.bar(num)
     3|     num
     4|   end
     5| end
     6|
     7| binding.b
=>#0    <main> at target.rb:1
(rdbg:commands) b Foo.bar
uninitialized constant Foo

Foo
^^^
#0  BP - Method (pending)  Foo.bar
(rdbg:commands) c
DEBUGGER:  BP - Method  Foo.bar at target.rb:2 is activated.
[2, 7] in target.rb
     2|   def self.bar(num)
     3|     num
     4|   end
     5| end
     6|
=>   7| binding.b
=>#0    <main> at target.rb:7
(rdbg:commands) bt
=>#0    <main> at target.rb:7
(rdbg:commands) Foo.bar(10)
[1, 7] in target.rb
     1| class Foo
     2|   def self.bar(num)
=>   3|     num
     4|   end
     5| end
     6|
     7| binding.b
=>#0    Foo.bar(num=10) at target.rb:3
  #1    <main> at (rdbg)/target.rb:1
  # and 3 frames (use `bt' command for all frames)

Stop by #0  BP - Method  Foo.bar at target.rb:2
(rdbg:commands) bt
=>#0    Foo.bar(num=10) at target.rb:3
  #1    <main> at (rdbg)/target.rb:1
  #2    [C] Binding#eval at ~/projects/debug/lib/debug/thread_client.rb:373
  #3    TracePoint.allow_reentry at <internal:trace_point>:151
  #4    <main> at target.rb:7
(rdbg:commands) c
10
(rdbg:commands) bt
=>#0    Foo.bar(num=10) at target.rb:3
  #1    <main> at (rdbg)/target.rb:1
  #2    [C] Binding#eval at ~/projects/debug/lib/debug/thread_client.rb:373
  #3    TracePoint.allow_reentry at <internal:trace_point>:151
  #4    <main> at target.rb:7
(rdbg) c    # continue command
```

**Expected behavior**

After exiting from the inner session (`target.rb:3`), the debugger should be back to the previous frame (`target.rb:7`).

**Additional context**

I think this can be solved by storing frames information (`@target_frames` and `@current_frame_index`) in stacks. I will open a PR for that shortly.



Discussion:

Patch:
diff --git a/lib/debug/console.rb b/lib/debug/console.rb
index 8e4dfaa7c..962dc8101 100644
--- a/lib/debug/console.rb
+++ b/lib/debug/console.rb
@@ -30,6 +30,18 @@ def reset(prompt = '', encoding:)
         prepend m
       end if SIGWINCH_SUPPORTED
 
+      def parse_input buff, commands
+        c, rest = get_command buff
+        case
+        when commands.keys.include?(c)
+          :command
+        when !rest && /\A\s*[a-z]*\z/ =~ c
+          nil
+        else
+          :ruby
+        end
+      end
+
       def readline_setup prompt
         load_history_if_not_loaded
         commands = DEBUGGER__.commands
@@ -38,7 +50,17 @@ def readline_setup prompt
         prev_output_modifier_proc = Reline.output_modifier_proc
         prev_prompt_proc = Reline.prompt_proc
 
-        Reline.prompt_proc = nil
+        # prompt state
+        state = nil # :command, :ruby, nil (unknown)
+
+        Reline.prompt_proc = -> args, *kw do
+          case state = parse_input(args.first, commands)
+          when nil, :command
+            [prompt, prompt]
+          when :ruby
+            [prompt.sub('rdbg'){colorize('ruby', [:RED])}] * 2
+          end
+        end
 
         Reline.completion_proc = -> given do
           buff = Reline.line_buffer
@@ -59,10 +81,9 @@ def readline_setup prompt
         Reline.output_modifier_proc = -> buff, **kw do
           c, rest = get_command buff
 
-          case
-          when commands.keys.include?(c = c.strip)
-            # [:DIM, :CYAN, :BLUE, :CLEAR, :UNDERLINE, :REVERSE, :RED, :GREEN, :MAGENTA, :BOLD, :YELLOW]
-            cmd = colorize(c.strip, [:CYAN, :UNDERLINE])
+          case state
+          when :command
+            cmd = colorize(c, [:CYAN, :UNDERLINE])
 
             if commands[c] == c
               rprompt = colorize("    # command", [:DIM])
@@ -70,12 +91,12 @@ def readline_setup prompt
               rprompt = colorize("    # #{commands[c]} command", [:DIM])
             end
 
-            rest = (rest ? colorize_code(rest) : '') + rprompt
-            cmd + rest
-          when !rest && /\A\s*[a-z]*\z/ =~ c
+            rest = rest ? colorize_code(rest) : ''
+            cmd + rest + rprompt
+          when nil
             buff
-          else
-            colorize_code(buff.chomp) + colorize("    # ruby", [:DIM])
+          when :ruby
+            colorize_code(buff.chomp)
           end
         end
 
@@ -90,9 +111,9 @@ def readline_setup prompt
       private def get_command line
         case line.chomp
         when /\A(\s*[a-z]+)(\s.*)?\z$/
-          return $1, $2
+          return $1.strip, $2
         else
-          line.chomp
+          line.strip
         end
       end
 
diff --git a/lib/debug/thread_client.rb b/lib/debug/thread_client.rb
index 123d952c4..344abe244 100644
--- a/lib/debug/thread_client.rb
+++ b/lib/debug/thread_client.rb
@@ -359,6 +359,9 @@ def tp_allow_reentry
     end
 
     def frame_eval_core src, b
+      saved_target_frames = @target_frames
+      saved_current_frame_index = @current_frame_index
+
       if b
         f, _l = b.source_location
 
@@ -372,6 +375,9 @@ def frame_eval_core src, b
           frame_self.instance_eval(src)
         end
       end
+    ensure
+      @target_frames = saved_target_frames
+      @current_frame_index = saved_current_frame_index
     end
 
     SPECIAL_LOCAL_VARS = [


Test patch:
diff --git a/test/debug/nested_break_test.rb b/test/debug/nested_break_test.rb
index dcd044043..6c6b86df1 100644
--- a/test/debug/nested_break_test.rb
+++ b/test/debug/nested_break_test.rb
@@ -9,18 +9,24 @@ def program
        1| def foo a
        2|   b = a + 1  # break
        3| end
-       4| x = 1        # break
-       5| x = 2
-       6| x = 3
+       4| def bar
+       5|   x = 1        # break
+       6| end
+       7| bar
+       8| x = 2
       RUBY
     end
 
     def test_nested_break
       debug_code program do
         type 'break 2'
-        type 'break 4'
+        type 'break 5'
         type 'c'
-        assert_line_num 4
+        assert_line_num 5
+
+        type 'up'
+        assert_line_text(/=>\#1/)
+
         type 'p foo(42)'
 
         if TracePoint.respond_to? :allow_reentry
@@ -29,9 +35,44 @@ def test_nested_break
           type 'p a'
           assert_line_text(/42/)
           type 'c'
+          assert_line_num 7
         end
 
         # pop nested break
+        assert_line_text(/43/)
+
+        type 'bt'
+        assert_line_text(/=>\#1/)
+
+        type 'c'
+      end
+    end
+
+    def test_multiple_nested_break
+      debug_code program do
+        type 'break 2'
+        type 'break 5'
+        type 'c'
+        assert_line_num 5
+
+        type 'p foo(42)'
+
+        if TracePoint.respond_to? :allow_reentry
+          # nested break
+          assert_line_num 2
+          type 'p foo(142)'
+          type 'bt'
+          assert_line_text(/\#9/) # TODO: can be changed
+
+          type 'c'
+          assert_line_text(/143/)
+
+          type 'bt'
+          assert_no_line_text(/\#9/)
+
+          type 'c'
+        end
+
         assert_line_text(/43/)
         type 'c'
       end