連鎖性言語を作る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