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

387aab4d07ef7a1ec813c1fe97cd8f4a?s=47 shumon84
December 18, 2019

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

387aab4d07ef7a1ec813c1fe97cd8f4a?s=128

shumon84

December 18, 2019
Tweet

Transcript

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


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


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


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

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


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


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

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


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

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

  8. goyacc について
 ただし goyacc は、あくまでパーサ(構文解析器)を作るだけなので、字句解析器は別途必要 
 字句解析器は、これまたGoのコンパイラに使われている text/scanner を使えば簡単に作れる 


    goyacc と text/scanner を使えば、コンパイラ作りのしんどい部分を大幅にスキップできる 
 8 しゅもん(@shumon_84) 

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

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

  10. goyacc について
 具体的な goyacc と text/scanner の使い方については、時間の関係で割愛します 
 簡単にオレオレ言語の抽象構文木が作れておもしろいので、よかったら一度やってみてください 


    公式のサンプルプロジェクト が参考になります
 10 しゅもん(@shumon_84) 

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


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

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

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


  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) 

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

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

    yyParse() ) からエラーハンドリングを、外だししていくところから始める 
 16 しゅもん(@shumon_84) 

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

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

  18. 構文解析器を作る
 エラー発生時は yyLexer.Error() が呼ばれるので、現状はそこでエラーを吐いて os.Exit(1) している。 
 自分でいじれるのは実質 yyLexer.Error() しかないので、ここから

    yyParse の外まで error を流したい。 
 → panic-recover を使って解決した 
 18 しゅもん(@shumon_84) 

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

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

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

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

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

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

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

    error に詰めることを考える。 
 22 しゅもん(@shumon_84) 

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

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

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

    
 エラーメッセージで一目で分かるようになった。 
 24 しゅもん(@shumon_84) 

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


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

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

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


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

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