Skip to content

Set evaluation result to _ local variable - #1159

Open
HeyNonster wants to merge 1 commit into
ruby:masterfrom
HeyNonster:nony/underscore-last-result
Open

HeyNonster wants to merge 1 commit into
ruby:masterfrom
HeyNonster:nony/underscore-last-result

Conversation

@HeyNonster

@HeyNonster HeyNonster commented Oct 15, 2025 •

Copy link
Copy Markdown
Contributor

This resolves #1069.

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 irb1.

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 debugger2. Otherwise, we would end up in IRB::Context#evaluate and we would call set_last_value3.

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 it1. However, because we're using the irb console we use a new irb Workspace which nils out _ when initialized4. To get around that, we set the local variable just before evaluating.

Footnotes

  1. (https://github.com/ruby/irb/blob/d43c3d764ae439706aa1b26a3ec299cc45eaed5b/lib/irb/context.rb#L460-L465) ↩ ↩2

  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 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]>
@cloudbees-smart-tests

cloudbees-smart-tests Bot commented Oct 15, 2025 •

Copy link
Copy Markdown

❌ 1/689 Tests Failed

/home/runner/work/debug/debug/test/protocol/catch_raw_dap_test.rb#test_catching_any_exception_works_correctly
-------------------------
| All Protocol Messages |
-------------------------

