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]