連鎖性言語を作る6

リテインスタックを作ろう。これは通常のデータスタックとは別のスタックで、一時保存用のものとなる。作るのは非常に簡単で、VMインスタンス変数を追加して、データスタックとのやりとりをする>r、r>を追加する。

    @retain_stack = []
      :">r" => ->{@retain_stack << @stack.pop},
      :"r>" => ->{@stack << @retain_stack.pop},
[ 1 ] [ 1 2 >r ] unit-test
[ 1 2 ] [ 1 2 >r >r r> r> ] unit-test

うん、簡単だった。これを使うとrotなどのワードがオレ言語で定義できるようになる。

: rot ( x1 x2 x3 -- x2 x3 x1 ) >r swap r> swap ;
: -rot ( x1 x2 x3 -- x3 x1 x2 ) swap >r swap r> ;

rot系はスタックに積んだデータが3つになった時に重宝する。ついでにまだ作ってなかった基本的なワードをオレ言語で実装しておく。

: nip ( x1 x2 -- x2 ) swap drop ;
: tuck ( x1 x2 -- x2 x1 x2 ) swap over ;
: 2dup ( x1 x2 -- x1 x2 x1 x2 ) over over ;
: 2drop ( x1 x2 -- ) drop drop ;
: 2swap ( x1 x2 x3 x4 -- x3 x4 x1 x2 ) >r -rot r> -rot ;
: 2over ( x1 x2 x3 x4 -- x1 x2 x3 x4 x1 x2 ) >r >r 2dup r> -rot r> -rot ;

ということで今回はRubyのメソッドを呼び出す機能にチャレンジする。

シンボル

Rubyのシンボルをデータスタックに格納する機能が欲しい。問題はAST上では文字列は文字列を格納、シンボルはワードを実行、という機能が既に割り当たってしまっているので、シンボルを扱う機能がそのままでは作ることができないところだ。ちょっと悩んだが、今回は自前のシンボルクラスを作ってそれをASTに置き、そいつの実行時はRubyのシンボルを格納するという動きにすることにする。
自前のクラスはOreSymbol。

class OreSymbol
  attr_reader :sym
  def initialize(name)
    @sym = name[1..-1].to_sym
  end

  def to_sym
    @sum
  end

  def ==(s)
    @sym == s.to_sym
  end
end

":"付きで書かれたワードはOreSymbolオブジェクトに変換してASTに格納される。VMはそれを発見するとRubyのSymbolに変換してデータスタックに積む。
Parserにこれを追加。

      elsif t[0] == ":"
        ast << OreSymbol.new(t)

ただし":"ひとつだけはコロン定義に使われるので、それより後に書かなければならない。んでVMのrunメソッドを書き換える。

  def run(ast)
    ast.each do |d|
      case d
      when Symbol
        @words[d].call
      when OreSymbol
        @stack << d.sym
      else
        @stack << d
      end
      break if @break_flg
    end
  end

これでシンボルが置けるようになる。

[ a ] [ :a ] unit-test
[ :a ] [ :a ] unit-test

どちらもテストは通る。OreSymbolは==でRubyのSymbolを取り出して比較するため、RubyのSymbolとも一致する。これが通るということは一度Quotationに格納した形でスタックに置いて、そこから取り出す方法でもいいということになるが、リテラルで書けたほうがラクなのでこれはこれでよいだろう。

定数取得とメソッド呼び出し

Rubyのメソッドを呼ぶのにレシーバが必要だが、現状では数字と文字列と配列とシンボルしかスタックに格納できないので呼べるものに限りがある。もっと色々なことがしたいのでRubyの定数を取得する機能を作る。

      :"get-const" => ->{c=@stack.pop;@stack << Object.const_get(c)},

シンボルを置いてget-constを呼ぶと定数を取得する機能である。これだけではテストのしようが無いのでsendも作る。

      :send => ->{r=@stack.pop;m=@stack.pop;a=@stack.pop;@stack << r.__send__(m, *a)},

引数をQuotationで置いて、後ろにメソッド名のシンボル、レシーバを配置した状態でsendを呼ぶとメソッドを呼び出すことができる。

[ "1" ] [ [ ] :to_s 1 send ] unit-test

これを組み合わせると例えば新規の配列を作ることができるようになる。

: new-quot [ ] :new :Array get-const send ;
[ [ ] ] [ new-quot ] unit-test

オレ言語ではコード上に[ ]と書いた場合、それはAST上に配置される空配列になるので、これにデータを追加したりするとAST上の配列が変更されてしまう。空配列に何かを追加したい場合はnew-quotを使ってQuotationを生成する、というのがオレ言語の流儀となる。いま決めた。

呼び出しの簡素化

Rubyのメソッドを呼び出すのに引数がいらない場合があり、その場合にも引数のQuotationが必要となると若干面倒なので、0sendという名前で引数の無いメソッド呼び出しを作ろう。これは簡単で、

: 0send [ ] -rot send ;
[ "1" ] [ :to_s 1 0send ] unit-test

という感じになる。ここの[ ]は変更されないのでこれでよい。次に、引数が1個固定の場合用の1sendを作る。いちいちQuotationに入れないといけないのは面倒である。そのためにはまずcurryワードを作る。

      :curry => ->{@stack.last << @stack.delete_at(-2)},
[ [ 1 ] ] [ 1 new-quot curry ] unit-test

うーん、やっぱり[ ]で新配列が作れたほうが見やすい気がする。ぶれぶれ。まあいいか。そのうえで1sendを作る。

: 1send >r >r new-quot curry r> r> send ;
[ [ 1 2 ] ] [ 2 :push 1 new-quot curry 1send ] unit-test

curryはQuotationの前に追加するが、RubyのArray#pushを呼べば後ろに追加することができる。

ブロックを渡す

Rubyのメソッドを呼ぶといえば避けて通れないのがブロック渡しである。これができないと何ができないってDXRubyのWindow.loopが呼べない(それかよ)。RubyのブロックはProcオブジェクトを渡してもいいので、オレ言語の処理であるQuotationを呼び出すProcを生成して渡すような感じで作れる。

      :sendb => ->{r=@stack.pop;m=@stack.pop;tmp=@stack.pop;b=->*x{@stack.concat(x);self.run(tmp);@stack.pop};a=@stack.pop;@stack << r.__send__(m, *a, &b)},

渡すProcは受け取った値をスタックに積んで、Quatationを呼び出して、スタックトップを返す。これで以下のようなコードが動く。

[ ] [ print ] :each [ 1 2 3 ] sendb

この時点で使うスタックが4個もあるので普通に使うには厳しい機能である。何かしらワードを定義して使いやすくしていくのがよさそうだ。例えばこう。

: map ( block receiver -- array ) new-quot -rot :map swap sendb ;
[ [ 2 3 4 ] ] [ [ 1 + ] [ 1 2 3 ] map ] unit-test

できた

Rubyの機能を呼ぶことができるようになった。これでできることはかなり増えたんじゃないかと思う。とはいえまだまだ何かをしようとすると色々と大変なので、追加しないといけない機能はたくさんあるだろう。
なんだか長くなってきたのでここまでのコードはGistに置いておいた。実質100行ちょいのしょぼいものだが興味ある人はどうぞ。