V>D {"seq":1,"command":"initialize","arguments":{"clientID":"vscode","clientName":"Visual Studio Code","adapterID":"rdbg","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us","supportsProgressReporting":true,"supportsInvalidatedEvent":true,"supportsMemoryReferences":true},"type":"request"}
V>D {"seq":2,"command":"attach","arguments":{"type":"rdbg","name":"Attach with rdbg","request":"attach","rdbgPath":"/home/runner/work/debug/debug/exe/rdbg","debugPort":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/ruby-debug-sock-501/ruby-debug-naotto-8845","autoAttach":true,"__sessionId":"141d9c79-3669-43ec-ac1f-e62598c5a65a"},"type":"request"}
V>D {"seq":3,"command":"setFunctionBreakpoints","arguments":{"breakpoints":[]},"type":"request"}
V>D {"seq":4,"command":"setExceptionBreakpoints","arguments":{"filters":[],"filterOptions":[{"filterId":"RuntimeError"}]},"type":"request"}
V>D {"seq":5,"command":"configurationDone","type":"request"}
V<D {"type":"response","command":"initialize","request_seq":1,"success":true,"message":"Success","body":{"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":true,"supportsConditionalBreakpoints":true,"supportTerminateDebuggee":true,"supportsTerminateRequest":true,"exceptionBreakpointFilters":[{"filter":"any","label":"rescue any exception","supportsCondition":true},{"filter":"RuntimeError","label":"rescue RuntimeError","supportsCondition":true}],"supportsExceptionFilterOptions":true,"supportsStepBack":true,"supportsEvaluateForHovers":true,"supportsCompletionsRequest":true},"seq":1}
V<D {"type":"event","event":"initialized","seq":2}
V<D {"type":"event","event":"output","body":{"category":"console","output":"Ruby REPL: You can run any Ruby expression here.\nNote that output to the STDOUT/ERR printed on the TERMINAL.\n[experimental]\n  `,COMMAND` runs `COMMAND` debug command (ex: `,info`).\n  `,help` to list all debug commands.\n"},"seq":3}
V<D {"type":"response","command":"attach","request_seq":2,"success":true,"message":"Success","seq":4}
V<D {"type":"response","command":"setFunctionBreakpoints","request_seq":3,"success":true,"message":"Success","seq":5}
V<D {"type":"response","command":"setExceptionBreakpoints","request_seq":4,"success":true,"message":"Success","body":{"breakpoints":[{"verified":true,"message":"#<DEBUGGER__::CatchBreakpoint:0x00007f71f6f45420 @pat=\"RuntimeError\", @key=[:catch, \"RuntimeError\"], @last_exc=nil, @deleted=false, @cond=nil, @command=nil, @path=nil, @tp=#<TracePoint:enabled>>"}]},"seq":6}
V<D {"type":"response","command":"configurationDone","request_seq":5,"success":true,"message":"Success","seq":7}
V<D {"type":"event","event":"stopped","body":{"reason":"pause","threadId":1,"allThreadsStopped":true},"seq":8}
V>D {"seq":6,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":6,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20251015-2727-y0wy3o.rb:1:in '<main>'"}]},"seq":9}
V>D {"seq":7,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":7,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20251015-2727-y0wy3o.rb:1:in '<main>'"}]},"seq":10}
V>D {"seq":8,"command":"stackTrace","arguments":{"threadId":1,"startFrame":0,"levels":20},"type":"request"}
V<D {"type":"response","command":"stackTrace","request_seq":8,"success":true,"message":"Success","body":{"stackFrames":[{"id":1,"name":"<main>","line":1,"column":1,"source":{"name":"debug-20251015-2727-y0wy3o.rb","path":"/tmp/debug-20251015-2727-y0wy3o.rb","sourceReference":0}}],"totalFrames":1},"seq":11}
V>D {"seq":9,"command":"scopes","arguments":{"frameId":1},"type":"request"}
V<D {"type":"response","command":"scopes","request_seq":9,"success":true,"message":"Success","body":{"scopes":[{"name":"Local variables","presentationHint":"locals","namedVariables":0,"indexedVariables":0,"expensive":false,"variablesReference":2},{"name":"Global variables","presentationHint":"globals","variablesReference":1,"namedVariables":41,"indexedVariables":0,"expensive":false}]},"seq":12}
V>D {"seq":10,"command":"variables","arguments":{"variablesReference":2},"type":"request"}
V<D {"type":"response","command":"variables","request_seq":10,"success":true,"message":"Success","body":{"variables":[{"name":"%self","value":"main","type":"Object","variablesReference":3,"indexedVariables":0,"namedVariables":1}]},"seq":13}
V>D {"seq":11,"command":"setExceptionBreakpoints","arguments":{"filters":[],"filterOptions":[{"filterId":"any"},{"filterId":"RuntimeError"}]},"type":"request"}
V<D {"type":"response","command":"setExceptionBreakpoints","request_seq":11,"success":true,"message":"Success","body":{"breakpoints":[{"verified":true,"message":"#<DEBUGGER__::CatchBreakpoint:0x00007f71f6f41ee0 @pat=\"Exception\", @key=[:catch, \"Exception\"], @last_exc=nil, @deleted=false, @cond=nil, @command=nil, @path=nil, @tp=#<TracePoint:enabled>>"},{"verified":true,"message":"#<DEBUGGER__::CatchBreakpoint:0x00007f71f6f41e20 @pat=\"RuntimeError\", @key=[:catch, \"RuntimeError\"], @last_exc=nil, @deleted=false, @cond=nil, @command=nil, @path=nil, @tp=#<TracePoint:enabled>>"}]},"seq":14}
V>D {"seq":12,"command":"continue","arguments":{"threadId":1},"type":"request"}
V<D {"type":"response","command":"continue","request_seq":12,"success":true,"message":"Success","body":{"allThreadsContinued":true},"seq":15}
V<D {"type":"event","event":"stopped","body":{"reason":"exception","description":"#<ZeroDivisionError: divided by 0> is raised.","text":"#<ZeroDivisionError: divided by 0> is raised.","threadId":1,"allThreadsStopped":true},"seq":16}
V>D {"seq":13,"command":"threads","type":"request"}
V<D {"type":"response","command":"threads","request_seq":13,"success":true,"message":"Success","body":{"threads":[{"id":1,"name":"#1 /tmp/debug-20251015-2727-y0wy3o.rb:1:in '<main>'"}]},"seq":17}
V>D {"seq":14,"command":"stackTrace","arguments":{"threadId":1,"startFrame":0,"levels":20},"type":"request"}
V<D {"type":"response","command":"stackTrace","request_seq":14,"success":true,"message":"Success","body":{"stackFrames":[{"id":2,"name":"Foo::Bar.a","line":4,"column":1,"source":{"name":"debug-20251015-2727-y0wy3o.rb","path":"/tmp/debug-20251015-2727-y0wy3o.rb","sourceReference":0}},{"id":3,"name":"<module:Foo>","line":7,"column":1,"source":{"name":"debug-20251015-2727-y0wy3o.rb","path":"/tmp/debug-20251015-2727-y0wy3o.rb","sourceReference":0}},{"id":4,"name":"<main>","line":1,"column":1,"source":{"name":"debug-20251015-2727-y0wy3o.rb","path":"/tmp/debug-20251015-2727-y0wy3o.rb","sourceReference":0}}],"totalFrames":4},"seq":18}

