Ruby 1.9.0のバイトコードをいじり倒す(その6)

いろいろなところで、Ruby 1.8.xと1.9.0の非互換性が話題になっていますが、バイトコードをいじくってちょっと乗り越えてみようという*冗談*です。
今回まな板にあげる非互換性は、ブロックローカル変数です。
たとえば、

a = 12
0.upto(1) do |n|
  a = a + 20
end
p a

といったプログラムは、ruby 1.8.6と1.9.0でこのように異なりますが、

d:\work\ruby>c:/cygwin/usr/local/bin/ruby -v
c:/cygwin/usr/local/bin/ruby -v
ruby 1.9.0 (2007-12-28 revision 0) [i386-cygwin]

d:\work\ruby>c:/cygwin/usr/local/bin/ruby sample.rb
c:/cygwin/usr/local/bin/ruby sample.rb
12


d:\work\ruby>ruby -v
ruby -v
ruby 1.8.6 (2007-03-13 patchlevel 0) [i386-cygwin]

d:\work\ruby>ruby sample.rb
ruby sample.rb
21

local.rbを咬ませて、バイトコードにパッチを当てると、1.8.6と同じ挙動を示します。

d:\work\ruby>c:/cygwin/usr/local/bin/ruby local.rb sample.rb
c:/cygwin/usr/local/bin/ruby local.rb sample.rb
21

種を明かすと、こんな感じのコードのパッチを当てています。

  1. ブロックの終わりに$__ブロック変数名というグローバル変数にブロック変数の値を代入する
  2. そのブロックを呼び出したsendの後に、1で代入したグローバル変数を再び、同名のブロック変数かローカル変数に代入しなおす

こんな感じで、非互換性が解決できるんじゃないでしょうか・・・、なんて嘘です。そんなに甘くないですね。

とりあえず、このプログラムはこの場合は動きます。
local.rbです。

require 'instruction'
require 'pp'

include VMLib
iseq = VM::InstructionSequence.compile_file(ARGV[0])
iseqt = InstSeqTree.new(nil, iseq)

iseqt.add_code_all_before_return {|fname, info, no, code|
  lambda {|level|
    res = []
    if code.header['type'] == :block then
      code.header['locals'].each_with_index do |nm, i|
        res.push [:getdynamic, i + 1, 0]
        res.push [:setglobal, ":$___#{nm}".intern]
      end
    end
    res
  }
}

iseqt.add_code_all_around_send {|fname, info, no, code|
  lambda {|mname, sendno, inst|
    bl = code.blockes[sendno]
    res = [inst]
    if bl then
      cv = code.header['locals']
      if code.header['type'] == :block then
        bl.header['locals'].each do |lv|
          if cv.include?(lv) then
            res.push [:getglobal, ":$___#{lv}".intern]
            res.push [:setdynamic, cv.index(lv) + 1, 0]
          end  
        end
      else
        bl.header['locals'].each do |lv|
          if cv.include?(lv) then
            res.push [:getglobal, ":$___#{lv}".intern]
            res.push [:setlocal, cv.index(lv) + 2]
          end  
        end
      end
    end
    res
  }
}

VM::InstructionSequence.load(iseqt.to_a).eval

instruction.rbはこの前載せたものよりずいぶんでっかくなっています(340行くらい)。ちょっと載せるのに躊躇してますので、もし試してみたいという奇特な人がいたらコメントください。