Skip to content

Set evaluation result to _ without clobbering the debuggee's _ - #1188

Open
joe-sharp wants to merge 2 commits into
ruby:masterfrom
joe-sharp:underscore-last-value
Open

joe-sharp wants to merge 2 commits into
ruby:masterfrom
joe-sharp:underscore-last-value

Conversation

@joe-sharp

@joe-sharp joe-sharp commented Nov 2, 2026 •

Copy link
Copy Markdown

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_binding is frame.binding.dup, and a duplicated binding shares the frame's locals. So local_variable_set(:_, ...) overwrites a _ the program already has, and the program keeps that value after continue:

def m; _ = 1; b = binding.dup; b.local_variable_set(:_, 99); p _; end; m
# => 99

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 original frame.binding doesn't define it (checking eval_binding would see the debugger's own _ after the first eval). When the program has its own _, it wins. This is the same idea as _return/_raised being renamed to %return/%raised when the program uses those names.

Only user evaluations update _. Previously every frame_eval updated the last value. That includes display expressions (which run at every stop), info ivars <expr>, ls <expr>, and DAP/CDP evaluations. Now only the p, pp and call eval paths update it. Plain REPL input goes through pp.

Tests

  • test_display_does_not_update_underscore_local
  • test_underscore_defined_by_program_is_not_clobbered: checks both what the debugger shows and what the program prints after continue.

rake test_console (310 tests) and rake test_protocol (64 tests) pass locally on Ruby 4.0.9.

IRB console mode

With irb_console enabled, a program-defined _ is still reset to nil. That reset comes from IRB, not from this change: IRB::WorkSpace#initialize runs @binding.local_variable_set(:_, nil), and IRB's debugger integration creates a new WorkSpace on 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 after continue) passes, along with the full test_console (311 tests) and test_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

HeyNonster and others added 2 commits November 2, 2026 01:13
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat/Bug: Set the value of _ (underscore) to the value of the last ruby expression

2 participants