diff --git a/lib/debug/thread_client.rb b/lib/debug/thread_client.rb index 0a92fe356..3bb33a8c8 100644 --- a/lib/debug/thread_client.rb +++ b/lib/debug/thread_client.rb @@ -434,16 +434,24 @@ def frame_eval_core src, b, binding_location: false [:return_value, "_return"], ] - def frame_eval src, re_raise: false, binding_location: false + def frame_eval src, re_raise: false, binding_location: false, update_last_value: false @success_last_eval = false - b = current_frame&.eval_binding || TOPLEVEL_BINDING + frame = current_frame + b = frame&.eval_binding || TOPLEVEL_BINDING - special_local_variables current_frame do |name, var| + special_local_variables frame do |name, var| b.local_variable_set(name, var) if /\%/ !~ name end + # eval_binding is a dup that shares the frame's locals, so setting an + # existing `_` would overwrite the debuggee's own variable. + if frame && !(frame.binding || TOPLEVEL_BINDING).local_variable_defined?(:_) + b.local_variable_set(:_, @last_value) + end + result = frame_eval_core(src, b, binding_location: binding_location) + @last_value = result if update_last_value @success_last_eval = true result @@ -1045,19 +1053,19 @@ def wait_next_action_ case eval_type when :p - result = frame_eval(eval_src) + result = frame_eval(eval_src, update_last_value: true) puts "=> " + color_pp(result, 2 ** 30) if alloc_path = ObjectSpace.allocation_sourcefile(result) puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}" end when :pp - result = frame_eval(eval_src) + result = frame_eval(eval_src, update_last_value: true) puts color_pp(result, SESSION.width) if alloc_path = ObjectSpace.allocation_sourcefile(result) puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}" end when :call - result = frame_eval(eval_src) + result = frame_eval(eval_src, update_last_value: true) when :display, :try_display failed_results = [] eval_src.each_with_index{|src, i| diff --git a/test/console/debugger_local_test.rb b/test/console/debugger_local_test.rb index f32f43d0c..dbb806ac0 100644 --- a/test/console/debugger_local_test.rb +++ b/test/console/debugger_local_test.rb @@ -19,6 +19,53 @@ def test_locals_added_in_locals_are_accessible_between_evaluations type "c" end end + + def test_evaluated_result_is_set_as_underscore_local + debug_code(program) do + type "_" + assert_line_text(/nil/) + type "3 * 3" + type "foo = _" + type "9 == foo" + assert_line_text(/true/) + type "c" + end + end + + def test_display_does_not_update_underscore_local + debug_code(program) do + type "display 42" + type "3 * 3" + type "display" + type "_" + assert_line_text(/9/) + type "c" + end + end + end + + class UnderscoreDefinedByProgramTest < ConsoleTestCase + def program + <<~RUBY + 1| _ = :mine + 2| binding.b + 3| p _ + 4| binding.b + 5| nil + RUBY + end + + def test_underscore_defined_by_program_is_not_clobbered + debug_code(program) do + type "c" + type "3 * 3" + type "_" + assert_line_text(/:mine/) + type "c" + assert_line_text(/:mine/) + type "c" + end + end end class RaisedTest < ConsoleTestCase diff --git a/test/console/irb_test.rb b/test/console/irb_test.rb index 746f9acdb..de4290050 100644 --- a/test/console/irb_test.rb +++ b/test/console/irb_test.rb @@ -84,6 +84,20 @@ def test_irb_console_config_activates_irb ENV["RUBY_DEBUG_IRB_CONSOLE"] = nil end + def test_irb_console_evaluated_result_is_set_as_underscore_local + debug_code(program, remote: false) do + type 'irb' + type "_" + assert_line_text(/nil/) + type "3 * 3" + assert_raw_line_text 'irb:rdbg(main):003> 3 * 3' + type "foo = _" + type "9 == foo" + assert_line_text(/true/) + type "c" + end + end + private # assert_line_text ignores the prompt line, so we can't use it to assert the prompt transition