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

时间:2017-03-09 13:53:08

标签: ruby debugging binding byebug

看到这个简单的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中 - 它已经知道行bar中定义的局部变量#10会影响方法{{1并且调试器实际上不再能够调用bar方法。此时不知道的是字符串bar将分配给'local string'。调试器返回bar的{​​{1}}。

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

nil

在第bar行中的运行时,Ruby仍然知道 (byebug) continue method "string from method" local-variable "local string" 确实是一种方法,它仍然可以在行#7中调用它。然后l ine bar实际定义了使用相同名称隐藏方法的局部变量,因此Ruby返回行#8#10中的预期。

问题:为什么调试器返回的值与原始代码不同?看起来它能够展望未来。这被视为功能还是错误?这种行为是否记录在案?

1 个答案:

答案 0 :(得分:3)

每当您进入调试会话时,您都会有效地对代码中该位置的绑定执行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指令,一个本地表,它存储有关方法的参数和方法中所有局部变量的信息,和一个捕获表,其中包含有关该方法中救援的信息(如果存在)。

这个问题的根本原因是,由于您在运行时使用eval动态编译代码,因此Ruby也会将包含未设置变量enry的本地表传递给eval。

首先,让我们使用一种非常简单的方法来演示我们期望的行为。

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,并将它存储在索引[2]的本地表中。如果我们有其他局部变量和参数,它们也会出现在此表中。

接下来,当我们尝试在分配foo之前调用时生成指令:

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

让我们剖析一下这里发生的事情。 Ruby根据以下模式编译YARV的函数调用:

  • 推送接收器:putself,指的是顶级功能范围
  • 推送参数:none here
  • 调用方法/函数:函数调用(FCALL)到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手头有完整的源代码时,它会知道本地人的定义时间,并将过早调用局部变量视为FCALL,就像你期望的那样。

现在让我们来看看行为不端的行为&#34;使用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的locals表中看到一个局部变量bar。我们还有以下eval指令:

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

让我们剖析一下这里发生的事情:

  • 推送接收器:再次putself,指的是顶级功能范围
  • 推送参数:&#34; bar&#34;
  • 调用方法/函数:函数调用(FCALL)到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是动态评估的,并且其代码的指令不会生成直到运行时,因此评估发生在已经确定的指令和本地表的上下文中,该表保存了幻像{{1}你看到了。不幸的是,在这个阶段,Ruby没有意识到bar被初始化&#34;低于&#34;评估声明。

如果想进行更深入的潜水,我建议您阅读Ruby Under a Microscope和评估的Ruby Hacking Guide's部分。