Всякий раз, когда вы переходите в сеанс отладки, вы эффективно выполняете 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 также передает в eval локальную таблицу, которая включает неустановленную переменную enry.
Для начала воспользуемся очень простым методом, демонстрирующим ожидаемое поведение.
def foo_boom
foo # => NameError
foo = 1 # => 1
foo # => 1
end
Мы можем проверить это, извлекая байтовый код YARV для существующего метода с помощью RubyVM::InstructionSequence.disasm(method)
. Примечание. Я собираюсь игнорировать вызовы трассировки, чтобы инструкции были аккуратными.
Вывод для 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 по следующему шаблону:
- Приемник push:
putself
, относится к верхнему уровню функции
- Толкающие аргументы: здесь нет
- Вызвать метод / функцию: вызов функции (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, как и следовало ожидать.
Теперь давайте посмотрим на "некорректную" версию, в которой используется 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)
Мы снова видим локальную переменную bar
в таблице locals с индексом 2. У нас также есть следующие инструкции для eval:
0004 putself
0005 putstring "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop
Давайте разберем, что здесь происходит:
- Нажать приемник: снова
putself
, ссылаясь на верхний уровень функции
- Проталкивать аргументы: "бар"
- Вызов метода / функции: вызов функции (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
оценивается динамически и инструкции для его кода не будут сгенерированы до времени выполнения, оценка происходит в контексте уже определенных инструкций и локальной таблицы, которая содержит фантом bar
, который вы видите. К сожалению, на данном этапе Ruby не знает, что bar
был инициализирован «ниже» оператора eval.
Для более глубокого погружения я бы рекомендовал прочитать Ruby Under a Microscope и раздел "Оценка" Руководства по взлому Ruby.
person
fny
schedule
14.03.2017
byebug
. Если вы заключите его вrescue NoMethodError
иputs defined?(bar)
до и после присвоения, вы увидите, что это"method"
перед этой строкой и"local-variable"
после нее. - person ndnenkov   schedule 09.03.2017"local-variable"
, тогда какp defined?(bar)
перед назначением будет печатать"method"
? - person Stefan   schedule 09.03.2017byebug
, но отладчик Ruby показывает такое же поведение. Перетащите код вtest.rb
(убрав ошибку). Затем запуститеruby -r debug test.rb
. В точке останова проверьте локальные переменные с помощьюv l
, иbar => nil
действительно появится - person gwcodes   schedule 09.03.2017bar
как локальную переменную в начале метода (local_variables
вернет[:bar]
). Но он обрабатывается как таковой только после присваивания, т.е.bar
преобразуется в метод заранее, а затем в локальную переменную. - person Stefan   schedule 09.03.2017byebug
специфично. Воспроизводится также сbinding.irb
из 2.4.0. - person ndnenkov   schedule 09.03.2017binding
. Из документации: Объекты класса Binding инкапсулируют контекст выполнения в определенном месте кода и сохраняют этот контекст для использования в будущем. Переменные, методы, значение self и, возможно, блок итератора, к которому можно получить доступ в этом контексте, все сохраняются. Это кажется не на 100% правильным, потому что ist не является в каком-то конкретном месте в код (привязка знает больше, чем место в коде), а также методы и переменные точно не сохраняются. Следует ли считать это ошибкой? - person spickermann   schedule 10.03.2017binding
, а сeval
, и нет, это не ошибка: просто невероятно сложно предотвратить такое поведение. Код сначала оценивается в байт-код YARV и таблицу локальных переменных, а затем во время выполненияeval()
выполняется в контексте существующего байт-кода и таблицы локальных переменных, в которой хранится ссылка на локальную переменную, которую вы видите. - person fny   schedule 16.03.2017