Create Your Own Programming Language

以前から言語を作る事に興味がわいていたのですが、中々難しそうに見えて尻込みして挑戦出来ていませんでした。それが、昨日急に言語を作りたい衝動にかられて wikipedia 巡りをしてしまいました。最近盛り上がってきつつあり、Rails にも採用された CoffeeScript は自分の中でも感動した言語だったので当然 wikipedia を参照したわけです。

CoffeeScript - wikipedia

CoffeeScript電子書籍である "Create Your Own Language" を読んで開発された。

上記引用の通り、電子書籍の Create Your Own Programming Language を読んで開発されたらしく、勢い勇んで洋書に手を出しました。もう勢いです。

以下のリンクから購入出来ます。
Create Your Own Programming Language

ページを開いていきなり "あなたが考えているより簡単に初めての言語を作れる" なんて謳い文句までついてくる始末。
英語も幸いにしてあまり難しくない表現で書かれているので僕程度の英語力でもなんとかなっています。問題は単語ですが、そこは iPad 様々ですかね。

全部で 160 ページくらいの薄い物なんで抵抗もあまり無く、(とは言っても未だ 30 ページ程度ですが。)読めているので
この本はすぐに最後まで読み切れそうです。

現在は Lexer を Ruby で書く部分です。今回の記事ではそのソースコードの解読をしておこうと思います。

lexer.rb

class Lexer
  # KEYWORDS では 言語内で用いる予約語を定義しているようです
  # TOY 言語に必要な機能のみを実装する事を目的にしているのであまり予約語は無いです
  KEYWORDS = ["def", "class", "if", "true", "false", "nil"]

  # tokenize ではトークンの生成を行なっています
  # code はプログラムのコードを受け取ります
  def tokenize(code)
    # 改行コード等 code の最後の特殊なコードの削除
    code.chomp!

    i = 0
    tokens = []
    current_indent = 0
    indent_stack = []

    while i < code.size
      # i の挙動を見る為に僕が仕込んだコードです
      # 本来必要の無いコード
      p i

      # 残っている code の抜き出し
      chunk = code[i..-1]

      # トークンの生成
      # String class の slice で regexp に最初にマッチする文字の抜き出し
      if identifier = chunk[/\A([a-z]\w*)/, 1]
        if KEYWORDS.include?(identifier)
          tokens << [identifier.upcase.to_sym, identifier]
        else
          tokens << [:IDENTIFIER, identifier]
        end
        # identifier のサイズ分だけ文字探査を進める
        i += identifier.size

      elsif constant = chunk[/\A([A-Z]\w*)/, 1]
        tokens << [:CONSTANT, constant]
        i += constant.size
        
      elsif number = chunk[/\A([0-9]+)/, 1]
        tokens << [:NUMBER, number.to_i]
        i += number.size

      elsif string = chunk[/\A"(.*?)"/, 1]
        tokens << [:STRING, string]
        i += string.size + 2

      # : 以降の indent サイズのチェック(if, def等)
      elsif indent = chunk[/\A\:\n( +)/m, 1]
        if indent.size <= current_indent
          raise "Bad indent level, got #{indent.size} indents." +
            "expected > #{current_indent}"
        end
        current_indent = indent.size
        indent_stack.push(current_indent)
        tokens << [:INDENT, indent.size]
        i += indent.size + 2

      # indent サイズのチェック
      elsif indent = chunk[/\A\n( *)/m, 1]
        if indent.size == current_indent
          tokens << [:NEWLINE, "\n"]
        elsif indent.size < current_indent
          while indent.size < current_indent
            indent_stack.pop
            current_indent = indent_stack.first || 0
            tokens << [:DEDENT, indent.size]
          end
          tokens << [:NEWLINE, "\n"]
        else
          raise "Missing"
        end
        i += indent.size + 1
      elsif operator = chunk[/\A(\|\||&&|==|!=|<=|>=)/, 1]
        tokens << [operator, operator]
        i += operator.size
      elsif chunk.match(/\A /)
        i += 1
      else
        value = chunk[0, 1]
        tokens << [value, value]
        i += 1
      end
    end

    while indent = indent_stack.pop
      tokens << [:DEDENT, indent_stack.first || 0]
    end

    tokens
  end
end

コメントアウトで説明を入れては見ましたが、以外とコードを見れば分かる内容 && メンド(ry なので後半はあまりコメント入れてません。

lexer_test.rb

require "test/unit"
require "lexer"

class LexerTest < Test::Unit::TestCase
  def setup
    @code = <<-CODE
if 1:
  print "..."
  if false:
    pass
  print "done!"
print "The End"
    CODE

    @tokens = [
               [:IF, "if"], [:NUMBER, 1],
               [:INDENT, 2], [:IDENTIFIER, "print"], [:STRING, "..."],
               [:NEWLINE, "\n"], [:IF, "if"], [:FALSE, "false"], [:INDENT, 4],
               [:IDENTIFIER, "pass"],
               [:DEDENT, 2], [:NEWLINE, "\n"], [:IDENTIFIER, "print"],
               [:STRING, "done!"],
               [:DEDENT, 0], [:NEWLINE, "\n"],
               [:IDENTIFIER, "print"], [:STRING, "The End"]
              ]
  end
  
  def test_tokenize
    assert_equal @tokens, Lexer.new.tokenize(@code)
  end
end

上記テストコードを以下の様に実行すると、
>|
$ ruby test/lexer_test.rb