Slide 1

Slide 1 text

サーバサイドエンジニアとして入社したら
 全く気付かないうちにUnityエンジニアになって
 
 コンパイラを作っていた話
 1 しゅもん(@shumon_84) 


Slide 2

Slide 2 text

自己紹介
 2 しゅもん(@shumon_84) 


Slide 3

Slide 3 text

自己紹介
 
 藤田 朱門 (@shumon_84)
 株式会社ミクシィ 2019 年度新卒 
 
 ● デジタルエンターテインメント事業部所属 
 ● 『共闘ことばRPG コトダマン』 のクライアント兼サーバ 
 ● 業務では Unity と Java をやっています 
 ● 好きな言語は Go
 ● 『Dockerで始めるゲームボーイアドバンス開発入門』 著者
 ○ 技術書典 8 では、新刊で vol.3 を頒布する予定です 
 3 しゅもん(@shumon_84) 


Slide 4

Slide 4 text

コンパイラを作るまでの経緯
 4 しゅもん(@shumon_84) 


Slide 5

Slide 5 text

コンパイラを作るまでの経緯
 4月 : サーバサイドエンジニア ( Go ) として新卒配属 
 ↓
 10月 : クライアントエンジニア ( Unity / C# ) に転向して、現在のコトダマンに異動 
 ↓
 11月 : API が 独自 IDL (インターフェース定義言語) で自動生成されてるけど、コンパイラ貧弱じゃね? 
 ↓
 現在 : コンパイラ自作するぞ 
 5 しゅもん(@shumon_84) 


Slide 6

Slide 6 text

goyacc について
 6 しゅもん(@shumon_84) 


Slide 7

Slide 7 text

goyacc について
 文法を食わせると、その文法をパースできるコードを自動生成する「yacc」というツールのGo版。 
 出力として Go を吐いてくれるパーサジェネレータ 
 元々 Go のコンパイラを作るのに使用されていたため、Go1.7までは go tool に標準で入っていた 
 (今のコンパイラは goyacc を使っていない) 
 → 詳しくはドキュメント参照 golang.org/x/tools/cmd/goyacc 
 7 しゅもん(@shumon_84) 


Slide 8

Slide 8 text

goyacc について
 ただし goyacc は、あくまでパーサ(構文解析器)を作るだけなので、字句解析器は別途必要 
 字句解析器は、これまたGoのコンパイラに使われている text/scanner を使えば簡単に作れる 
 goyacc と text/scanner を使えば、コンパイラ作りのしんどい部分を大幅にスキップできる 
 8 しゅもん(@shumon_84) 


Slide 9

Slide 9 text

goyacc について
 goyacc に文法を教えるには、バッカス・ナウア記法という「文法を書くための言語」を使う。 
 - <文> ::= <宣言> | <式> ';' - <宣言> ::= <型> <文字列> ';' | <型> <文字列> '=' <式> ';' - <型> ::= <文字列> | <型> '[' ']' | '*' <型> - <式> ::= <数> | <式> <演算子> <式> こんな感じで <文法X> ::= Xの定義1 | Xの定義2 | ...... と書くだけ。多分フィーリングで書ける。 
 よく言語仕様書とかで、見かけるやつなので馴染み深い人もいるはず。 
 9 しゅもん(@shumon_84) 


Slide 10

Slide 10 text

goyacc について
 具体的な goyacc と text/scanner の使い方については、時間の関係で割愛します 
 簡単にオレオレ言語の抽象構文木が作れておもしろいので、よかったら一度やってみてください 
 公式のサンプルプロジェクト が参考になります
 10 しゅもん(@shumon_84) 


Slide 11

Slide 11 text

独自 IDL について
 11 しゅもん(@shumon_84) 


Slide 12

Slide 12 text

独自 IDL について
 独自 IDL と言っても、それほど独特な文法ではない。 
 ● *.str : 型を記述するファイル 
 ● *.act : RPCのインターフェースを記述するファイル 
 12 しゅもん(@shumon_84) 


Slide 13

Slide 13 text

構文解析器を作る
 13 しゅもん(@shumon_84) 


Slide 14

Slide 14 text

構文解析器を作る
 1. BNF ( バッカス・ナウア記法 ) で *.str と *.act の文法を定義する 
 2. text/scanner を使って、 goyacc に対応した独自 IDL 用の字句解析器を作る 
 3. 抽象構文木のノードになる型を作る 
 4. parser.yにBNFを書く 
 5. parser.yに各定義と各ノード型の紐付けを書く 
 6. parser.y にマッチした定義と対応する型に定義を割り当て、ノードにする処理を書く 
 7. goyacc で parser.y から parser.go を生成 
 あとは parser.go に生成された yyParse() という関数に字句解析器を渡すと、抽象構文木を作ってくれる 
 14 しゅもん(@shumon_84) 


Slide 15

Slide 15 text

構文解析器を作る
 構文解析器完成!
 と思いきや、まだまだ完成ではない。 
 この構文解析器を使って本物の IDL をパースすると、なにやらエラーが出た 
 15 しゅもん(@shumon_84) 


Slide 16

Slide 16 text

構文解析器を作る
 エラーメッセージが貧弱すぎて使い物にならない。 
 goyacc の生成物に詳細エラー出力オプションも一応存在するが、文面がユーザフレンドリーでない。 
 せめてファイル名とファイルの行数くらいは出したい。できれば文法をどう間違えたのかも教えてほしい。 
 まずは自動生成部分 ( yyParse() ) からエラーハンドリングを、外だししていくところから始める 
 16 しゅもん(@shumon_84) 


Slide 17

Slide 17 text

構文解析器を作る
 goyacc 生成のパーサの起点になる yyParse() のインターフェースはこんな感じ。 
 パースに成功したとき 0 を返し、失敗したときは 1 を返す。( C 言語っぽい ) 
 yyParse() は自動生成なので内部エラーを error として返すのは難しい。 
 
 17 しゅもん(@shumon_84) 


Slide 18

Slide 18 text

構文解析器を作る
 エラー発生時は yyLexer.Error() が呼ばれるので、現状はそこでエラーを吐いて os.Exit(1) している。 
 自分でいじれるのは実質 yyLexer.Error() しかないので、ここから yyParse の外まで error を流したい。 
 → panic-recover を使って解決した 
 18 しゅもん(@shumon_84) 


Slide 19

Slide 19 text

構文解析器を作る
 yyParse() を Parse() でラップする 
 → lexer.Error() で panic() を投げるようにする 
 → yyParse() の外まで error が流れてくる 
 → yyParse() をラップした関数で recover() する 
 → recover() した error をラップ関数の外に渡す 
 19 しゅもん(@shumon_84) 


Slide 20

Slide 20 text

構文解析器を作る
 このアプローチを取ることで、 
 ● Parse()だけを公開する形でparse package の切り出しが容易になった 
 ● 字句解析器を完全に隠蔽できるようになった 
 ● 第1返り値に抽象構文木、第二返り値に error を返す関数になり、Goらしく扱えるようになった 
 一応、 panic-recover を積極的に使うのは Go のプログラムではアンチパターンとされているけど、 
 ジャンプが自動生成されたコードの中に閉じていて、人間がメンテする部分のコードからは、 
 隠蔽されているので許して欲しい 
 20 しゅもん(@shumon_84) 


Slide 21

Slide 21 text

構文解析器を作る
 これで error を外に運ぶ仕組みが出来たので、次はエラーメッセージに行数などのメタ情報を埋め込む。 
 lexer は text/scanner の scanner.Scanner をコンポジットし、 
 scanner.Scanner は scanner.Position をコンポジットしているので、 
 lexer からも Scanner や Position が見えることを利用して、 
 エラー発生時に読んでいたトークンや、そのトークンがあった行数と桁数をエラーメッセージに付け加える 
 21 しゅもん(@shumon_84) 


Slide 22

Slide 22 text

構文解析器を作る
 もう一度実行してみる。 
 
 
 行数などが分かるようになってエラーを起こしてる箇所のアタリをつけられるようになったが、 
 まだどういった理由でエラーが発生したのかが不明瞭。 
 次は文脈に沿ったエラー理由を error に詰めることを考える。 
 22 しゅもん(@shumon_84) 


Slide 23

Slide 23 text

構文解析器を作る
 例えば「セミコロンが抜けている」という文脈依存エラーを出したいなら、BNFを少し工夫するとよい 
 フィールド定義の正しいBNFは次のように表せる 
 - <フィールド> ::= <型> <文字列> ';' ここで、わざと「間違った文法」をBNFに追加する 
 - <フィールド> ::= <型> <文字列> | <型> <文字列> ';' この 赤い方の定義 にマッチしたとき、セミコロンが抜けている旨を error に詰めると、 
 文脈依存なエラーメッセージが作れる。 
 23 しゅもん(@shumon_84) 


Slide 24

Slide 24 text

構文解析器を作る
 もう一度実行してみる。 
 
 
 劇的にエラーメッセージが親切になった。 
 13行目の “User owner”の後ろにセミコロンが抜けていることが、 
 エラーメッセージで一目で分かるようになった。 
 24 しゅもん(@shumon_84) 


Slide 25

Slide 25 text

まとめ
 25 しゅもん(@shumon_84) 


Slide 26

Slide 26 text

まとめ
 ● 【実績解除】業務で自作コンパイラ 
 ● goyacc があればパーサを作るのは超簡単 
 ● 分かりやすいエラーメッセージを出すのが異常に難しい 
 ● あらかじめ誤った文法を定義しておくのは制御しやすくて便利 
 ● 1つのファイルから複数のコンパイルエラーを検出できない問題がまだ残ってる 
 ● 独自IDL使うのやめたら?
 ○ わかる〜、でもフレームワークにがっちり組み込まれてて、独自IDLを切り離すのは難しい 
 26 しゅもん(@shumon_84) 


Slide 27

Slide 27 text

話し切れなかったこと
 27 しゅもん(@shumon_84) 


Slide 28

Slide 28 text

話し切れなかったこと
 ● マルチOS対応のために文字コード変換を挟む話 
 ● 通常コメントとドキュメントコメントで別々に扱う仕組みの話 
 ● リファレンスに存在しない文法が存在した話(partial class, 継承) 
 ● 意味解析の話
 ● 解析結果を中間形式に保存して高速化する話 
 ● テンプレートエンジンでコード生成する話 
 ● 並行処理で出力が散らかるのを防ぐ話 
 このままだと2時間ぐらい話してしまうので、続きは懇親会で!
 28 しゅもん(@shumon_84)