Lambdaカクテル

Common Lispを書くMT-03ライダー(初心者)です

Let's get arrested badge

Smart::Argsのパーサを書いた

この記事は,はてなエンジニアAdvent Calendar 2018の17日目の記事です.昨日はid:aerealさんによるTheSchwartzの失敗したjobとかerrorがいつどのように消えていくのか - Sexually Knowingでした.

今日ははてなでもよく使われているSmart::Argsのパーサを書いた話をしようと思います.

俺たちのPerl 5

はてなではPerl 5がしばしばプロダクトの開発に使われています.そしてPerlで動的な引数型チェックをできるようにするSmart::Argsというライブラリがあり,よく使われています.Smart::Argsを使うと,以下のような体裁で引数の型チェックを実行時に行うことができます.

package Dummy;
use Smart::Args qw(args);
# クラス定義略
sub foo {
  args my $class => 'ClassName',
          my $arg1 => 'Int',
          my $arg2 => { isa => 'HashRef', default => +{} };
}

このサブルーチンを呼び出すには,Dummy->foo(arg1 => 1, arg2 => +{ bar => "buzz" });というふうに,ハッシュの形で引数をとります. Common LispやRubyに存在するキーワード引数に似ていますね.

またハッシュではなく通常のサブルーチン同様に引数をとるためのargs_posもあります.これはサブルーチン定義で利用しているargsargs_posに変えることで,上述のサブルーチンをDummy->foo(1, { bar => "buzz" });というふうに呼ぶことができるようになります.

Smart::Argsの詳細については,CPANのページをごらんください.

ちなみにこれを改良したSmart::Args::TypeTinyid:akiymさんが作られています.

貧弱な開発環境

さて,便利な型チェックライブラリが存在する一方で,エディタやIDEがPerlのために提供する支援はあまり充実してはいません. 他の言語には用意されているような,自動的にメソッド名や引数を補完するといった機能を提供するプラグインはほぼありません.このような状況になっているのは,柔軟な文法が静的な解析を困難にしていることに遠因があるようです.この状況はSmart::Argsでも同様で,Smart::Argsを使って定義されたサブルーチンの引数を的確にサポートできる開発環境を,わたしは存じません.

まずはパーサから

そこで今回私はSmart::Argsを使って定義されたサブルーチンの引数をサポートできるようにするための橋頭堡として,まずはSmart::Argsの文法をパースするためのライブラリを作成しました.

github.com

roswellを使ってcommon lisp処理系をインストールした状態で,rosコマンドを使ってサンプルperl moduleファイルをパースしてみましょう.

# Roswellをセットアップする
# In OS X
$ sudo brew install roswell
$ ros setup
$ git clone git@github.com:windymelt/smart-args-parser.git
$ cd smart-args-parser
$ ros -S . roswell/parse-smart-args.ros tests/Test.pm

すると,tests/Test.pmをパースした結果が表示されます.

(("sub2" SMART-ARGS-PARSER::ARGS
  ((SMART-ARGS-PARSER::ONEARG (VARIABLE . "class")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ClassName")
   (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg2")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "Int")
   (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg3")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ArrayRef")))
 ("sub1" SMART-ARGS-PARSER::ARGS
  ((SMART-ARGS-PARSER::ONEARG (VARIABLE . "class")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ClassName")
   (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg1")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "Str"))))

今のところ機能はこれだけですが,これからパッケージとサブルーチンとの紐付け・Language Server Protocolの作成などを行おうと思っています.

どんな仕組みでパースしているの

当初はSmart::Argsの構文を正規表現でパースしようと思っていましたが,コーディング途中で到底これではパースできないと思いPEG を用いたパースに置き換えました.argsargs_posの構文には,型の指定・デフォルト値の指示等の目的でHashRefが登場し,これがネストする可能性があるためです.何重にもネストする構文を正規表現でパースすることは難しいのです.

PEG

args/args_posの構文は,あらかじめ定義されたPEG文法ファイルをもとにパースされます.

github.com

Common Lispでは,ESRAPライブラリを使うことでpackrat parserを作成することができ,ESRAP-PEGライブラリを使うことで,PEG文法ファイルをもとに,ESRAPを使ってPEGパーサを組み立てることができます. このツールでもESRAP/ESRAP-PEGを使ってargs/args_posのパーサを作成し,またパース結果を評価して「変数」や「文字列」といった意味のある単位に再編するためのコードを実装して,最終的に上掲のS式を出力させています.

実装の詳細ですが,処理の流れは以下のようになっています.

  1. PEG文法ファイルからpackratパーサが構築される
  2. ヘルパーメソッドがファイルからサブルーチン部分を抜き出す(ここは正規表現で行う)
  3. 抜き出されたサブルーチンの冒頭部分(セミコロンが出現するまで)を順次packratパーサに渡していく
    1. トークナイズとある程度の構造化が行われる
  4. パースされた結果をさらに適切な形に変形させる
    1. 必要ないトークンを削除し,文字の連続を文字列に変換する,などを行う
  5. 変換結果を印字する

ここがきつかったです

PEGの文法に慣れていなかったため,うっかり無限に再帰させてしまったり,パースしにくい表現にしてしまったりしました.

ここがおもしろかったです

パースに失敗するとどこで失敗したかが表示されるので,自分の入力ミスだったことがすぐに分かって面白かったです.

(esrap:parse 'smart-args-parser::hashref "{ foo -> 1 }")
At

  { foo -> 1 }
        ^ (Line 1, Column 6, Position 6)

In context HASHPAIR:

While parsing COMMA. Expected:

     the character , (COMMA)

While parsing FATCOMMA. Expected:

     the string "=>"

While parsing peg-derived character class rule with clauses (32 9 10) . Problem:

  The production
  
    #\-
  
  does not satisfy the predicate SMART-ARGS-PARSER::|peg-derived semantic-checker for character class with clauses (32 9 10) |.

まとめ

今回は,Smart::ArgsをCommon Lispでパースしたお話をしました.

明日はid:taraoさんです!