本セッションの登壇者
今日はこの「Rustソースコードのざっくりとした歩き方」というタイトルで発表したいと思います。
まず簡単な自己紹介です。TaKO8Ki(タコヤキ)というハンドルネームをよく使っていて、本名は前田喬之といいます。Rust界隈では、コンパイラーコントリビューターというチームで、Rustへのコミットをしていて、最近だとさっき記事も取り上げていただいたように、Rust Foundatin(https://foundation.rust-lang.org/ )の2023年のフェローとして活動していたりします。ソフトエンジニアとしてマネーフォワードで働いていたりもします。
まず今回のテーマについて説明して、本題に入っていこうかなと思うんですけど、今日はRustコンパイラのソースコードをほとんど読んだことがない方や、Rustはわかるという方がRustコンパイラーにコントリビュートするためのざっくりとした入門みたいなテーマでやっていきます。なので、端折っているところがありますがご了承ください。
まずはセットアップ
最初に、セットアップするところから説明していきたいと思います。
ご存じの方もいると思いますが、Rustにコントリビュートする時に、「Rust Compiler Developer Guide」というすごくわかりやすいドキュメントがあるので、ソースコードを読む時は1回これにさっと目を通してからがよいと思います。網羅されているので基本はこれを読めばだいたいのことがわかります。今回はこれも含めて、付加情報をつけながら説明していけたらと思います。
最初にgit clone
するところから始めます。もしリアルタイムでできる人は一緒にやったりすると楽しいかもしれないと思うので、もしよければやってみてください。
次に、rustリポジトリのビルドには、x.pyというツールを使います。これでRustコンパイラや標準ライブラリをまるっとビルドできます。詳細はリンクを見てください。
次にサブコマンドsetup
を叩いてセットアップをします。これを実行するといろいろ出てきた後に設定のターゲットを選択することになると思うんですけど、今回はコンパイラメインなので、compilerというのを選択すると、それに適したconfigやpre-pushが設定されます。
それ以外にもVSCodeユーザー向けにsettings.jsonがRustリポジトリ配下に自動的に追加されます。rust-analyzer用の設定です。
もちろんsetup
以外にもサブコマンドはいくつかあって、たとえばcheck
、build
など、cargoと同じような感覚で使うことができます。
試しにビルドしてみましょう。./x.py build
のように入力してビルドを実行します。初回は時間がかかるので、割と待つことになるかもしれません。
次に、ビルドしたコンパイラをデバッグ時にcargoやrustcで使いたいので、これをtoolchainとして登録します。この[your host]
と書いてあるところは、それぞれに置き換えてください。toolchainとして登録してリンクして、cargoやrustcで+stage1
を付けることでビルドしたコンパイラを使うことができます。
*rustup
についてはこちらをご覧ください。
エラー箇所の特定方法とログの見方
ここまででひととおり準備できたので、テストを実行してみたいと思います。これもわりと時間がかかるので、今実際にやってる方は一旦飛ばしちゃっても問題ないと思います。
セットアップの最後に、コンパイラのデバッグをどうやるかという問題があると思います。
簡単に説明すると、backtraceが欲しいケースではこのように環境変数を設定してビルドするとbacktraceが見られます。(nightlyのインストールが必要です。)
エラーの発生箇所が知りたい時は、Zフラグを使うことでコンパイラのどの箇所で定義されているエラーかを見ることができます。
また、Rustではloggingにtracingという、tokio organizationで開発されているcrateを使っているので、それを使ったことある方は同じ感覚で使うことができます。具体的には、RUSTC_LOG
という環境変数でログのフィルタリングをします。今回のこのケースだと、rustc_parse
にcrateのログとデバッグログを表示するというフィルタリングになっているので、自分が表示したいログのファイルのパスに合わせてここを適宜変更します。
ここまででセットアップが終わったので、この後に開発をする際は、修正して、UIテストを追加して、テストを実行して壊れていないか確認した上で、プルリクを作ってマージされるという流れが基本的には多いです。
コードを読んでみよう:プレフィックスに着目
では、実際にコードを読んでいきたいと思います。全部をこの時間内に紹介するのは厳しそうなので、今回は比較的やさしそうなparserを読んでみたいと思います。基本的には「parse」がプレフィックスとしてつくmethodやfunctionを目印に読んでいけば読みやすいかなと思います。Rustのparserはトップダウンな再帰下降パーサーを用いています。詳しくはリンクを参照してください。
rustcのparse crateに関してはエントリーポイントは基本この2つです。‘parse crate_mod‘は内部で‘parse_mod‘を呼んでいるので、‘parse_mod‘の方を読んでいきます。
すると、オレンジの枠内にloopでitemをparseしている箇所があります。
ここでparse_item
を実行しているので、そちらに飛んでみると、こういう感じの実装になっていて、このparse_item_
の方に定義ジャンプしたいと思います。
一番下の方でparse_item_common
というのを実行しているので、そこにさらに飛んでみます。
まだコアな実装まで来ていない気がするので、今度はこの下のほうの parse_item_common_
の方にジャンプしてみたら、それっぽい実装が見えてきました。
オレンジで囲んでいるところで、parse_item_kind
というのを呼んでいます。名前的にコアな実装な気がします。
では、parse_item_kind
に定義ジャンプしてみると、それぞれのアイテムごとに条件分岐してparseXXというmethodを呼ぶ一番上のmethodのようなので、これを読み進めていきたいと思います。
実際に少しスクロールしてみると、enumをparseしている箇所が見えます。
せっかくなのでこのparse_item_enum
というmethodを覗いてみましょう。
確かにこのmethodの下の方で、コンマ区切りでEnum variantをparseしている箇所が見つかります。
エラーメッセージを修正してコンパイラを再ビルド
ここまで来たらなんとなくそれぞれどういう箇所でどういうparseをしているのかというのがわかってくると思うので、実際に変更を加えてコンパイラをビルドしてみたいと思います。たとえば、さっき見たこの‘parse_item_enum‘中に、このオレンジで囲ったエラーメッセージが見えると思います。これをリポジトリの中で検索してみると、
こういう感じのUIテスト用のファイルが見つかります。
UIテストというのはコンパイルによるstdout/stderrと実行可能ファイルを実行した結果をチェックするためのテストです。
では、さっき見たUIテストの中身を、デバッグ用のcrateを作って‘main.rs‘にコピーしてみましょう。
cargo new
で適当にcrateを作って、試しにstage1のコンパイラでビルドしてみます。
すると、こういう感じのエラーが得られると思います。枠内にさっき見つけたエラーが出てきていますね。
コンパイラの実装に戻って、試しにこのエラーメッセージを修正してみましょう。何でもいいんですけど、後ろの方に適当なエラーメッセージを追加します。
もう一度コンパイラを再ビルドして、もう一度cargo +stage1 build
でデバッグ用のcrateをビルドしてみると、さっき変更したエラーメッセージが出てくると思います。
こんな感じで、修正を加えてビルドして、実際にエラーメッセージが変わってるというのがわかるという、この一連の流れが一つ変更を加えるということで、これでRustコントリビューターっぽいことができるようになったという感じです。
crateは違っても同じように読み進められる
今回はrustc_parse
を取り上げたんですけれども、他にももちろん、rustc_ast_lowering
、rust_ast_passes
などいろいろなcrateがあります。たとえば、rustc_ast_lowering
はrustc_parse
で作ったASTをHIRという中間言語にloweringするためのcrateです。ここでは「parse」と同じように「lower」をプレフィックスに持つmethodやfunctionを目印に読んでいくとわかりやすいです。
実際に見てみるとrustc_ast_lowering
のエントリーポイントはこのlower_to_hir
というfunctionです。
同じように覗いてみると、この枠内にast_index
でループを回してlower_node
というmethodを呼んでいる箇所が見つかります。
lower_node
の実装はこういう感じです。条件分岐してそれぞれのAstOwnerのタイプごとにlower_XXXというメソッドを呼んでいるのがわかります。ここまでくると、他のcrateも同じように読めるような気がしてきませんか。
基本的にはこんな感じで、他にもrustc_resolve
、rustc_hir
、rustc_hir_typeck
などもRustを書いたことがあれば同じように下に下に読み進めていけばなんとなく途中まではわかると思います。でも、rustc_borrowck
のようなcrateは複雑なので難しいかもしれません。
issueに取り組んでみよう:推測する力が大事
ここまででどういうふうに読めばいいか雰囲気がつかめたと思うので、これを元に実際にissueに取り組む流れを見ていきたいと思います。自分が過去に取り組んだわりとシンプルなissueを参考に見ていきます。
このissueでは、このようなソースコードが与えられた時に、
issueが上げられた時点ではこういうエラーとsuggestionが得られるということが書いてあります。issueがなぜ上がったかというと、このエラーメッセージが正しくないということです。
正しくないというのは、dereferenceのときに括弧で囲わないといけないのですが、表示されたsuggestionをそのまま適用してもまたコンパイルエラーになるので、正しくはこういうふうにsuggestionを出す必要があります。
これを修正するにあたって最初に何をしたらいいのかというと、さっきのようにエラーメッセージを検索するか、デバッグ時にZフラグを使ってそもそもこのエラーがどこで発生しているのかを特定します。
ここでやらないといけないのは、dereferenceする対象がmethodのreceiverであるときに、条件分岐してそれ用のハンドリングをしてあげることです。
何から始めたらいいかわからないと仮定して、まずこのspan
に定義ジャンプしようと思います。
そうすると、同じメソッドの上の方、画面では一番下にlet span = obligation.cause.span
という行があり、このspanがobligation.cause.spanというフィールドから取得されることがわかります。上の方を見ると何かはわかりませんが、同じようにobligation.cause
がありますね。
下の方でこのhir_idというものが取れると、HIRの対象のNodeのタイプを判別することができるということが、このオレンジの箇所からなんとなく推測できます。
obligation.cause
の中に、エラーの発生箇所のspanとかhir_idなどの情報が集められていそうなことが、1行目からわかります。詳細は枠の中ですね。
このNode enumの定義を見てみると、ずらっと並んでいる中の枠内にさっき見た‘Expr‘があります。
拡大するとこのようになっているので、さらにExprのenumの中にあるExpr struct
に飛んでみます。
見ると、Exprはkindを持っているので、これでmethodのreceiverかどうか、そもそもmethod callかどうかを判別できそうです。
ただ、上の方を見てみると、arg_hir_idはありますがそもそもこれがmethod callかどうかというのを判別しないとダメなので、さらに拡大したHIR IDが必要です。このFunction ArgumentObligation
というのが何かわからないと仮定して、試しに定義ジャンプして実装を見に行ってみます。
すると、arg_hir_id
以外にcall_hir_id
というのがあるのがわかります。これを使えばいけそうだと推測してやってみましょう。
この実装をcall_hir_id
を使って修正します。まず、得られたcall_hir_id
をもとに、このExprがそもそもmethod callかfunction callかを判別します。
method callで、かつsuggestionを出す対象がそのmethod call のreceiverかどうかを判別して、receiverなら括弧で囲うという処理を加えてやります。
あとは、対応するUIテストを書いて、すべてのテストにパスすることを確認して、プルリクを作って終わりです。これが実際にissueに取り組む流れです。
全体像がつかめなくとも部分的なコントリビュートはできる!
最後にまとめです。基本的には、「Rust Compiler Developer Guide」を読めば、だいたいのことはわかりますので、まずそれを読むのが一番いいと思います。お見せしたように下に下に掘り下げて読んでいけば意外と読めることがお分かりいただけたと思います。最後に、issueに取り込むときに分からないことがあっても、部分部分を推測しながら身につけていけば、全体像を把握しなくても部分的な修正を加えることはできたりします。
以上です。ありがとうございました。