--------------------------
| Last Protocol Messages |
--------------------------

{
  "seq": 13,
  "command": "threads",
  "type": "request"
}
{
  "type": "response",
  "command": "threads",
  "request_seq": 13,
  "success": true,
  "message": "Success",
  "body": {
    "threads": [
      {
        "id": 1,
        "name": "#1 /tmp/debug-20251015-2727-y0wy3o.rb:1:in '<main>'"
      }
    ]
  },
  "seq": 17
}
{
  "seq": 14,
  "command": "stackTrace",
  "arguments": {
    "threadId": 1,
    "startFrame": 0,
    "levels": 20
  },
  "type": "request"
}
{
  "type": "response",
  "command": "stackTrace",
  "request_seq": 14,
  "success": true,
  "message": "Success",
  "body": {
    "stackFrames": [
      {
        "id": 2,
        "name": "Foo::Bar.a",
        "line": 4,
        "column": 1,
        "source": {
          "name": "debug-20251015-2727-y0wy3o.rb",
          "path": "/tmp/debug-20251015-2727-y0wy3o.rb",
          "sourceReference": 0
        }
      },
      {
        "id": 3,
        "name": "<module:Foo>",
        "line": 7,
        "column": 1,
        "source": {
          "name": "debug-20251015-2727-y0wy3o.rb",
          "path": "/tmp/debug-20251015-2727-y0wy3o.rb",
          "sourceReference": 0
        }
      },
      {
        "id": 4,
        "name": "<main>",
        "line": 1,
        "column": 1,
        "source": {
          "name": "debug-20251015-2727-y0wy3o.rb",
          "path": "/tmp/debug-20251015-2727-y0wy3o.rb",
          "sourceReference": 0
        }
      }
    ],
    "totalFrames": 4
  },
  "seq": 18
}

--------------------
| Debuggee Session |
--------------------

> DEBUGGER: Debugger can attach via UNIX domain socket (/run/user/1001/rdbg-2727-17)
> DEBUGGER: wait for debugger connection...
> DEBUGGER: Connected.


-------------------
| Failure Message |
-------------------

expected:
{
  "type": "response",
  "command": "stackTrace",
  "request_seq": 14,
  "success": true,
  "message": "Success",
  "body": {
    "stackFrames": [
      {
        "name": "[C] Integer#/",
        "line": 4,
        "column": 1,
        "source": {
          "name": "(?-mix:debug-20251015-2727-y0wy3o.rb)",
          "path": "(?-mix:\\/tmp\\/debug-20251015-2727-y0wy3o.rb)",
          "sourceReference": 0
        },
        "id": 2
      },
      {
        "name": "Foo::Bar.a",
        "line": 4,
        "column": 1,
        "source": {
          "name": "(?-mix:debug-20251015-2727-y0wy3o.rb)",
          "path": "(?-mix:\\/tmp\\/debug-20251015-2727-y0wy3o.rb)",
          "sourceReference": 0
        },
        "id": 3
      },
      {
        "name": "<module:Foo>",
        "line": 7,
        "column": 1,
        "source": {
          "name": "(?-mix:debug-20251015-2727-y0wy3o.rb)",
          "path": "(?-mix:\\/tmp\\/debug-20251015-2727-y0wy3o.rb)",
          "sourceReference": 0
        },
        "id": 4
      },
      {
        "name": "<main>",
        "line": 1,
        "column": 1,
        "source": {
          "name": "(?-mix:debug-20251015-2727-y0wy3o.rb)",
          "path": "(?-mix:\\/tmp\\/debug-20251015-2727-y0wy3o.rb)",
          "sourceReference": 0
        },
        "id": 5
      }
    ]
  }
}

