defmacroをrubyで実現するメモ

defmacroをrubyで実現できそうなので、メモを書いておきます。3行書いてこんがらがってきたのでプログラムはまだです。この日記を元に、時間があるときにrubyに落としたいと思います。

  1. 構文はdefmacro :foo {|arg| .... }でfoo(arg)とすると、ブロックを評価する。評価結果は文字列かシンボル等の配列でevalで評価できるrubyのプログラムである。defmacroはメソッドである。
  2. マクロ名の呼び出し(1の例ではfoo(arg))となっているところのsend命令を探し、そこにArray#newメソッド (複数の引数を配列にまとめるため)とマクロ実行部分(__macroexec)の呼出し命令に置き換える。__macroexeには引数が増えて、現在のbindingオブジェクトとdefmacroで渡されたブロックのprocオブジェクトが必要になるので、それらもYARVのスタックに積む命令を追加する。
  3. 2のバイトコードのパッチ当てを行うためにはdefmacroメソッドをパッチ当ての前に実行しておく必要がある。そして、マクロ名とbody部のprocオブジェクトをHash等で覚えておく。
  4. __macroexecの中でマクロ置き換えに相当することを行う。これは簡単でこんな感じ
  def __macroexec(body, bind, args)
      eval(body.call(args), bind)
  end

プログラム書けるのはいつかなー、この後仕事です。

追記

この方法では毎回マクロ展開が起こって激遅でした。次のようにしたほうが良いと思います。

  1. バイトコードパッチ時にマクロのブロックを評価する
  2. 評価したrubyプログラムをVM::Compileを使ってコンパイルする
  3. send命令回りをそのコンパイルしたコードで置き換える

面倒なのは1.でブロックを評価するときにはまだマクロを呼び出したコードの環境(binding)がないことと、3.でsend命令をマクロのbodyで置き換えるとき、引数の評価も削除する必要があるということです。

3.の引数の評価の削除は、send命令の引数の数で示されるスタックレベルになるまでバイトコード命令を逆にたどればいいのではないかと思います。

1.のブロック評価時に環境がないという問題は、かなり面倒です。いっそのことマクロの呼び出し元の変数は一切アクセスできないという仕様にしてもそこそこ実用的だとは思います。でも、Ruby版「On Lisp」を実現しようとすると致命的です。ローカル変数の変数領域上でのオフセットはRubyレベルで決定できるのでRubyコンパイラRubyで書くつもりになれば出来るんじゃないかなと思います。

追記(2/25)

わけの分からない文章を晒してしまい済みません。自分用のメモとご理解ください。

1.のブロック評価時に環境がないというのは次のようにダミーのメソッド定義を付加すればよいと思います。
ローカル変数はVM::InstructionSequence.compile(...).to_aで作られた配列表現に変数の一覧があるので、それを元にダミーの関数定義を周りにつけます。
例えば、

  defmacro :inc {|a| "#{a} = #{a} + 1"}

  inc(:var)

と呼び出した場合を考えます。そうすると、

  var = var + 1

というコードをインライン展開するわけですが、そのとき実際には、ダミーのローカル変数のブロックを加えて次のようなコードをコンパイルします。

def dummy(引数列)
  他の変数があればそれも初期化する
  var = nil
  {
     var = var + 1
  }
end

実際に挿入するのはvar = var + 1だけですが、環境を作ってもらうためにダミーコードを付加します。