読者です 読者をやめる 読者になる 読者になる

ElixirでDesel-langのパーサを書いた

Advent Calendar 2016 Elixir 作った 言語処理

qiita.com これの11日目です。

はじめに

今回作ったのはDesel言語を扱うためのコマンドラインツールです。(以下desel-cli
github.com

動作例

$ echo "%A a b c\n" | desel - %A
a
b
c
$ echo "%A -a b c\n" | desel - %A
desel: failed to parse the input

0: %A -a b c
      ^ unexpected token
suggestion: element or expression

本記事では、その実装方法などを解説します。
Desel言語自体についてはこの記事では特に触れないので、そちらに興味のある方はryo33/desel-langを参照していただけると幸いです。

コマンドラインツール部分

Elixirでコマンドラインツールを作るのは結構簡単です。
mix.exs内に

  def project do
    [...
     escript: [main_module: YourCLIModule],
     ...]
  end

として、YourCLIModule内でmain/1を定義するだけです。
desel-cliYourCLIModuleに該当するのはlib/desel/cli.exです。
ファイルや標準入力から読み込んだあと、パーサなどの他モジュールにそれを渡してその結果を出力しています。
ここのコードは、重要な処理は特にしていないうえに、かなり雑に書かれてあるのであんまり見る価値はないです。

字句解析と構文解析

desel-cliではパーサコンビネータryo33/Parselix) を用いて字句解析と構文解析を同時に行っています。
コードはlib/desel/parser.exです。
ParselixParselix.Basicからimportしてきた関数、謎の命名規則などが大量に存在しているので何が何だかわからない事になっていますが、 BNFと対比させてみると何をやっているかはわかりやすくなると思います。
BNFgrammer.ebnfにありますが、 ここも雑なので合っているか保証できないです。
ほとんどがBNFから持ってきただけですが、パース結果を扱いやすい形に変換するmap/2や パースに失敗したときのメッセージを'unexpected token'みたいな感じに置き換えるexpect/2などが色々頑張ってます。

パース結果の処理

この処理がこのコマンドラインツールのメインの処理です。
コードはlib/desel/data.exにあります。
ここでやっている処理は主に2つで、パース結果の変換(from_ast/1)と式の評価(elements_by/2)です。
式の評価では、集合に限って(今のところ*1)キャッシュを保存する用にしたり、 いい感じの再帰にしたりして結構頑張っています。*2

Parselixについて

Parselixは僕が「Elixir面白そうだし入門がてら何か作るか」となったときに、 ちょうどパーサコンビネータに興味を持っていたため作ったものです。
今になって読んでみると結構ひどい見た目で、 Elixir初心者だったからと言い訳できないようなところもあったので、この機会に色々書き換えました。
parserマクロで定義できる関数のarityが任意になったのが一番でかくて、パイプで処理を書くときに

[left, parser, right]
|> sequence
|> (&(pick({&1, &2}))).(1)

と、非常に読みにくいコードになっていたところが*3

[left, parser, right]
|> sequence
|> pick(1)

とできるようになりました。

これから

CLIモジュールのテストは面倒くさくて書いていなかったのでそれと、近いうちにREADMEを書きます。
あと、適切でないパース時のエラー表示がまだ多いので、それを直していくつもりです。

最後に

Elixir v1.4はmix escript.installでhexやgithubからコマンドをインストールできるのが最高です。

*1:多分これ以外をやってもあんまり意味ない

*2:この再帰Enum.reduceなどで書いたらかなり読みづらいコードになるはず

*3:そもそも、これを無理やりパイプで書こうとするな