前回書いたとおりRustで再実装をしているのだけど、パーサーとレキサーの再実装ができた。 Rustでの再実装前と同等の機能までは実装できると思うが、%記法については実装を見送った。
%記法の実装見送りについて
Rubyっぽい記法を取り込みたいということで、%記法を実装していた。
Ruby上の使い方と同様で echo %q{ほ'げほ'げ}
みたいなクォート文字を内包しているときにエスケープなしで使うような使い方を想定していた。
しかしながら、例えば echo %hogehoge%
みたいなコマンドをうちたくなった場合今の実装の場合ではエラーとなってしまう。
理由は、%始まりの文字列がただの文字列なのか%記法なのか区別がつかないから*1である。
回避策としては、%記法に合致しない文字列は通常の文字列として処理するとか、利用者が気をつけることにしてエスケープして利用するなど考えたが、どちらもあまり直感的ではないなっと思い実装を保留してしまった。
またいい案が思いついたら実装するかもしれないのでそれまでは一旦保留となった。
パーサーとレキサーの自作
前回書いたとおりnomの利用をやめて、パーサーとレキサーを自作した。
理由はシンプルで、シェルの構文がかなり曖昧でnomで書こうとするとかなり複雑になるからである。
例えば echo hoge
は echo というコマンドにhogeという引数を渡す文であるとわかるのだけど、 シェルの場合 echo > hoge huga
となる場合がありかなり複雑になる*2。
また、if文を追加することを考えたときに echo if hoge
のifはif文なのか引数なのか文脈を見て判断しなくてはならずかなり複雑な仕様になっている。
この複雑さを解決するために、レキサーを自作し文脈から適切トークンを生成するようにしている。 そのため、煩雑なコードになってしまっている気がする。 しかしながら、その分パーサーはシンプルになっていると思う。 パーサーの実装は自転車本に載っていたLL(1)法で実装している。 参考書のおかげもあって1から作ったにしてはよく実装できたなっと思っている。
テストコードとマクロ
パーサーとレキサーはそこまで時間をかけずにかけたのだが、テストコードを何回か書き直していたために時間がかかってしまった。 テストコードを書くこと自体はそこまで嫌いではないのだけど、入力データを用意するのが大変なので工夫したくなる。 そこで、Rustの強力なマクロ機能を使ってテストコードを書いていた。 test_caseというマクロを作り↓みたいな感じでテストコードを書いていた。
#[test] fn test_foo() { test_case!{ // (1) test_function => { "input1" => Ok("expect1"), // (2) "input2" => Ok("expect2"), // (3) : } } }
test_functionというテスト対象に対して、input1を入力したらOk("expect1")が来ることを期待していて、input2を入力したらOk("expect2")が来ることを期待している。 test_functionの部分はexprを受け入れるようにしているので、クロージャーにすれば前処理もできるようになっている。
個人的には便利だと思っていたのだけど、これは削除した。 理由は、テストがコケた場合に指摘される行が、テストがコケた行ではなくマクロの利用部分になってしまうからである。 具体的には、例えば上の例でinput2の行がコケたとする。この場合、input2の行(3)を指摘してほしいのだけどtest_case!が書かれている行(1)が指摘される。 テストコードが少ないうちならデバッグするのも難しくないが、コードが増えてきた場合特定するのが手間になるのでやめてしまったという感じである。 面倒だけどassertを書いていくのが一番堅実であると思う。
次の実装
パーサー、レキサーの実装が終えたので今は実行部分を作っている。 mrubyでやっていたシステムプログラミングの知識を使ってRustで再実装している。 今の所めちゃくちゃヤバそうという感じはないのでシュッと実装できるはず…はず。