146. Racc メモ (2019/12/21)
雑なパー サを眺めながら Racc の使いかたメモ。
class KernelParser
Racc に渡すクラスの宣言。名前はなんでもいい。
prechigh
nonassoc UMINUS
left '*' '/'
left '+' '-'
preclow
この辺は演算子の結合のしかたと優先順位を定義しているらしい。
start innerkernel
これはルールのうちここから構文解析始めるという宣言
rule
ここから構文規則、という宣言。
innerkernel: nsdeclarations iodeclarations statements {result=Kernelprogram.new(val)}
Racc の規則の構文は Yacc 風で
A: B C
| D
と書くと、 A は BとCがその順番に並んだもの、または D となる。 "|" はも
ちろん複数書くこともできる。。上の例は、プログラムトップレベルである
innerkernel は nsdeclarations iodeclarations statements がこの順番に並
んだものである、という意味になる。そのあとのブロック
{result=Kernelprogram.new(val)}
は、この構文規則が適用された時のパーサーの動作になる。 result はこの構
文規則が上位の構文規則なり最終的な解釈結果なりとして返すもの、つまり、
この場合 innerkernel に対応するもので、
この名前でないといけないらしい(その辺ドキュメント存在してない気が)
val は右側でマッチしたものがはいる配列になる。つまり、
val[0] は nsdeclarations に対応する何かになる。
ここはまだ全然意味がわからないところだが、この部分で構文解析した
結果の構文木を作る。このコードの場合、こちらで定義した Kernelprogram
クラスのインスタンスを作って返すことになる。 Kernelprogram は
def initialize(x)
@nsdeclarations, @iodeclarations, @statements =*x
end
というインスタンス作成関数をもつので、単にこれらのメンバー変数に
それぞれが代入される。
nsdeclarations:nsdeclaration
|nsdeclaration nsdeclarations {result = val[0]+val[1]}
は、 nsdeclarations は1つの nsdeclaration か、または
nsdeclaration のあとに nsdeclarations があるものである、ということで、
あんまりわかりやすくないが A が B が1個以上並んだものである、という定
義の時の通常の表現らしい。動作は
{result = val[0]+val[1]}
で、両方が配列と思ってつなげている。 nsdeclaration だけの時に動作が記
述されていないが、この時はデフォルト動作の
{result = val[0]}
がとられる(と思う)
nsdeclaration: nstype varname EOL {result = [Nsdeclaration.new(val)]}
は
epj jparticle
epi iparticle
fi force
の1行を解釈する部分で、FDPS では epi, epj, force という概念で呼ばれる3つの
粒子型派生クラスがあり、その名前を定義する部分の解釈である。 nstype が
(何故 ns なのか憶えてないけど)この3つの派生クラスのどれであるかを示し、
その次はそれにつける名前である。ここでも、動作は
{result = [Nsdeclaration.new(val)]}
で、こちらで定義した Nsdeclaration クラスのインスタンスを返す。ここで1
要素の配列にしているのは、複数行にする処理を上の
{result = val[0]+val[1]}
で書けるようにする、つまり、宣言が1行でも複数行でも同じデータ構造にす
るためである。
nstype : EPINAME
| EPJNAME
| FNAME
の部分は、nstype は何かである。大文字の EPJNAME とかは、終端子(ここで
構文解釈終わる。いわゆるトークン)になる(らしい)。EOL も同様である。また、
varname : TEXT
も同様である。従って、 nsdeclaration は、EPINAMEまたはEPJNAMEまたは
FNAME、TEXT、EOL の
順番に並んだものである、ということになる。
パーサーの入力は何か、というと、これはトークンの配列になる。Racc におけ
るトークンは [:FOO, bar] の形の2要素配列であり、
[:TEXT, "baz"] であれば TEXT トークンでその値が "baz" ということにな
る。プログラムの
epj jparticle
は
[[:EPJNAME, "epi"],[:TEXT, "jparticle"],[:EOL,:EOL]]
みたいなトークン列に変換されてからパーサーに入力されて欲しい、というこ
とになる。これは Racc の機能ではなくて、class KernelParser にこちらで
parse という関数を作って、その中で @q という変数にトークン列をいれる。
1行をトークンに分けるのは、標準ライブラリ Ripper を使って
a=Ripper.tokenize(str.chomp).select{|s| s=~/\S+/}
ですませている。 select しているのは、 Ripper は Ruby 言語自体用のトークナイザな
ので空白もトークンとして返すので、それを消している。
それから do_parse という関数を呼ぶと、 Racc のパーサー生成関数が動くと
いうことになる。この例では、 改行に意味があるプログラムとしていて、
EOL は改行である。なので、トークン作るところは、1行読んで、それを
トークンに分けて、最後に EOL を追加する、という動作になる。
iodeclaration: iotype type varname fdpsname EOL {result = [Iodeclaration.new(val)]}
| iotype type varname EOL {result=[Iodeclaration.new(val[0..2]+[val[2]])]}
は形は nsdeclaration とあまり変わらないが、4要素のものと3要素のもので、
3要素のものは val[3] を val[2] で埋めた配列を Iodeclaration.new に渡す
ことで省略値を設定している。
statement : TEXT "=" expression EOL {result = [Statement.new([nil,val[0],val[2]])]}
| type TEXT "=" expression EOL{result = [Statement.new([val[0],val[1],val[3]])]}
は実際の実行文(まだ代入文しかない)で、まだ割合適当な構文解析なので左辺
に文字列ならなんでもくるが文字列しかこない。但し、 type、つまり、型が
あるものと、省略したもののどちらかも扱える。Racc の機能ではないが省略
されていたら推論するような例になっている。代入文は
(型) 名前 = 表現
となっているわけである。
expression:
のところは長いが、4則演算、外部関数、単項マイナスを定義している。
これを呼ぶと、Kernelprogram 型の変数が返ってきて、それはあと全部の情報をもつ構
文木になっている、ということになる。