連鎖性言語を作る4

色々と調べていたらRubyでForthを作った人のブログ(Forthを作るのじゃ | TECHSCORE BLOG)があった。この記事では作っているのがForthなのでQuotationは無く、シンプルな作りによりParserも無いため、コロン定義は例のコンパイルモード/実行モード切り替え方式により実装されている。他の部分は妙に似た雰囲気になっているので比較対象としては丁度いいかもしれない。誰が作ってもこんな感じになるのな?

ということで今回はループにチャレンジしようと思ったのだが、ループするなら脱出条件の設定が必要で、そのためには条件分岐が必要で、条件分岐を作るためには比較用ワードが必要で、比較するならブール値が必要ということで、まずはブール値をどうするかを考えるところから。先なげーな?

ブール値

Rubyにはtrue、falseというオブジェクトがあるがオレ言語には無い。これを実装する方法として、例えばtrue、falseという名前のワードを定義して、その実装としてスタックにtrue、falseオブジェクトを積む、というのはどうだろう、と考えたのだが、こうするとunit-testするのにtrue、falseオブジェクトをQuotationに入れる手段が無くなってしまう。つまりリテラルとしてtrue、falseが必要だと言うことなので、Parserで"true"、"false"とついでに"nil"をそれぞれのオブジェクトに変換してASTに格納するようにしよう。
Parserに以下を追加する。

      elsif t == "true"
        ast << true
      elsif t == "false"
        ast << false
      elsif t == "nil"
        ast << nil

比較

とりあえず==だけ作ってみよう。以下をVMのワードハッシュに追加する。

      :"==" => ->{a=@stack.pop;b=@stack.pop;@stack << (b == a)},

Forth系言語では比較は=だが、Rubyの上に構築しているので==にしておく。代入の構文も無いので=の使い道は無いのだが。

[ true ] [ 1 1 == ] unit-test
[ false ] [ 1 2 == ] unit-test

条件分岐

Forthの条件分岐はIF〜ELSE〜THENというワードで処理するが、実行時処理ではなくコンパイルモードで処理されて内部命令に分解される。Factorでは以下のような形でifワードで処理する。

bool [ 真の処理 ] [ 偽の処理 ] if

正直どっちも読みにくい。が、もっと良い書き方もとりあえず思いつかないのでFactor風に作ってみよう。
以下をVMのワードハッシュに追加する。

      :if => ->{e=@stack.pop;t=@stack.pop;b=@stack.pop;self.run(b ? t : e)},

これで以下のようなテストが通るようになる。

[ 1 ] [ true [ 1 ] [ 2 ] if ] unit-test
[ 2 ] [ false [ 1 ] [ 2 ] if ] unit-test
[ 2 ] [ nil [ 1 ] [ 2 ] if ] unit-test
[ 1 ] [ 5 [ 1 ] [ 2 ] if ] unit-test
[ 1 ] [ "a" [ 1 ] [ 2 ] if ] unit-test

nilはfalse、5はtrue、"a"もtrueということで、Rubyと同様の判定がされるようになった。

ループ

条件分岐ができたのでループが作れる。
VMにこんなコードを追加する。

      :loop => ->{quot=@stack.pop;loop{self.run(quot)}},

ただしこれでは無限ループしてしまうので、breakも作る。

      :break => ->{break},

するとこのようなコードが

0 [ 1 + dup 5 == [ break ] [ dup print ] if ] loop

動かない。無限ループしてしまう。breakはlambdaを抜けるだけなのである。
そこでVMのコードをこのように変更する。

  def run(ast)
    nbreak = ast.each do |d|
      if Symbol === d
        break if d == :break
        @words[d].call
      else
        @stack << d
      end
    end
    break if nbreak == nil
  end

しかしこれは動かない以前にコンパイルエラーになる。breakがメソッド内のトップレベルにいると「ループ内じゃねーよ」ってRubyに怒られるのだな。

挫折

ということでloopしてbreakという作戦は失敗に終わった。ま、ループを実装する手段はいくらでもあるわけだが、これが一番簡単そうだったという話で、もうちょいめんどい手を考えればどうにでも作れるはずである。しばらく考えてみる。