サーバサイドエンジニアとして入社したら 全く気付かない間にUnityエンジニアになって コンパイラを作っていた話

387aab4d07ef7a1ec813c1fe97cd8f4a?s=47 shumon84
December 18, 2019

サーバサイドエンジニアとして入社したら 全く気付かない間にUnityエンジニアになって コンパイラを作っていた話

387aab4d07ef7a1ec813c1fe97cd8f4a?s=128

shumon84

December 18, 2019
Tweet

Transcript

  1. 3.

    自己紹介
 
 藤田 朱門 (@shumon_84)
 株式会社ミクシィ 2019 年度新卒 
 


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

  2. 5.

    コンパイラを作るまでの経緯
 4月 : サーバサイドエンジニア ( Go ) として新卒配属 
 ↓


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

  3. 7.

    goyacc について
 文法を食わせると、その文法をパースできるコードを自動生成する「yacc」というツールのGo版。 
 出力として Go を吐いてくれるパーサジェネレータ 
 元々 Go

    のコンパイラを作るのに使用されていたため、Go1.7までは go tool に標準で入っていた 
 (今のコンパイラは goyacc を使っていない) 
 → 詳しくはドキュメント参照 golang.org/x/tools/cmd/goyacc 
 7 しゅもん(@shumon_84) 

  4. 9.

    goyacc について
 goyacc に文法を教えるには、バッカス・ナウア記法という「文法を書くための言語」を使う。 
 - <文> ::= <宣言> |

    <式> ';' - <宣言> ::= <型> <文字列> ';' | <型> <文字列> '=' <式> ';' - <型> ::= <文字列> | <型> '[' ']' | '*' <型> - <式> ::= <数> | <式> <演算子> <式> こんな感じで <文法X> ::= Xの定義1 | Xの定義2 | ...... と書くだけ。多分フィーリングで書ける。 
 よく言語仕様書とかで、見かけるやつなので馴染み深い人もいるはず。 
 9 しゅもん(@shumon_84) 

  5. 12.

    独自 IDL について
 独自 IDL と言っても、それほど独特な文法ではない。 
 • *.str :

    型を記述するファイル 
 • *.act : RPCのインターフェースを記述するファイル 
 12 しゅもん(@shumon_84) 

  6. 14.

    構文解析器を作る
 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) 

  7. 17.

    構文解析器を作る
 goyacc 生成のパーサの起点になる yyParse() のインターフェースはこんな感じ。 
 パースに成功したとき 0 を返し、失敗したときは 1

    を返す。( C 言語っぽい ) 
 yyParse() は自動生成なので内部エラーを error として返すのは難しい。 
 
 17 しゅもん(@shumon_84) 

  8. 19.

    構文解析器を作る
 yyParse() を Parse() でラップする 
 → lexer.Error() で panic()

    を投げるようにする 
 → yyParse() の外まで error が流れてくる 
 → yyParse() をラップした関数で recover() する 
 → recover() した error をラップ関数の外に渡す 
 19 しゅもん(@shumon_84) 

  9. 20.

    構文解析器を作る
 このアプローチを取ることで、 
 • Parse()だけを公開する形でparse package の切り出しが容易になった 
 • 字句解析器を完全に隠蔽できるようになった

    
 • 第1返り値に抽象構文木、第二返り値に error を返す関数になり、Goらしく扱えるようになった 
 一応、 panic-recover を積極的に使うのは Go のプログラムではアンチパターンとされているけど、 
 ジャンプが自動生成されたコードの中に閉じていて、人間がメンテする部分のコードからは、 
 隠蔽されているので許して欲しい 
 20 しゅもん(@shumon_84) 

  10. 21.

    構文解析器を作る
 これで error を外に運ぶ仕組みが出来たので、次はエラーメッセージに行数などのメタ情報を埋め込む。 
 lexer は text/scanner の scanner.Scanner

    をコンポジットし、 
 scanner.Scanner は scanner.Position をコンポジットしているので、 
 lexer からも Scanner や Position が見えることを利用して、 
 エラー発生時に読んでいたトークンや、そのトークンがあった行数と桁数をエラーメッセージに付け加える 
 21 しゅもん(@shumon_84) 

  11. 23.

    構文解析器を作る
 例えば「セミコロンが抜けている」という文脈依存エラーを出したいなら、BNFを少し工夫するとよい 
 フィールド定義の正しいBNFは次のように表せる 
 - <フィールド> ::= <型> <文字列>

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

  12. 26.

    まとめ
 • 【実績解除】業務で自作コンパイラ 
 • goyacc があればパーサを作るのは超簡単 
 • 分かりやすいエラーメッセージを出すのが異常に難しい

    
 • あらかじめ誤った文法を定義しておくのは制御しやすくて便利 
 • 1つのファイルから複数のコンパイルエラーを検出できない問題がまだ残ってる 
 • 独自IDL使うのやめたら?
 ◦ わかる〜、でもフレームワークにがっちり組み込まれてて、独自IDLを切り離すのは難しい 
 26 しゅもん(@shumon_84) 

  13. 28.

    話し切れなかったこと
 • マルチOS対応のために文字コード変換を挟む話 
 • 通常コメントとドキュメントコメントで別々に扱う仕組みの話 
 • リファレンスに存在しない文法が存在した話(partial class,

    継承) 
 • 意味解析の話
 • 解析結果を中間形式に保存して高速化する話 
 • テンプレートエンジンでコード生成する話 
 • 並行処理で出力が散らかるのを防ぐ話 
 このままだと2時間ぐらい話してしまうので、続きは懇親会で!
 28 しゅもん(@shumon_84)