yarv2llvmにはマクロがあります

追記
具体例を追加しました
テストが通るようになりました。マクロではなく従来の処理部分のバグがテストに漏れて残っていました。


 yarv2llvmにとても不完全ですがマクロが入りました。でも、エンバグして他のテストが通らなくなりました(最近こんなのばっかり)。githubのradical_threadブランチにアップしています。
こんなプログラムが動きます。ちょうど、Common Lispのdefmacroみたいな感じです。プログラムのテンプレートにもバッククオートが使えました(ラッキーです)。データ埋め込みは同じ文法(,と,@)にするとRubyではめちゃめちゃ煩雑なので#{}にしました。  

YARV2LLVM::define_macro :myif do || 
   `if #{para[:args][2]} then #{para[:args][1]} else #{para[:args][0]} end` 
end

def tdefine_macro
  myif(true, p("hello"), p("world"))
end

パラメータの受け渡しがめちゃめちゃ適当ですが、para[:args][n]でn番目の引数を得ることができます。内部データ構造をそのまま使っているので数字の順番が逆です。:argsは引数ですが、そのほかにもいろいろメタな情報を渡せるようにすると面白いかなと思います。もっとちゃんとしたインタフェースにする予定です。

やってることがかなり面倒です。次の階層のRubyプログラムが勢ぞろいします。

  • yarv2llvm本体
  • yarv2llvmでコンパイルするソースプログラム
  • yarv2llvmマクロの定義
  • yarv2llvmマクロの定義をYARVに変換したコードをRubyに変換したもの
  • yarv2llvmマクロの定義をYARVに変換したコードをRubyに変換したものを実行した結果得られたマクロの展開結果

自分でもすぐに忘れてしまいそうなので概要をメモっておきます。

  • define_macroのコンパイル処理で第1引数(メソッド名)と渡ってきたブロックを記録しておく
  • ブロックはYARVの形で渡ってくるのでこれを再びRubyに戻す。YARVレベルで実行しないのはYARVインタプリタを書くよりRubyに変換する処理を書いたほうが楽なことと、いろいろ細工をしたいからです。
    • 細工の例としてバッククオートの処理の変換があります
    • 通常、バッククオートはデータ埋め込み部分を文字に変換してそれを全部つなげて1つの文字列にして、メソッド:`に渡すという処理をしています。でも、この処理ではマクロ機能は実現できない(たとえば、文字列化できないデータなんか(procオブジェクトとか)は渡せない)ので、都合がよいようにYARVからRubyへの変換処理で細工しています。
    • 具体的には、データ埋め込みで直接データを文字列化するのではなく、仮のラベルを振っておいて、そのラベルと対応するデータのハッシュテーブルも生成します。そして、マクロを実行することで生成されたRubyプログラムをコンパイルするとき、ハッシュテーブルも渡してやります。これで、文字列化できないデータもマクロで渡してやることができます。
  • 続く。。。

myifを例に取ると、

YARV2LLVM::define_macro :myif do |arg| 
  `if #{para[:args][2]} then #{para[:args][1]} else #{para[:args][0]} end` 
end

とmyifが定義された場合、

myif(true, p("hello"), p("world"))

は、YARVバイトコードを経て次のようなRubyプログラムに変換されます。

__state = nil
while true
  case __state
  when nil
__state = :label_0
when :label_0
    __state = :label_61
when :label_61

break (compile_for_macro("if  gEN0  then  gEN1  else  gEN2  end", {:gEN0 => lambda { |pa|
  @expstack.push [((para[:args])[2])[0],
    lambda {|b, context|
      context = ((para[:args])[2])[1].call(b, context)
      context
  }]
},:gEN1 => lambda { |pa|
  @expstack.push [((para[:args])[1])[0],
    lambda {|b, context|
      context = ((para[:args])[1])[1].call(b, context)
      context
  }]
},:gEN2 => lambda { |pa|
  @expstack.push [((para[:args])[0])[0],
    lambda {|b, context|
      context = ((para[:args])[0])[1].call(b, context)
      context
  }]
},}, para))
    break 
  end
end

compile_for_macroは、yarv2llvmのメソッドで引数のRubyのプログラムをYARVを経てLLVMに変換します。第2引数にメソッド名とそのメソッドがどうインライン展開されるかのハッシュテーブルを渡します。
この生成されたRubyコードをevalで評価して、出てきたLLVMをそのまま生成コードにくっつけることでマクロを実現しています。
最終的にはこんな感じのビットコードになります。

bb1:		; preds = %bb
	br i1 true, label %bb2, label %bb3

bb2:		; preds = %bb1
	%5 = call i32 @rb_str_new_cstr(i8* getelementptr ([6 x i8]* @0, i32 0, i32 0))		; <i32> [#uses=1]
	call void @rb_p(i32 %5)
	br label %bb4

bb3:		; preds = %bb1
	%6 = call i32 @rb_str_new_cstr(i8* getelementptr ([6 x i8]* @1, i32 0, i32 0))		; <i32> [#uses=1]
	call void @rb_p(i32 %6)
	br label %bb4

bb4:		; preds = %bb3, %bb2
	%7 = phi i8* [ getelementptr ([6 x i8]* @1, i32 0, i32 0), %bb3 ], [ getelementptr ([6 x i8]* @0, i32 0, i32 0), %bb2 ]		; <i8*> [#uses=2]