Repository navigation
Conversation
This PR makes a change to the thread client to set the `_` local variable to the result of the last evaluation similar to the behavior in irb[^1]. The reason `_` doesn't work even when using the `irb` console is because we leave the `irb` evaluation early if the input should be handled by the debugger[^2]. Otherwise, we would end up in IRB::Context#evaluate` and we would call `set_last_value`[^3]. There's a bit of a quirk here when using the `irb` console. I was hoping in `thread_client` to evaluate the result then set the `_` local variable and the `@last_value` ivar all in the same method, similar to how `irb` does it[^1]. However, because we're using the `irb` console we use a new `irb` `Workspace` which nils out `_` when initialized[^4]. To get around that, we set the local variable just before evaluating. [^1]:(https://github.com/ruby/irb/blob/d43c3d764ae439706aa1b26a3ec299cc45eaed5b/lib/irb/context.rb#L460-L465) [^2]:(https://github.com/ruby/irb/blob/d43c3d764ae439706aa1b26a3ec299cc45eaed5b/lib/irb.rb#L195-L198) [^3]:(https://github.com/ruby/irb/blob/d43c3d764ae439706aa1b26a3ec299cc45eaed5b/lib/irb/context.rb#L550-L558) [^4]:(https://github.com/ruby/irb/blob/d43c3d764ae439706aa1b26a3ec299cc45eaed5b/lib/irb/workspace.rb#L81) This PR was created by Zendesk's Ruby infrastructure team: Co-authored-by: Benjamin Quorning <[email protected]> Co-authored-by: Edyta Rozczypała <[email protected]> Co-authored-by: Jury Razumau <[email protected]> Co-authored-by: Leonid Batizhevskii <[email protected]> Co-authored-by: Luis Manotas <[email protected]> Co-authored-by: Thomas Countz <[email protected]>
eval_binding is a dup of the frame binding, and a dup shares the frame's locals, so setting `_` when the debuggee already has one overwrote the program's own variable (and it stayed overwritten after `continue`). Only inject `_` when the original frame binding doesn't define it. Also only update the last value for user-driven evaluations (p, pp, call) so `display` expressions and other internal evals don't replace `_`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This resolves #1069. It builds on #1159 by @HeyNonster and the Zendesk Ruby infrastructure team. Their commit is cherry-picked as-is, with authorship and co-authors kept, and a follow-up commit addresses the review question there about what happens when
_already exists.Changes on top of #1159
Don't clobber the debuggee's
_.eval_bindingisframe.binding.dup, and a duplicated binding shares the frame's locals. Solocal_variable_set(:_, ...)overwrites a_the program already has, and the program keeps that value aftercontinue:Setting a name that doesn't exist yet only creates it on the dup, so that case is safe. Now
_is injected only when the originalframe.bindingdoesn't define it (checkingeval_bindingwould see the debugger's own_after the first eval). When the program has its own_, it wins. This is the same idea as_return/_raisedbeing renamed to%return/%raisedwhen the program uses those names.Only user evaluations update
_. Previously everyframe_evalupdated the last value. That includesdisplayexpressions (which run at every stop),info ivars <expr>,ls <expr>, and DAP/CDP evaluations. Now only thep,ppandcalleval paths update it. Plain REPL input goes throughpp.Tests
test_display_does_not_update_underscore_localtest_underscore_defined_by_program_is_not_clobbered: checks both what the debugger shows and what the program prints aftercontinue.rake test_console(310 tests) andrake test_protocol(64 tests) pass locally on Ruby 4.0.9.IRB console mode
With
irb_consoleenabled, a program-defined_is still reset tonil. That reset comes from IRB, not from this change:IRB::WorkSpace#initializeruns@binding.local_variable_set(:_, nil), and IRB's debugger integration creates a newWorkSpaceon the frame's binding at every prompt (Irb#debug_readline), which overwrites the existing variable.That's fixed separately in ruby/irb#1266. With that branch on the load path, a debugger test for IRB console mode (a program-defined
_keeps its value across prompts and aftercontinue) passes, along with the fulltest_console(311 tests) andtest_protocol(64 tests) suites. That test isn't included here yet because it fails against released IRB. I can add it once ruby/irb#1266 is merged and released, or now if you'd rather it land alongside a version requirement.🤖 Generated with Claude Code