result:
{
  "type": "response",
  "command": "stackTrace",
  "request_seq": 14,
  "success": true,
  "message": "Success",
  "body": {
    "stackFrames": [
      {
        "id": 2,
        "name": "Foo::Bar.a",
        "line": 4,
        "column": 1,
        "source": {
          "name": "debug-20251015-2727-y0wy3o.rb",
          "path": "/tmp/debug-20251015-2727-y0wy3o.rb",
          "sourceReference": 0
        }
      },
      {
        "id": 3,
        "name": "<module:Foo>",
        "line": 7,
        "column": 1,
        "source": {
          "name": "debug-20251015-2727-y0wy3o.rb",
          "path": "/tmp/debug-20251015-2727-y0wy3o.rb",
          "sourceReference": 0
        }
      },
      {
        "id": 4,
        "name": "<main>",
        "line": 1,
        "column": 1,
        "source": {
          "name": "debug-20251015-2727-y0wy3o.rb",
          "path": "/tmp/debug-20251015-2727-y0wy3o.rb",
          "sourceReference": 0
        }
      }
    ],
    "totalFrames": 4
  },
  "seq": 18
}.
<"[C] Integer#/"> expected but was
<"Foo::Bar.a">.

[-> View Test suite health in main branch]

@ko1

ko1 commented Dec 5, 2025

Copy link
Copy Markdown
Collaborator

how to handle _ if it is already available?

@HeyNonster

Copy link
Copy Markdown
Contributor Author

how to handle _ if it is already available?

@ko1 We can try to make this not clobber an existing _ local variable but would it be confusing if debug and irb had different behavior in that regard?

As an example:

irb

irb(main):001> _ = "foo"
=> "foo"
irb(main):002> "bar"
=> "bar"
irb(main):003> _
=> "bar"
irb(main):004> def _;"baz";end
=> :_
irb(main):006> "bar"
=> "bar"
irb(main):007> _
=> "bar"

@joe-sharp

Copy link
Copy Markdown

Hi! I filed the original issue for this, #1069 (#1069), along with the IRB side of it, ruby/irb#891 (ruby/irb#891), where _ stops working after entering debug. Thanks for picking it up!

On @ko1's question about an existing _: I think the debugger should leave it alone, even though IRB overwrites it. The two situations differ. In plain irb, _ lives in IRB's own workspace, so overwriting it doesn't affect anything. In the debugger, eval_binding is frame.binding.dup, and the dup shares the frame's locals. So setting _ writes into the program being debugged, and the program keeps the debugger's value after continue:

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

_ is a common name for throwaway values (a, _ = pair, each { |_, v| ... }), so this could quietly change program behavior during a debug session.

I opened #1188, which builds on this PR (your commit is cherry-picked with authorship and co-authors kept) and adds:

  • _ is injected only when the original frame binding doesn't define it, so the program's own _ wins. That's the same idea as _return/_raised being renamed to %return/%raised when the program uses those names.
  • Only user evaluations (p, pp, call) update _, so display expressions and other internal evals don't replace it.

Happy to close #1188 and have those changes folded into this PR instead, whichever is easier for everyone.

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

3 participants