連鎖性言語を作る

スタック指向言語、とか、連鎖性言語、などと呼ばれる言語がある。有名どころではForthやPostScriptあたりで、かなり古くから存在していて、最近ではFactorといった関数オブジェクトを扱える実用的なものまである。
基本的なコードの例としては、

1 2 + print

て感じで、文字を空白で区切って記述する。1と2をスタックに積んで、+を呼び出すと+はスタックから2個取り出して加算結果を積んで、スタックの中身は3になり、それを表示する、と。パッと見、逆ポーランド記法的な書き方になっているが、実際そうで、

1 2 + 3 4 + * print

これで21が表示される。1と2を足して3と4を足して掛ける、と日本語表現と相性が良いので日本語プログラミング言語のMindとかなでしこも見た目は日本語だが実体は連鎖性言語である。
さて、最近「なんか言語作ってみたいなあー」などとぼんやり思っていたのだが、難しいことはできそうにないので簡単に作れそうな連鎖性言語を作ってみよう。

字句解析

何が簡単かって、字句解析、構文解析がほとんど必要無いというあたり。空白で区切って順番に処理すればよい。作るのが簡単なぶん読み書きが辛くなるのはそういうもんだろう。

token = []
DATA.readlines.each do |t|
  token.concat t.split(" ")
end
p token
__END__
1 2 +
3 4 +
*
print

実行結果は["1", "2", "+", "3", "4", "+", "*", "print"]となる。

構文解析

全部文字列ではあとあと面倒なので、処理しやすいようにそれぞれ変換する。Forth系言語では数字や文字列はリテラル、それ以外はワードとして扱う。ワードは基本的には関数だが、特殊な処理をするためのものもある。
今回は数字と文字列(""でくくられたもの)と、ワードを区別して配列に格納することにする。リテラルはそのままRubyのオブジェクトへ変換、ワードはシンボルに変換して持っておこう。

class Parser
  def parse(token)
    ast = []
    token.each do |t|
      if t =~ /^\d+$/
        ast << t.to_i
      elsif t =~ /^".*"$/
        ast << t[1..-2]
      else
        ast << t.to_sym
      end
    end
    ast
  end
end

token = []
DATA.readlines.each do |t|
  token.concat t.split
end
p Parser.new.parse(token)
__END__
1 2 +
3 4 +
*
print

実行結果は[1, 2, :+, 3, 4, :+, :*, :print]。

VMを作る

VMと言っても難しいものではなくて、渡されたASTを前から順番に処理(リテラルはスタックに積む、ワードは実行)するだけ。ベタで書いてしまおう。

class Parser
  def parse(token)
    ast = []
    token.each do |t|
      if t =~ /^\d+$/
        ast << t.to_i
      elsif t =~ /^".*"$/
        ast << t[1..-2]
      else
        ast << t.to_sym
      end
    end
    ast
  end
end

token = []
DATA.readlines.each do |t|
  token.concat t.split
end
ast = Parser.new.parse(token)

class VM
  def run(ast)
    stack = []
    ast.each do |d|
      if Symbol === d
        case d
        when :+
          stack << (stack.pop + stack.pop)
        when :*
          stack << (stack.pop * stack.pop)
        when :print
          puts stack.pop
        end
      else
        stack << d
      end
    end
  end
end

VM.new.run(ast)

__END__
1 2 +
3 4 +
*
print

これで実行結果は21。できた。簡単すぎてこれだけでは面白く無い(手抜きすぎなだけだが)。
Forthでもワードを自分で定義できるし、Factorにはクオーテーションという関数オブジェクトもあるので、今後はそのへんを追加してみよう。条件分岐も繰り返しもできないのでそのへんも欲しい。