Create Your Own Programming Language
以前から言語を作る事に興味がわいていたのですが、中々難しそうに見えて尻込みして挑戦出来ていませんでした。それが、昨日急に言語を作りたい衝動にかられて wikipedia 巡りをしてしまいました。最近盛り上がってきつつあり、Rails にも採用された 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