次に来ることができる文字列のトークンを知る

Yuichiro Kaneko氏:「What is expected?」という話をします。

まず私の自己紹介です。金子雄一郎と申します。今日はArm Treasure Dataという会社から来ました。ふだんはAudienceチームというところに所属をしていて、Prestoのクエリの最適化をしたり、Digdagのワークフローを動的に生成するようなRuby on Railsのアプリケーションを書いたり、あまり得意じゃないですがJavaのKey-Value Storeのサービスをメンテナンスしています。

CRubyのコミッターを2015年の12月から始めて今年で丸4年になりますが、今年はあまりアクティブではなかったです。GitHubではyui-knkというアイコンで活動していて、TwitterやGitHubではこの右下のフクロウのアイコンで活動をしています。

我々の企業はエンジニアを募集していて、私のチームはここになりますので、「我こそは!」という方はぜひこのサイトから申し込んでください。僕がよろこびます。

本題に入ります。例えば、「def」と書いたあとに……。ああ、これはPythonじゃなくてRubyですよ?

(会場笑)

Rubyで「def」と書いたあと、ここにどのような文字が入るか、みなさんわかりますか?

例えば「m1」(メソッド1)というメソッドを書くことができますよね。

ですけどRubyはちょっとひねくれた言語でして、実はここに「def」と書くこともできます。なぜだかわかりますか?

そんなに難しくはありません。このように、「def def a; end」と書くことができて、これをRubyの-wcに食わせると「Syntax OK」が返ってきます。

なにが起こっているかというと、「def」という名前のメソッドで引数を1個aに取って、中身はなにもないメソッドの定義をしているというシンタックスです。

具体的に言うと、2つ目のdefは1つ目のdefと違い、k_defじゃなくてreswordsというトークンに置き換わります。それはfnameとして展開されるので、k_defのあとに置くことができます。このような話がこの後も続いていきます。

つまり「What is expected?」というのはテストの話ではありません。ある時点で次に来ることができる文字列のトークンというのがあるけれども、それを僕たちはどうやって知ることができるんだ? という話になります。

ちなみにスライドには書いてないですけど、これができると何がうれしいかと言うと、例えばsyntax errorになったときに「本来だったらこう入れてくれれば、このあとの処理を続けることができたんだけどなあ」という話であったり。また、みなさん使っていると思いますけど、Ripperというライブラリがあって、あれを使うと入力の途中の状態でも次に来ることができるトークンがわかります。

なので例えばmethod nameの一覧を出したりローカル変数の一覧を出したりする処理を書くことができて、これはirbとかのコンプリーションとかに展開することが可能と考えています。

Rubyのスクリプトがふだんどう処理されているか

基本的にはパーザの話をするんですけど、その前に全体像として、みなさんがふだん書いているRubyのスクリプトがどう処理されているかという話をしたいと思います。

みなさんが書くRubyのスクリプトというのは、この図の上にあるようにトークナイズというフェーズを経てトークンになり、パーズというフェーズを経てASTになり、そのあとコンパイルというフェーズを経てバイトコードはInstructionSequenceとかに変換されたりします。

これだけ言われてもなにもわからないかもしれません。下にデバッグ時のオプションを書いたので、興味のある方はRubyの引数としてdumpというものを食わしてあげて「y」とか「p」とか「i」と入れてあげると、それぞれどういう内部処理が行われているかをチェックすることができます。

「1+2」という式を例に、どういう処理が行われているかという話をしたいと思います。これはどう見ても3ですけど、実際にはまずトークナイズというフェーズがあって、Rubyのパーザの世界においては「1+2」の「1」とか「2」とかにあまり意味がないと考えることがあるんです。

つまり「1」とか「2」というのは整数値であって、別にそれが「1」だろうと「2」だろうと「100」だろうと構文上は何でもいいわけです。だから最初の処理として、まずトークンというかたちで抽象化してあげて、「これが整数、tINTEGERですよ」という情報を付加してあげる必要がある。

もちろん最後に本当にevaluationする処理では、「1+2」と「2+3」は違うので、その「1」とか「2」という情報をキープしながらもラベルを付けてあげるという処理になります。

これはさっきのdump=yというオプションで出すことができて、こんな感じのコードを書いてあげると、この右側の部分、赤で囲われた「1」とか「2」というところにそれぞれの意味がありつつ、左側の「integer literal」であるとか「+」を、トークンとして取ることができます。

パーザの処理について

トークンに分けた次のフェーズで、パーザの処理が入るんですけど、ここでは主に抽象構文木というものを作ることを想定しています。

パーザの処理とはなにかと言うと、今みなさんが書いた「Rubyのスクリプト」あるいは「Rubyのスクリプトみたいなもの」が、本当にRubyの構文として正しいかどうか。つまりシンタックスとして問題がないかというチェックをするのがパーザのフェーズです。

パーズだけをやってしまうと「君の書いたコードはOKだよ」「君の書いたコードはNGだよ」という情報が入るだけで終わってしまうんですけど、後続の処理に渡すためにはここはパーズの処理をしながら、抽象構文木と言われる木構造のデータ構造を作って次の処理に渡せるようなかたちで処理を進めていきます。

Rubyの中にparse.yというファイルがあって、これはRubyの構文がどういうものであるかを定めているものの一部抜粋なんですけど、このブロックに分類されているそれぞれがいろんなルールを持っているということです。左から右に向かって読むことができます。

例えば上のnumericというものはsimple_numericもしくはマイナスが付いてるsimple_numericであるというのが一番上の定義なんですね。simple_numericとは何かと言うと、例えば整数値、小数値、有理数値、虚数値というのが入ってきます。

今度は逆に読むこともできて、例えばみなさんが1と書いたならば、それはトークンとしてはtINTEGERなんですけど、それはsimple_numericであり、simple_numericはnumericであると読むこともできます。

パーザの処理の流れ

パーザの処理としては、一番下にある「program」というものがすべてのRubyのシンタックスの頂点に存在するので、みなさんの書いたものが下から順に拾いあげられて、最終的にゴールであるprogramになればOKだし、ならなかったらダメという処理をしています。

これもさっき説明したdumpオプションのyというものです。yというのはたぶんyaccのyだと思うんですけど、表示することができて、dump=y -eでストリングを食わせるとこんな感じでいろんな情報が出てくるんです。

さっき言ったトークンの情報がsimple_numericになって、それからnumericと認識されて、それはliteralのことでありprimaryのことであり、それはargのことであり、argまでくると次は+を認識して……と。

同じような処理がバーッと走って、最終的にはこの一番下のprogramになるから、「『1+2』という文字列はRubyの式として正しいから処理ができる」とチェックをします。

さっき言ったとおり、ここで終わっちゃうとただのシンタックスチェックになってしまうので、そのあとの処理のために、こんなふうにトークンのルールを適用しながら木構造を作ってあげて、これを次の処理に渡していきます。

最後の処理はコンパイルというフェーズで、Rubyのバーチャルマシンが認識できるバイトコードを作っていきます。

この辺は僕の専門じゃないので、端的にコンパイルした結果を表示するオプションだけを示すに留めておきたいと思います。こんな感じです。下のやつの読み方は、スタックマシーンなので、1というオブジェクトをプッシュして、2を置いて+のメソッドを呼んであげる。

そうすると「1+」は引数1個のメソッドの呼び出しなので、「1+2」の「2」が1個の引数になって、それで処理が終わる。この場合には3ということがわかります。

話が長くなったんですけど、今日の話ではexpected tokensというのは構文解析上の問題なので、パージングの処理をずっとしていきたいと思います。