为什么Ruby调试器在运行时返回与代码不同的值



请参阅此简单的Ruby类:

require 'byebug'
class Foo
  def run
    byebug
    puts defined?(bar)
    puts bar.inspect
    bar = 'local string'
    puts defined?(bar)
    puts bar.inspect
  end
  def bar
    'string from method'
  end
end
Foo.new.run

运行此类时,可以在调试器的控制台中观察到以下行为:

    $ ruby byebug.rb
    [2, 11] in /../test.rb
        2:
        3: class Foo
        4:   def run
        5:     byebug
        6:
    =>  7:     puts defined?(bar)
        8:     puts bar.inspect
        9:
       10:     bar = 'local string'
       11:

在断点处,调试器返回以下值:

    (byebug) defined?(bar)
    "local-variable"
    (byebug) bar.inspect
    "nil"

请注意,尽管调试器的断点位于行#5中 - 它已经知道将在行#10中定义的局部变量bar,它将遮蔽方法bar,而调试器实际上不再可以调用bar方法。目前不知道的是字符串'local string'将分配给bar。调试器返回barnil

让我们继续使用Ruby文件中的原始代码,然后查看其输出:

    (byebug) continue
    method
    "string from method"
    local-variable
    "local string"

在运行时,#7 Ruby仍然知道bar确实是一种方法,并且仍然能够将其称为#8行中。然后,l Ine #10实际上定义了以相同名称阴影的局部变量,因此Ruby像LINE #12#13中的预期一样返回。

问题:为什么调试器返回与原始代码不同的值?似乎它能够展望未来。这被认为是功能还是错误?此行为记录了吗?

每当您插入调试会话时,您都会有效地执行eval对代码中该位置的绑定。这是一个更简单的代码,它重新创建了使您发疯的行为:

def make_head_explode
  puts "== Proof bar isn't defined"
  puts defined?(bar)   # => nil
  puts "== But WTF?! It shows up in eval"
  eval(<<~RUBY)
    puts defined?(bar) # => 'local-variable'
    puts bar.inspect   # => nil
  RUBY
  bar = 1
  puts "n== Proof bar is now defined"
  puts defined?(bar)   # => 'local-variable'
  puts bar.inspect     # => 1
end

当方法make_head_explode被馈送到解释器时,它将其编译为YARV说明,本地表格,该表存储了该方法的参数和所有本地变量的信息,以及该方法中的所有局部变量,以及在方法中包含有关RESISE的信息如果存在。

此问题的根本原因是,由于您在运行时使用eval动态编译代码,因此Ruby通过本地表,其中包括一个不设置变量ENRY,也可以进行评估。

开始,让我们使用一种非常简单的方法来证明我们期望的行为。

def foo_boom
  foo         # => NameError
  foo = 1     # => 1
  foo         # => 1
end

我们可以通过使用RubyVM::InstructionSequence.disasm(method)提取现有方法的YARV字节代码来对其进行检查。请注意,我将忽略跟踪呼叫以保持说明整洁。

RubyVM::InstructionSequence.disasm(method(:foo_boom))的输出更少的跟踪:

== disasm: #<ISeq:foo_boom@(irb)>=======================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] foo
0004 putself
0005 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0008 pop
0011 putobject_OP_INT2FIX_O_1_C_
0012 setlocal_OP__WC__0 2
0016 getlocal_OP__WC__0 2
0020 leave                                                            ( 253)

现在让我们浏览痕迹。

local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] foo

我们可以在这里看到YARV已确定我们具有局部变量foo,并将其存储在我们的index [2]的本地表中。如果我们有其他本地变量和论点,它们也会出现在此表中。

接下来,我们将在分配foo之前生成的说明:

  0004 putself
  0005 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
  0008 pop

让我们剖析这里发生的情况。Ruby Commiles函数根据以下模式要求YARV:

  • 推送接收器:putself,指函数的顶级范围
  • 推荐参数:这里没有
  • 调用方法/函数:函数调用(fcall)to foo

接下来,我们将有指令将foo设置为全局变量:

0008 pop
0011 putobject_OP_INT2FIX_O_1_C_
0012 setlocal_OP__WC__0 2
0016 getlocal_OP__WC__0 2
0020 leave                                                            ( 253)

关键要点:当Yarv拥有整个源代码时,它知道何时定义当地人,并且将过早调用对本地变量作为fcalls,就像您期望的那样。

现在让我们看一个使用eval

的"行为不当"版本
def bar_boom
  eval 'bar'     # => nil, but we'd expect an errror
  bar = 1         # => 1
  bar
end

RubyVM::InstructionSequence.disasm(method(:bar_boom))输出较少的跟踪:

== disasm: #<ISeq:bar_boom@(irb)>=======================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] bar
0004 putself
0005 putstring        "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop
0013 putobject_OP_INT2FIX_O_1_C_
0014 setlocal_OP__WC__0 2
0018 getlocal_OP__WC__0 2
0022 leave                                                            ( 264)

再次,我们在索引2的本地表中看到了本地变量bar

0004 putself
0005 putstring        "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop

让我们解剖此处会发生什么:

  • 推送接收器:再次putself,指函数的顶级范围
  • 推荐参数:" bar"
  • 调用方法/函数:函数调用(fcall)to eval

之后,我们对bar的标准分配。

0013 putobject_OP_INT2FIX_O_1_C_
0014 setlocal_OP__WC__0 2
0018 getlocal_OP__WC__0 2
0022 leave                                                            ( 264)

如果我们在这里没有eval,Ruby会知道将bar的呼叫视为函数调用,这将像我们以前的示例中一样被炸毁。但是,由于对eval进行了动态评估,并且直到运行时才能生成其代码的说明,因此评估发生在已经确定的指令和本地表的上下文中,该指令和本地表中保留了您看到的Phantom bar。不幸的是,在此阶段,Ruby不知道bar是在" eval语句"中初始化的。

要进行更深入的潜水,我建议您在显微镜下阅读Ruby和Ruby Hacking指南的评估部分。

最新更新