注意
実用性は皆無で勉強用です。私メモ用の記事になります。
経緯
mruby等をみてて、言語を作るのってどうしてるんだろうってのに興味を持ったので、さわりだけ理解してみたいと想い、字句解析器、構文解析器を作ってみようと思い立ちました。mrubyはbisonを使っていますが、c言語ベースなので私には敷居が高いツールでした。raccはrubyベースのyaccのようなものですので、まだ敷居が低いかなと。
参考資料
mruby (on github): 言わずと知れたrubyの組み込み言語。mrbgemsとしてmruby-compilarというのがあり、その中に.yファイルがあります。
streem (on github): matzが開発している新しい思想のプログラミング言語。.yファイルが比較的小さいので、基礎的な構文の理解の助けにもなるのではと思って参考にしました。
Raccの使い方
Ruby: Racc
プチ言語仕様
commandという名前の関数を指定できる。指定した引数を表示するのみ。
- 引数内で指定される数値は整数として生成される。
- 文字列はダブルクオテーションをつけるつけない関係なく文字列として処理される。
- 括弧をつけると、バイナリ文字列として生成される。
- コメントは一行コメントのみ。#を先頭につけることでそれ以降行の終端までコメントとなる。
command.txt
# aaa
command(1,any_val1,3,4,5,
"aa bb cc",
(ee ff),
1
)
# bbb
command(2,any_val2,1,1,1,1,
"aa bb cc dd",
(ee ff),
1
)
出力
command {
1
"any_comment1"
3
4
5
"aa bb cc dd"
"\xEE\xFF"
1
}
command {
1
"any_comment2"
1
1
1
1
"aa bb cc dd"
"\xEE\xFF"
1
}
*.y
command.rb
に command関数の定義を記述してあり、*.yファイル内のstmt で command.rb をロードして、command関数に引数を渡しているだけです。
lex.rb.y
class MyLexParser
rule
all_stmt : stmt stmt
| stmt
stmt : cmd left args right
{
load("#{val[0]}.rb")
self.send(val[0].to_sym, val[2])
}
cmd : IDENT
number : DEC { result = val[0].to_i }
left : '('
right : ')'
ids : ids IDENT { result = val[0]+val[1] }
| IDENT
args : args ',' args { result = [val[0], val[2]].flatten }
| left ids right { result = [val[1]].pack("H*") }
| number { result = [val[0]] }
| STRING { result = [val[0]] }
| IDENT { result = [val[0]] }
end
---- header
require 'pp'
require 'strscan'
---- inner
def parse(str)
s = StringScanner.new(str)
@q = []
until s.eos?
s.scan(/#[^\n]*\n/) ? nil :
s.scan(/\d+/) ? @q << [:DEC, s.matched] :
s.scan(/\s+/) ? nil :
s.scan(/,/) ? @q << [',', s.matched] :
s.scan(/\"([^\\\"]|\\.)*\"/) ? @q << [:STRING, s.matched[1..-2]] :
s.scan(/[a-zA-Z0-9\-\_]+/) ? @q << [:IDENT, s.matched] :
s.scan(/\(/) ? @q << ['(', s.matched] :
s.scan(/\)/) ? @q << [')', s.matched] :
(raise "scanner error")
end
do_parse
end
def next_token
@q.shift
end
---- footer
if __FILE__ == $0
parser = MyLexParser.new
begin
parser.parse(File.read("command.txt"))
rescue Racc::ParseError => e
$stderr.puts e
end
end
command.rb
def command(vals)
puts "command {"
vals.each do |line|
if line.class == String
puts " " + line.inspect
else
puts " #{line}"
end
end
puts "}"
end
実行
$ racc -g -o out.rb lex.rb.y
$ ruby out.rb