Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

はじめに

Rust へのコントリビューションに関心をお寄せいただき、ありがとうございます! コントリビュートする方法は数多くあり、私たちはそのすべてに感謝しています。

初めてコントリビュートする場合は、walkthrough の章が、典型的なコントリビューションの流れについての良い例になります。

このドキュメントは、網羅的であることを意図したものではありません。 最も有用な事項のクイックガイドとして作られています。 詳細については、 コンパイラをビルドして実行する方法を参照してください。

質問する

質問がある場合は、Rust Zulip サーバーまたは internals.rust-lang.org に投稿してください。 より多くのリソースについては、公式ウェブサイトのチームとワーキンググループの一覧コミュニティページを参照してください。

念のためお伝えすると、すべてのコントリビューターは私たちの行動規範に従うことが期待されています。

コンパイラチーム(または t-compiler)は通常、Zulip の #t-compiler チャンネルにいます。 コンパイラがどのように動作するかについての質問は、#t-compiler/help で行えます。

ぜひ質問してください! 多くの人が「専門家の時間を無駄にしている」と感じると報告していますが、 t-compiler の誰もそのようには感じていません。 コントリビューターは私たちにとって重要です。

また、抵抗がなければ、公開トピックを優先してください。そうすることで他の人も 質問と回答を見ることができ、場合によってはこのガイドへ取り込めるかもしれません :)

ヒント: 英語のネイティブスピーカーではなく、文章を書くことに不安がある場合は、翻訳ツールを使って補助してみてください。 ただし、長く複雑な単語を生成する LLM ツールの使用は避けてください。 日々のチームワークでは、理解しやすくするために シンプルで明確な言葉 が最適です。 小さなタイプミスや文法ミスでさえ、あなたをより人間らしく見せることがあり、人々は人間とのほうがうまくつながれます。

専門家

すべての t-compiler メンバーが rustc のすべての部分の専門家であるわけではありません。 これはかなり大きなプロジェクトです。 コンパイラのさまざまな部分について誰が専門知識を持っていそうかを調べるには、 triagebot の割り当てグループを参照してください。 triagebot.toml ファイル内で [assign* から始まるセクションです。 ただし、誰に ping すればよいか分からない場合でも、遠慮なく質問してください。

コンパイラの特定の部分について専門家を見つけるもう 1 つの方法は、最近コミットした人を確認することです。 たとえば、1.68.2 リリース以降に名前解決に最近取り組んだ人を見つけるには、 git shortlog -n 1.68.2.. compiler/rustc_resolve/ を実行できます。 “Rollup merge” で始まるコミットや @bors によるコミットは無視してください (これらのコミットの詳細については、CI コントリビューション手順を参照してください)。

エチケット

質問には、できるだけ多くの有用な情報を含めるよう配慮していただきたいですが、 Rust へのコントリビュートに慣れていない場合、これが難しいことも理解しています。

何の文脈も提供せずに誰かに ping するだけだと少し迷惑になることがあり、 ノイズを生むだけにもなり得ます。そのため、t-compiler の人たちは 1 日に多くの ping を受け取っているという事実に配慮していただくようお願いします。

何に取り組むべきですか?

Rust プロジェクトはかなり大きく、プロジェクトのどの部分が支援を必要としているのか、 また初心者にとって良い出発点なのかを知るのは難しい場合があります。 以下に、推奨される出発点をいくつか示します。

簡単な issue またはメンター付きの issue

どこから始めればよいかを探している場合は、次の issue 検索を確認してください。 これらのラベルの説明については、Triage を参照してください。 関心のある分野に検索を絞り込むこともできます。 例:

  • repo:rust-lang/rust-clippy は clippy の issue のみを表示します
  • label:T-compiler はコンパイラに関連する issue のみを表示します
  • label:A-diagnostics は診断関連の issue のみを表示します

重要な作業や初心者向けの作業のすべてに issue ラベルが付いているわけではありません。 ラベル付けされていない作業を見つける方法については、以下を参照してください。

繰り返し発生する作業

作業の中には、1 人で行うには大きすぎるものがあります。 この場合、コントリビューター間で作業を調整するために「Tracking issue」を用意するのが一般的です。 大きな時間的コミットメントなしに取り組み始めやすい追跡 issue の例をいくつか示します。

  • 繰り返し発生する作業項目をここに追加してください。

繰り返し発生する作業をさらに見つけた場合は、遠慮なくここに追加してください!

Clippy の issue

Clippy プロジェクトは、コントリビューションのプロセスを可能な限り新規参加者にとって親しみやすいものにするため、 長い時間をかけてきました。 プロセスとコンパイラ内部に慣れるために、まずこれに取り組むことを検討してください。

開始方法の手順については、Clippy コントリビューションガイドを参照してください。

診断関連の issue

多くの診断関連の issue は自己完結しており、コンパイラに関する詳細な背景知識を必要としません。 診断関連の issue の一覧はこちらで確認できます。

放棄されたプルリクエストを引き継ぐ

コントリビューターがプルリクエストを送信したものの、後でそれに取り組む十分な時間がないことに気付いたり、 単純にもう関心がなくなったりすることがあります。 そのような PR は最終的にクローズされることが多く、S-inactive ラベルが付けられます。 これらの PR のいくつかを調べて、作業を引き継いでみることができます。 そのような PR の一覧はこちらで確認できます。

その間に PR が別の方法で実装されている場合は、S-inactive ラベルを 削除する必要があります。 そうでなく、変更にまだ関心があるように見える場合は、 最新の main ブランチの上にプルリクエストをリベースし、新しい プルリクエストを送信して、その機能の作業を続けることができます。

テストを書く

解決済みだが回帰テストがない issue には、E-needs-test ラベルが付けられます。 単体テストを書くことは、リスクが低く、 優先度も低めのタスクであり、新しいコントリビューターがテスト基盤と コントリビューションワークフローに慣れるための素晴らしい機会を提供します。 needs test issue の一覧はこちらで確認できます。

std(標準ライブラリ)へコントリビュートする

std-dev-guideを参照してください。

他の Rust プロジェクトにコードで貢献する

rust-lang/rust リポジトリ以外にも、cargomirirustup など、貢献できるプロジェクトは数多くあります。

これらのリポジトリには、それぞれ独自のコントリビューションガイドラインや手順がある場合があります。 その多くはワーキンググループによって管理されています。 詳細については、それらのリポジトリの README にあるドキュメントを参照してください。

その他の貢献方法

他にも貢献できる方法は数多くあります。特に、巨大な rust-lang/rust コードベースにいきなり飛び込むことに不安がある場合に有用です。

以下のタスクは、多くの背景知識がなくても実行でき、しかも非常に役立ちます。

クローンとビルド

“コンパイラをビルドして実行する方法”を参照してください。

コントリビューターの手順

このセクションは “コントリビューションの手順” の章に移動しました。

その他のリソース

このセクションは “このガイドについて” の章に移動しました。

このガイドについて

このガイドは、rustc(Rust コンパイラ)がどのように動作するかを文書化するのに役立てること、 また新しいコントリビューターが rustc 開発に参加しやすくすることを目的としています。

このガイドはいくつかの部で構成されています。

  1. rustc のビルドとデバッグ: どのような形でコントリビュートする場合にも役立つはずの情報、 すなわちビルド、デバッグ、プロファイリングなどについての情報が含まれています。
  2. Rust へのコントリビュート: どのような形でコントリビュートする場合にも役立つはずの情報、 すなわちコントリビューションの手順、git と GitHub の使用、機能の安定化などについての情報が含まれています。
  3. ブートストラップ: Rust コンパイラが以前のバージョンを使って自身をビルドする仕組みについて説明します。これには、 ブートストラッププロセスの概要とデバッグ方法の紹介が含まれます。
  4. 高レベルなコンパイラアーキテクチャ: コンパイラの高レベルなアーキテクチャとコンパイルプロセスの各段階について説明します。
  5. ソースコード表現: ユーザーから受け取った生のソースコードを、 コンパイラが扱いやすいさまざまな形式へ変換するプロセスについて説明します。
  6. 支援インフラストラクチャ: コマンドライン引数の規約、rustc_driver や rustc_interface のようなコンパイラのエントリーポイント、エラーと lint の設計および実装を扱います。
  7. 解析: コードのさまざまな性質をチェックし、コンパイルプロセスの後続段階(例: 型チェック)に情報を提供するために コンパイラが使用する解析について説明します。
  8. MIR からバイナリへ: リンク済みの実行可能な機械語コードがどのように生成されるか。
  9. 末尾に、有用な参考情報を含む付録があります。 用語集を含め、異なる情報を扱うものがいくつかあります。

絶え間ない変化

rustc は実際のプロダクション品質のプロダクトであり、 かなりの数のコントリビューターによって継続的に作業されていることを念頭に置いてください。 そのため、コードベースの変更や技術的負債も相応に存在します。 加えて、このガイド全体で論じられている多くのアイデアは、 まだ完全には実現されていない理想化された設計です。 これらすべてのため、このガイドをあらゆる点で完全に最新の状態に保つことは非常に困難です!

もちろん、このガイド自体もオープンソースであり、 ソースは GitHub リポジトリでホストされています。 ガイドに誤りを見つけた場合は、issue を提出してください。 さらによいのは、修正を含む PR を開くことです!

このガイドにコントリビュートする場合は、 このガイドのドキュメント作成に関する対応する小節を参照してください。

「『すべての条件づけられたものは無常である』―― これを智慧によって見るとき、人は苦しみから離れる。」 ダンマパダ、第277偈

情報を見つけるその他の場所

現在あなたが読んでいるこのガイドには、 コンパイラのさまざまな部分がどのように動作するか、 そしてコンパイラにどのようにコントリビュートするかについての情報が含まれています。

次のサイトも役立つかもしれません。

  • rustc API docs – コンパイラ、devtools、内部ツール向けの rustdoc ドキュメント
  • Forge – Rust インフラストラクチャ、チーム手順などに関するドキュメントが含まれています
  • compiler-team – Rust コンパイラチームの拠点であり、チーム手順、 アクティブなワーキンググループ、チームカレンダーについての説明があります。
  • std-dev-guide – 標準ライブラリを開発するための同様のガイド。
  • rust-analyzer book – rust-analyzer のドキュメント。
  • The t-compiler Zulip
  • Rust Internals フォーラム。質問したり、Rust の内部について議論したりする場所です
  • Rust リファレンス。Rust の内部について特に述べているわけではありませんが、 それでも非常に優れたリソースです
  • 古くなってはいますが、Tom Lee の優れたブログ記事は非常に役立ちます
  • Rust Compiler Testing Docs
  • @bors については、このチートシートが役立ちます
  • プログラミング中、Google はいつでも役に立ちます。 すべての Rust ドキュメントを検索(標準ライブラリ、 コンパイラ、各種書籍、リファレンス、ガイド)して、言語やコンパイラに関する 情報をすばやく見つけることができます。
  • また、Rustdoc に組み込まれた検索機能を使用して、見ている crate 内の 型や関数に関するドキュメントを見つけることもできます。 型シグネチャでも検索できます! たとえば、* -> vec を検索すると、Vec<T> を返す関数がすべて見つかるはずです。 ヒント: Rustdoc の任意のページで ? を入力すると、さらに多くのヒントやキーボードショートカットを確認できます!

コンパイラをビルドして実行する方法

profile = "library" のユーザー、または download-rustc = true | "if-unchanged" を使用しているユーザーは、 download-rustc が有効な場合(つまり、コンパイラに変更がない場合)の ./x test library/std フローは現在壊れていることに注意してください。 これは https://github.com/rust-lang/rust/issues/142505 で追跡されています。 このケースで影響を受けるのは ./x test フローだけであり、 ./x {check,build} library/std は引き続き動作するはずです。

短期的には、./x test library/std のために download-rustc を無効にする必要がある場合があります。 これは次のいずれかの方法で行えます。

  1. ./x test library/std --set rust.download-rustc=false
  2. または、bootstrap.tomlrust.download-rustc = false を設定します。

残念ながら、その場合は stage 1 コンパイラをビルドする必要があります。 bootstrap チームはこれに取り組んでいますが、 保守可能な修正の実装にはしばらく時間がかかっています。

コンパイラは x.py というツールを使用してビルドされます。 これを実行するには Python がインストールされている必要があります。

クイックスタート

コンパイラを実行できるようにするための、より簡潔なクイックスタートについては、クイックスタートを参照してください。

ソースコードを取得する

メインのリポジトリは rust-lang/rust です。 これには、コンパイラ、 標準ライブラリ(corealloctestproc_macro などを含む)、 および多数のツール(例: rustdoc、ブートストラップ基盤など)が含まれています。

rustc に取り組む最初のステップは、リポジトリをクローンすることです。

git clone https://github.com/rust-lang/rust.git
cd rust

リポジトリを部分クローンする

リポジトリのサイズが大きいため、遅いインターネット接続でクローンすると長い時間がかかることがあり、 すべてのファイルとディレクトリの完全な履歴を保存するためのディスク容量も必要です。 代わりに、git に 部分クローン を実行するよう指示できます。これにより、現在のファイル内容のみを完全に取得し、 たとえば履歴をさかのぼるときに、追加のファイル内容を自動的に取得します。 すべての git コマンドは通常どおり動作し続けますが、まだ読み込まれていない履歴上の地点を参照するには インターネット接続が必要になります。

git clone --filter='blob:none' https://github.com/rust-lang/rust.git
cd rust

: このリンク では、この種のチェックアウトについてより詳しく説明しており、shallow clone などの 他のモードとの比較も行っています。

リポジトリを shallow clone する

部分クローンより古い代替手段として、代わりにリポジトリを shallow clone する方法があります。 そのためには、git clone コマンドで --depth N オプションを使用できます。 これは、git に「shallow clone」を実行するよう指示し、リポジトリをクローンしますが、 最後の N 個のコミットまで履歴を切り詰めます。

--depth 1 を渡すと、git はリポジトリをクローンしますが、履歴を main ブランチ上の最新の コミットまで切り詰めます。これは通常、ソースコードの閲覧やコンパイラのビルドには十分です。

git clone --depth 1 https://github.com/rust-lang/rust.git
cd rust

: shallow clone では、実行できる git コマンドが制限されます。 コンパイラに取り組み、貢献するつもりであれば、 通常は上記のようにリポジトリを完全にクローンするか、 代わりに部分クローンを実行することが推奨されます。

たとえば、git bisectgit blame にはコミット履歴へのアクセスが必要なため、 リポジトリが --depth 1 でクローンされている場合は動作しません。

x.py とは何か?

x.pyrust リポジトリのビルドツールです。 ドキュメントのビルド、テストの実行、コンパイラと標準ライブラリのビルドを行えます。

この章では、生産的に作業するための基本に焦点を当てていますが、 x.py についてさらに学びたい場合は、この章を読んでください

また、x.py ではなく x を使用することが推奨されます。その理由は次のとおりです。

./x は、すべてのシステムで動作する可能性が最も高いです(Unix では Python バージョン検出を行うシェルスクリプトを実行し、 Windows ではおそらく powershell スクリプトを実行します。多くの場合単に ファイルをエディタで開いてしまう ./x.py よりも、壊れにくいのは確かです)。1

x.ps1 のような、プラットフォーム関連のスクリプトは x.py の周辺にあります)

これは絶対的なものではないことに注意してください。 たとえば、Win10 上の VSCode で Nushell を使用している場合、 x または ./x と入力しても、プログラムが起動されるのではなく、依然として x.py がエディタで開かれます。

このガイドの残りの部分では、x.py を直接ではなく x を使用します。 次のコマンドは、

./x check

次のように置き換えることができます。

./x.py check

x.py を実行する

x.py コマンドは、ほとんどの Unix システムで次の形式で直接実行できます。

./x <subcommand> [flags]

これは、ドキュメントと例が x.py を実行していると想定している方法です。 いくつかの代替方法は次のとおりです。

# 必要な `python3` コマンドがない場合の Unix シェルで
./x <subcommand> [flags]

# Windows Powershell で(powershell がスクリプトを実行するように構成されている場合)
./x <subcommand> [flags]
./x.ps1 <subcommand> [flags]

# Windows コマンド プロンプトで(.py ファイルが Python を実行するように構成されている場合)
x.py <subcommand> [flags]

# Python を自分で実行することもできます。例:
python x.py <subcommand> [flags]

Windows では、Powershell コマンドによって次のようなエラーが表示される場合があります。

PS C:\Users\vboxuser\rust> ./x
./x : File C:\Users\vboxuser\rust\x.ps1 cannot be loaded because running scripts is disabled on this system. For more
information, see about_Execution_Policies at https://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ ./x
+ ~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

powershell がローカルスクリプトを実行できるようにすることで、このエラーを回避できます。

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

x.py を少し便利に実行する

src/tools/x には、x.py をラップする x というバイナリがあります。 これが行うことは x.py の実行だけですが、システム全体にインストールでき、チェックアウトの任意のサブディレクトリから実行できます。 また、使用する適切なバージョンの python も検索します。

cargo install --path src/tools/x でインストールできます。

これが別のグローバルにインストールされたバイナリユーティリティであることを明確にしておくと、これは x.py とは何か セクションで説明したものに似ていますが、 プラットフォーム関連のスクリプトを実行するためにシェルを呼び出すのではなく、 独立したプロセスとして動作して x.py を実行します。

bootstrap.toml を作成する

まず、./x setup を実行して compiler のデフォルトを選択します。 これにより、いくつかの初期化が行われ、妥当なデフォルトを備えた bootstrap.toml が作成されます。 別のデフォルトを使用する場合(コンパイラ以外の rust の領域、 たとえば rustdoc に貢献したい場合には、おそらくそうすることになるでしょう)、 そのデフォルトに関する情報(src/bootstrap/defaults にあります)を必ず読んでください。 他のデフォルトではビルドプロセスが異なる場合があります。

あるいは、bootstrap.toml を手で書くこともできます。 利用可能なすべての設定とそれらの働きについては、bootstrap.example.toml を参照してください。 変更する一般的な設定については、src/bootstrap/defaults を参照してください。 すでに rustc をビルド済みで、LLVM に関連する設定を変更した場合、その後の設定変更を有効にするには ./x clean --all を実行しなければならないことがあります。 ./x clean では LLVM は再ビルドされないことに注意してください。

一般的な x コマンド

ここでは、rustcstdrustdoc、およびその他のツールで作業する際に 最もよく使われる x コマンドの基本的な呼び出しを示します。

CommandWhen to use it
./x checkほとんどのものがコンパイルできるかを素早く確認する場合。rust-analyzer はこれを自動的に実行できます
./x buildrustcstdrustdoc をビルドします
./x testすべてのテストを実行します
./x fmtすべてのコードをフォーマットします

記載されているとおり、これらのコマンドは妥当な出発点です。 ただし、本格的な開発作業のために学んでおく価値のある追加のオプションや引数が、 それぞれにあります。 特に、./x build./x test には、 コードの一部だけをコンパイルまたはテストする方法が多数用意されており、多くの時間を節約できます。

また、xcompilerlibrary、 および src/tools ディレクトリに対するあらゆる種類のパスサフィックスをサポートしていることにも注意してください。 そのため、x test src/tools/tidy の代わりに単に x test tidy を実行できます。 また、x build library/std の代わりに x build std を実行できます。

詳細については、テストrustdoc の章を参照してください。

コンパイラをビルドする

ビルドには比較的大きなストレージ容量が必要になることに注意してください。 コンパイラをビルドするには、10 または 15 ギガバイト以上を利用できるようにしておくとよいでしょう。

bootstrap.toml を作成したら、x を実行する準備が整います。 ここには多くのオプションがありますが、ローカルコンパイラをビルドする際の おそらく最適な「定番」コマンドから始めましょう。

./x build library

このコマンドが行うことは次のとおりです。

  • stage0 コンパイラと stage0 std を使用して rustc をビルドします。
  • 直前にビルドされた stage1 コンパイラで library(標準ライブラリ)をビルドします。
  • stage1 コンパイラと stage1 標準ライブラリを含む、動作する stage1 sysroot を組み立てます。

この最終成果物(stage1 コンパイラ + そのコンパイラを使用してビルドされた libs)が、 他の Rust プログラムをビルドするために必要なものです(#![no_std] または #![no_core] を使用する場合を除きます)。

stage1 std のビルドがボトルネックになることにおそらく気づくでしょうが、 心配はいりません。(ハック的な)回避策があります… std の再ビルドを避けるセクションを参照してください。

完全なビルドが必要ない場合もあります。 メソッド名の変更や、何らかの関数のシグネチャ変更のような、 「型に基づくリファクタリング」を行う場合は、代わりに ./x check を使用すると、はるかに高速にビルドできます。

このコマンド全体で得られるのは、完全な rustc ビルドの一部だけであることに注意してください。 完全な rustc ビルド(./x build --stage 2 rustc で得られるもの)には、さらにかなり多くの手順があります。

  • stage1 コンパイラで rustc をビルドします。
    • ここで生成されるコンパイラは「stage2」コンパイラと呼ばれ、前のコマンドで得られた stage1 std を使用します。
  • stage2 コンパイラで librustdoc とその他多数のものをビルドします。

これを行う必要はほとんどありません。

特定のコンポーネントをビルドする

標準ライブラリで作業している場合、おそらく他のすべてのデフォルトコンポーネントをビルドする必要はありません。 代わりに、次のように名前を指定して特定のコンポーネントをビルドできます。

./x build --stage 1 library

x setup の実行時に library プロファイルを選択した場合は、--stage 1 を省略できます(これが デフォルトです)。

ツールをビルドしたい場合は、次を使用できます。

./x build src/tools/cargo

ツールのテストに関するセクションも確認できます。

rustup ツールチェーンを作成する

rustc のビルドに成功すると、build ディレクトリ内に多数のファイルが作成されます。 生成された rustc を実際に実行するには、rustup ツールチェーンを作成することを推奨します。 以下に示す最初のコマンドは、上記の手順でビルドされた stage1 ツールチェーンを stage1 という名前で作成します。 2 番目のコマンドは、stage1 コンパイラを使用して stage2 ツールチェーンを作成します。 これは将来、テストスイート全体を実行する場合に必要になりますが、 このページではビルドされません。 stage2 のビルドは stage1 の場合と同じ ./x build コマンドで行いますが、 代わりにステージが 2 であることを指定します。

rustup toolchain link stage1 build/host/stage1
rustup toolchain link stage2 build/host/stage2

これで、ビルドした rustc をツールチェーン経由で実行できます。 -vV を付けて実行すると、ローカル環境からのビルドであることを示す -dev で終わる バージョン番号が表示されるはずです。

$ rustc +stage1 -vV
rustc 1.48.0-dev
binary: rustc
commit-hash: unknown
commit-date: unknown
host: x86_64-unknown-linux-gnu
release: 1.48.0-dev
LLVM version: 11.0

rustup ツールチェーンは、build ディレクトリ内でコンパイルされた指定のツールチェーンを指すため、 そのツールチェーン/ステージに対して x build または x test が実行されるたびに、 rustup ツールチェーンも更新されます。

注意: ビルドしたツールチェーンには cargo が含まれていません。 この場合、rustup は インストール済みの nightlybeta、または stable ツールチェーンの cargo の使用にフォールバックします (この順序で)。 不安定な cargo フラグを使用する必要がある場合は、まだであれば必ず rustup install nightly を実行してください。 カスタムツールチェーンに関する rustup ドキュメントを参照してください。

注意: rust-analyzer と IntelliJ Rust プラグインは、proc macros を扱うために rust-analyzer-proc-macro-srv というコンポーネントを使用します。 プロジェクトでカスタムツールチェーンを使用するつもりがある場合 (例: rustup override set stage1 経由)、このコンポーネントを ビルドするとよいでしょう。

./x build proc-macro-srv-cli

クロスコンパイル用のターゲットをビルドする

他のターゲット向けにクロスコンパイルできるコンパイラを生成するには、 任意の数の target フラグを x build に渡します。 たとえば、ホストプラットフォームが x86_64-unknown-linux-gnu で、 クロスコンパイルターゲットが wasm32-wasip1 の場合は、次のようにビルドできます。

./x build --target x86_64-unknown-linux-gnu,wasm32-wasip1

生成されるコンパイラが proc macros やビルドスクリプトを含む crate をビルドできるようにしたい場合は、 ホストプラットフォーム(この場合は x86_64-unknown-linux-gnu)のターゲットサポートを 明示的にビルドする必要があることに注意してください。

x build にフラグを渡さずに常に他のターゲット向けにビルドしたい場合は、 次のように bootstrap.toml[build] セクションで設定できます。

build.target = ["x86_64-unknown-linux-gnu", "wasm32-wasip1"]

一部のターゲット向けにビルドするには、外部依存関係がインストールされている必要がある点に注意してください (例: musl ターゲットをビルドするには、musl のローカルコピーが必要です)。 ターゲット固有の設定(例: musl のローカルコピーへのパス)は、 bootstrap.toml で指定する必要があります。 ターゲット固有の設定キーについては、bootstrap.example.toml を参照してください。

ターゲットをビルドするために必要な完全な設定例については、 rustc book にアクセスし、 左側の「Platform Support」見出しの下にある任意のターゲットを選択して、 そのターゲット用のコンパイラをビルドすることに関連するセクションを参照してください。 rustc book に対応するページがないターゲットについては、 Rust インフラストラクチャ自体がクロスコンパイルのセットアップと設定に使用している Dockerfile を調べる と役立つ場合があります。

rustup ツールチェーンを作成する前のセクションの手順に従っている場合、 コンパイラをビルドしたら、次のようにクロスコンパイルに使用できるようになります。

cargo +stage1 build --target wasm32-wasip1

その他の x コマンド

他にも便利な x コマンドがいくつかあります。 それらの一部については、他のセクションで詳しく説明します。

  • ビルド関連:
    • ./x build – stage 1 コンパイラを使用してすべてをビルドします。 std までだけではありません
    • ./x build --stage 2rustdoc を含め、stage 2 コンパイラですべてをビルドします
  • テストの実行(詳細については テストの実行に関するセクション を参照してください):
    • ./x test library/stdstd のユニットテストと統合テストを実行します
    • ./x test tests/uiui テストスイートを実行します
    • ./x test tests/ui/const-generics - ui テストスイートの const-generics/ サブディレクトリにあるすべてのテストを実行します
    • ./x test tests/ui/const-generics/const-types.rs - ui テストスイートの 単一のテスト const-types.rs を実行します

ビルドディレクトリのクリーンアップ

場合によっては最初からやり直す必要がありますが、通常はそうではありません。 これを実行する必要がある場合、bootstrap が正常に動作していない可能性が非常に高いため、 何が問題なのかについてバグを報告するべきです。 すべてをクリーンアップする必要がある場合は、1 つのコマンドを実行するだけで済みます!

./x clean

rm -rf build でも機能しますが、その場合は LLVM を再ビルドする必要があり、 高速なコンピューターでも長い時間がかかることがあります。

ディスク容量に関する注意

コンパイラをビルドするには(特に stage 1 を超える場合)、かなりの空きディスク 容量が必要になることがあり、およそ 100GB 程度になる可能性があります。 これは、rust-analyzer 用に別のビルドディレクトリ(例: build-rust-analyzer)がある場合に さらに大きくなります。これは、各ユーザーに 設定されたディスク クォータ がある dev-desktop で発生しやすいですが、ローカル開発にも当てはまります。 場合によっては、次のことが必要になることがあります。

  • build/ ディレクトリを削除する。
  • build-rust-analyzer/ ディレクトリを削除する(別の rust-analyzer ビルドディレクトリがある場合)。
  • cargo-bisect-rustc を使用している場合は、不要なツールチェーンをアンインストールする。 インストールされているツールチェーンは rustup toolchain list で確認できます。

  1. issue#1707

クイックスタート

これは、コンパイラを動作させるためのクイックスタートガイドです。各手順の詳細については、この章の他のページを参照してください。

まず、リポジトリをクローンします。

git clone https://github.com/rust-lang/rust.git
cd rust

コンパイラをビルドするときは、cargo を直接使うのではなく、“x” というラッパーを使います。これは ./x で呼び出します。

ビルド用の設定を作成する必要があります。適切なデフォルト設定を作成するには ./x setup を使用します。

./x setup

次に、コンパイラをビルドできます。コンパイラ、標準ライブラリ、およびいくつかのツールをビルドするには ./x build を使用します。また、単にチェックするだけであれば ./x check も使用できます。これらのコマンドはいずれも、特定のコンポーネントやパスを引数として受け取ることができます。たとえば、コンパイラだけをチェックするには ./x check compiler を使用します。

./x build

標準ライブラリのコンパイル方法に影響しないコンパイラの変更を行う場合(たとえば、エラーメッセージの変更など)は、再コンパイルを避けるために --keep-stage-std 1 を使用してください。

コンパイラと標準ライブラリをビルドすると、動作するコンパイラツールチェーンが手元にできます。リンクすることで、rustup から使用できます。

rustup toolchain link stage1 build/host/stage1

: ./x setup tools を使用した場合、デフォルトのステージは 1 ではなく 2 に設定されます。コマンドをそれに合わせて調整してください。

rustup toolchain link stage2 build/host/stage2

これで、ビルドにリンクされた stage1 という名前のツールチェーンができました。これを使ってコンパイラをテストできます。

rustc +stage1 testfile.rs

変更を行った後は、./x test でコンパイラのテストスイートを実行できます。

./x test はテストスイート全体を実行しますが、これは遅く、通常は必要なものではありません。通常、コンパイラを変更した後に必要なのは ./x test tests/ui です。これは、特定のテストファイルに対してコンパイラを呼び出し、その出力を確認するすべての UI テストをテストします。

./x test tests/ui

変更を行い、新しい出力で .stderr ファイルを更新したい場合は、--bless を使用してください。

おめでとうございます。これでコンパイラに変更を加える準備ができました! さらに質問がある場合は、章全体に答えが含まれているかもしれません。含まれていない場合は、Zulip で気軽に助けを求めてください。

VSCode、Vim、Emacs、Helix、または Zed を使用している場合、./x setup はエディター設定をセットアップするかどうかを尋ねます。詳細については、推奨ワークフローを確認してください。

前提条件

依存関係

rust-lang/rust の INSTALL を参照してください。

ハードウェア

ビルドするにはインターネット接続が必要です。 ブートストラップ処理では、 git サブモジュールの更新とベータコンパイラのダウンロードを行います。 非常に高速である必要はありませんが、高速であれば役に立ちます。

厳密なハードウェア要件はありませんが、コンパイラのビルドは 計算負荷が高いため、より高性能なマシンのほうが役に立ちます。また、 Raspberry Pi でビルドしようとすることはお勧めしません! 以下を推奨します。

  • 30GB 以上の空きディスク容量。 そうでない場合は、インクリメンタルキャッシュを継続的に削除する必要があります。 容量は多いほどよいです。コンパイラは少々 容量を消費しがちです。これは私たちも認識している問題です。
  • 8GB 以上の RAM
  • 2 コア以上。 コア数が多いと非常に役に立ちます。 10 個、20 個、あるいはそれ以上でも多すぎることはありません!

より高性能なマシンでは、ビルドがはるかに速くなります。 マシンの性能があまり高くない場合、 よく使われる戦略は、ローカルマシンでは ./x check のみを使い、 PR ブランチにプッシュしたときに CI ビルドで変更をテストさせることです。

そこそこ高性能なノート PC でも、コンパイラのビルドには 30 分以上かかります。 LLVM をソースからビルドしなくて済むように、 CI からダウンロードすることをお勧めします (こちらを参照)。

cargo と同様に、ビルドシステムは可能な限り多くのコアを使用します。 これにより、メモリ不足になることがあります。 -j を使用して、同時実行ジョブ数を調整できます。 フルビルドに約 45 分から 1 時間を超える時間がかかる場合、 おそらくほとんどの時間をメモリのスワップイン・スワップアウトに費やしています。 -j1 を試してください。

空きディスク容量があまり多くない場合は、 インクリメンタルコンパイルを無効にしたいかもしれません(こちらを参照)。 これによりコンパイルに 時間がかかるようになります(特にリベース後)が、 インクリメンタルキャッシュによる大量の容量消費を抑えられます。

推奨ワークフロー

完全なブートストラップ処理にはかなり時間がかかります。 作業を楽にするための提案をいくつか示します。

pre-push フックのインストール

CI は、コード品質を確保するための内部ツールである tidy に合格しない場合、ビルドを自動的に失敗させます。 必要であれば、各 push 時に ./x test tidy を自動的に実行し、コードが基準を満たしていることを確認する Git hook をインストールできます。 フックが失敗した場合は、./x test tidy --bless を実行して変更をコミットしてください。 後で pre-push の動作が望ましくないと判断した場合は、.git/hooks 内の pre-push ファイルを削除できます。

ビルド済みの git フックは src/etc/pre-push.sh にあります。 これを .git/hooks フォルダーに pre-push としてコピーできます(.sh 拡張子は付けません!)。

./x setup の実行手順の一部としてフックをインストールすることもできます!

設定拡張

異なるタスクに取り組む場合、異なるブートストラップ設定を切り替える必要があるかもしれません。 将来の使用に備えて古い設定を保持しておきたい場合もあります。 しかし、生の設定値を ランダムなファイルに保存して手動でコピー&ペーストすると、特にさまざまな設定の長い履歴がある場合、すぐに煩雑になります。

複数の設定の管理を簡単にするために、設定拡張を作成できます。

たとえば、cross.toml という名前の単純な設定ファイルを作成できます。

[build]
build = "x86_64-unknown-linux-gnu"
host = ["i686-unknown-linux-gnu"]
target = ["i686-unknown-linux-gnu"]


[llvm]
download-ci-llvm = false

[target.x86_64-unknown-linux-gnu]
llvm-config = "/path/to/llvm-19/bin/llvm-config"

次に、これを bootstrap.toml に含めます。

include = ["cross.toml"]

拡張の中に拡張を再帰的に含めることもできます。

注: include フィールドでは、上書きロジックは右から左の順序に従います。 たとえば、 include = ["a.toml", "b.toml"] では、拡張 b.tomla.toml を上書きします。 また、親の拡張は常に内側の拡張を上書きします。

rust-analyzerrustc 用に設定する

“library” ツリーのチェック

“library” ツリーのチェックには stage1 コンパイラが必要であり、コンピューターによっては重い処理になる場合があります。 このため、ブートストラップには --skip-std-check-if-no-download-rustc というフラグがあり、rust.download-rustc が利用できない場合は “library” ツリーのチェックをスキップします。 rust-analyzer によってコンピューターに重い負荷をかけるのを避けたい場合は、rust-analyzer 設定の ./x check コマンドに --skip-std-check-if-no-download-rustc フラグを追加できます。

プロジェクトローカルの rust-analyzer セットアップ

rust-analyzer は、ファイルを保存するたびにコードのチェックとフォーマットを行うのに役立ちます。 デフォルトでは、rust-analyzercargo check コマンドと rustfmt コマンドを実行しますが、 rustc に取り組む場合、これらのツールのより適したバージョンを使用するように、これらのコマンドを上書きできます。 カスタムセットアップにより、rust-analyzer はソースのチェックに ./x check を使用し、 フォーマットには stage 0 の rustfmt を使用できます。

デフォルトの rust-analyzer.check.overrideCommand コマンドラインは、リポジトリ内のすべての クレートとツールをチェックします。 特定の部分に取り組んでいる場合は、 チェック時間を節約するために、作業中の部分だけをチェックするようにコマンドを上書きできます。 たとえば、コンパイラに取り組んでいる場合は、 コンパイラ部分だけをチェックするためにコマンドを x check compiler --json-output に上書きできます。 利用可能な部分を確認するには、x check --help --verbose を実行できます。

./x setup editor を実行すると、サポートされているエディターのいずれかについて、プロジェクトローカルの LSP 設定 ファイルを作成するよう求められます。 ./x setup の実行手順の一部として設定ファイルを作成することもできます。

rust-analyzer に別のビルドディレクトリを使用する

デフォルトでは、rust-analyzer がチェックまたはフォーマットコマンドを実行する場合、手動のコマンドラインビルドと 同じビルドディレクトリを共有します。 これは、次の 2 つの理由で不便な場合があります。

  • 各ビルドはビルドディレクトリをロックし、もう一方を待機させるため、rust-analyzer がバックグラウンドで コマンドを実行している間は、コマンドラインビルドを実行できなくなります。
  • コンパイラフラグやその他の設定の競合により、いずれかのビルドが以前にビルドされた アーティファクトを削除してしまうリスクが高まり、場合によっては追加の再ビルドが必要になります。

これらの問題を回避するには、次のようにします。

  • エディターの rust-analyzer 設定内のすべてのカスタム x コマンドに --build-dir=build-rust-analyzer を追加します。 (必要に応じて別のディレクトリ名を選んでもかまいません。)
  • rust-analyzer.rustfmt.overrideCommand 設定を変更して、別のビルドディレクトリ内の rustfmt のコピーを指すようにします。
  • rust-analyzer.procMacro.server 設定を変更して、別のビルドディレクトリ内の rust-analyzer-proc-macro-srv のコピーを指すようにします。

コマンドラインビルドと rust-analyzer に別々のビルドディレクトリを使用するには、 追加のディスク容量が必要です。

Visual Studio Code

./x setup editorvscode を選択すると、Visual Studio code を設定する .vscode/settings.json ファイルを作成するよう求められます。 推奨される rust-analyzer 設定は src/etc/rust_analyzer_settings.json にあります。

保存時に ./x check を実行するのが不便な場合、VS Code では代わりに Build Task を使用できます。

// .vscode/tasks.json
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "./x check",
            "command": "./x check",
            "type": "shell",
            "problemMatcher": "$rustc",
            "presentation": { "clear": true },
            "group": { "kind": "build", "isDefault": true }
        }
    ]
}

Neovim

Neovim ユーザーには、いくつかの選択肢があります。

  1. 最も簡単な方法は neoconf.nvim を使用することですが、これは 非推奨の require('lspconfig') API を使用しており、neovim 0.11+ で警告が表示されます。
  2. coc.nvim を使用することも別の選択肢ですが、node.js がインストールされている必要があります。
  3. rust-analyzer 設定を読み込むためのカスタムスクリプトを使用します。

neoconf.nvim

neoconf.nvim により、ネイティブ LSP でプロジェクトローカルの設定 ファイルを使用できます。 使用方法の手順は以下のとおりです。 これらは rust-analyzer がすでに Neovim で設定されていることを必要とする点に注意してください。 その手順は こちら にあります。

  1. まずプラグインをインストールします。 これは README の手順に従って行えます。
  2. ./x setup editor を実行し、vscode を選択して .vscode/settings.json ファイルを作成します。 このファイルが検出されると、プロジェクトが開かれたときに neoconf は rust-analyzer 設定を自動的に読み取り、更新できます。

coc.nvim

coc.nvim を使用している場合は、./x setup editor を実行して vim を選択し、 .vim/coc-settings.json を作成できます。 設定は :CocLocalConfig で編集できます。 推奨設定は src/etc/rust_analyzer_settings.json にあります。

カスタム LSP 設定

neovim 0.11+ を実行している場合は、 nvim-lspconfig とカスタムスクリプトだけで rust-analyzer を設定できます。

  1. rust-analyzer LSP がセットアップされていることを確認します
  2. 次の内容で $HOME/.config/nvim/after/plugged/rust_analyzer.lua を作成します。
```lua
-- 上書きする前に、nvim-lspconfig/lsp/rust_analyzer.lua からデフォルト関数を取得します。
-- このファイルは after/plugin にあるため、nvim-lspconfig がすでに初期化済みであることが保証されます。
local default_root_dir = vim.lsp.config["rust_analyzer"].root_dir
local default_before_init = vim.lsp.config["rust_analyzer"].before_init

vim.lsp.config("rust_analyzer", {
    cmd = { "rust-analyzer" },
    filetypes = { "rust" },
    -- rust-lang/rust をサポートするには、rust リポジトリ内にいることを検出し、cargo プロジェクトのルートではなく
    -- git ルートを使用する必要があります。
    root_dir = function(bufnr, on_dir)
        local git_root = vim.fs.root(bufnr, { ".git" })
        if git_root then
            if vim.uv.fs_stat(vim.fs.joinpath(git_root, "src/etc/rust_analyzer_zed.json")) then
                on_dir(git_root)
                return
            end
        end
        -- rust-lang/rust に一致しないものについては、デフォルトの root_dir にフォールバックします
        default_root_dir(bufnr, on_dir)
    end,
    before_init = function(init_params, config)
        -- rust-lang/rust 内にいる場合は、特別な rust-analyzer 設定を使用する必要があります。
        local settings = vim.fs.joinpath(config.root_dir, "src/etc/rust_analyzer_zed.json")
        if vim.uv.fs_stat(settings) then
            local file = io.open(settings)
            -- nvim 0.12+ はコメントをサポートしています。それ以外の場合は content:gsub("//[^\n]*", "") が必要です。
            local json = vim.json.decode(file:read("*a"), { skip_comments = true })
            file:close()
            config.settings["rust-analyzer"] = vim.tbl_deep_extend(
                "force", -- 競合がある場合は特別な設定で上書きします。
                config.settings["rust-analyzer"] or {},
                json.lsp["rust-analyzer"].initialization_options
            )
        end
        default_before_init(init_params, config)
    end,
})

vim.lsp.enable("rust_analyzer")

上述のビルドタスクを使用したい場合は、自分の設定内で独自のコマンドを作成するか、 overseer.nvim のようなプラグインをインストールできます。 これは VSCode の task.json ファイルを読み取る ことができ、上記と同じ手順に従えます。

Emacs

Emacs は、Eglot を通じて プロジェクトローカル設定による rust-analyzer のサポートを提供します。 Eglot を rust-analyzer とともにセットアップする手順は こちらにあります。 Rust 開発全般向けに Emacs と Eglot のセットアップが済んだら、 ./x setup editor を実行して emacs を選択できます。これにより、 Eglot 向けの推奨設定を含む .dir-locals.el を作成するよう促されます。 推奨設定は src/etc/rust_analyzer_eglot.el にあります。 プロジェクト固有の Eglot 設定について詳しくは、 マニュアルを参照してください。

Helix

Helix には LSP と rust-analyzer のサポートが組み込まれています。 こちらで説明されているように、 languages.toml を通じて設定できます。 ./x setup editor を実行して helix を選択できます。これにより、 Helix 向けの推奨設定を含む languages.toml を作成するよう促されます。 推奨設定は src/etc/rust_analyzer_helix.toml にあります。

Zed

Zed には LSP と rust-analyzer のサポートが組み込まれています。 こちらで説明されているように、 .zed/settings.json を通じて設定できます。 ./x setup editorzed を選択すると、推奨設定で Zed を設定する .zed/settings.json ファイルを作成するよう促されます。 推奨される rust-analyzer 設定は src/etc/rust_analyzer_zed.json にあります。

確認、確認、そして再確認

単純なリファクタリングを行う場合、./x check を継続的に実行すると便利です。 上述のように rust-analyzer をセットアップしていれば、これは ファイルを保存するたびに自動的に行われます。 ここでは、コンパイラがビルドできることを確認しているだけですが、 多くの場合、それだけで十分です(たとえば、メソッド名を変更する場合)。 実際にテストを実行する必要があるときに ./x build を実行できます。

実際には、コードが動作するか 100% 確信できない場合でも、テストを後回しにすると便利なことがあります。 その場合、リファクタリングのコミットを積み重ね続け、後のある時点でテストだけを実行できます。 その後、git bisect を使って、どのコミットが問題を引き起こしたのかを正確に突き止められます。 このスタイルのうれしい副作用は、 最後にはかなり細かい単位のコミットの集合が残り、そのすべてが ビルドでき、テストに合格していることです。 これはレビューに役立つことが多いです。

rustup が nightly を使用するように設定する

ブートストラッププロセスの一部では、rustfmt のようなツールの固定された nightly バージョンを使用します。 リポジトリ内で cargo fmt のようなものが正しく動作するようにするには、 rustup で nightly ツールチェーンをインストール し、次のコマンドを実行します。

cd <path to rustc repo>
rustup override set nightly

これを worktree をセットアップした すべてのディレクトリに対して行うことを忘れないでください。 src/stage0 の固定された nightly バージョンを使用する必要がある場合もありますが、 多くの場合は通常の nightly チャネルで動作します。

注記 実際に x が使用する rustfmt で vscode を設定する方法については VSCode に関するセクションを、ブートストラップされたコンパイラ用に rustup ツールチェーンをセットアップする方法については rustup に関するセクションを参照してください

注記 これで rustc を cargo で直接ビルドできるようになるわけでは_ありません_。 コンパイラまたは標準ライブラリで作業するには、引き続き x を使用する必要があります。これは単に cargo fmt を使えるようにするだけです。

CI-rustc によるより高速なビルド

コンパイラに取り組んでいない場合、多くの場合はコンパイラツリーをビルドする必要はありません。 たとえば、コンパイラのビルドをスキップし、library ツリーまたは src/tools 配下のツールだけをビルドできます。 これを実現するには、設定で download-rustc オプションを設定して有効にする必要があります。 これにより bootstrap は、stage > 0 の ステップに対して最新の nightly コンパイラを使用します。つまり、事前コンパイル済みコンパイラが 2 つ存在することになります: stage0 コンパイラと、stage > 0 のステップ用の download-rustc コンパイラです。 この方法では、ツリー内コンパイラをビルドする必要がまったくなくなります。 その結果、ツリー内コンパイラをビルドしないことで、ビルド時間が大幅に短縮されます。

--keep-stage-std によるより高速な再ビルド

コンパイラがビルドできるかどうかを確認するだけでは不十分な場合があります。 よくある例として、何らかの状態の値を調べたり、問題をよりよく理解したりするために debug! 文を追加する必要がある場合があります。 その場合、実際には完全なビルドは必要ありません。 bootstrap のキャッシュ無効化をバイパスすることで、多くの場合、 これらのビルドを非常に高速に完了できます(たとえば約 30 秒)。唯一の注意点は、 これには多少のごまかしが必要であり、動作しないコンパイラが生成される可能性があることです(ただし、 それは簡単に検出して修正できます)。

使用したいコマンドの流れは次のとおりです。

  • 初回ビルド: ./x build library
  • 2 回目以降のビルド: ./x build library --keep-stage-std=1
    • ここでは --keep-stage-std=1 フラグを追加していることに注意してください

前述のとおり、--keep-stage-std=1 の効果は、古い標準ライブラリを 再利用できると単に_仮定_することです。 コンパイラを編集している場合、これは 多くの場合正しいです。結局、標準ライブラリは変更していないからです。 しかし、正しくない場合もあります。たとえば、型やその他の状態を rlib ファイルへコンパイラがどのようにエンコードするかを制御する、 コンパイラの「メタデータ」部分を編集している場合や、メタデータに含まれるもの (MIR の定義など)を編集している場合です。

要約すると、--keep-stage-std=1 を使用していると、コンパイル時に奇妙な挙動が 発生する可能性があります – たとえば、奇妙な ICE やその他の panic です。 その場合は、コマンドから --keep-stage-std=1 を単に削除して再ビルドしてください。 それで問題は解決するはずです。

テストを実行するときにも --keep-stage-std=1 を使用できます。 次のようになります。

  • 初回テスト実行: ./x test tests/ui
  • 2 回目以降のテスト実行: ./x test tests/ui --keep-stage-std=1

インクリメンタルコンパイルの使用

さらに --incremental フラグを有効にして、2 回目以降の再ビルドでさらに時間を節約できます。

./x test tests/ui --incremental --test-args issue-1234

毎回のコマンドにこのフラグを含めたくない場合は、bootstrap.toml で有効にできます。

[rust]
incremental = true

インクリメンタルコンパイルは通常より多くのディスク容量を使用することに注意してください。 ディスク容量が気になる場合は、build ディレクトリのサイズをときどき確認するとよいでしょう。

最適化の微調整

optimize = false を設定すると、コンパイラがテストには遅すぎるようになります。 ただし、テストサイクルを改善するために、再ビルドが必要な crate に対してのみ最適化を選択的に無効化できます (source)。 たとえば、rustc_mir_build に取り組んでいる場合、rustc_mir_buildrustc_driver の crate はインクリメンタル再ビルドに最も時間がかかります。 そのため、ルートの Cargo.toml に次のように設定できます。

[profile.release.package.rustc_mir_build]
opt-level = 0
[profile.release.package.rustc_driver]
opt-level = 0

複数のブランチで同時に作業する

複数のブランチで並行して作業するのは少し面倒な場合があります。というのも、 あるブランチでコンパイラをビルドすると、古いビルドとインクリメンタル コンパイルキャッシュが上書きされるからです。 1 つの解決策は、リポジトリの clone を複数用意することですが、 それは Git メタデータを複数回保存することを意味し、 各 clone を個別に更新しなければなりません。

幸い、Git には worktrees と呼ばれるよりよい解決策があります。 これにより、同じ Git データベースを共有する複数の「working tree」を作成できます。 さらに、 すべての worktree が同じオブジェクトデータベースを共有するため、いずれかで ブランチ(例: main)を更新すれば、どの worktree からでも新しい commit を使用できます。 ただし、1 つ注意点として、submodule は共有されません。 それらは依然として複数回 clone されます。

Rust リポジトリのルートディレクトリ内にいるとして、次のコマンドを実行することで、 新しい “rust2” ディレクトリに「リンクされた working tree」を作成できます。

git worktree add ../rust2

main を基にした新しいブランチ用に新しい worktree を作成する場合は、次のようになります。

git worktree add -b my-feature ../rust2 main

その後、その rust2 フォルダーを rustc の変更とビルドのための独立したワークスペースとして使用できます!

nix での作業

いくつかの nix 設定が src/tools/nix-dev-shell に定義されています。

direnv を使用している場合は、src/tools/nix-dev-shell/envrc-flake または src/tools/nix-dev-shell/envrc-shell へのシンボリックリンクを作成できます

ln -s ./src/tools/nix-dev-shell/envrc-flake ./.envrc # flake を使用

または

ln -s ./src/tools/nix-dev-shell/envrc-shell ./.envrc # nix-shell を使用

flake を使用している場合は、次のコマンドでそれも更新するようにしてください。

nix flake update --flake ./src/tools/nix-dev-shell

この shell は、すべての依存関係が正しく設定された状態で ./x.py スクリプトを実行する x という名前のコマンドを作成します。

注意

NixOS ではないディストリビューションで nix を使用する場合、 bootstrap.tomlbuild.patch-binaries-for-nix = true を設定する必要がある場合があることに注意してください。Bootstrap は nix 内で実行されているかどうかを検出し、自動的にパッチ適用を有効にしようとしますが、この 検出では false negative が発生する可能性があります。

nix shell を使用して bootstrap.toml を管理することもできます。

let
  config = pkgs.writeText "rustc-config" ''
    # bootstrap.toml の内容をここに記述します
  ''
pkgs.mkShell {
  /* ... */
  # この環境変数は bootstrap に bootstrap.toml の場所を伝えます。
  RUST_BOOTSTRAP_CONFIG = config;
}

Shell 補完

Bash、Zsh、Fish、PowerShell を使用している場合、x.py 用に自動生成された shell 補完スクリプトは src/etc/completions にあります。

source ./src/etc/completions/x.py.<extension> を使用して、選択した shell 用の補完を 読み込むことができます。または、PowerShell の場合は & .\src\etc\completions\x.py.ps1 を使用できます。 これを shell の起動スクリプト(例: .bashrc)に追加すると、この補完が自動的に 読み込まれます。

配布アーティファクトをビルドする

配布用にコンパイラをビルドし、パッケージ化したい場合があります。 そのためには、次のコマンドを実行します。

./x dist

ソースからインストールする

Rust(および設定で構成されたツール)を、ソースからビルドしてインストールしたい場合があります。 その場合は、次のコマンドを実行します。

./x install

注: コンパイラへの変更をテストしている場合は、コンパイラを(./x build で)ビルドしてから、 こちらで説明されているようにツールチェーンを作成するとよいでしょう。

たとえば、作成したツールチェーンの名前が “foo” の場合は、 rustc +foo ... のように呼び出します(ここで … は残りの引数を表します)。

Rust(および設定ファイル内のツール)をグローバルにインストールする代わりに、DESTDIR 環境変数を設定してインストールパスを変更できます。 インストールパスをより動的に設定したい場合は、 設定ファイル内のインストールオプションを使用することを推奨します。

ドキュメントのビルド

この章では、標準ライブラリ(std)やコンパイラ(rustc)など、 ツールチェーンコンポーネントのドキュメントをビルドする方法について説明します。

  • すべてをドキュメント化する

    これは beta ツールチェーンの rustdoc を使用するため、 stage 1 rustdoc とは(わずかに)異なる出力が生成されます。 rustdoc は活発に開発されているためです。

    ./x doc
    

    ドキュメントが CI 上と同じように見えることを確実にしたい場合は、次のようにします。

    ./x doc --stage 1
    

    これにより、(現在の)rustdoc がビルドされ、 その後、それを使用してコンポーネントがドキュメント化されます。

  • 個別のテストを実行したり、特定のコンポーネントをビルドしたりする場合とほぼ同様に、 必要なドキュメントだけをビルドできます。

    ./x doc src/doc/book
    ./x doc src/doc/nomicon
    ./x doc compiler library
    

    ブックの完全な一覧については、nightly ドキュメントのインデックスページを参照してください。

  • rustc の内部項目をドキュメント化する

    コンパイラのドキュメントはデフォルトではビルドされません。 x doc でデフォルトで作成するには、bootstrap.toml を変更します。

    build.compiler-docs = true
    

    有効にすると、内部コンパイラ項目のドキュメントもビルドされることに注意してください。

    注: コンパイラのドキュメントはこちらのリンクにあります。

Rustdoc の概要

rustdoc は、コンパイラおよび標準ライブラリと同じツリー内にあります。 この章では、その仕組みについて説明します。 Rustdoc の機能とその使い方については、 Rustdoc book を参照してください。 rustdoc の仕組みの詳細については、“Rustdoc internals” の章を参照してください。

rustdocrustc の内部機構(そして、もちろん標準ライブラリ)を使用するため、 rustdoc をビルドする前に、コンパイラと std を一度ビルドする必要があります。

Rustdoc は、クレート librustdoc 内に完全に実装されています。 これは、クレートの内部表現(HIR)が得られ、アイテムの型に関するいくつかのクエリを実行できるようになる地点までコンパイラを実行します。 HIRqueries については、リンク先の章で説明されています。

その後、librustdoc は一連のドキュメントをレンダリングするために、主に 2 つのステップを実行します。

  • AST を、ドキュメントの作成により適した形式へ「クリーン」します(また、 コンパイラ内の変更の影響をやや受けにくくします)。
  • このクリーンされた AST を使用して、クレートのドキュメントを 1 ページずつレンダリングします。

当然ながら、実際にはこれ以外にも多くの処理があり、これらの説明では 多くの詳細を簡略化していますが、これが大まかな概要です。

(補足: librustdoc はライブラリクレートです! rustdoc バイナリは src/tools/rustdoc のプロジェクトを使用して作成されます。 ただし、文字どおりそれが行っていることは、このクレートの lib.rs にある main() を呼び出すことだけです。)

チートシート

  • 開始する前に ./x setup tools を実行します。 これにより、rustdoc やその他のツールの開発に適した設定で x が構成されます。これには、 rustc をビルドする代わりにコピーをダウンロードすることも含まれます。
  • コンパイルエラーをすばやく確認するには、./x check rustdoc を使用します。
  • 他のプロジェクトで実行できる利用可能な rustdoc を作成するには、./x build library rustdoc を使用します。
    • rustdoc --test を使用できるようにするには、library/test を追加します。
    • stage2 という名前のカスタムツールチェーンを rustup 環境に追加するには、 rustup toolchain link stage2 build/host/stage2 を実行します。 それを実行した後は、任意のディレクトリで cargo +stage2 doc を実行すると、 ローカルでコンパイルした rustdoc を使用してビルドされます。
  • この rustdoc を使用して標準ライブラリのドキュメントを生成するには、./x doc library を使用します。
    • 完成したドキュメントは build/host/doccoreallocstd の下)で利用できます。
    • それらのドキュメントを Web サーバーにコピーしたい場合は、 build/host/doc 全体をコピーしてください。CSS、JS、フォント、ランディングページはそこにあるためです。
    • フロントエンドのデバッグでは、bootstrap.tomlrust.docs-minification オプションを無効にします。
  • stage1 rustdoc を使用してテストを実行するには、./x test tests/rustdoc* を使用します。
    • テストの詳細については、Rustdoc internals を参照してください。
  • rustdoc の JavaScript チェック(eslintes-checktsc)を実行するには、./x test tidy --extra-checks=js を使用します。

注: ./x test tidy は、JS/TS ソースが変更された場合、これらのチェックをすでに自動的に実行します。--extra-checks=js はそれらを明示的に強制します。

JavaScript CI チェック

Rustdoc の JavaScript と TypeScript は、CI 中に eslintes-checktsc によってチェックされます(compiletest によってではありません)。 これらは tidy ジョブの一部として実行されます。

./x test tidy --extra-checks=js

--extra-checks=js フラグは、CI で実行されるフロントエンドの lint を有効にします。

コード構造

このセクション内のすべてのパスは、rust-lang/rust リポジトリ内の src/librustdoc/ からの相対パスです。

  • HTML 出力コードのほとんどは、html/format.rshtml/render/mod.rs にあります。 それらは、impl std::fmt::Display を返す多数の関数内にあります。
  • 上記の関数によってレンダリングされるデータ型は、clean/types.rs で定義されています。 HIRrustc_middle::ty IR からそれらを作成する役割を持つ関数は、 clean/mod.rs にあります。
  • rustdoc をテストハーネスとして使用することに特有の部分は、doctest.rs にあります。
  • Markdown レンダラーは html/markdown.rs で読み込まれます。これには、 指定された Markdown ブロックから doctest を抽出する関数も含まれます。
  • フロントエンドの CSS と JavaScript は html/static/ に保存されています。
    • JavaScript について。 型注釈は TypeScript-flavored JSDoc コメントと外部の .d.ts ファイルを使用して記述されます。 この方法により、コード自体はプレーンで有効な JavaScript のままになります。 tsc は linter としてのみ使用します。

テスト

rustdoc の統合テストは、複数のテストスイートに分割されています。 詳細については、Rustdoc テストスイートを参照してください。

制約

私たちは、JavaScript が無効な場合やローカルファイルを閲覧している場合でも、rustdoc がある程度適切に動作するように努めています。 [サポート対象ブラウザーの一覧][a list of supported browsers]があります。

ローカルファイル(file:/// URL)のサポートには、いくつかの意外な制約が伴います。 localStorage や Service Workers のように、安全なオリジンを必要とする特定のブラウザー機能は、 信頼性高く動作しません。 そのような機能は引き続き使用できますが、それらがなくてもページが引き続き利用可能であることを確認する必要があります。

Rustdoc は[関数本体の型チェックを行いません][platform-specific docs]。 これは、[typeck の組み込みクエリをオーバーライドすること][override queries]、 [名前解決エラーを抑制すること][silencing name resolution errors]、および[不透明型を解決しないこと][not resolving opaque types]によって機能します。 これにはいくつかの注意点があります。特に、rustdoc は 本体の型チェックを必要とするコンパイラのどの部分も実行できません。 たとえば、.rlib ファイルを生成したり、ほとんどの lint を実行したりすることはできません。 最終的にはこのモデルから移行したいと考えていますが、 [これを使用している人々][async-std]のために、何らかの代替手段が必要です。 [さまざまな][zulip stop accepting broken code] [以前の][rustdoc meeting 2024-07-08] [Zulip][compiler meeting 2023-01-26] [議論][notriddle rfc]を参照してください。 このハックが削除された場合に壊れるコードの例については、 [tests/rustdoc-ui/error-in-impl-trait] を参照してください。 [platform-specific docs]: https://doc.rust-lang.org/rustdoc/advanced-features.html#interactions-between-platform-specific-docs [override queries]: https://github.com/rust-lang/rust/blob/52bf0cf795dfecc8b929ebb1c1e2545c3f41d4c9/src/librustdoc/core.rs#L299-L323 [silencing name resolution errors]: https://github.com/rust-lang/rust/blob/52bf0cf795dfecc8b929ebb1c1e2545c3f41d4c9/compiler/rustc_resolve/src/late.rs#L4517 [not resolving opaque types]: https://github.com/rust-lang/rust/blob/52bf0cf795dfecc8b929ebb1c1e2545c3f41d4c9/compiler/rustc_hir_analysis/src/check/check.rs#L188-L194 [async-std]: https://github.com/rust-lang/rust/issues/75100 [rustdoc meeting 2024-07-08]: https://rust-lang.zulipchat.com/#narrow/channel/393423-t-rustdoc.2Fmeetings/topic/meeting.202024-07-08/near/449969836 [compiler meeting 2023-01-26]: https://rust-lang.zulipchat.com/#narrow/channel/238009-t-compiler.2Fmeetings/topic/.5Bweekly.5D.202023-01-26/near/323755789 [zulip stop accepting broken code]: https://rust-lang.zulipchat.com/#narrow/stream/266220-rustdoc/topic/stop.20accepting.20broken.20code [notriddle rfc]: https://rust-lang.zulipchat.com/#narrow/channel/266220-t-rustdoc/topic/Pre-RFC.3A.20stop.20accepting.20broken.20code [tests/rustdoc-ui/error-in-impl-trait]: https://github.com/rust-lang/rust/tree/163cb4ea3f0ae3bc7921cc259a08a7bf92e73ee6/tests/rustdoc-ui/error-in-impl-trait [a list of supported browsers]: https://rust-lang.github.io/rfcs/1985-tiered-browser-support.html#supported-browsers

複数回の実行、同じ出力ディレクトリ

Rustdoc は、さまざまな入力に対して複数回実行し、その出力先を同じディレクトリに設定できます。 これは、cargo が現在のクレートの依存関係のドキュメントを生成する方法です。 ユーザーが、自分にとって必要なすべてのドキュメントを含む大きな ドキュメントバンドルを望む場合は、手動で行うこともできます。

HTML は各クレートについて個別に生成されますが、出力ディレクトリにクレートを追加するにつれて 更新する、クレート間にまたがる情報がいくつかあります。

  • crates<SUFFIX>.js は、出力ディレクトリ内のすべてのクレートのリストを保持します。
  • search-index<SUFFIX>.js は、検索可能なすべての項目のリストを保持します。
  • 各トレイトについて、implementors/.../trait.TraitName.js の下にファイルがあり、 そのトレイトの実装者のリストが含まれます。 実装者はそのトレイトとは異なる クレートに存在する場合があり、新しい実装者を見つけるたびに JS ファイルが更新されます。

ユースケース

rustdoc に取り組む際に念頭に置いておくべき、主要なユースケースがいくつかあります。

標準ライブラリドキュメント

これらは Rust のリリースプロセスの一環として https://doc.rust-lang.org/std で公開されます。 Stable リリースは、https://doc.rust-lang.org/1.57.0/std/ のような 特定のバージョン付き URL にもアップロードされます。 Beta と nightly のドキュメントは、 https://doc.rust-lang.org/beta/std/https://doc.rust-lang.org/nightly/std/ で公開されます。 ドキュメントは promote-release tool でアップロードされ、S3 から CloudFront で配信されます。

標準ライブラリドキュメントには、alloc、core、proc_macro、std、test の 5 つのクレートが含まれます。

docs.rs

クレートが crates.io に公開されると、docs.rs は自動的にそのドキュメントをビルドして 公開します。たとえば https://docs.rs/serde/latest/serde/ です。 これは常に現在の nightly rustdoc でビルドされるため、rustdoc に取り込まれた変更は、docs.rs に即座に公開される影響を 与えるという意味で「insta-stable」です。 古いドキュメントは再ビルドされる場合もありますが、常にそうとは限らないため、 docs.rs で古いリリースを閲覧していると UI に多少の違いが見られます。 クレート作者は再ビルドをリクエストでき、その場合は最新の rustdoc で実行されます。

docs.rs は、ストレージを節約し、上部にナビゲーションバーを表示するために、 rustdoc の出力にいくつかの変換を行います。 特に、main.js や rustdoc.css のような特定の静的 ファイルは、同じバージョンの rustdoc の複数の呼び出し間で共有される場合があります。 crates.js や sidebar-items.js のような他のファイルは、呼び出しごとに異なります。 フォントのようなさらに別のものは、決して変更されません。 これらのカテゴリは src/librustdoc/html/render/write_shared.rsSharedResource enum を使用して区別されます

docs.rs 上のドキュメントは常に一度に 1 つのクレートについて生成されるため、 検索機能とサイドバー機能には現在のクレートの依存関係は含まれません。

ローカルで生成されたドキュメント

クレート作者は、ローカルにチェックアウトしたクレートで cargo doc --open を実行してドキュメントを確認できます。 これは、書いているドキュメントが有用で正しく表示されることを確認するのに役立ちます。 また、自分が作者ではないものの利用したいクレートのドキュメントを閲覧する人にとっても有用です。 どちらの場合でも、通常は表示されないプライベートメソッドやフィールドなどを 確認するために、Cargo フラグ --document-private-items を使用できます。

デフォルトでは、cargo doc はクレートとそのすべての依存関係のドキュメントを生成します。 その結果、非常に大きなドキュメントバンドルになり、大きな(そして遅い)検索コーパスになることがあります。 Cargo フラグ --no-deps はこの挙動を抑制し、そのクレートだけのドキュメントを生成します。

セルフホストされたプロジェクトドキュメント

一部のプロジェクトは独自のドキュメントをホストしています。 これは、ローカルでドキュメントを生成し、それを単純に Web サーバーへコピーすることで簡単に行えます。 Rustdoc の HTML 出力は、フラグによって広範にカスタマイズできます。 ユーザーはテーマを追加し、デフォルトテーマを設定し、任意の HTML を注入できます。 詳細は rustdoc --help を参照してください。

新しいターゲットの追加

これは、新しいターゲットのサポートを追加するための一連の手順です。 到達し得る最終状態やそこに至る経路は数多くあるため、すべてのセクションが あなたの目的に関連するとは限りません。

関連するドキュメントについては、ターゲット階層ポリシーも参照してください。

新しい LLVM の指定

非常に新しいターゲットの場合、現在 Rust に同梱されているものとは異なる LLVM のフォークを使用する必要があるかもしれません。 その場合は、src/llvm-project git サブモジュールに移動し(サブモジュールが更新されるように、 少なくとも一度は ./x check を実行する必要があるかもしれません)、 自分のフォークに適したコミットをチェックアウトしてから、その新しいサブモジュール参照を メインの Rust リポジトリにコミットします。

例は次のとおりです。

cd src/llvm-project
git remote add my-target-llvm some-llvm-repository
git checkout my-target-llvm/my-branch
cd ..
git add llvm-project
git commit -m 'Use my custom LLVM'

ビルド済み LLVM の使用

すでにビルド済みのローカル LLVM チェックアウトがある場合、 冗長なビルドを避けるために、そのビルドをシステム LLVM として扱うよう Rust を構成できる場合があります。

bootstrap.tomltarget セクションを使用して、ビルド済みバージョンの LLVM を使用するよう Rust に指示できます。

[target.x86_64-unknown-linux-gnu]
llvm-config = "/path/to/llvm/llvm-7.0.1/bin/llvm-config"

システム LLVM を使用しようとしている場合、以前に次のパスが確認されていますが、 あなたのシステムでは異なる可能性があります。

  • /usr/bin/llvm-config-8
  • /usr/lib/llvm-8/bin/llvm-config

コード生成テストで使用される LLVM の FileCheck ツールをインストールしておく必要があることに注意してください。 このツールは通常 LLVM と一緒にビルドされますが、独自のインストール済み LLVM を使用する場合は、 何らかの別の方法で FileCheck を提供する必要があります。 Debian ベースのシステムでは、llvm-N-tools パッケージをインストールできます(ここで N は LLVM のバージョン番号です。例: llvm-8-tools)。あるいは、bootstrap.tomlllvm-filecheck 設定項目で FileCheck へのパスを指定するか、 bootstrap.tomlrust.codegen-tests 項目でコード生成テストを無効化できます。

ターゲット仕様の作成

まずはターゲット JSON ファイルから始めるべきです。 --print target-spec-json を使用すると、既存のターゲットの仕様を確認できます。

rustc -Z unstable-options --target=wasm32-unknown-unknown --print target-spec-json

その JSON をファイルに保存し、ターゲットに合わせて適切に変更します。

ターゲット仕様の追加

JSON 仕様を記入し、ある程度正常にコンパイルできるようになったら、 その仕様をコンパイラ自体にコピーできます。

rustc_target::spec モジュール内の supported_targets マクロにある 大きなテーブルに行を追加する必要があります。 次に、新しいターゲットに対応する、target 関数を含むファイルを追加します。

例として使用できる既存のターゲットを探してください。

このターゲットをブートストラップで使用するには、src/bootstrap/src/core/sanity.rsSTAGE0_MISSING_TARGETS リストにターゲットトリプルを明示的に追加する必要があります。 これは、デフォルトのブートストラップコンパイラ(通常はベータコンパイラ)が、 追加したばかりの新しいターゲットを認識しないために必要です。 したがって、ブートストラップがこのターゲットはまだ stage0 コンパイラではサポートされていないことを 認識できるように、STAGE0_MISSING_TARGETS に追加する必要があります。

const STAGE0_MISSING_TARGETS: &[&str] = &[
+   "NEW_TARGET_TRIPLE"
];

クレートへのパッチ適用

libccc など、コンパイラが依存しているクレートに変更を加える必要があるかもしれません。 その場合は、Cargo の [patch] 機能を使用できます。 たとえば、未リリース版の libc を使用したい場合は、トップレベルの Cargo.toml ファイルに追加できます。

diff --git a/Cargo.toml b/Cargo.toml
index 1e83f05e0ca..4d0172071c1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -113,6 +113,8 @@ cargo-util = { path = "src/tools/cargo/crates/cargo-util" }
 [patch.crates-io]
+libc = { git = "https://github.com/rust-lang/libc", rev = "0bf7ce340699dcbacabdf5f16a242d2219a49ee0" }

 # 何が起きているのかについては、`src/tools/rustc-workspace-hack/README.md` のコメントを参照してください
 # ここ
 rustc-workspace-hack = { path = 'src/tools/rustc-workspace-hack' }

その後、cargo update -p libc を実行してロックファイルを更新します。

ローカルの path 依存関係にパッチを適用すると、 その依存関係に対する警告が有効になることに注意してください。 一部の依存関係は警告なしではなく、bootstrap.tomlrust.deny-warnings 設定により、 ビルドが突然失敗し始める可能性があります。 警告に対処するには、次のようにするとよいでしょう。

  • 依存関係を変更して警告を取り除く
  • または、ローカル開発目的で、bootstrap.toml に rust.deny-warnings = false を設定して警告を抑制する。

クロスコンパイル

JSON とコードの両方にターゲット仕様が用意できたら、rustc をクロスコンパイルできます。

DESTDIR=/path/to/install/in \
./x install -i --stage 1 --host aarch64-apple-darwin.json --target aarch64-apple-darwin \
compiler/rustc library/std

ターゲット仕様がすでにブートストラップコンパイラで利用可能な場合は、 両方の引数で JSON ファイルの代わりにそれを使用できます。

ターゲットを tier 2(ターゲット)から tier 2(ホスト)へ昇格する

tier 2 ターゲットには 2 つのレベルがあります。

ターゲットをクロスコンパイルからネイティブへ昇格する例については、 #75914 を参照してください。

コンパイラの最適化ビルド

可能な限り最適化された rustc のビルドをコンパイルするために使用できる、追加のビルド設定オプションやテクニックが複数あります(たとえば Linux ディストリビューション向けに rustc をビルドする場合)。 さまざまな Rust ターゲットに対するこれらの設定オプションの状況は、こちらで追跡されています。 このページでは、自分で rustc をビルドする際に、これらの手法をどのように使用できるかを説明します。

リンク時最適化

リンク時最適化は、プログラムのパフォーマンスを向上させることができる強力なコンパイラ技法です。 rustc をビルドする際に (Thin-)LTO を有効にするには、bootstrap.tomlrust.lto 設定オプションを "thin" に設定します。

rust.lto = "thin"

rustc 向けの LTO は、現在 x86_64-unknown-linux-gnu ターゲットでのみサポートおよびテストされていることに注意してください。他のターゲットでも動作する可能性はありますが、保証はありません。 特に、LTO で最適化された rustc は、現在 Windows で誤ったコンパイルを生成します。

Linux で LTO を有効にすると、最大 10% の高速化が得られています

メモリアロケータ

rustc に別のメモリアロケータを使用すると、大きなパフォーマンス上の利点が得られる場合があります。 jemalloc アロケータを有効にしたい場合は、bootstrap.tomlrust.jemalloc オプションを true に設定できます。

rust.jemalloc = true

このオプションは現在、Linux および macOS ターゲットでのみサポートされていることに注意してください。

コード生成ユニット

rustc クレートごとのコード生成ユニット数を減らすと、コンパイラのより高速なビルドを生成できます。 bootstrap.toml で以下のオプションを使用すると、rustclibstd のコード生成ユニット数を変更できます。

rust.codegen-units = 1
rust.codegen-units-std = 1

命令セット

デフォルトでは、rustc は(選択されたターゲットに応じて)汎用的で保守的な命令セットアーキテクチャ向けにコンパイルされ、できるだけ多くの CPU をサポートするようになっています。 特定の命令セットアーキテクチャ向けに rustc をコンパイルしたい場合は、RUSTFLAGStarget_cpu コンパイラオプションを設定できます。

RUSTFLAGS="-C target_cpu=x86-64-v3" ./x build ...

LLVM も特定の命令セット向けにコンパイルしたい場合は、bootstrap.tomlllvm フラグを設定できます。

llvm.cxxflags = "-march=x86-64-v3"
llvm.cflags = "-march=x86-64-v3"

プロファイル誘導最適化

プロファイル誘導最適化(または、より一般的にはフィードバック指向最適化)を適用すると、rustc のパフォーマンスを最大 15% まで大きく向上させることができます(12)。 ただし、これらの技法は設定オプションで単純に有効化できるものではなく、rustc を複数回コンパイルし、選択したベンチマークでプロファイルする複雑なビルドワークフローが必要です。

エンドユーザーに配布されるビルド向けに、PGO(プロファイル誘導最適化)と BOLT(リンク後バイナリオプティマイザ)を使用して rustc を最適化するために使われる opt-dist というツールがあります。 src/tools/opt-dist にあるこのツールを調査し、それをベースにカスタム PGO ビルドワークフローを構築することも、直接使用してみることもできます。 このツールは現在、Rust の継続的インテグレーションワークフローでの使い方にかなりハードコードされており、異なる環境で動作させるには、いくつかのカスタム変更が必要になる可能性があることに注意してください。

このツールを使用するには、いくつかの外部依存関係を用意する必要があります。

  • Python3 インタープリタ(x.py を実行するため)。
  • llvm-profdata バイナリを含む、コンパイル済みの LLVM ツールチェーン。 必要に応じて、BOLT を使用したい場合は、 llvm-boltmerge-fdata バイナリがツールチェーン内で利用可能でなければなりません。

これらの依存関係は、Environment 構造体の実装によって opt-dist に提供されます。 これは、PGO/BOLT パイプラインが実行されるディレクトリと、Python や LLVM などの外部依存関係も指定します。

以下は、opt-dist をローカルで(CI の外部で)使用する方法の例です。

  1. opt-dist はメトリクスが有効になっていることを想定しているため、bootstrap.toml ファイルでメトリクスを有効にします。
    build.metrics = true
    
  2. 次のコマンドでツールをビルドします。
    ./x build tools/opt-dist
    
  3. local モードでツールを実行し、必要なパラメータを指定します。
    ./build/host/stage1-tools-bin/opt-dist local \
      --target-triple <target> \ # ターゲットを選択します。例: "x86_64-unknown-linux-gnu"
      --checkout-dir <path>    \ # rust チェックアウトへのパス。例: "."
      --llvm-dir <path>        \ # ビルド済み LLVM ツールチェーンへのパス。例: "/foo/bar/llvm/install"
      -- python3 x.py dist       # 実際のビルドコマンドを渡します
    
    変更可能な追加パラメータを確認するには、--help を実行できます。

注: opt-dist をローカルで実行する代わりに、実際の CI パイプラインを実行したい場合は、 cargo run --manifest-path src/ci/citool/Cargo.toml run-local dist-x86_64-linux を実行できます。

コンパイラのテスト

Rust プロジェクトでは、ビルドシステム(./x test)によって統括される、さまざまな種類のテストが実行されます。このセクションでは、各種テストツールの概要を簡単に説明します。以降の章では、テストの実行新しいテストの追加について詳しく説明します。

テストの種類

Rust ディストリビューション内のものを検証するために、いくつかの種類のテストがあります。ほぼすべては ./x test によって駆動されますが、以下で述べるように例外もあります。

Compiletest

コンパイラ自体をテストするための主要なテストハーネスは、compiletest というツールです。

compiletest は、テストスイートとして整理された、さまざまなスタイルのテストの実行をサポートします。テストモードは、一連のテストスイートに対して共通のプリセット/動作を提供する場合があります。compiletest がサポートするテストは tests ディレクトリにあります。

Compiletest の章では、このツールの使い方について詳しく説明しています。

例: ./x test tests/ui

パッケージテスト

標準ライブラリと多くのコンパイラパッケージには、典型的な Rust の #[test] ユニットテスト、統合テスト、ドキュメンテーションテストが含まれています。library/ または compiler/ ディレクトリ内のほぼ任意のパッケージについて、./x test にパスを渡すことができ、x は基本的にそのパッケージに対して cargo test を実行します。

例:

コマンド説明
./x test library/stdstd のテストのみを実行します
./x test library/corecore のテストのみを実行します
./x test compiler/rustc_data_structuresrustc_data_structures のテストを実行します

標準ライブラリは、その機能をカバーするためにドキュメンテーションテストに大きく依存しています。ただし、必要に応じてユニットテストや統合テストも使用できます。ほぼすべてのコンパイラパッケージでは、doctest が無効化されています。

すべての標準ライブラリおよびコンパイラのユニットテストは、別個の tests ファイルに配置されます(これは tidy によって強制されます)。これにより、テストファイルが変更された場合でも、クレートを再コンパイルする必要がなくなります。例:

#[cfg(test)]
mod tests;

この方法で行わず、core のようなものに取り組んでいる場合、標準ライブラリ全体と rustc 全体を再コンパイルする必要があります。

./x test には、これらのパッケージテストの動作を制御するための CLI オプションがいくつか含まれています。

  • --doc — パッケージ内のドキュメンテーションテストのみを実行します。
  • --all-targets — ドキュメンテーションテストを除くすべてのテストを実行します。
  • --tests — ユニットテストと統合テストのみを実行します

Tidy

Tidy は、長い行を拒否するなど、ソースコードのスタイルとフォーマット規約を検証するために使用されるカスタムツールです。詳細については、コーディング規約に関するセクションまたは Tidy Readme を参照してください。

例: ./x test tidy

フォーマット

Rustfmt は、コンパイラ全体で一貫したスタイルを強制するためにビルドシステムに統合されています。フォーマットチェックは、前述の Tidy ツールによって自動的に実行されます。

例:

コマンド説明
./x fmt --checkフォーマットをチェックし、フォーマットが必要な場合はエラーで終了します。
./x fmtコードベース全体に対して rustfmt を実行します。
./x test tidy --bless最初に rustfmt を実行してコードベースをフォーマットし、その後 tidy チェックを実行します。

Book ドキュメントテスト

公開されているすべての book には、それぞれ独自のテストがあり、主に Rust コード例が通ることを検証するためのものです。内部的には、これらは基本的に markdown ファイルに対して rustdoc --test を使用しています。./x test に book へのパスを渡すことで、テストを実行できます。

例: ./x test src/doc/book

ドキュメントリンクチェッカー

すべてのドキュメントにわたるリンクはリンクチェッカーツールで検証され、次のように呼び出すことができます。

./x test linkchecker

これにはすべてのドキュメントをビルドする必要があり、しばらく時間がかかる場合があります。

distcheck

distcheck は、ビルドシステムによって作成されたソースディストリビューション tarball が展開され、ビルドされ、すべてのテストを実行できることを検証します。

./x test distcheck

ツールテスト

Rust に含まれているパッケージについても、すべてのテストが実行されます。これには cargo、clippy、rustfmt、miri、bootstrap(Rust ビルドシステム自体のテスト)などが含まれます。

ほとんどのツールは src/tools ディレクトリにあります。ツールのテストを実行するには、そのパスを ./x test に渡すだけです。

例: ./x test src/tools/cargo

通常、これらのツールでは、ツールのディレクトリ内で cargo test を実行します。

指定した一連のテストのみを実行したい場合は、コマンドに --test-args FILTER_NAME を追加します。

例: ./x test src/tools/miri --test-args padding

CI では、一部のツールは失敗が許容されています。失敗すると対応するチームに通知が送信され、toolstate Web サイトで追跡されます。詳細については、toolstate ドキュメントを参照してください。

エコシステムテスト

Rust は、回帰を検出し、言語の進化について情報に基づいた判断を行うために、実世界のコードとの統合をテストします。エコシステムテストには、Crater を含むいくつかの種類があります。詳細については、エコシステムテストの章を参照してください。

パフォーマンステスト

コンパイラのパフォーマンスのテストと追跡には、別のインフラストラクチャが使用されます。詳細については、パフォーマンステストの章を参照してください。

コード生成バックエンドテスト

コード生成バックエンドテストを参照してください。

その他の情報

その他の有用なテスト関連情報は、その他の情報にあります。

参考資料

次のブログ記事も参考になる場合があります。

テストの実行

x を使用してテストコレクション全体を実行できます。 ただし、ローカル開発中に 全体の テストコレクションを実行したいことはほとんどありません。 非常に長い時間がかかるためです。 ローカル開発については、テストのサブセットを実行する方法に関する後続のサブセクションを参照してください。

単に ./x test を実行すると、ステージ 1 コンパイラをビルドしてから、テストスイート全体を実行します。 これには tests/ だけでなく、library/compiler/src/tools/ パッケージのテストなども含まれます。

通常は、変更が実行されると想定されるテストスイートのサブセット(またはそれよりさらに小さいテストの集合)のみを実行したいはずです。 PR CI はテストコレクションのサブセットを実行し、merge queue CI はすべてのテストコレクションを実行します。

./x test

テスト結果はキャッシュされ、以前に成功したテストはテスト中に ignored されます。 各テストの stdout/stderr の内容とタイムスタンプファイルは、指定された <target-tuple> に対して build/<target-tuple>/test/ の下にあります。 テストを強制的に再実行するには(たとえば、テストランナーが変更に気づかない場合)、--force-rerun CLI オプションを使用できます。

外部依存関係の要件に関する注意

一部のテストスイートには外部依存関係が必要な場合があります。これは特に debuginfo テストに当てはまります。一部の debuginfo テストでは、Python が有効な gdb が必要です。 gdb 内から python コマンドを使用することで、インストールされている gdb が Python をサポートしているかをテストできます。 呼び出した後、Python コード(例: print("hi"))を入力し、 return に続いて CTRL+D を押すと実行できます。gdb をソースからビルドしている場合は、 --with-python=<path-to-python-binary> で設定する必要があります。

テストスイートのサブセットを実行する

特定の PR に取り組んでいる場合、通常はより小さいテストの集合を実行したいはずです。 たとえば、rustc を変更した後、物事が概ね正しく動作しているかを確認するために使用できる 良い「スモークテスト」は、ui テストスイート(tests/ui)を実行することです。

./x test tests/ui

もちろん、テストスイートの選択はいくらか任意であり、実行している作業に適していない場合があります。 たとえば、debuginfo をハックしている場合は、debuginfo テストスイートの方が適しているかもしれません。

./x test tests/debuginfo

任意のテストスイートについて、特定のテストのサブディレクトリだけをテストする必要がある場合は、 そのディレクトリをフィルターとして ./x test に渡すことができます。

./x test tests/ui/const-generics

MSYS2 に関する注意

MSYS2 ではパスが奇妙なようで、./x testtests/ui/const-genericstests\ui\const-generics も認識しません。その場合は、たとえば ./x test ui --test-args="tests/ui/const-generics" を使用して回避できます。

同様に、パスを渡すことで単一のファイルをテストできます。

./x test tests/ui/const-generics/const-test.rs

x は、まだパスを渡して単一のツールテストを実行することをサポートしていません。 以下で説明するように、--test-args 引数を使用する必要があります。

./x test src/tools/miri --test-args tests/fail/uninit/padding-enum.rs

tidy スクリプトのみを実行する

./x test tidy

標準ライブラリでテストを実行する

./x test --stage 0 library/std

これは std のテストのみを実行することに注意してください。 core やその他のクレートをテストしたい場合は、それらを明示的に指定する必要があります。

tidy スクリプトと標準ライブラリのテストを実行する

./x test --stage 0 tidy library/std

ステージ 1 コンパイラを使用して標準ライブラリでテストを実行する

./x test --stage 1 library/std

実行したいテストスイートを列挙することで、 まったく変更していないコンポーネントのテストを実行せずに済みます。

bors は完全なステージ 2 ビルドでのみテストを実行することに注意してください。 そのため、テストはステージ 1 でも 通常は 問題なく動作しますが、いくつかの制限があります。

ステージ 2 コンパイラを使用してすべてのテストを実行する

./x test --stage 2
これを行う必要はほとんどありません。 CI がこれらのテストを実行してくれます。

コンパイラ/ライブラリでユニットテストを実行する

次のようにして、特定のファイルでユニットテストを実行したい場合があります。

./x test compiler/rustc_data_structures/src/thin_vec/tests.rs

しかし残念ながら、それは不可能です。 代わりに次を呼び出す必要があります。

./x test compiler/rustc_data_structures/ --test-args thin_vec

個別のテストを実行する

よく行われるもう 1 つのことは、個別のテストを実行することです。 多くの場合、それは修正しようとしているテストです。 前述のように、これを行うには完全なファイルパスを渡すか、代わりに --test-args オプションを付けて x を呼び出すことができます。

./x test tests/ui --test-args issue-1234

内部では、テストランナーは標準の Rust テストランナー(#[test] で得られるものと同じ)を呼び出すため、 このコマンドは名前に “issue-1234” を含むテストでフィルタリングすることになります。 したがって、--test-args は関連するテストの集合を実行するための良い方法です。

テスト実行時に rustc に引数を渡す

RUSTFLAGS を使用せずに、特定のコンパイラ引数を指定していくつかのテストを実行すると便利な場合があります (たとえば、不安定な機能の開発中に -Z フラグを使用する場合)。

これは、./x test--compiletest-rustc-args オプションを使用して行うことができ、 テストのビルド時にコンパイラへ追加の引数を渡せます。

参照ファイルの編集と更新

コンパイラの出力を意図的に変更した場合、または新しいテストを作成している場合は、 テストサブコマンドに --bless を渡すことができます。

たとえば、tests/ui のいくつかのテストが失敗している場合、次のコマンドを実行できます。

./x test tests/ui --bless

これにより、すべての test/ui テストの .stderr.stdout、または .fixed ファイルが自動的に調整されます。 もちろん、--bless フラグなしでテストを実行するときと同じように、 --test-args your_test_name フラグを使用して特定のテストだけを対象にすることもできます。

テスト実行の設定

テストを実行するためのオプションはいくつかあります。

  • bootstrap.toml には rust.verbose-tests オプションがあります。false の場合、各テストは 単一のドットを出力します(デフォルト)。 true の場合、すべてのテストの名前が出力されます。 これは Rust test harness--quiet オプションと同等です。
  • 環境変数 RUST_TEST_THREADS には、テストに使用する並行スレッド数を設定できます。

--pass $mode を渡す

pass の UI テストには現在、check-passbuild-passrun-pass の 3 つのモードがあります。 --pass $mode が渡されると、テストファイルにディレクティブ //@ no-pass-override が存在しない限り、 これらのテストは指定された $mode で強制的に実行されます。 たとえば、tests/ui 内のすべてのテストを check-pass として実行できます。

./x test tests/ui --pass check

--pass $mode を渡すことで、テスト時間を短縮できます。 各モードについては、pass/fail 期待値の制御を参照してください。

異なる「compare modes」でテストを実行する

UI テストでは、コンパイラが特定の「モード」にあるかどうかによって、出力が異なる場合があります。 たとえば、Polonius モードを使用している場合、テスト foo.rs はまず期待される出力を foo.polonius.stderr から探し、見つからなければ通常の foo.stderr にフォールバックします。 以下は、Polonius モードで UI テストスイートを実行します:

./x test tests/ui --compare-mode=polonius

詳細については、比較モードを参照してください。

テストを手動で実行する

単に手作業でテストを実行する方が簡単で速い場合があります。 ほとんどのテストは単なる .rs ファイルなので、rustup ツールチェーンを作成した後、次のようにできます:

rustc +stage1 tests/ui/issue-1234.rs

これははるかに高速ですが、常にうまくいくとは限りません。 たとえば、一部のテストには、特定のコンパイラフラグを指定するディレクティブや、他のクレートに依存するディレクティブが含まれており、それらのオプションがないと同じようには実行されない場合があります。

リモートマシンでテストを実行する

テストはリモートマシン上で実行できます(たとえば、異なるアーキテクチャ向けのビルドをテストする場合)。 これは、ビルドマシン上の remote-test-client を使用して、リモートマシン上で実行されている remote-test-server にテストプログラムを送信することで行います。 remote-test-server はテストプログラムを実行し、結果をビルドマシンに送り返します。 remote-test-server認証なしのリモートコード実行 を提供するため、使用場所には注意してください。

これを行うには、まずリモートマシン向けに remote-test-server をビルドします(例として RISC-V を使用します):

./x build src/tools/remote-test-server --target riscv64gc-unknown-linux-gnu

バイナリは ./build/host/stage2-tools/$TARGET_ARCH/release/remote-test-server に作成されます。 これをリモートマシンにコピーします。

リモートマシン上で、--bind 0.0.0.0:12345 フラグ(必要に応じて --verbose フラグも)を付けて remote-test-server を実行します。 出力は次のようになるはずです:

$ ./remote-test-server --verbose --bind 0.0.0.0:12345
starting test server
listening on 0.0.0.0:12345!

サーバーを 0.0.0.0 にバインドすると、あなたのマシンに到達可能なすべてのホストが、あなたのマシン上で任意のコードを実行できるようになることに注意してください。 ポート 12345 への外部アクセスをブロックするファイアウォールを設定するか、バインド時により制限的な IP アドレスを使用することを強く推奨します。

remote-test-server が動作しているかどうかは、接続して ping\n を送信することでテストできます。 pong が返ってくるはずです:

$ nc $REMOTE_IP 12345
ping
pong

リモートランナーを使用してテストを実行するには、TEST_DEVICE_ADDR 環境変数を設定してから、通常どおり x を使用します。 たとえば、IP アドレス 1.2.3.4 の RISC-V マシン向けに ui テストを実行するには、次のようにします:

export TEST_DEVICE_ADDR="1.2.3.4:12345"
./x test tests/ui --target riscv64gc-unknown-linux-gnu

remote-test-server が verbose フラグ付きで実行されていた場合、テストマシン上の出力はおおよそ次のようになります

[...]
run "/tmp/work/test1007/a"
run "/tmp/work/test1008/a"
run "/tmp/work/test1009/a"
run "/tmp/work/test1010/a"
run "/tmp/work/test1011/a"
run "/tmp/work/test1012/a"
run "/tmp/work/test1013/a"
run "/tmp/work/test1014/a"
run "/tmp/work/test1015/a"
run "/tmp/work/test1016/a"
run "/tmp/work/test1017/a"
run "/tmp/work/test1018/a"
[...]

テストは、リモートマシン上ではなく、x を実行しているマシン上でビルドされます。 予期せずビルドに失敗するテスト(または不正なビルド出力を生成する ui テスト)は、リモートマシン上で一度も実行されないまま失敗する場合があります。

remote-test-serverx コマンドから到達できない場合に備えて、デフォルトのタイムアウトは 30 分です。 このタイムアウトは、TEST_DEVICE_CONNECT_TIMEOUT_SECONDS 環境変数を使用して変更できます。

エミュレータでテストする

一部のプラットフォームは、すぐには利用できないアーキテクチャ向けにエミュレータ経由でテストされます。 標準ライブラリが十分にサポートされており、ホストオペレーティングシステムが TCP/IP ネットワークをサポートしているアーキテクチャについては、リモートマシンでテストするための上記の手順を参照してください(この場合、リモートマシンはエミュレートされています)。

エミュレータ内でのテスト実行をオーケストレーションするためのツール群もあります。 arm-androidarm-unknown-linux-gnueabihf などのプラットフォームは、GitHub Actions 上でエミュレーション下のテストを自動的に実行するように設定されています。 以下では、ターゲットのテストがエミュレーション下でどのように実行されるかを見ていきます。

armhf-gnu 用の Docker イメージには、ARM CPU アーキテクチャをエミュレートするための QEMU が含まれています。 Rust ツリーには、テストプログラムとライブラリをエミュレータに送信し、エミュレータ内でテストを実行し、結果を読み取るためのプログラムである remote-test-clientremote-test-server というツールが含まれています。 Docker イメージは remote-test-server を起動するように設定されており、ビルドツールは remote-test-client を使用してサーバーと通信し、テスト実行を調整します(src/bootstrap/src/core/build_steps/test.rs を参照)。

iOS/tvOS/watchOS/visionOS シミュレータで実行する場合も、同様にそれを “リモート” マシンとして扱えます。 ここで興味深い点は、シミュレータインスタンスとホスト macOS の間でネットワークが共有されているため、ローカルループバックアドレス 127.0.0.1 を使用できることです。 次のような方法で動作するはずです:

# iOS シミュレータ向けにテストサーバーをビルドします:
./x build src/tools/remote-test-server --target aarch64-apple-ios-sim

# 既にシミュレータインスタンスを開いている場合は、以下からデバイス UUID をコピーします:
xcrun simctl list devices booted
UDID=01234567-89AB-CDEF-0123-456789ABCDEF

# または、新しいシミュレータインスタンスを作成して起動します:
xcrun simctl list runtimes
xcrun simctl list devicetypes
UDID=$(xcrun simctl create $CHOSEN_DEVICE_TYPE $CHOSEN_RUNTIME)
xcrun simctl boot $UDID
# 詳細は https://nshipster.com/simctl/ を参照してください。

# ポート 12345 でランナーを起動します:
xcrun simctl spawn $UDID ./build/host/stage2-tools/aarch64-apple-ios-sim/release/remote-test-server -v --bind 127.0.0.1:12345

# 新しいターミナルで、ランナー経由でテストを実行します:
export TEST_DEVICE_ADDR="127.0.0.1:12345"
./x test --host='' --target aarch64-apple-ios-sim --skip tests/debuginfo
# FIXME(madsmtm): debuginfo テストが動作するようにします(`.dSYM` フォルダをターゲットにコピーする必要があるかもしれません)。

wasi (wasm32-wasip1) でテストを実行する

一部のテストは wasm ターゲット固有です。 これらのテストを実行するには、x test--target wasm32-wasip1 を渡す必要があります。 さらに、wasi sdk が必要です。 コンピュータ上に sysroot を取得するには、wasi sdk repository のインストール手順に従ってください。 wasm32-wasip1 target support page には、sdk がビルドできなければならない最小バージョンが指定されています。 時間がかかり、非常に気になる c++ の警告を大量に出す cmake コマンドがいくつかあります… その後、bootstrap.toml で次のように sysroot を指定します:

[target.wasm32-wasip1]
wasi-root = "<wasi-sdk location>/build/sysroot/install/share/wasi-sysroot"

私の場合は rust フォルダーの隣に git clone したので、../wasi-sdk/build/.... でした。 これで、テストはそのまま実行できるはずで、他に何も設定する必要はありません。

Docker でテストする

src/ci/docker ディレクトリには、GitHub Actions で実行される Linux ベースのジョブ用の Docker イメージ定義が含まれています(Linux 以外のジョブは Docker の外部で実行されます)。 これらのジョブはローカルの開発マシンで実行でき、 ローカルシステムとは異なる環境をテストするのに役立ちます。 Linux、Windows、または macOS システムに Docker をインストールする必要があります(通常、Linux のほうが Windows や macOS よりもはるかに高速です。後者は Linux 環境をエミュレートするために仮想マシンを使用するためです)。

CI で実行されるジョブは一連の bash スクリプトを通じて設定されており、その挙動をローカルで再現するのは必ずしも簡単ではありません。 CI ジョブをできるだけ簡単な方法でローカル実行したい場合は、CI で起きることを可能な限り忠実に再現しようとする、提供されているヘルパー citool を使用できます。

cargo run --manifest-path src/ci/citool/Cargo.toml run-local <job-name>
# 例:
cargo run --manifest-path src/ci/citool/Cargo.toml run-local dist-x86_64-linux-alt

上記のスクリプトがうまく動作しない場合、Docker イメージの実行をより細かく制御したい場合、または Docker ジョブの実行中に正確に何が起きるのかを理解したい場合は、以下を読み進めてください。

run.sh スクリプト

src/ci/docker/run.sh スクリプトは、特定の Docker イメージをビルドして実行し、 そのイメージ内で Rust をビルドし、テストを実行するか、配布用に設計された一連のアーカイブを準備するために使用されます。 このスクリプトは、ローカルの Rust ソースツリーを読み取り専用モードで、obj ディレクトリを読み書き可能モードでマウントします。 コンパイラーの成果物はすべて obj ディレクトリに保存されます。 シェルは obj ディレクトリで開始されます。 そこから、Docker イメージで定義されたビルドを開始する ../src/ci/run.sh を実行します。

src/ci/docker/run.sh <image-name> を直接実行できます。 run.sh スクリプトに関する重要な注意点がいくつかあります。

  • CI 上で実行される場合、このスクリプトはすべてのサブモジュールがチェックアウトされていることを期待します。 ジョブからアクセスされるサブモジュールの一部が利用できない場合、ビルドはエラーになります。 したがって、必要なサブモジュールがすべてローカルでチェックアウトされていることを確認する必要があります。 これは git で手動で行うことも、bootstrap.tomlbuild.submodules = true を設定して x build のようなコマンドを実行し、bootstrap に最も重要なサブモジュールをダウンロードさせることもできます ただし、実行しようとしている特定の CI ジョブに対しては、これだけでは十分でない場合があることに注意してください。
  • <image-name> は、src/ci/docker/host-* ディレクトリのいずれかにある単一のディレクトリに対応します。 一部のジョブは同じイメージを実行しますが、異なる環境変数や Docker ビルド引数を使用するため、イメージ名が必ずしもジョブ名に対応するとは限らないことに注意してください これは、CI ジョブをローカルで実行することを難しくしている複雑さの一部です。
  • “dist” ジョブ(dist- で始まるジョブ)を実行する場合は、DEPLOY=1 環境変数を設定する必要があります。
  • “alternative dist” ジョブ(dist- で始まり -alt で終わるジョブ)を実行する場合は、DEPLOY_ALT=1 環境変数を設定する必要があります。
  • 一部の std テストには IPv6 サポートが必要です。 Linux 上の Docker では、デフォルトで無効になっているようです。 コンテナーを作成する前に、IPv6 を有効にするために enable-docker-ipv6.sh のコマンドを実行してください。 これは一度だけ行えば十分です。

対話モード

特定の Docker イメージをビルドしてから、その中でカスタムコマンドを実行し、対象システムの挙動を試せるようにすると便利な場合があります。 これは対話モードを使用して行うことができ、 src/ci/docker/run.sh --dev <image-name> を使用してコンテナー内で bash シェルを開始します。

Docker コンテナー内では、特定のタスクを実行するために個別のコマンドを実行できます。 たとえば、UI テストだけを実行するために ../x test tests/ui を実行できます。

対話モードの使用に関する追加の注意点をいくつか示します。

  • シェルを終了するとコンテナーは自動的に削除されますが、 ビルド成果物は obj ディレクトリに残ります。 異なる Docker イメージ間で切り替えている場合、obj ディレクトリに保存された以前の環境の成果物が ビルドシステムを混乱させる可能性があります。 コンテナー内でビルドする前に、obj ディレクトリの一部または全部を削除する必要がある場合があります。
  • コンテナーは最小限で、最小限のパッケージセットしか含まれていません。 apt install less vim のように、いくつかのものをインストールしたくなるかもしれません。
  • コンテナー内で複数のシェルを開くことができます。 まずコンテナー 名(短いハッシュ)が必要です。これはシェルプロンプトに表示されます。または、コンテナーの外部で docker container ls を実行して、利用可能なコンテナーを一覧表示できます。 コンテナー名がわかったら、docker exec -it <CONTAINER> /bin/bash を実行します。ここで <CONTAINER>4ba195e95cef のようなコンテナー名です。

CI によるテスト

私たちの CI システムの主な目的は、テストスイートに合格することによって、 rust-lang/rustmain ブランチが常に有効な状態にあることを保証することです。

大まかに言うと、rust-lang/rust でプルリクエストを開くと、 次のことが起こります。

  • PR に push されるたびに、テストとチェックの小さなサブセットが実行されます。 これは一般的なエラーの検出に役立つはずです。
  • PR が承認されると、bors bot がその PR を[マージキュー]にエンキューします。
  • PR がキューの先頭に到達すると、bors はマージコミットを作成し、 その上で完全なテストスイートを実行します。 マージコミットには特定の PR が 1 つだけ含まれる場合もあれば、CI コストとマージ遅延を削減するために、 複数の PR をまとめた “rollup” である場合もあります。
  • テストスイート全体が完了すると、2 つのことが起こり得ます。 CI が失敗して開発者による対処が必要なエラーが発生するか、CI が成功して マージコミットが main ブランチに push されます。

CI で実行される内容を変更したい場合は、CI ジョブの変更を参照してください。

CI ワークフロー

私たちの CI は主に GitHub Actions 上で実行され、.github/workflows/ci.yml で定義された単一のワークフローを使用します。 このワークフローには、実行するすべての CI ジョブに共通化された多数のステップが含まれています。 対応するブランチまたは PR にコミットが push されると、ワークフローは src/ci/citool crate を実行し、実行すべき具体的な CI ジョブを動的に生成します。 このスクリプトは、私たちのすべての CI ジョブの宣言的な設定を含む jobs.yml ファイルを入力として使用します。

ほぼすべてのビルドステップは、個別のスクリプトを shell out します。これにより、CI はかなり プラットフォーム非依存になります(つまり、GitHub Actions に過度に依存していません)。 GitHub Actions に依存しているのは、CI プロセスのブートストラップと、 プロセスを駆動するスクリプトのオーケストレーションだけです。

本質的には、すべての CI ジョブは、さまざまなオペレーティングシステム、ターゲット、プラットフォームにわたって、 異なる設定で ./x test./x dist、またはその他のコマンドを実行します。 実行されるジョブには、dist ジョブと非 dist ジョブという 2 つの大きなカテゴリがあります。

  • Dist ジョブは、特定のプラットフォーム向けにコンパイラの完全なリリースをビルドします。 これには、rustup 経由で配布するすべてのツールが含まれます。 それらのビルドはその後 rust-lang-ci2 S3 バケットにアップロードされ、rustup-toolchain-install-master ツールを使って ローカルにインストールできるようになります。 同じビルドは実際のリリースにも使用されます。私たちのリリースプロセスは基本的に、 これらのアーティファクトを rust-lang-ci2 から本番エンドポイントへコピーし、署名することで構成されています。
  • 非 dist ジョブは、そのプラットフォーム上で私たちの完全なテストスイートと、 rustup 経由で配布するすべてのツールのテストスイートを実行します。 テストする内容の量は プラットフォームに依存します(たとえば、一部のテストは Tier 1 プラットフォームでのみ実行されます)。また、 一部のより高速なプラットフォームは、CI リソースの浪費を避けるために同じ builder 上にまとめられます。

入力イベント(通常はブランチへの push)に基づいて、3 種類のビルド(ジョブの集合)のいずれかを実行します。

  1. PR ビルド
  2. Auto ビルド
  3. Try ビルド

Pull Request ビルド

プルリクエストに push されるたびに、一連の pr ジョブが実行されます。 現在、これらは Linux 上で実行される x86_64-gnu-llvm-Xx86_64-gnu-toolspr-check-1pr-check-2 および tidy ジョブを実行します。 これらは、一般的な問題を検出するはずの、比較的短時間 (約 40 分)で軽量なテストスイートを実行します。 より具体的には、一連の lint を実行し、Windows mingw 向けのクロスコンパイルチェック ビルド(アーティファクトは生成しない)を試み、LLVM の system バージョンを使って コンパイラをテストします。 残念ながら、すべての PR の各コミットに対して完全なテストスイートを実行するには、 リソースが多すぎます。

doc comment に関する注意

2024 年 10 月時点の PR CI は、デフォルトでは ./x doc xxx を実行しようとしないことに注意してください。これは、./x doc xxx の失敗につながる壊れた intradoc link がある場合、 完全なマージキュー CI パイプラインのかなり遅い段階でそれが発生することを意味します。

そのため、doc comment の変更については、早期に検出できるように ./x doc xxx をローカルで実行することをお勧めします。

PR ジョブは jobs.ymlpr セクションで定義されています。 その結果は、PR ページ下部の “CI checks” セクションで、 PR 上から直接確認できます。

Auto ビルド

コミットを main ブランチにマージできるようにする前に、完全なテストスイートに合格する必要があります。 これを auto ビルドと呼びます。 このビルドは、オペレーティングシステムやターゲットをまたいでさまざまなテストを実行する、数十個の CI ジョブを実行します。 完全なテストスイートはかなり低速です。 すべての auto CI ジョブが完了するまでに数時間かかることがあります。

ほとんどのプラットフォームではビルドステップのみを実行し、一部は制限されたテストセットを実行します。 完全なテストスイートを実行するのは一部のみです(Rust の platform tiers を参照)。

Auto ジョブは jobs.ymlauto セクションで定義されています。 これらは rust-lang/rust リポジトリの automation/bors/auto ブランチ上で実行され、 最終結果は、対応する PR に bors が作成するコメントを通じて報告されます。 ライブ結果は the GitHub Actions workflows page で確認できます。

任意の時点で、実行中の auto ビルドは最大 1 つです。 詳細は bors による PR の逐次マージ を参照してください。

通常、auto ジョブが失敗すると、CI ワークフロー全体が直ちに終了します。 しかし、CI 上で一定期間テストしてからマージをブロックするようにするために、 「非ブロッキング」または任意の auto ジョブを作成すると有用な場合があります。 これは、それらのジョブが flaky になり得る場合に有用です。

そのためには、そのようなジョブに optional- というプレフィックスを付け、jobs.ymlcontinue_on_error: true を設定します。

Try ビルド

特定の PR について CI 上でテストスイートのサブセットを実行したり、 その PR からコンパイラアーティファクトのセットをビルドしたりしたい場合があります。 これはマージを試みずに行います。 これを “try build” と呼びます。 try build は、適切な権限を持つユーザーが @bors try コマンドを含む PR コメントを投稿した後に開始されます。

try build にはいくつかのユースケースがあります。

  • 私たちの rustc-perf ベンチマークスイートを使用して、一連のパフォーマンスベンチマークを実行する。 これには動作するコンパイラビルドが必要であり、Linux 上で最適化された コンパイラのバージョンをビルドする dist-x86_64-linux CI ジョブを実行する try build によって生成できます(このジョブは現在、try build を開始するとデフォルトで実行されます)。 try build を作成して パフォーマンスベンチマークのためにスケジュールするには、@bors try @rust-timer queue コマンドの組み合わせを使用できます。
  • Crater run を使用して、Rust エコシステム全体に対する PR の影響を確認する。 この場合も、動作するコンパイラビルドが必要であり、 dist-x86_64-linux CI ジョブによって生成できます。
  • 特定の CI ジョブ(例: Windows テスト)を PR 上で実行し、そのジョブによって実行されるテストスイートに 合格するかどうかを素早くテストする。 デフォルトでは、@bors try を含むコメントを送信すると、 jobs.ymltry セクションに定義されているジョブが実行されます。 このモードを「高速 try ビルド」と呼びます。 このような try ビルドではテストは一切実行されず、コンパイル警告も許容されます。 これは、Crater 実行やパフォーマンスベンチマークのために、 完全に正しく動作していない可能性があっても、 できるだけ速く最適化済みツールチェーンを取得したい場合に便利です。

高速 try ビルドで実行される CI ジョブには、デフォルトの try ジョブのフルビルドと区別するために、 特別なサフィックス(-quick)が付きます。 代わりにフルビルドを行いたい場合は、 ジョブパターン(後述)にそのジョブ名を指定してください。

try ビルドでカスタム CI ジョブを実行し、それらがすべてのテストに合格し、 コンパイル警告を一切生成しないことを確認したい場合は、ジョブパターンを指定して実行する CI ジョブを選択できます。 これは次の 2 つの方法のいずれかで使用できます。

  • PR の説明に try-job: <job pattern> ディレクティブのセットを追加し(後述)、 単に @bors try を実行できます。 CI はこれらのディレクティブを読み取り、指定したジョブを実行します。 これは、 PR を段階的に変更した後で、同じ try ジョブのセットを複数回再実行したい場合に便利です。
  • try コマンドの jobs パラメーターを使用してジョブパターンを指定できます: @bors try jobs=<job pattern>。 これは、特定のジョブを指定した一回限りの try ビルドに便利です。 jobs パラメーターは PR の説明内のディレクティブよりも優先度が高いことに注意してください。
    • たとえば @bors try jobs=job1,job2,job3 のように、複数のパターンを指定することもできます。

各ジョブパターンは、ジョブの正確な名前、または複数のジョブに一致する glob パターンのいずれかにできます。 たとえば *msvc**-alt です。 1 回の try ビルドで開始できるジョブは最大 20 個です。 PR の説明で glob パターンを使用する場合、 たとえばアスタリスクが含まれている場合に GitHub がそのパターンを Markdown として描画するのを避けるため、 必要に応じてバッククォート(`)で囲むことができます。このエスケープは、 @bors jobs= パラメーターを使用する場合には機能しないことに注意してください。

ジョブパターンは、jobs.ymlauto または optional セクションに定義されている 1 つ以上のジョブに一致する必要があります。

  • auto ジョブは、コミットが main ブランチにマージされる前に実行されます。
  • optional ジョブは、try ビルドを通じて明示的に要求された場合にのみ実行されます。 通常、tier 2 および tier 3 ターゲットに使用されます。

try ビルドを行う理由の 1 つは、上記のように @rust-timer queue を使って perf 実行を行うことです。 この perf ビルドは、その後 main 上の何らかのコミットと比較されます。 @bors try parent=<sha> を使用すると、try ビルドとその後の perf 実行を main 上の特定のコミットに基づかせることができ、 perf 比較をできるだけ公平にするのに役立ちます。

try-job PR 説明ディレクティブの使用

  1. 実行したい try-job のセットを特定します。CI ジョブの名前は jobs.yml で確認できます。

  2. PR の説明を修正して、パターンのセットを含めます(通常は PR の説明の末尾)。 例:

    This PR fixes #123456.
    
    try-job: x86_64-msvc
    try-job: test-various
    try-job: `*-alt`
    

    try-job パターンは、それぞれ独立した行に記述する必要があります。

  3. @bors try で指定された try ジョブを実行します。前述のとおり、これにはユーザーが (1) try 権限を持っているか、(2) try 権限を持つ誰かによって @bors delegate=trytry 権限を委譲されている必要があります。

通常、これは jobs.yml を手動で編集するよりも簡単です。 ただし、この方法で実行されるテストのセットを調整できないため、 柔軟性は低くなる場合があります。

try ビルドは rust-lang/rust リポジトリ配下の automation/bors/try ブランチで実行され、 その結果は GitHub Actions ワークフローページ で確認できますが、 通常は対応する PR に bors が投稿するコメントによって結果が通知されます。

異なる PR 間では複数の try ビルドを並行して実行できますが、任意の時点で 1 つの PR 上で実行できる try ビルドは最大 1 つです。

CI ジョブの変更

CI で実行される内容を変更したい場合は、jobs.yml ファイルの prauto、または try セクションを単に変更できます。

一時的に実行内容を変更することもできます。たとえば、ローカルでのテストが難しい 特定のプラットフォームや構成をテストする場合です(たとえば、Windows ビルドが失敗したが、 Windows マシンにアクセスできない場合など)。 そのような状況では、ためらわずに CI リソースを使用してください。

任意の CI ジョブを実行するには、次の 2 つの方法があります。

  • try ビルド機能を使用し、try ビルドで実行したい CI ジョブを PR の説明で指定します。
  • jobs.ymlpr セクションを変更して、 PR への各 push 後にどの CI ジョブを実行するかを指定します。 これは、try ビルドを繰り返し開始するよりも速い場合があります。

PR への各 push 後に実行されるジョブを変更するには、auto セクションから ジョブ定義の 1 つを pr セクションへ単にコピーできます。 たとえば、x86_64-msvc ジョブは 64 ビット MSVC テストを実行する役割を担っています。 これを pr セクションにコピーすると、次のように、PR にコミットが push された後に そのジョブが実行されるようになります。

pr:
  ...
  - image: x86_64-gnu-tools
    <<: *job-linux-16c
  # この項目は `auto` セクションからコピーされました
  # vvvvvvvvvvvvvvvvvv
  - image: x86_64-msvc
    env:
      RUST_CONFIGURE_ARGS: --build=x86_64-pc-windows-msvc --enable-profiler
      SCRIPT: make ci-msvc
    <<: *job-windows-8c

その後、ファイルをコミットし、GitHub 上の PR ブランチへ push できます。 すると GitHub Actions は、PR への各 push 後にこの CI ジョブを実行するはずです。

実験が終わったら、一時的な変更であるはずの jobs.yml への変更を削除することを忘れないでください!

try ジョブをまだ実行している間は PR タイトルの先頭に [WIP] を付け、 テスト目的で CI ジョブを変更するコミットには [DO NOT MERGE] を付けるのが良い習慣です。

CI を使用することは歓迎されますが、これは並行実行数に限りがある共有リソースであることを意識してください。 一度にあまり多くのジョブを有効にしないようにしてください。 ほとんどの場合、1 つか 2 つで十分なはずです。

bors による PR の直列マージ

CI サービスは通常、ブランチの最後のコミットを main の最後のコミットとマージしたものをテストします。 これは機能が単独で動作するかどうかを確認するには優れていますが、 マージされた後にコードが動作することを保証するものではありません。 このような破損は通常、ビルドの実行後に、互換性のない別の PR がマージされたときに発生します。

常に動作する main ブランチを確保するため、手動マージは禁止しています。 代わりに、すべての PR は私たちのボットである bors を通じて承認されなければなりません。 承認されたすべての PR は マージキュー に入れられ (優先度と作成日時でソートされ)、1 つずつ自動的にテストされます。 すべてのビルダーが green であれば PR はマージされ、そうでなければ失敗が 記録され、PR は再度承認される必要があります。 Bors は CI サービスと直接やり取りするわけではありませんが、テストしたい マージコミットを特定のブランチ(automation/bors/autoautomation/bors/try など)にプッシュすることで動作します。 これらのブランチは CI チェックを実行するように設定されています。 その後、Bors は Commit Statuses または Check Runs のいずれかをリッスンすることでビルド結果を検出します。 マージコミットは最新の main に基づいており、一度にテストできるのは 1 つだけなので、 結果が green であれば、main はそのマージコミットへ fast-forward されます。

残念ながら、1 度に 1 つの PR しかテストできないことに加え、CI が長時間(フル実行で約 2 時間)かかるため、1 日に多くの PR をマージすることはできず、 1 回の失敗がスループットに大きく影響します。 1 日にマージできる PR の最大数はおよそ ~10 です。

CI の実行時間が長く、大規模なビルダープールが必要になる主な理由は、 完全なリリース成果物が dist- ビルダーでビルドされるためです。 これらのリリース成果物には次の利点があるため、その価値があります。

  • 後日であっても perf テストを可能にする。
  • 後でバグが見つかったときに二分探索を可能にする。
  • 常にリリースしている状態であれば問題を早期に発見できるため、リリース品質を確保できる。

ロールアップ

一部の PR では、フルテストスイートを実行する必要はありません。たとえば、 タイポ修正や README の改善のような些細な変更はビルドを壊すべきではなく、それらすべてを 2 時間以上かけてテストするのは無駄です。 これを解決するために、私たちは定期的に「ロールアップ」を作成します。これは、保留中の些細な PR を複数マージして、 まとめてテストできるようにした PR です。 ロールアップは、チームメンバーが merge queue の “create a rollup” ボタンを使って手動で作成します。 チームメンバーは自身の判断で、PR にリスクがあるかどうかを決定します。

Docker

macOS と Windows 上のものを除くすべての CI ジョブは、そのプラットフォーム専用の Docker container 内で実行されます。 これには私たちにとって多くの利点があります。

  • 基盤となるイメージの変更に関係なく、ビルド環境が一貫している (trusty イメージから xenial への切り替えは、私たちにとって問題なく行えました)。

  • 最大限のバイナリ互換性を確保するために、古いビルド環境を使用できる。 たとえば、Linux ビルダーでは 古い CentOS リリースを使用しています。

  • Docker イメージキャッシュのおかげで、ツール(QEMU や Android エミュレーターなど)を毎回再インストールせずに済む。

  • ユーザーは次のコマンドを実行するだけで、同じ環境で同じテストをローカルに実行できる。

    cargo run --manifest-path src/ci/citool/Cargo.toml run-local <job-name>
    

    これは失敗のデバッグに役立ちます。 ライセンスやその他の制約により、ローカルで利用できるのは Linux Docker イメージのみであることに注意してください。

dist- という接頭辞が付いた Docker イメージは成果物のビルドに使用され、 その接頭辞がないものはテストやチェックを実行します。

また、CI ではあまり一般的でないアーキテクチャ(主に Tier 2 および Tier 3 プラットフォーム)のテストも実行します。 これらのプラットフォームは x86 ではないため、すべてを QEMU 内で実行するか、 そのプラットフォームのテストを実行したくない場合はクロスコンパイルのみを行います。

これらのビルダーは、GitHub が私たちのためにセットアップし保守している特別なビルダープール上で実行されています。

キャッシュ

私たちの CI ワークフローでは、主に 2 つの用途でさまざまなキャッシュ機構を使用しています。

Docker イメージのキャッシュ

Linux ベースのビルダーの大半を実行するために使用している Docker イメージは、完全にビルドするのに長い時間がかかります。 ビルドを高速化するために、Docker registry caching を使用してこれらをキャッシュし、中間成果物は ghcr.io に保存しています。 また、ビルド済みの Docker イメージも ghcr にプッシュしているため、他のツール (rustup)や、Docker ビルドをローカルで実行している開発者がそれらを再利用できます(ビルドを高速化するため)。

複数の分岐したブランチ(mainbetastable)をテストしているため、 イメージに対して単一のキャッシュに依存することはできません。そうしないと、あるブランチ上のビルドが 他のブランチのキャッシュを上書きしてしまいます。 代わりに、すべての Dockerfile と関連スクリプトの内容から作成した カスタムハッシュで識別される、異なるタグの下にイメージを保存しています。

CI はハッシュキーを計算し、次のいずれかが変更された場合に Docker イメージのキャッシュが 無効化されるようにしています。

  • Dockerfile
  • Dockerfile 内で Docker イメージにコピーされるファイル
  • GitHub runner のアーキテクチャ(x86 または ARM)

Sccache による LLVM キャッシュ

私たちはさまざまな CI ジョブで C/C++ のものをいくつかビルドしており、中間 LLVM 成果物をキャッシュするために Sccache に依存しています。 Sccache は Mozilla が開発した分散 ccache であり、 オブジェクトストレージバケットをストレージバックエンドとして使用できます。

Sccache では、ハッシュキーを自分たちで計算する必要はありません。 Sccache は、ソースコード、コンパイラーのバージョン、重要な環境変数など、 関連する入力の変更を検出すると、自動的にキャッシュを無効化します。 そのため、私たちは Cargo の上に Sccache ラッパーを渡すだけで、残りは Sccache が処理します。

永続的な成果物は S3 バケット rust-lang-ci-sccache2 に保存しています。 そのため CI が実行されると、Sccache は、LLVM が同じ C/C++ コンパイラーでコンパイルされており、 LLVM ソースコードが同一であることを確認した場合、個々のコンパイル済み翻訳単位を S3 から取得します。

CI 周辺のカスタムツール

長年にわたり、私たちは CI 体験を改善するためのカスタムツールをいくつか開発してきました。

PR にエラーメッセージを表示する Rust Log Analyzer

rust-lang/rust のビルドログは非常に大きく、ログを見て ビルド失敗の原因を見つけるのは現実的ではありません。 そのため、私たちは Rust Log Analyzer(RLA)というボットを開発しました。このボットは 失敗時にビルドログを受け取り、エラーメッセージを自動的に抽出して、 PR スレッドに投稿します。

このボットはエラー文字列を探すようにハードコードされているわけではなく、多数の ビルド失敗を使って、ビルド間で共通している行とそうでない行を認識するように訓練されています。 生成されるスニペットがときどき奇妙になることはありますが、このボットは、 これまで見たことのないエラーであっても、関連する行を特定するのにかなり優れています。

許可された失敗をサポートする Toolstate

rust-lang/rust リポジトリは CI でコンパイラーだけをテストしているわけではなく、 さまざまなツールやドキュメントもテストしています。 一部のドキュメントは git サブモジュール経由で取り込まれます。 ドキュメントが修正されるまで rustc PR のマージをブロックしてしまうと、私たちは 鶏と卵の問題に陥ってしまいます。なぜなら、そのドキュメントの CI は、 テスト対象としてまだマージされていないバージョンの rustc を必要とするため、更新しても通らないからです (そして通常、CI が通っていることを要求しています)。

この問題を避けるため、サブモジュールの失敗は許可され、その状態は rust-toolstate に記録されます。 サブモジュールが壊れると、ボットが自動的に メンテナーに ping して破損を知らせ、その失敗を toolstate リポジトリに記録します。 その後、リリースプロセスは nightly 上の壊れたツールを無視し、 配布される nightly からそれらを削除します。 ツールの失敗はほとんどの場合許容されますが、リリースの 1 週間前には自動的に禁止されます。nightly でツールが壊れているかどうかは問題にしませんが、beta と stable では動作しなければならないため、nightly を beta に昇格させる数日前には nightly でも動作している必要があります。

詳細は toolstate ドキュメントで確認できます。

公開 CI ダッシュボード

Rust CI を監視するには、infra チームが管理している公開ダッシュボードを確認できます。

以下は、ダッシュボード内の便利なパネルです。

  • パイプライン所要時間: auto ビルドの実行にどれくらい時間がかかるかを確認します。
  • 最も遅いジョブ: 実行に最も時間がかかっているジョブを確認します。
  • ジョブ所要時間の中央値の変化: 以前より遅くなっているジョブを確認します。 これはリグレッションの検出に役立ちます。
  • 最も失敗しているジョブ: 最も多く失敗しているジョブを確認します。

ダッシュボードの詳細については、Datadog CI ドキュメントを参照してください。

CI 設定の判定

特定のジョブについて、CI でどの bootstrap.toml 設定が使用されているかを判定したい場合は、ビルドログを見るのが最も簡単でしょう。 そのためには、次のようにします。

  1. Rust CI の成功したワークフロー実行ページに移動し、 最新のものをクリックします。
  2. 左側で、関心のあるジョブを選択します。
  3. 歯車アイコンをクリックし、“View raw logs” を選択します。
  4. 文字列 “Configure the build” を検索します。
  5. すべてのビルド設定は、build.configure-args というテキストがある行に一覧表示されています。

新しいテストの追加

一般に、rustc のバグを修正するすべての PR には、何らかの回帰テストが 付随していることを期待しています。 このテストは main では失敗し、PR 後には 成功する必要があります。 これらのテストは、過去の過ちを繰り返さないようにするために 非常に役立ちます。

最初に決めるべきことは、どの種類のテストを追加するかです。 これは変更の性質と、何を検証したいかによって決まります。 大まかなガイドラインをいくつか示します。

  • コンパイラーテストの大半は compiletest で行われます。
    • compiletest テストの大半は、tests/ui ディレクトリにある UI テストです。
  • 標準ライブラリへの変更は、通常、標準ライブラリ内でテストされます。
    • 標準ライブラリのテストの大半は、典型的な API の挙動を例示し検証する doctest として書かれます。
    • 追加のユニットテストlibrary/${crate}/tests に置く必要があります(${crate} は通常 std です)。
    • alloc または core クレートのテストは、それぞれ別のクレート alloctests または coretests に置かなければなりません。
      • NOTE: 不安定な機能のユニットテストを追加する場合、#![feature(...)] 宣言は library/${crate}tests/lib.rs ではなく library/${crate}tests/tests/lib.rs に追加しなければなりません。
  • コードが独立したシステムの一部であり、コンパイラー出力をテストしていない場合は、 ユニットテストまたは統合テストの使用を検討してください。
  • rustdoc を実行する必要がありますか? rustdoc または rustdoc-ui テストを優先してください。 場合によっては rustdoc-js も必要になります。
  • その他の compiletest テストスイートは、一般に特別な目的で使用されます。
    • gdb または lldb を実行する必要がありますか? debuginfo テストスイートを使用してください。
    • LLVM IR または MIR IR を調べる必要がありますか? codegen または mir-opt テストスイートを使用してください。
    • 生成されたバイナリを何らかの方法で調べる必要がありますか? あるいは他のすべてのテストスイートでは目的に対して制限が強すぎますか? その場合は run-make を使用してください。
      • ツリー内の rustc と組み合わせてツリー内の cargo を検証する必要がある場合は、 run-make-cargo を使用してください。
    • より専門的なテストスイートについては、compiletest の章を参照してください。

追加するテストの種類を決めたら、長期にわたって扱いやすいテストを作成する方法について、 ベストプラクティスを参照してください (つまり、数年後にテストが失敗したり変更が必要になったりした場合に、 どうすれば作業を容易にできるか、ということです)。

UI テストの手順

以下は、最も一般的なコンパイラーテストの 1 つである UI テストを作成するための基本的なガイドです。 このチュートリアルでは、async のエラーメッセージ用のテストを追加します。

ステップ 1: テストファイルを追加する

最初のステップは、tests/ui ツリー内のどこかに Rust ソースファイルを 作成することです。 テストを作成するときは、適切な場所と名前を見つけるよう最善を尽くしてください (詳細はテストの整理を参照)。 命名は開発で最も難しい部分なので、 ここから先はすべて楽になるはずです!

async テストを tests/ui/async-await/await-without-async.rs に置きましょう。

// ユーザーが非`async`関数内で`await`を書いたときに診断を提供する。
//@ edition:2018

async fn foo() {}

fn bar() {
    foo().await
}

fn main() {}

このテストについて注目すべき点がいくつかあります。

  • 先頭は、テストの目的を説明する短いコメントで 始める必要があります。
  • //@ edition:2018 コメントはディレクティブと呼ばれ、 テストをどのようにビルドするかについて compiletest に指示を提供します。 ここでは、async が動作するようにエディションを 設定する必要があります(デフォルトは 2015 エディションです)。
  • その後にテストのソースが続きます。 簡潔で要点を押さえたものにするようにしてください。 バグ報告から例を最小化しようとしている場合は、 ある程度の労力が必要になることがあります。
  • このテストは空の fn main 関数で終えています。 これは、UI テストのデフォルトが bin クレートタイプであり、テストに「main not found」エラーを 出したくないためです。 代わりに、#![crate_type="lib"] を追加することもできます。

ステップ 2: 期待される出力を生成する

次のステップは、コンパイラーから期待される出力スナップショットを作成することです。 これは --bless オプションで行えます。

./x test tests/ui/async-await/await-without-async.rs --bless

これにより、コンパイラーがビルドされ(まだビルドされていない場合)、 テストがコンパイルされ、コンパイラーの出力が tests/ui/async-await/await-without-async.stderr というファイルに配置されます。

しかし、このステップは失敗します! 次のようなエラーメッセージが表示されるはずです。

error: /rust/tests/ui/async-await/await-without-async.rs:7: unexpected error: ‘7:10: 7:16: await is only allowed inside async functions and blocks E0728’

これは、stderr にソースファイル内のエラーアノテーションと一致しないエラーが 含まれているためです。

ステップ 3: エラーアノテーションを追加する

すべてのエラーには、エラーのテキストを含むコメントでソース内に アノテーションを付ける必要があります。 この場合、次のコメントをテストファイルに追加できます。

fn bar() {
    foo().await
    //~^ ERROR `await`は`async`関数およびブロック内でのみ許可されています
}

//~^ の波線キャレットコメントは、そのエラーが 前の行に属していることを compiletest に伝えます (詳細はエラーアノテーションセクションを参照)。

保存して、もう一度テストを実行します。

./x test tests/ui/async-await/await-without-async.rs

これで成功するはずです。やった!

ステップ 4: 出力を確認する

前のステップとある程度並行して、作成された .stderr ファイルを確認し、 期待どおりに見えるかを調べる必要があります。 新しい診断メッセージを追加している場合は、 メッセージ全体がどれだけ読みやすく見えるか、 特に Rust に慣れていない人にとってどうかも考えるよい機会です。

例の tests/ui/async-await/await-without-async.stderr ファイルは 次のようになります。

error[E0728]: `await` is only allowed inside `async` functions and blocks
  --> $DIR/await-without-async.rs:7:10
   |
LL | fn bar() {
   |    --- this is not `async`
LL |     foo().await
   |          ^^^^^^ only allowed inside `async` functions and blocks

error: aborting due to previous error

For more information about this error, try `rustc --explain E0728`.

通常のコンパイラー出力とは少し異なって見える点があることに 気づくかもしれません。

  • $DIR は、システム間で異なるパス情報を取り除きます。
  • LL 値は行番号を置き換えます。 これにより、ソース内の小さな変更が大きな差分を引き起こすことを避けられます。 詳細は正規化セクションを参照してください。

この段階では、最後のいくつかのステップを何度か繰り返して、 テストを調整し、テストを再 bless し、出力を再確認する必要があるかもしれません。

ステップ 5: 他のテストを確認する

診断メッセージを追加または変更すると、テストスイート内の他のテストに 影響することがあります。 PR を投稿する前の最後のステップは、他に影響を与えていないかを 確認することです。 通常は UI スイートを実行するのが良い出発点です。

./x test tests/ui

他のテストが失敗し始めた場合、何が変更されたのか、そして 新しい出力が妥当かどうかを調査する必要があるかもしれません。

また、--bless フラグを使って出力を再承認する必要があるかもしれません。

テストの内容を説明するコメント

テストファイルの最初のコメントは、テストの要点を要約し、 そのテストで何が重要なのかを強調するべきです。 そのテストに関連する issue 番号がある場合は、その issue 番号を含めてください。

このコメントは、それほど詳しくする必要はありません。 次のようなものだけでも十分な場合があります: 「#18060 の回帰テスト: match アームが誤った順序でマッチしていた」。

これらのコメントは、後であなたのテストが壊れたときに、他の人にとって非常に役立ちます。なぜなら、 問題が何であるかを強調できることが多いからです。 また、何らかの 理由でテストをリファクタリングする必要がある場合にも役立ちます。なぜなら、そのテストのどの部分が 重要だったのかを他の人に知らせてくれるからです。 多くの場合、テストは本来テストするはずだったことを もはやテストしなくなったために書き直さなければならず、その場合、そのテストが正確に何をテストする はずだったのかを知ることが役立ちます。

テストを書くためのベストプラクティス

この章では、テストの作成と変更に関するベストプラクティスについて説明します。 私たちが作成するテストは、何年も後になっても、元の作成者に確認したり、 大量の git 考古学を行ったりする必要なく、理解しやすく変更しやすいものにしたいと考えています。

自分が作成したテストを、数年後にあまり文脈がない状態で失敗したテストを見ている 別のコントリビューターになったつもりでレビューするのはよい習慣です (これは数日後や数か月後の自分自身にも役立ちます!)。 そして自問してください。どうすれば自分やその人たちの作業を楽にできるだろうか?

これを具体的に捉えやすくするため、まずは、別のコントリビューターの作業を 可能な限り困難にするテストを書く方法についての余談から始めましょう。

余談: シンプルなテスト妨害フィールドマニュアル

別のコントリビューターの作業を可能な限り困難にするには、次のようなことをするとよいでしょう。

  • テストに、他の文脈を一切含めず issue 番号だけの名前を付ける。例: issue-123456.rs
  • そのテストが何を検証しようとしているのかについてのコメントや、関連する文脈へのリンクを 一切含めない。
  • (本来は最小化できるにもかかわらず)巨大なテストを含め、 そのテストが実際にテストしようとしている核心から注意をそらす、 本質的でない部分を含める。
  • テストが確認しようとしている内容にとって重要ではない、無関係な構文エラーやその他のエラーを 大量に含める。
  • スニペットを奇妙な形式で整形する。
  • 使用されておらず無関係な機能を大量に含める。
  • たとえば ignore-windows compiletest directives を含めるが、それらが なぜ 必要なのかについて説明しない。

テストの命名

読者が、issue 番号を入力して github 検索を掘り進め、 そのテストが何を検証しようとしているのかを探さなくても済むように、 テストが何を検証しているのかをすぐに理解できるようにします。 これには、関連するテストの集まりとして --test-args でテストをフィルターできるようになる、 という追加の利点もあります。

  • テストが検証しようとしている内容、または回帰を防ごうとしている内容に基づいて名前を付ける。
  • 簡潔に保つ。
  • issue 番号だけをテスト名として使うのを避ける。
  • 自動補完の劣化につながるため、テスト名を issue-xxxxx プレフィックスで始めるのを避ける。

issue 番号だけをテスト名として使うのを避ける

代わりに、テストコメント内のリンクや #123456 として含めることを推奨します。あるいは、 issue 番号を含めることに意味がある場合は、 macro-external-span-ice-123956.rs のような短いキーワードも含めてください。

tests/ui/typeck/issue-123456.rs                              // 悪い
tests/ui/typeck/issue-123456-asm-macro-external-span-ice.rs  // 悪い(タブ補完にとって)
tests/ui/typeck/asm-macro-external-span-ice-123456.rs        // 良い
tests/ui/typeck/asm-macro-external-span-ice.rs               // 良い

issue-123456.rs は、そのテストが実際に何を検証しているのかをすぐには何も教えてくれないため、 追加で検索する必要があります。テスト名のプレフィックスとして issue 番号を含めると、 タブ補完の有用性が下がります (テストディレクトリで ls したときに issue-xxxxx プレフィックスが大量に出てくる場合など)。 issue へのリンクはテストコメント内に置くことができます。

//! 外部クレート由来のネストしたマクロを含む `asm!` マクロが、
//! コードポイント境界アサーション ICE につながらないことを確認する。
//!
//! <https://github.com/rust-lang/rust/issues/123456> の回帰テスト。

このルールの例外の 1 つが crash tests です。そこでは、 テストが issue 番号だけで命名されるのが標準です。というのも、その目的は、 もはや ICE/クラッシュしなくなった issue 由来のスニペットを追跡することであり、 それらは修正 PR で削除されるか、適切な ui/その他のテストへ変換されるためです。

テストの構成

  • ほとんどのテストスイートでは、テストを置くための意味的に適切なサブディレクトリを見つけるようにしてください。
    • たとえば、RFC 2093 の実装に特化した場合、 tests/ui/rfc-2093-infer-outlives/ 配下に一連のテストをまとめることができます。 ディレクトリ名には、その RFC が何に関するものかを含めてください。
  • run-make/run-make-support テストスイートでは、それぞれの rmake.rs は、 それぞれ tests/run-make/ または tests/run-make-cargo/ の直下のサブディレクトリ内に 含まれていなければなりません。 現時点では、さらに深いネストはサポートされていません。 この場合も、テスト名に issue 番号「だけ」を使うのは避けてください。

テストの説明

変更によってテストが失敗した場合に、他のコントリビューターがそのテストの内容を理解できるように、 テストには、その意図/目的、関連する文脈へのリンク(issue 番号やその他の議論を含む)、 場合によっては関連リソース(たとえば、特定の挙動について Win32 API へリンクすると役立つことがあります) について十分なドキュメントを含めるべきです。

よいコメントを含むテストの概要

//! テストが何を検証しているのかの簡潔な概要。
//! 例: #123456 の回帰テスト: coverage 属性が非 item に適用されたときに
//!     ICE しないことを確認する。
//!
//! 任意: 関連するテスト/issue、外部 API/ツール、クラッシュの
//!     仕組み、どのように修正されたか、FIXME、制限事項などについての備考。
//! 例: このテストは `tests/attrs/linkage.rs` に似ているが、このテストは
//!     異なるコードパスを検証する `#[coverage]` を特に確認している。
//!     ICE は属性検証中に、`def_path_str` を構築しようとしたものの、
//!     プラットフォームが windows の場合にのみ診断を出力したため、
//!     unix で ICE が発生したことで引き起こされた。
//!
//! 関連する issue や議論へのリンク。以下に例を示す:
//! <https://github.com/rust-lang/rust/issues/123456> の回帰テスト。
//! <https://github.com/rust-lang/rust/issues/101345> も参照。
//! <https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/123456-example-topic> の議論を参照。
//! [`clone(2)`] を参照。
//!
//! [`clone(2)`]: https://man7.org/linux/man-pages/man2/clone.2.html

//@ ignore-windows
// 理由: (なぜこのテストは windows で無視されるのか?なぜ特に
// windows-gnu や windows-msvc ではないのか?)

// 任意: テストケースの概要: どの肯定的ケースが確認されるのか?
// どの否定的ケースが確認されるのか?特有の癖はあるか?

fn main() {
    #[coverage]
    //~^ ERROR coverage attribute can only be applied to function items.
    let _ = {
        // 読者が注意を払うべき点を強調するコメント。
        fn foo() {}
    };
}

どれだけの文脈/説明が必要かは、作成者とレビュー担当者の裁量に委ねられます。 よい経験則として、テストで検証している自明でない事柄には、 他のコントリビューターが理解する助けとなる説明を付けるべきです。 これには、次のような備考が含まれる場合があります。

  • ICE が発生する仕組みがかなり複雑な場合、その仕組み。
  • 関連する issue やテスト(たとえば、このテストは別のテストに似ているが、 …という理由で分けて維持されている、など)。
  • プラットフォーム固有の挙動。
  • 外部依存関係や API の挙動: syscall、リンカー、ツール、 環境など。

テスト内容

  • テストは可能な限り最小限になるようにしてください。
  • 重要でないコードを最小限に抑え、特に stderr スナップショットを乱雑にし得る不要な構文エラーや型エラーを 最小限に抑えてください。
  • 関係のない警告を抑制するには、#![allow(...)] または #![expect(...)] を使用してください。
  • 可能であれば、意味的に有意義な名前を使用してください(例: fn bare_coverage_attributes() {})。

不安定なテスト

すべてのテストは、再現可能で信頼できるものになるよう努める必要があります。 不安定なテストは最悪の種類のテストであり、そもそもテストが存在しないことよりも悪いとさえ言えるでしょう。

  • 不安定なテストは、まったく関係のない PR で失敗する可能性があり、他の コントリビューターを混乱させ、テストの失敗が関係しているかどうかを調べる時間を浪費させる可能性があります。
  • 不安定なテストは、そのテスト結果から有用な情報を何も提供しません。 不安定で信頼できないということ以外は分かりません。テストが成功しても不安定なら、単に運が良かっただけでしょうか? テストが不安定で失敗したなら、それは単に偶発的なものでしょうか?
  • 不安定なテストは、テストスイート全体への信頼を低下させます。 テストスイートが不安定なテストによって ランダムに偶発的に失敗する可能性がある場合、テストスイート全体が成功したのでしょうか、それとも 単に運が良かった/悪かっただけなのでしょうか?
  • 不安定なテストはフル CI でランダムに失敗する可能性があり、それまでのフル CI リソースを浪費します。

Compiletest ディレクティブ

ディレクティブの一覧については、compiletest ディレクティブを参照してください。

  • ignore-*/needs-*/only-* ディレクティブについては、極めて明白な場合を除き、 そのディレクティブが必要な理由について簡潔な注釈を記載してください。例: "//@ ignore-wasi (wasi codegens the main symbol differently)"
  • //@ ignore-auxiliary を使用する場合は、対応するメインのテストファイルを指定してください。 例: //@ ignore-auxiliary (used by `./foo.rs`)

FileCheck のベストプラクティス

詳細については、LLVM FileCheck ガイドを参照してください。

  • 特定のレジスタ番号や基本ブロック番号がテストにとって特別または重要でない限り、 それらにマッチさせることは避けてください。 適切な場合は、それらにマッチさせるためにパターンを使用することを検討してください。

TODO

具体的な助言は未定です。

Compiletest

はじめに

compiletest は Rust テストスイートの主要なテストハーネスです。 これにより、テスト作成者は大量のテスト(Rust コンパイラには数千ものテストがあります)を整理し、効率的にテストを実行(並列実行がサポートされています)し、個別のテストおよびテストのグループの挙動と期待される結果を設定できます。

macOS ユーザーへの注意

macOS ユーザーの場合、SIP(System Integrity Protection)が Apple にネットワークリクエストを送信して、コンパイル済みバイナリを継続的にチェックする場合があるため、テストの実行時に大幅なパフォーマンス低下が発生することがあります。

次の設定を調整することで解決できます: Privacy & Security -> Developer Tools -> Add Terminal (Or VsCode, etc.)

compiletest は、テストコードがコンパイル時または実行時に成功/失敗するかをチェックできます。

テストは通常、テストコードの前および/または内部のコメントにアノテーションを付けた Rust ソースファイルとして構成されます。 これらのコメントは、テストを実行するかどうか、どのように実行するか、どのような挙動を期待するかなどを compiletest に指示する役割を果たします。 これらのアノテーションの詳細については、ディレクティブおよび以下のテストスイートのドキュメントを参照してください。

新しいテストを作成するためのチュートリアルや、良いテストを書くための助言については、新しいテストの追加およびベストプラクティスの章を参照してください。また、テストスイートの実行方法については、テストの実行の章を参照してください。

compiletest には --test-args を使用するか、-- の後に配置することで引数を渡せます。例:

  • x test --test-args --force-rerun
  • x test -- --force-rerun

さらに、bootstrap は一般的な引数をいくつか直接受け付けます。例:

x test --no-capture --force-rerun --run --pass

Compiletest 自体は、関係するアーティファクト(主にコンパイラ)が変更されていない場合、テストの実行を避けようとします。 入力のいずれも変更されていない場合でもテストを再実行するには、x test --test-args --force-rerun を使用できます。

テストスイート

すべてのテストは tests ディレクトリ内にあります。 テストは「スイート」に整理され、各スイートは個別のサブディレクトリにあります。 各テストスイートは、コンパイラの挙動や正しさのチェック方法が異なり、少しずつ異なる動作をします。 たとえば、tests/incremental ディレクトリにはインクリメンタルコンパイルのテストが含まれています。 各種スイートは、src/tools/compiletest/src/common.rspub enum Mode 宣言で定義されています。

詳細情報へのリンク付きで、次のテストスイートを利用できます:

コンパイラ固有のテストスイート

テストスイート目的
uiコンパイルおよび/または生成された実行ファイルの実行から得られる stdout/stderr スナップショットをチェックする
ui-fulldepsrustc のリンク可能なビルドを必要とする ui テスト(extern crate rustc_span; を使用する場合やプラグインとして使用される場合など)
prettypretty printing をチェックする
incrementalインクリメンタルコンパイルの挙動をチェックする
debuginfoデバッガを実行して debuginfo の生成をチェックする
codegen-*コード生成をチェックする
codegen-unitscodegen unit の分割をチェックする
assemblyアセンブリ出力をチェックする
mir-optMIR の生成と最適化をチェックする
coverageカバレッジ計測をチェックする
coverage-run-rustdoc計測された doctest も実行する coverage テスト
crashes意図しない修正を検出するため、特定の入力でコンパイラが ICE/panic/crash することをチェックする

汎用テストスイート

run-make は、Rust プログラムを使用する汎用テストです。

build-std テストスイート

build-std は -Zbuild-std が動作することをテストします。

Rustdoc テストスイート

テストスイート目的
rustdoc-htmlrustdoc の HTML 出力をチェックする
rustdoc-guiWeb ブラウザを使用して rustdoc の GUI をチェックする
rustdoc-jsrustdoc の検索エンジンとインデックスをチェックする
rustdoc-js-stdstd ライブラリドキュメント上の rustdoc の検索エンジンとインデックスをチェックする
rustdoc-jsonrustdoc の JSON 出力をチェックする
rustdoc-uirustdoc のターミナル出力をチェックする(関連項目

rustdoc 固有のテストの一部は ui/rustdoc/ にもあります。 これらのテストは、rustdoc の実行の一部として出力される特定の lint が、rustc の実行時にも実行されることを保証します。 rustdoc に関する Run-make テストは通常、run-make/rustdoc-*/ という名前です。

Pretty-printer テスト

tests/pretty 内のテストは、rustc の「pretty-printing」機能を実行します。 rustc-Z unpretty CLI オプションを使用すると、入力ソースを、マクロ展開後の Rust ソースなど、さまざまな形式に変換します。 プリティプリンターテストには、以下で説明するいくつかのディレクティブがあります。 これらのコマンドはテストの挙動を大きく変更できますが、コマンドがない場合の デフォルトの挙動は次のとおりです。

  1. ソースファイルに対して rustc -Zunpretty=normal を実行します。
  2. 前のステップの出力に対して rustc -Zunpretty=normal を実行します。
  3. 前の 2 つのステップの出力は同じである必要があります。
  4. 型チェックできることを確認するために、出力に対して rustc -Zno-codegen を実行します (cargo check と同様)。

上記のコマンドのいずれかが失敗した場合、そのテストは失敗します。

プリティプリントテストのディレクティブは次のとおりです。

  • pretty-mode は、プリティプリントテストを実行するモード(つまり、 -Zunpretty への引数)を指定します。 指定されていない場合のデフォルトは normal です。
  • pretty-compare-only は、プリティテストでプリティプリントされた 出力のみを比較するようにします(上記のステップ 3 の後で停止します)。 型チェックのために展開された出力をコンパイルしようとはしません。 これは、有効な Rust に展開されない pretty-mode や、 展開された出力をコンパイルできないその他の状況で必要です。
  • pp-exact は、プリティプリントテストが特定の出力になることを保証するために使用されます。 値なしで指定された場合、プリティプリント出力は 元のソースと一致する必要があることを意味します。 //@ pp-exact:foo.pp のように値付きで指定された場合、プリティプリントされた出力が 指定されたファイルの内容と一致することを保証します。 それ以外の場合、pp-exact が指定されていなければ、 プリティプリントされた出力はもう一度プリティプリントされ、2 回の プリティプリントの出力が比較されて、プリティプリントされた出力が 定常状態に収束することを確認します。

インクリメンタルテスト

tests/incremental のテストはインクリメンタルコンパイルをテストします。 これらは revisions ディレクティブを使用して、compiletest にコンパイラーを 一連のステップで実行するよう指示します。

Compiletest は -C incremental フラグ付きの空のディレクトリから開始し、 その後、前のステップのインクリメンタル結果を再利用しながら、各リビジョンに対してコンパイラーを実行します。

各リビジョン名は、次のいずれかで始まる必要があります。

  • cpass - テストは正常にコンパイルされなければなりません(チェックビルド、コード生成なし)
  • bfail — テストはコンパイルに失敗しなければなりません(完全ビルド、コード生成あり)
  • bpass — テストは正常にコンパイルされなければなりません(完全ビルド、コード生成あり)
  • rpass — テストは正常にコンパイルおよび実行されなければなりません

リビジョンを一意にするには、rpass1rpass2 のようなサフィックスを追加する必要があります。

ソースの変更をシミュレートするために、compiletest は現在のリビジョン名を指定した --cfg フラグも渡します。

たとえば、これは関数の変更をシミュレートして 2 回実行されます。

//@ revisions: rpass1 rpass2

#[cfg(rpass1)]
fn foo() {
    println!("one");
}

#[cfg(rpass2)]
fn foo() {
    println!("two");
}

fn main() { foo(); }

インクリメンタルテストは、特定の部分文字列がコンパイラー出力のどこにも 現れてはならないことを指定する forbid-output ディレクティブをサポートしています。 これは、特定のエラーが現れないことを保証するのに有用ですが、エラーメッセージは 時間とともに変わるため壊れやすく、テストがもはや適切な内容をチェックしていなくても合格し続ける可能性があります。

インクリメンタルテストでは #[rustc_clean(...)] 属性を使用できます。 この属性は、現在のコンパイルセッションのフィンガープリントを前回のものと比較します。 最初のリビジョンでは、有効な rustc_clean 属性を決して持つべきではありません。これは常に dirty になるためです。

デフォルトモードでは、フィンガープリントが同じでなければならないことをアサートします。 この属性は次の引数を取ります。

  • cfg="<cond>" — cfg 条件 <cond> をチェックし、その cfg 条件が true と評価された場合にのみチェックを実行します。 これは、特定のリビジョンでのみ rustc_clean 属性を実行するために使用できます。
  • except="<query1>,<query2>,..." — 列挙されたクエリについて、クエリ結果が同じではなく 異なっていなければならないことをアサートします。
  • loaded_from_disk="<query1>,<query2>,..." — 列挙されたクエリについて、クエリ結果が 実際にディスクから読み込まれたことをアサートします(単に green とマークされたのではありません)。 これは、テストが特定のクエリ結果について実際にデシリアライズ ロジックを実行していることを保証するのに有用です。 これは except と組み合わせることができます。

rustc_clean を使用するテストの簡単な例は hello_world test です。

Debuginfo テスト

tests/debuginfo のテストはデバッグ情報の生成をテストします。 これらはプログラムをビルドし、デバッガーを起動し、デバッガーにコマンドを発行します。 1 つのテストは cdb、gdb、lldb で動作できます。

ほとんどのテストには、適切なデバッグ情報を生成するために、 //@ compile-flags: -g ディレクティブまたはそれに類するものを含めるべきです。

行にブレークポイントを設定するには、その行に // #break コメントを追加します。

デバッグ情報テストは、一連のデバッガーコマンドと、 デバッガーから期待される出力を指定する “check” 行で構成されます。

コマンドは // $DEBUGGER-command:$COMMAND という形式のコメントであり、 $DEBUGGER は使用されているデバッガー、$COMMAND は実行するデバッガーコマンドです。

デバッガーの値として使用できるものは次のとおりです。

  • cdb
  • gdb
  • gdbg — Rust サポートなしの GDB(7.11 より古いバージョン)
  • gdbr — Rust サポートありの GDB
  • lldb
  • lldbg — Rust サポートなしの LLDB
  • lldbr — Rust サポートありの LLDB(これは現在は存在しません)

出力をチェックするコマンドは // $DEBUGGER-check:$OUTPUT という形式であり、 $OUTPUT は期待される出力です。

たとえば、次はテストをビルドし、デバッガーを開始し、 ブレークポイントを設定し、プログラムを起動し、値を調べ、デバッガーが出力する内容をチェックします。

//@ compile-flags: -g

//@ lldb-command: run
//@ lldb-command: print foo
//@ lldb-check: $0 = 123

fn main() {
    let foo = 123;
    b(); // #break
}

fn b() {}

現在使用されているデバッガーに基づいてテストを無効化するために、次の ディレクティブを使用できます。

  • min-cdb-version: 10.0.18317.1001 — cdb のバージョンが指定されたバージョン未満の場合、 テストを無視します
  • min-gdb-version: 8.2 — gdb のバージョンが指定されたバージョン未満の場合、テストを無視します
  • ignore-gdb-version: 9.2 — gdb のバージョンが指定されたバージョンと等しい場合、 テストを無視します
  • ignore-gdb-version: 7.11.90 - 8.0.9 — gdb のバージョンが 範囲内(両端を含む)の場合、テストを無視します
  • min-lldb-version: 310 — lldb のバージョンが指定されたバージョン未満の場合、テストを無視します
  • rust-lldb — lldb に Rust プラグインが含まれていない場合、テストを無視します。 注: “Rust” 版の LLDB はもう存在しないため、これは常に無視されます。 これはおそらく削除すべきです。

compiletest に --debugger オプションを渡すことで、テストを実行する単一のデバッガーを指定できます。 たとえば、./x test tests/debuginfo -- --debugger gdb は GDB コマンドのみをテストします。

lldb debuginfo テストをローカルで実行する際の注意

lldb debuginfo テストをローカルで実行したい場合、現時点では Windows で 次のことが必要です:

  • Python 3.10 がインストールされていること。
  • python310.dllPATH 環境変数で利用可能であること。これは python.org から入手する標準の Python インストーラーでは提供されません。 手動で PATH に追加する必要があります。

そうしないと、lldb debuginfo テストが不可解な形でクラッシュする可能性があります。

Windows 11 で cdb.exe を取得する際の注意

cdb.exe は、Visual Studio インストーラー(例: Visual Studio 2022 インストーラー)の 「Desktop Development with C++」ワークロードプロファイルの一部である、適切な 「Windows 11 SDK」とともに取得されます。

ただし、既定ではこれだけでは十分ではありません。cdb.exe が必要な場合は、 Installed Apps に移動し、最新の「Windows Software Development Kit」を見つける必要があります (そして、OS が Windows 11 と呼ばれているにもかかわらず、これが Windows 10.0.22161.3233 と表示されることもあります)。その後、 cdb.exe を取得するために、「Modify」->「Change」をクリックしてから、 「Debugging Tools for Windows」を選択する必要があります。

コード生成テスト

tests/codegen-llvm のテストは LLVM コード生成をテストします。 これらは、--emit=llvm-ir フラグを指定してテストをコンパイルし、LLVM IR を出力します。 その後、LLVM の FileCheck ツールを実行します。 テストには、生成されたコードをチェックするためにさまざまな // CHECK コメントが注釈として付けられます。 チュートリアルおよび詳細については、FileCheck ドキュメントを参照してください。

同様の一連のテストについては、アセンブリテスト も参照してください。

既定では、コード生成テストには //@ needs-target-std暗黙的に 指定されます (ターゲットが std をサポートする必要があることを意味します)。ただし、 テストソースで #![no_std]/#![no_core] 属性が指定されている場合は除きます。 この動作を上書きして、テストが #![no_std]/#![no_core] であっても、 ターゲットが std をサポートする場合にのみテストを実行するように、 明示的に //@ needs-target-std と書くことができます。

#![no_std] のクロスコンパイルテストを扱う必要がある場合は、 minicore テスト補助 の章を参照してください。

アセンブリテスト

tests/assembly-llvm のテストは LLVM アセンブリ出力をテストします。 これらは、--emit=asm フラグを指定してテストをコンパイルし、アセンブリ出力を含む .s ファイルを出力します。 その後、LLVM の FileCheck ツールを実行します。

各テストには、アセンブリ出力の種類を示すために、 emit-asm または ptx-linker のいずれかの値を持つ //@ assembly-output: ディレクティブを注釈として付ける必要があります。

次に、アセンブリ出力をチェックするために、さまざまな // CHECK コメントを注釈として付ける必要があります。 チュートリアルおよび詳細については、FileCheck ドキュメントを参照してください。

同様の一連のテストについては、コード生成テスト も参照してください。

#![no_std] のクロスコンパイルテストを扱う必要がある場合は、 minicore テスト補助 の章を参照してください。

命令サポートに基づく条件付きアセンブリテスト

特定のアセンブリ命令が利用可能であることに依存するテストでは、 //@ needs-asm-mnemonic: <MNEMONIC> ディレクティブを使用できます。 ターゲットバックエンドが指定された命令ニーモニックをサポートしていない場合、このテストはスキップされます。

たとえば、RET 命令を必要とするテストは次のとおりです:

//@ needs-asm-mnemonic: RET

Codegen-units テスト

tests/codegen-units のテストは、 単相化 コレクターと CGU 分割をテストします。

これらのテストは、単相化コレクションパスの結果を出力するフラグ、 すなわち -Zprint-mono-items を指定して rustc を実行し、 その後、ファイル内の特別な注釈を使用してそれと比較することで動作します。

次に、テストには //~ MONO_ITEM name という形式のコメントを注釈として付ける必要があります。 ここで name は、fn <u32 as Trait>::foo のように rustc によって出力される単相化済み文字列です。

CGU 分割をチェックするには、//~ MONO_ITEM name @@ cgu という形式のコメントを使用します。 ここで cgu は、CGU 名と角括弧内のリンケージ情報を空白区切りにしたリストです。 例: //~ MONO_ITEM static function::FOO @@ statics[Internal]

Mir-opt テスト

tests/mir-opt のテストは、生成された MIR の一部をチェックし、 それが正しく生成され、期待される最適化を行っていることを確認します。 詳細については、MIR 最適化 の章を確認してください。

Compiletest は、MIR 出力をダンプして最適化のベースラインを設定するために、 いくつかのフラグを指定してテストをビルドします:

  • -Copt-level=1
  • -Zdump-mir=all
  • -Zmir-opt-level=4
  • -Zvalidate-mir
  • -Zdump-mir-exclude-pass-number

テストには、期待される MIR 出力を含むファイルを指定する // EMIT_MIR コメントを注釈として付ける必要があります。 初期の期待ファイルを作成するには、x test --bless を使用できます。

EMIT_MIR コメントにはいくつかの形式があります:

  • // EMIT_MIR $MIR_PATH.mir — これは、指定されたファイル名が MIR ダンプからの正確な出力と一致することをチェックします。 たとえば、 my_test.main.SimplifyCfg-elaborate-drops.after.mir はそのファイルを テストディレクトリから読み込み、rustc からのダンプと比較します。

    “after” ファイル(最適化後のもの)をチェックすることは、 最適化後の最終状態に関心がある場合に有用です。 完全性のために “before” ファイルを使用したいまれなケースもあります。

  • // EMIT_MIR $MIR_PATH.diff — ここで $MIR_PATH は、 my_test_name.my_function.EarlyOtherwiseBranch のような MIR ダンプのファイル名です。 Compiletest は .before.mir ファイルと .after.mir ファイルの差分を取り、その diff 出力を EMIT_MIR コメントの期待される .diff ファイルと比較します。

    これは、最適化によって MIR がどのように変化するかを確認したい場合に有用です。

  • // EMIT_MIR $MIR_PATH.dot — 追加の MIR データをダンプする特定のフラグ (例: .dot ファイルを生成する -Z dump-mir-graphviz)を使用している場合、 これは出力が指定されたファイルと一致することをチェックします。

既定では、32 ビットターゲットと 64 ビットターゲットは同じダンプファイルを使用しますが、 定数内のポインターやその他のビット幅に依存するものが存在する場合、これは問題になる可能性があります。 その場合は、テストに // EMIT_MIR_FOR_EACH_BIT_WIDTH を追加することで、 32bit システムと 64bit システム用に別々のファイルが生成されるようにできます。

run-make テスト

tests/run-make および tests/run-make-cargo のテストは、Rust レシピ を使用する汎用 テストです。これは任意の Rust コード(rustc の呼び出しなど)を可能にする小さなプログラム (rmake.rs)であり、run_make_support ライブラリによってサポートされています。 Rust レシピを使用すると、究極の柔軟性が得られます。

run-make テストは、他のどのテストスイートもニーズにより適していない場合に使用する必要があります。 run-make-cargo テストスイートはさらに、ツリー内の cargo をビルドして、 ツリー内の rustc と組み合わせてツリー内の cargo をテストする必要がある ユースケースをサポートします。 run-make テストスイートはツリー内の cargo にアクセスできません(そのため、 反復をより高速に行えるテストスイートにできます)。

build-std テスト

tests/build-std 内のテストは、-Zbuild-std が動作することを確認します。 これは現在、単一のレシピを持つ run-make テストスイートにすぎません。 このレシピはテストケースを生成し、それらを並列に実行します。

Rust レシピの使用

各テストは、rmake.rs Rust プログラムを持つ個別のディレクトリに配置する必要があります。 このプログラムはレシピと呼ばれます。レシピは、run_make_support ライブラリがリンクされた状態で compiletest によってコンパイルされ、実行されます。

新しいユーティリティや機能が必要な場合は、 run_make_support ライブラリを拡張し、改善することを検討してください。

//@ only-<target>//@ ignore-<target> のような Compiletest ディレクティブは、 UI テストと同様に rmake.rs でサポートされています。 ただし、リビジョンやディレクティブによる補助ファイルのビルドは、現在サポートされていません。

rmake.rsrun-make-support は、nightly/unstable 機能を使用してはなりません。 これらは、beta または stable rustc である可能性のある stage 0 rustc でコンパイルできなければならないためです。

デフォルトでは、run-make テストは各サブプロセスコマンドとその stdout/stderr を出力します。 cg_clif などの panic=abort テストスイートで --no-capture を指定して実行すると、 これにより端末が大量の出力で埋め尽くされる可能性があります。 成功したテストでこの出力を抑制するには、 --verbose-run-make-subprocess-output を省略してください。失敗したテストは常に出力されます。

./x test tests/run-make --no-capture --verbose-run-make-subprocess-output=false

rmake.rs テストがコンパイルできるかを素早く確認する

rmake.rs を stage0 コンパイラでコンパイルするよう強制することで、 stage1 rustc をビルドせずに rmake.rs テストがコンパイルできるかを素早く確認できます。

$ COMPILETEST_FORCE_STAGE0=1 x test --stage 0 tests/run-make/<test-name>

もちろん、一部のテストはこの方法では正常に実行されません。

rmake.rs で rust-analyzer を使用する

他のテストプログラムと同様に、run-make テストで使用される rmake.rs スクリプトには、 デフォルトでは rust-analyzer 連携がありません。

特定のテストで作業しているときにこれを回避するには、 テストのディレクトリに一時的に Cargo.toml ファイルを作成します (例: tests/run-make/sysroot-crates-are-unstable/Cargo.toml)。 内容は次のとおりです。

この Cargo.toml やその Cargo.lock を実際の PR に追加しないよう注意してください!

# これが外側のワークスペースの一部ではないことを cargo に認識させる。
[workspace]

[package]
name = "rmake"
version = "0.1.0"
edition = "2021"

[dependencies]
run_make_support = { path = "../../../src/tools/run-make-support" }

[[bin]]
name = "rmake"
path = "rmake.rs"

次に、対応するエントリを "rust-analyzer.linkedProjects" に追加します (例: .vscode/settings.json)。

"rust-analyzer.linkedProjects": [
  "tests/run-make/sysroot-crates-are-unstable/Cargo.toml"
],

カバレッジテスト

tests/coverage 内のテストは、異なる方法でカバレッジ計測をテストする複数のテストモードで共有されます。 coverage テストスイートを実行すると、 各テストがすべての異なるカバレッジモードで自動的に実行されます。

各モードには、そのモードだけでカバレッジテストを実行するためのエイリアスもあります。

./x test coverage # tests/coverage のすべてをすべてのカバレッジモードで実行する
./x test tests/coverage # 上と同じ

./x test tests/coverage/if.rs # 指定されたテストをすべてのカバレッジモードで実行する

./x test coverage-map # tests/coverage のすべてを "coverage-map" モードのみで実行する
./x test coverage-run # tests/coverage のすべてを "coverage-run" モードのみで実行する

./x test coverage-map -- tests/coverage/if.rs # 指定されたテストを "coverage-map" モードのみで実行する

何らかの理由で、特定のテストをカバレッジテストモードのいずれかで実行すべきでない場合は、 //@ ignore-coverage-map または //@ ignore-coverage-run ディレクティブを使用してください。

coverage-map スイート

coverage-map モードでは、これらのテストは LLVM によって出力される ソースコード領域とカバレッジカウンターの間のマッピングを検証します。 テストを --emit=llvm-ir でコンパイルし、その後カスタムツール(src/tools/coverage-dump)を使用して、 IR に埋め込まれたカバレッジマッピングを抽出し、整形して出力します。 これらのテストはプロファイラーランタイムを必要としないため、PR CI ジョブで実行され、ローカルでの 実行や bless が容易です。

これらのカバレッジマップテストは、MIR lowering や MIR 最適化の変更に影響を受けやすく、 異なるものの同一のカバレッジレポートを生成するマッピングを生じることがあります。

経験則として、カバレッジ固有のコードを変更しない PR では、 coverage-run テストが引き続き成功している限り、実際の変更を気にすることなく、 必要に応じて coverage-map テストを遠慮なく re-bless してください。

coverage-run スイート

coverage-run モードでは、これらのテストはカバレッジレポートのエンドツーエンドテストを実行します。 カバレッジ計測を有効にしてテストプログラムをコンパイルし、そのプログラムを実行して 生のカバレッジデータを生成し、その後 LLVM ツールを使用してそのデータを 人間が読めるコードカバレッジレポートに処理します。

計測されたバイナリは LLVM プロファイラーランタイムにリンクされる必要があるため、 coverage-run テストは、bootstrap.toml でプロファイラーランタイムが有効になっていない限り 自動的にスキップされます。

build.profiler = true

これはまた、これらが通常 PR CI ジョブでは実行されないことを意味します。ただし、マージに使用される CI ジョブ一式の一部としては実行されます。

coverage-run-rustdoc スイート

tests/coverage-run-rustdoc 内のテストは、計測された doctest も実行し、 それらをカバレッジレポートに含めます。 これにより、メインの coverage スイートのみを実行する場合に rustdoc をビルドする必要がなくなります。

クラッシュテスト

tests/crashes は、コンパイラが ICE、panic、またはその他の方法でクラッシュすることが期待される テストのコレクションとして機能し、意図しない修正が追跡されるようにします。 以前は、これは https://github.com/rust-lang/glacier で行われていましたが、 rust-lang/rust testsuite の内部で行う方が便利です。

このスイート内のテストが rustc に ICE、panic、またはその他の方法でクラッシュを引き起こさせることは必須です。 rustc が 1 または 0 以外の終了ステータスで終了した場合、テストは「成功」します。

詳細な stdout/stderr を確認したい場合は、 COMPILETEST_VERBOSE_CRASHES=1 を設定する必要があります。例:

$ COMPILETEST_VERBOSE_CRASHES=1 ./x test tests/crashes/999999.rs --stage 1

誰でも、issue tracker から “untracked” crashes を追加できます。 1 つの PR に複数の issue のテストケースを含めることを強く推奨します。 そうする場合、各 issue 番号をファイル名に記載し(12345.rs で十分です)、 さらにファイル内にも //@ known-bug: #12345 ディレクティブで記載してください。 PR がマージされたら、関連する issue に S-bug-has-testラベル付けしてください。

クラッシュのいずれかを修正した場合は、それを tests/ui 内の適切な サブディレクトリに移動し、意味のある名前を付けてください。 このテストが存在する理由を説明する doc comment をファイルの先頭に追加してください。 以前はその例によって rustc がどのようにクラッシュしていたのか、 またそれを修正するために何が行われたのかを簡潔に説明できるとなお良いです。

プルリクエストの説明に

Fixes #NNNNN
Fixes #MMMMM

を追加すると、マージ時に対応するチケットが自動的にクローズされます。

修正がまず issue のサブセットだけでなく、根本原因を実際に修正していることを確認してください。 issue 番号は、ファイル名またはテストファイル内の //@ known-bug ディレクティブで確認できます。

補助クレートのビルド

一部のテストでは、追加の補助クレートをコンパイルする必要があることがよくあります。 これを支援する複数のディレクティブがあります。

  • aux-build
  • aux-crate
  • aux-bin
  • aux-codegen-backend
  • proc-macro

aux-build は、指定されたソースファイルから別のクレートをビルドします。 ソースファイルは、テストファイルの隣にある auxiliary というディレクトリ内に置く必要があります。

//@ aux-build: my-helper.rs

extern crate my_helper;
// ... my_helper を使用できます。

aux クレートは、可能であれば dylib としてビルドされます(それをサポートしていないプラットフォームの場合、 または aux ファイル内で no-prefer-dynamic ヘッダーが指定されている場合を除きます)。 extern クレートを見つけるために -L フラグが使用されます。

aux-crateaux-build と非常によく似ています。 ただし、extern クレートにリンクするために --extern フラグを使用し、 そのクレートを extern prelude として利用できるようにします。 これにより、依存関係の名前変更など、--extern フラグの追加構文を指定できます。 たとえば、//@ aux-crate:foo=bar.rsauxiliary/bar.rs をコンパイルし、テスト内で foo という名前で利用できるようにします。 これは Cargo が依存関係の名前変更を行う方法に似ています。 --extern 修飾子を 指定することもできます。 たとえば、//@ aux-crate:noprelude:foo=bar.rs です。

aux-binaux-build と似ていますが、ライブラリではなくバイナリをビルドします。 バイナリは、テストの作業ディレクトリを基準とした auxiliary/bin で利用できます。

aux-codegen-backendaux-build と似ていますが、その後、メインファイルをビルドする際に コンパイル済みの dylib を -Zcodegen-backend に渡します。 これは、コンパイラクレートの使用を必要とするため、tests/ui-fulldeps 内のテストでのみ機能します。

補助 proc-macro

proc-macro 依存関係が必要な場合は、proc-macro ディレクティブを使用できます。このディレクティブは aux-build とまったく同じように動作します。つまり、 proc-macro テスト補助ファイルを、メインテストファイルと同じ親フォルダー配下の auxiliary フォルダーに置く必要があります。 ただし、proc-macro テスト補助については、aux-build と比較して さらに 4 つの追加の既定動作があります。

  1. aux テストファイルは --crate-type=proc-macro でビルドされます。
  2. aux テストファイルは -C prefer-dynamic なしでビルドされます。つまり、aux クレートの dylib を生成しようとはしません。
  3. aux クレートは、--extern <aux_crate_name> により、extern prelude 経由で テストファイルから利用できるようになります。 UI テストはデフォルトで edition 2015 であるため、メインテストファイルが 2018 以降の edition を使用していない限り、 use インポートで aux クレート名を使用したい場合は、引き続き extern <aux_crate_name> を指定する必要があることに注意してください。
  4. proc_macro クレートは extern prelude モジュールとして利用できるようになります。 extern proc_macro; についても、同じ edition 2015 とそれ以降の edition の区別が適用されます。

たとえば、テスト tests/ui/cat/meow.rs と proc-macro 補助 tests/ui/cat/auxiliary/whiskers.rs があるとします。

tests/ui/cat/
    meow.rs                 # メインテストファイル
    auxiliary/whiskers.rs   # 補助
// tests/ui/cat/meow.rs

//@ proc-macro: whiskers.rs

extern crate whiskers; // ui テストはデフォルトで edition 2015 のため必要

fn main() {
  whiskers::identity!();
}
// tests/ui/cat/auxiliary/whiskers.rs

extern crate proc_macro;
use proc_macro::*;

#[proc_macro]
pub fn identity(ts: TokenStream) -> TokenStream {
    ts
}

注記: 現在、proc-macro ヘッダーは rustdoc テストの build-aux-doc ヘッダーと併用できません。その場合は、 aux-build ヘッダーを使用し、proc-macro 内で #![crate_type="proc_macro"]//@ force-host および //@ no-prefer-dynamic ヘッダーを使用する必要があります。

リビジョン

リビジョンを使用すると、1 つのテストファイルを複数のテストに使用できます。 これは、ファイルの先頭に特別なディレクティブを追加することで行います。

//@ revisions: foo bar baz

これにより、テストは 3 回コンパイル(およびテスト)されます。1 回は --cfg foo、1 回は --cfg bar、もう 1 回は --cfg baz です。 したがって、テスト内で #[cfg(foo)] などを使用して、これらの結果をそれぞれ調整できます。

ディレクティブと期待されるエラーメッセージを特定のリビジョンに合わせてカスタマイズすることもできます。 これを行うには、ディレクティブの場合は //@ の後に [revision-name] を追加し、 UI エラー注釈の場合は // の後に追加します。次のようになります。

// cfg `foo` の場合にのみ渡すフラグ:
//@[foo]compile-flags: -Z verbose-internals

#[cfg(foo)]
fn test_foo() {
    let x: usize = 32_u32; //[foo]~ ERROR mismatched types
}

//[foo,bar,baz]~^ のように、複数のリビジョンをカンマ区切りリストで指定できます。

LLVM FileCheck ツールを使用するテストスイートでは、現在のリビジョン名も FileCheck ディレクティブの追加プレフィックスとして登録されます。

//@ revisions: NORMAL COVERAGE
//@[COVERAGE] compile-flags: -Cinstrument-coverage
//@[COVERAGE] needs-profiler-runtime

// COVERAGE:   @__llvm_coverage_mapping
// NORMAL-NOT: @__llvm_coverage_mapping

// CHECK: main
fn main() {}

リビジョンに合わせてカスタマイズした場合に、すべてのディレクティブが意味を持つわけではないことに注意してください。 たとえば、ignore-test ディレクティブ(およびすべての「ignore」ディレクティブ)は現在、 特定のリビジョンではなく、テスト全体にのみ適用されます。 リビジョンに合わせてカスタマイズした場合に実際に機能することを意図しているディレクティブは、 エラーパターンとコンパイラフラグだけです。

これらのテストスイートはリビジョンをサポートしていないことに注意してください。

  • codegen-units
  • run-make
  • rustdoc-html
  • rustdoc-json

未使用のリビジョン名を無視する

通常、他のディレクティブやエラーアノテーションで言及されるリビジョン名は、revisions ディレクティブで宣言された実際のリビジョンに対応していなければなりません。 これは ./x test tidy チェックによって強制されます。

何らかの理由でリビジョン名をリビジョンリストから一時的に削除する必要がある場合は、代わりにそのリビジョン名を //@ unused-revision-names: ヘッダーに追加することで、上記のチェックを抑制できます。

未使用名として * を指定すると(つまり //@ unused-revision-names: *)、任意の未使用リビジョン名に言及できるようになります。

比較モード

Compiletest は、比較モード と呼ばれるさまざまなモードで実行できます。比較モードは、異なるコンパイラフラグを有効にした状態で、すべてのテストの動作を比較するために使用できます。 これにより、特定のフラグでどのような違いが現れる可能性があるかを明らかにし、発生し得る問題を確認できます。

別のモードでテストを実行するには、--compare-mode CLI フラグを渡す必要があります。

./x test tests/ui --compare-mode=next-solver

指定できる比較モードは次のとおりです。

  • polonius-Zpolonius=next を指定して Polonius で実行します。
  • next-solver-Znext-solver を指定して次世代 trait solver で実行します。
  • next-solver-coherence-Znext-solver=coherence を指定して次世代 trait solver で coherence を実行します。
  • split-dwarf-Csplit-debuginfo=unpacked を指定して、アンパックされた split-DWARF で実行します。
  • split-dwarf-single-Csplit-debuginfo=packed を指定して、パックされた split-DWARF で実行します。

UI テストが異なるモードに対して異なる出力をどのようにサポートしているかについて詳しくは、UI 比較モードを参照してください。

CI では、比較モードは 1 つの Linux ビルダーでのみ、かつ次の設定でのみ使用されます。

  • tests/debuginfo: split-dwarf モードを使用します。 これは、split-DWARF を有効にしたときに debuginfo テストが影響を受けないことを確認するのに役立ちます。

比較モードはリビジョンとは別のものであることに注意してください。 ./x test tests/ui を実行するとすべてのリビジョンがテストされますが、比較モードは --compare-mode フラグを介して個別に手動で実行する必要があります。

並列フロントエンド

Compiletest は、--parallel-frontend-threads フラグを指定して、コンパイラを並列モードで実行できます。 これは、コンパイラが並列モードでも非並列モードと同じ出力を生成することを確認し、並列モードで発生し得る問題を確認するために使用できます。

並列モードでテストを実行するには、--parallel-frontend-threads CLI フラグを渡す必要があります。

./x test tests/ui -- --parallel-frontend-threads=N --iteration-count=M

ここで、N は並列フロントエンドで使用するスレッド数、M は各テストを並列モードで実行する回数です(非決定性を検出できる可能性を高めるため)。

また、--parallel-frontend-threads を指定して実行する場合、並列フロントエンドからの出力は行の順序という点で非決定的になり得るため、すべてのテストに対して compare-output-by-lines ディレクティブが暗黙的に指定されます。

並列フロントエンドは現時点では UI テストでのみ利用可能で、現在は他のテストスイートではサポートされていません。

UIテスト

UIテストは、compiletest の特定のテストスイートです。

はじめに

tests/ui 内のテストは、汎用テストの集合であり、 主にコンパイラのコンソール出力の検証に焦点を当てていますが、 他にも多くの目的に使用できます。 たとえば、テストは、その動作を検証するために 生成されたプログラムを実行するように設定することもできます。

tests/ui 配下の各サブディレクトリの目的の概要については、 README.mdを参照してください。 これは、新しいテストを書き、それを配置するカテゴリを探している場合に役立ちます。

#![no_std] のクロスコンパイルテストを扱う必要がある場合は、 minicore テスト補助の章を参照してください。

テストの一般的な構造

テストは、tests/ui ディレクトリに配置された Rust ソースファイルで構成されます。 テストは、その目的とテストカテゴリに基づいて適切なサブディレクトリに配置しなければなりません。 テストを tests/ui に直接配置することは許可されていません。

Compiletest は rustc を使用してテストをコンパイルし、その出力を、 テストの隣に配置された .stdout または .stderr ファイルに保存されている期待出力と比較します。 詳細については、出力の比較を参照してください。

さらに、エラーと警告はソースファイル内のコメントで注釈付けするべきです。 詳細については、エラー注釈を参照してください。

//@ で始まる特殊なコメント形式の compiletest ディレクティブは、 テストをどのようにコンパイルするか、および期待される動作が何かを制御します。

ほとんどのテストはコンパイラエラーをテストするため、テストはコンパイルに失敗することが期待されます。 その動作はディレクティブで変更できます。pass/fail 期待値の制御を参照してください。

デフォルトでは、テストは実行可能バイナリとしてビルドされます。 異なるクレートタイプが必要な場合は、必要に応じて #![crate_type] 属性を使用して設定できます。

出力の比較

UIテストは、コンパイラからの期待出力を、テストの隣にある .stderr および .stdout スナップショットに保存します。 通常、これらのファイルは --bless CLI オプションで生成し、その後手動で検査して、期待する内容が含まれていることを確認します。

不要な差異を無視するために出力は正規化されます。正規化セクションを参照してください。 ファイルが存在しない場合、compiletest は対応する出力が空であることを期待します。

正規化、リビジョン、および以下の他のツールの大半を使用する一般的な理由は、 プラットフォーム間の差異に対処することです。 これらのツールの代替案も検討してください。 たとえば、テストをクロスコンパイルを使用するように修正して、無効である可能性のあるすべての ABI をテストする代わりに、 すべてのプラットフォームで無効な extern "rust-invalid" ABI を使用するなどです。

stdout/stderr ファイルは複数存在できます。 一般的な形式は次のとおりです。

*test-name*`.`*revision*`.`*compare_mode*`.`*extension*
  • test-name にドットを含めることはできません。 これは、テスト出力ファイル名の一般形式を予測可能な形式にして、 迷子のテスト出力ファイルを追跡するためにパターンマッチできるようにするためです。
  • revisionリビジョン名です。 リビジョンを使用していない場合は含まれません。
  • compare_mode比較モードです。 これは、指定された比較モードが有効な場合にのみチェックされます。 ファイルが存在しない場合、 compiletest は比較モードなしのファイルをチェックします。
  • extension はチェック対象の出力の種類です。
    • stderr — コンパイラの stderr
    • stdout — コンパイラの stdout
    • run.stderr — テスト実行時の stderr
    • run.stdout — テスト実行時の stdout
    • 64bit.stderr — 64ビットターゲットで stderr-per-bitwidth ディレクティブを使用したコンパイラの stderr
    • 32bit.stderr — 32ビットターゲットで stderr-per-bitwidth ディレクティブを使用したコンパイラの stderr

単純な例は、foo.rs テストの隣にある foo.stderr です。 より複雑な例は foo.my-revision.polonius.stderr です。

compiletest が出力ファイルをチェックする方法を変更するディレクティブがいくつかあります。

  • stderr-per-bitwidth — ターゲットのポインタ幅に基づいて別々の出力ファイルをチェックします。 代わりに normalize-stderr ディレクティブを使用することを検討してください(正規化を参照)。
  • dont-check-compiler-stderr — コンパイラからの stderr を無視します。
  • dont-check-compiler-stdout — コンパイラからの stdout を無視します。
  • compare-output-by-lines — 一部のテストでは出力の順序が非決定的であるため、行単位で比較する必要があります。

UIテストは -Zdeduplicate-diagnostics=no フラグ付きで実行されます。これは rustc の 組み込みの診断重複排除メカニズムを無効にします。 つまり、出力に重複したメッセージが表示される場合があります。 これは、重複した診断が生成されている状況を明らかにするのに役立ちます。

正規化

コンパイラ出力は、主にファイル名に関するプラットフォーム間の出力差異をなくすために正規化されます。

Compiletest はコンパイラ出力に対して次の置換を行います。

  • テストが定義されているディレクトリは $DIR に置換されます。 例: /path/to/rust/tests/ui/error-codes
  • 標準ライブラリソースへのディレクトリは $SRC_DIR に置換されます。 例: /path/to/rust/library
  • $SRC_DIR 内のパスの行番号と列番号は LL:COL に置換されます。 これは、標準ライブラリのレイアウト変更が .stderr ファイルに広範な変更を 引き起こさないようにするのに役立ちます。 例: $SRC_DIR/alloc/src/sync.rs:53:46
  • テストの出力が置かれるベースディレクトリは $TEST_BUILD_DIR に置換されます。 これは、ごくまれな状況でのみ現れます。 例: /path/to/rust/build/x86_64-unknown-linux-gnu/test/ui
  • 標準ライブラリソースへの実ディレクトリは $SRC_DIR_REAL に置換されます。
  • コンパイラソースへの実ディレクトリは $COMPILER_DIR_REAL に置換されます。
  • タブは \t に置換されます。
  • パス内のバックスラッシュ(\)は、(ヒューリスティックを使用して)スラッシュ(/)に変換されます。 これは Windows スタイルのパスとの差異を正規化するのに役立ちます。
  • CRLF 改行は LF に変換されます。
  • //~ ERROR some message のようなエラー行注釈は削除されます。
  • さまざまな v0 およびレガシーのシンボルハッシュは、 [HASH]<SYMBOL_HASH> のようなプレースホルダーに置換されます。

さらに、コンパイラは -Z ui-testing フラグ付きで実行されます。これにより、 コンパイラ自身が診断出力にいくつかの変更を適用し、UIテストにより適したものにします。

たとえば、出力内の行番号を匿名化します(各ソース行の先頭に付く行番号は LL に置換されます)。 極めてまれな状況では、このモードはディレクティブ //@ compile-flags: -Z ui-testing=no で無効化できます。

-Z ui-testing=no を使用する場合、UIテストスイートを実行しているターミナルの幅に応じて テストが失敗または成功しないように、--diagnostic-width 引数も設定するべきです。 注: テストを指す --> 行の行番号と列番号は正規化されず、 そのまま残されます。 これにより、コンパイラが引き続き正しい位置を指し示し、 stderr ファイルの可読性が保たれます。 理想的にはすべての行/列情報が保持されるべきですが、ソースへの小さな変更によって 大きな diff が発生し、マージコンフリクトやテストエラーがより頻繁に発生します。

場合によっては、これらの組み込みの正規化では不十分です。 そのような場合は、 normalize-* ディレクティブを使用してカスタム正規化ルールを指定できます。例:

//@ normalize-stdout: "foo" -> "bar"
//@ normalize-stderr: "foo" -> "bar"
//@ normalize-stderr-32bit: "fn\(\) \(32 bits\)" -> "fn\(\) \($$PTR bits\)"
//@ normalize-stderr-64bit: "fn\(\) \(64 bits\)" -> "fn\(\) \($$PTR bits\)"

これは、32 ビットプラットフォームでは、コンパイラが stderr に fn() (32 bits) を書き込むたびに、代わりに fn() ($PTR bits) と読めるよう正規化する必要があることをテストに伝えます。 64 ビットについても同様です。 置換は、regex クレートが提供するデフォルトの正規表現フレーバーを使用する正規表現によって実行されます。

対応する参照ファイルは、正規化された出力を使用して 32 ビットプラットフォームと 64 ビットプラットフォームの両方をテストします。

...
   |
   = note: source type: fn() ($PTR bits)
   = note: target type: u16 (16 bits)
...

具体的な使用例については、ui/transmute/main.rsmain.stderr を参照してください。

エラーアノテーション

エラーアノテーションは、コンパイラが出力すると期待されるエラーを指定します。 これらは、エラーが位置するソース内の行に「関連付け」られます。

fn main() {
    boom  //~ ERROR このスコープに値 `boom` が見つかりません [E0425]
}

UI テストにはコンパイラ出力全体を含む .stderr ファイルがありますが、 UI テストでは、エラーがソース内にもアノテーションされている必要があります。 この冗長性は、.stderr ファイルが通常 自動生成されるため、ミスを避けるのに役立ちます。 また、.stderr ファイルとソースを比較しなくても、1 つのファイルを見るだけで、 エラーのスパンがどこを指すと期待されるかを直接確認するのにも役立ちます。 最後に、追加の予期しないエラーが生成されないことを保証します。

これらにはいくつかの形式がありますが、一般的には診断レベル (ERROR など)と、期待されるエラー出力の部分文字列を含むコメントです。 メッセージ全体を書き出す必要はありませんが、 自己説明的になるよう、メッセージの重要な部分を必ず含めてください。

ほとんどのエラーアノテーションは、診断の行と一致する必要があります。 メッセージを行と一致させる方法はいくつかあります(以下の例を参照してください)。

  • ~: エラーレベルとメッセージを現在の行に関連付けます
  • ~^: エラーレベルとメッセージを直前のエラーアノテーション行に関連付けます。 追加するキャレット(^)ごとに 1 行ずつ加算されるため、~^^^ は エラーアノテーション行の 3 行上を意味します。
  • ~|: エラーレベルとメッセージを、直前のコメント同じ行に関連付けます。これは、同じ行に関連付けられたメッセージが 複数ある場合に、複数のキャレットを使用するより便利です。
  • ~v: エラーレベルとメッセージを次のエラーアノテーション行に関連付けます。 追加する記号(v)ごとに 1 行ずつ加算されるため、~vvv は エラーアノテーション行の 3 行下を意味します。

例:

let _ = same_line; //~ ERROR 未宣言の変数
fn meow(_: [u8]) {}
//~^ ERROR サイズ不定
//~| ERROR 無名パラメーター

//~(または他のバリアント)と後続のテキストの間の空白文字は 無視できます(つまり、//~ ERROR//~ERROR の間に意味上の違いはありませんが、 コードベースでは前者の方が一般的です)。

~? <diagnostic kind>(例: ~? ERROR)は、 行情報がまったくない診断、または行情報がメインのテストファイル1 の外側にある診断を一致させるために使用されます。 これらのアノテーションは、テストファイル内の任意の行に配置できます。

エラーアノテーションの例

以下は、UI テストソースの異なる行にあるエラーアノテーションの例です。

エラー行に配置

//~ ERROR イディオムを使用します。

fn main() {
    let x = (1, 2, 3);
    match x {
        (_a, _x @ ..) => {} //~ ERROR `_x @` はタプル内では許可されていません
        _ => {}
    }
}

エラー行の下に配置

上の行数を示すために、文字列内のキャレット数を指定して //~^ イディオムを使用します。 以下の例では、エラー行は エラーアノテーション行の 4 行上にあるため、アノテーションには 4 つのキャレットが含まれています。

fn main() {
    let x = (1, 2, 3);
    match x {
        (_a, _x @ ..) => {}  // <- エラーはこの行にあります
        _ => {}
    }
}
//~^^^^ ERROR `_x @` はタプル内では許可されていません

上のエラーアノテーション行で定義されたものと同じエラー行を使用

上のエラーアノテーション行と同じエラー行を定義するには、 //~| イディオムを使用します。

struct Binder(i32, i32, i32);

fn main() {
    let x = Binder(1, 2, 3);
    match x {
        Binder(_a, _x @ ..) => {}  // <- エラーはこの行にあります
        _ => {}
    }
}
//~^^^^ ERROR `_x @` はタプル構造体内では許可されていません
//~| ERROR このパターンには 1 個のフィールドがありますが、対応するタプル構造体には 3 個のフィールドがあります [E0023]

エラー行の上に配置

下の行数を示すために、文字列内の v の数を指定して //~v イディオムを使用します。 これは通常、ファイル末尾で発生する閉じられていない区切り文字や 閉じられていないリテラルのようなエラーに一致する lexer または parser テストで使用されます。

// ignore-tidy-trailing-newlines
//~v ERROR このファイルには閉じられていない区切り文字があります
fn main((ؼ

行情報のないエラー

行情報のないエラーに一致させるには、//~? を使用します。 //~? は精密であり、行情報が利用可能なエラーには一致しません。 コンパイラ診断に対して一致させたいテストでは、 //@ error-pattern は不正確で網羅的ではないため、これよりも //~? を優先すべきです。

//@ compile-flags: --print yyyy

//~? ERROR 不明な print 要求: `yyyy`

error-pattern

error-pattern ディレクティブ は、特定のスパンを持たない実行時メッセージや、 例外的な場合にはコンパイル時メッセージに使用できます。

このテストについて考えてみましょう。

fn main() {
    let a: *const [_] = &[1, 2, 3];
    unsafe {
        let _b = (*a)[3];
    }
}

これが「インデックスが範囲外」を表示することを保証したいものの、実行時エラーにはスパンがないため、 ERROR アノテーションを使用することはできません。 その場合は、error-pattern ディレクティブを使用します。

//@ error-pattern: インデックスが範囲外
fn main() {
    let a: *const [_] = &[1, 2, 3];
    unsafe {
        let _b = (*a)[3];
    }
}

コンパイル時出力を厳密にテストするには、スパンのない診断に対する //~? アノテーションを含め、 可能な限り行アノテーション //~ を使用するようにしてください。 コンパイル時の出力がターゲット依存である、または冗長すぎる場合は、ディレクティブ //@ dont-require-annotations: <diagnostic-kind> を使用して、行アノテーションのチェックを 網羅的でないものにします。 このモードでは、コンパイラーメッセージの一部がアノテーションでカバーされないままでもかまいません。

実行時出力をチェックする場合は、//@ check-run-results の方が望ましい場合があります。

error-pattern は、上記のいずれもうまくいかない場合にのみ使用してください。たとえば、実行時のパニック出力に含まれる 特定の文字列パターンを探す場合などです。

行アノテーション //~error-pattern は互換性があり、同じテスト内で使用できます。

診断の種類(エラーレベル)

使用できる診断の種類は次のとおりです。

  • ERROR
  • WARN(または WARNING
  • NOTE
  • HELP
  • SUGGESTION
  • RAW

SUGGESTION 種類は、診断の提案に対して期待される置換テキストを指定するために使用されます。 RAW 種類は、構造化 JSON の代わりに、またはそれに加えて、コンパイラーによって出力されることがある非構造化出力の行にマッチさせるために使用できます。

ERRORWARN 種類は、デフォルトで行アノテーション //~ によって網羅的にカバーされている必要があります。

他の種類は、その種類のアノテーションがテストファイル内に少なくとも 1 つ現れる場合にのみ、行アノテーションが必要です。 たとえば、//~ NOTE が 1 つあると、ファイル内の他のすべての //~ NOTE も 明示的に書き出す必要があります。

網羅的なアノテーションを無効にするには、ディレクティブ //@ dont-require-annotations を使用します。 たとえば、ノートを選択的にアノテーションするには //@ dont-require-annotations: NOTE を使用します。 ターゲット依存のコンパイラー出力のような重大な理由がない限り、このディレクティブを ERRORWARN に使用するのは避けてください。

一部の診断は、その種類やディレクティブに関係なく、行アノテーションが必要になることはありません。 例としては、複数行診断の副次的な行や、 aborting due to N previous errors のような一般的な診断があります。

UI テストは、未使用警告をすべて無視するために、デフォルトで -A unused フラグを使用します。これは、 未使用警告が通常テストの焦点ではないためです。 ただし、単純なコードサンプルでは未使用警告が発生することがよくあります。 テストが特に 未使用警告をテストしている場合は、必要に応じて適切な #![warn(unused)] 属性を追加してください。

cfg リビジョン

リビジョンを使用する場合、現在のリビジョンに基づいて異なるメッセージを 条件付きでチェックできます。 これは、次のようにリビジョンの cfg 名を角括弧内に配置することで行います。

//@ edition:2018
//@ revisions: mir thir
//@[thir] compile-flags: -Z thir-unsafeck

async unsafe fn f() {}

async fn g() {
    f(); //~ ERROR unsafe 関数の呼び出しは unsafe です
}

fn main() {
    f(); //[mir]~ ERROR unsafe 関数の呼び出しは unsafe です
}

この例では、2 番目のエラーメッセージは mir リビジョンでのみ出力されます。 thir リビジョンでは最初のエラーのみが出力されます。

cfg によってコンパイラーが異なる出力を生成する場合、テストは異なる出力に対応する 複数の .stderr ファイルを持つことができます。 上記の例では、異なるリビジョンの異なる出力を含む .mir.stderr ファイルと .thir.stderr ファイルが存在することになります。

注: cfg リビジョンは、ソースコード内の #[cfg] 属性でも機能します。

慣例として、常に偽となる設定を持たせるために FALSE cfg が使用されます。

合格/不合格の期待値を制御する

デフォルトでは、UI テストは コンパイルエラーを生成する ことが期待されます。これは、ほとんどの テストが無効な入力とエラー診断をチェックしているためです。 ただし、コンパイルが成功することを期待する UI テストを作成することもでき、さらに結果のプログラムを 実行することもできます。 次のディレクティブのいずれかを追加してください。

  • 合格ディレクティブ:
    • //@ check-pass — コンパイルは成功する必要がありますが、codegen はスキップします (これはコストが高く、ほとんどの場合失敗するはずがないためです)。
    • //@ build-pass — コンパイルとリンクは成功する必要がありますが、 結果のバイナリは実行しません。
    • //@ run-pass — コンパイルは成功する必要があり、結果の バイナリを実行すると、成功を示すコード 0 で終了する必要があります。
  • 不合格ディレクティブ:
    • //@ check-fail — コンパイルは失敗する必要があります(codegen フェーズはスキップされます)。 これは UI テストのデフォルトです。
    • //@ build-fail — コンパイルは codegen フェーズ中に失敗する必要があります。 これにより rustc が 2 回実行されます。
      • 1 回目は、codegen フェーズなしでコンパイルが成功することを確認するためです
      • 2 回目は、完全なコンパイルが失敗することを確認するためです
    • //@ run-fail — コンパイルは成功する必要がありますが、結果の バイナリを実行すると、通常の失敗を示す範囲 1..=127 のコードで 終了する必要があります。 unwind サポートのないターゲットでは、クラッシュも受け入れられます。
    • //@ run-crash — コンパイルは成功する必要がありますが、結果の バイナリを実行するとクラッシュして失敗する必要があります。 クラッシュは「範囲 0..=127 のコードで終了しないこと」と定義されます。
      • Linux での例: SIGABRT または SIGSEGV による終了。
      • Windows での例: STATUS_ILLEGAL_INSTRUCTION0xC000001D)のコードで終了。
    • //@ run-fail-or-crash — コンパイルは成功する必要がありますが、結果の バイナリを実行すると、run-fail または run-crash のいずれかになる必要があります。 一部のターゲットではテストがクラッシュするが、他のターゲットでは単に失敗する場合に便利です。

run-passrun-failrun-crash、および run-fail-or-crash テストでは、 プログラム自体の出力はデフォルトではチェックされません。

プログラムの実行出力をチェックしたい場合は、check-run-results ディレクティブを含めてください。 これにより、プログラムの実際の出力と比較するための .run.stderr および .run.stdout ファイルがチェックされます。

*-pass ディレクティブを持つテストは、--pass コマンドラインオプションで上書きできます。

./x test tests/ui --pass check

--pass オプションは UI テストにのみ影響します。 --pass check を使用すると、UI テストスイートをはるかに高速に実行できます(私のシステムではおよそ 2 倍高速です)が、当然ながら それほど多くの部分は実行されません。

その上書きではテストが正しく動作しない場合、--pass CLI フラグを無視するために no-pass-override ディレクティブを使用できます。

既知のバグ

known-bug ディレクティブは、まだ修正されていない既知のバグを示すテストに使用できます。 既知のバグに対するテストを追加することは、次のような複数の理由で有用です。

  1. バグが修正されたときに便利に再利用できる機能テストを維持する。
  2. バグが偶然修正された場合に失敗する番兵を提供する。 これにより開発者に通知され、関連する issue が修正されたことを把握でき、 場合によってはクローズできます。

このディレクティブは、カンマ区切りの issue 番号を引数として取るか、"unknown" を取ります。

  • //@ known-bug: #123, #456(issue が rust-lang/rust 上にある場合)
  • //@ known-bug: rust-lang/chalk#123456# の前に任意のテキストを許可します。これは issue が別のリポジトリにある場合に便利です)
  • //@ known-bug: unknown (既知の issue がまだない場合。存在しない場合は、開くことが望ましいです)

known-bug を含むテストには、エラーアノテーションを含めないでください。 テストには、他の通常のディレクティブと stdout/stderr ファイルは引き続き含める必要があります。

テストの編成

テストファイルを配置する場所を決めるときは、実行しようとしている内容に 最も一致するサブディレクトリを探すようにしてください。 できるだけ整理された状態を保つよう努めてください。 確かに、一部のテストは複数のカテゴリにまたがる場合があり、 既存のレイアウトがうまく合わないこともあるため、難しい場合があります。

テストには、そのテストが何をチェックしているかを簡潔に説明する名前を付けてください。 テスト名に issue 番号を含めることは避けてください。 これについてより詳しい説明は、ベストプラクティスを参照してください。

理想的には、そのテストでどのコードがテストされているかを識別しやすくする ディレクトリにテストを追加するべきです(例: tests/ui/borrowck/reject-move-out-of-borrow-via-pat.rs

新しい機能を書くときは、テストを保存するサブディレクトリを作成することを検討してもよいでしょう。 たとえば、RFC 1234(“Widgets”)を実装している場合は、 tests/ui/rfc1234-widgets/ のようなディレクトリにテストを配置するのが理にかなっているかもしれません。

別の場合には、すでに適切なディレクトリが存在していることもあります。

時間の経過とともに、tests/ui ディレクトリは非常に速く成長してきました。 tidy には、どのサブディレクトリも 1000 個を超えるエントリを持たないことを 保証するチェックがあります。 ファイルが多すぎると、エディタ/IDE に優しくなく、 GitHub UI も 1000 個を超えるエントリを表示しないため、問題が発生します。 ただし、tests/ui(UI テストのルートディレクトリ)と tests/ui/issues ディレクトリには 1000 個を超えるエントリがあるため、これらのディレクトリには別の上限を設定しています。 そのため、そこに新しいテストを置くことは避け、より関連性の高い場所を探すようにしてください。

たとえば、テストがクロージャに関連している場合は、tests/ui/closures に配置するべきです。 上限に達した場合は、ここを調整することで上限を増やすことができます。

Rustfix テスト

UI テストでは、診断の提案が正しく適用されること、および 結果の変更が正しくコンパイルされることを検証できます。 これは run-rustfix ディレクティブで行えます。

//@ run-rustfix
//@ check-pass
#![crate_type = "lib"]

pub struct not_camel_case {}
//~^ WARN `not_camel_case` はアッパーキャメルケースの名前にするべきです
//~| HELP 識別子をアッパーキャメルケースに変換してください
//~| SUGGESTION NotCamelCase

Rustfix テストには、提案が適用された後のソースファイルを含む .fixed 拡張子のファイルが必要です。

  • テストが実行されると、compiletest はまず正しい lint/警告が生成されることをチェックします。
  • 次に、提案を適用して .fixed と比較します(一致している必要があります)。
  • 最後に、修正済みソースがコンパイルされ、このコンパイルは成功する必要があります。

通常、rustfix テストを作成するときは、x test --bless オプションで .fixed ファイルを自動生成します。

run-rustfix ディレクティブは、たとえそれらが MachineApplicable でなくても、すべての提案を適用します。 これが問題になる場合は、run-rustfix に加えて rustfix-only-machine-applicable ディレクティブを追加できます。 これは、異なる提案レベルが混在しており、 機械的に適用可能ではないものの一部がきれいに適用されない場合に使用するべきです。

比較モード

比較モードは、通常コンパイルされるときとは 異なるフラグで全テストを実行するために使用できます。 場合によっては、これによりコンパイラから異なる出力が生成されることがあります。 これをサポートするために、比較モードに基づく出力を含む 異なる出力ファイルを保存できます。

たとえば、Polonius モードを使用している場合、テスト foo.rs はまず 期待される出力を foo.polonius.stderr で探し、見つからない場合は通常の foo.stderr にフォールバックします。 これは、異なるモードによって異なる診断や動作が生じることがあるため便利です。 これにより、モード間で差分があるテストを追跡し、 それらの診断の違いを視覚的に確認しやすくなります。

まれに、動作が異なるテストに遭遇した場合は、 代替の stderr ファイルを生成するために、次のようなものを実行できます。

./x test tests/ui --compare-mode=polonius --bless

現在、UI テストについては CI でチェックされている比較モードはありません。

rustc_* TEST 属性

コンパイラは、内部機能 rustc_attrs によってゲートされる、 追加のコンパイラ内部情報をダンプする perma-unstable な #[rustc_*] 属性をいくつか定義しています。 詳細については、コンパイラのデバッグの対応するサブセクションを参照してください。

これらは、通常なら「ユーザー向け」の Rust だけで同じことを行うのが非常に難しい場合に、 内部コンパイラ状態をより正確に、読みやすく、簡単にテストするために使用できます。 実際、これは「UI」(ユーザーインターフェイス)という用語をわずかに悪用し、 そのような UI テストをブラックボックステストからホワイトボックステストに変えるものだと言えるでしょう。 慎重かつ控えめに使用してください。

UI テストモードでプリセットされる lint レベル

デフォルトでは、UI テストモード配下のテストスイート(tests/uitests/ui-fulldeps、 ただし tests/rustdoc-ui は除く)は、次を指定します。

  • -A unused
  • -W unused_attributes(これらは ui テストにとって関心の対象になりやすいため)
  • -A internal_features
  • -A incomplete_features
  • -A unused_parens
  • -A unused_braces

詳細については、runtestを参照してください。

条件は次のとおりです。

  • ui テストの pass モードが run 未満である(つまり check または build)。
  • 比較モードが指定されていない。

これは、ui テストでは非常にノイズが多くなる可能性があるためです。

必要に応じて、compile-flags の lint レベルフラグや ソース内の lint レベル属性でこれらを上書きできます。

なお、rustfix バージョンには -A unused が渡されないため、 rustfix 済みファイルで unused lint を抑制するには、#[allow(unused)] が必要になる場合があります (unused lint 自体に対して rustfix をテストしている可能性があるためです)。


  1. これは、aux ファイルや、こちらで制御できないソースとは異なる、~? アノテーションを持つファイルです。

Compiletest ディレクティブ

ディレクティブは、テストをどのようにビルドし解釈するかを compiletest に伝える特別なコメントです。 rmake.rsrun-make テストにも現れることがあります。

通常、ディレクティブはこのテストの要点を説明する短いコメントの後に置かれます。 Compiletest のテストスイートでは、コメントがディレクティブであることを示すために //@ を使用します。 たとえば、このテストでは、テストのコンパイル時に rustc に渡すカスタムフラグを指定するために、//@ compile-flags コマンドを使用しています。

// オーバーフローチェックが無効な場合の `0 - 1` の挙動をテストする。

//@ compile-flags: -C overflow-checks=off

fn main() {
    let x = 0 - 1;
    ...
}

ディレクティブは単独で記述することも(例: //@ run-pass)、値を取ることもできます(例: //@ compile-flags: -C overflow-checks=off)。

ディレクティブは 1 行につき 1 つ記述します。同じ行に複数のディレクティブを書くことはできません。 たとえば、//@ only-x86 only-windows と書いた場合、 only-windows は別個のディレクティブではなく、コメントとして解釈されます。

compiletest ディレクティブの一覧

以下は compiletest ディレクティブの一覧です。 利用可能な場合、ディレクティブはそのコマンドをより詳しく説明するセクションにリンクされています。 この一覧は網羅的ではない可能性があります。 ディレクティブは一般に、compiletest ソースの directives.rs にある TestProps 構造体を参照することで見つけられます。

アセンブリ

ディレクティブ説明対応するテストスイート指定可能な値
assembly-outputチェックするアセンブリ出力の種類assemblyemit-asm, bpf-linker, ptx-linker

補助ビルド

補助クレートのビルドを参照してください

ディレクティブ説明対応するテストスイート指定可能な値
aux-bin補助バイナリをビルドし、テストディレクトリからの相対パス auxiliary/bin で利用可能にするrun-make/run-make-cargo を除くすべて補助 .rs ファイルへのパス
aux-build指定されたソースファイルから別個のクレートをビルドするrun-make/run-make-cargo を除くすべて補助 .rs ファイルへのパス
aux-crateaux-build と似ているが、extern prelude として利用可能にするrun-make/run-make-cargo を除くすべて[<extern_modifiers>:]<extern_prelude_name>=<path/to/aux/file.rs>
aux-codegen-backendaux-build と似ているが、メインファイルのビルド時にコンパイル済み dylib を -Zcodegen-backend に渡すui-fulldepscodegen backend ファイルへのパス
proc-macroaux-build と似ているが、aux に対してホストを強制し、-Cprefer-dynamic1 を使用しない。run-make/run-make-cargo を除くすべて補助 proc-macro .rs ファイルへのパス
build-aux-docs補助対象についてもドキュメントをビルドする。これは aux-crate ではなく aux-build でのみ動作することに注意。run-make/run-make-cargo を除くすべてN/A

結果の期待値の制御

pass/fail 期待値の制御を参照してください。

ディレクティブ説明対応するテストスイート指定可能な値
check-passビルド(codegen なし)は成功するべきuiN/A
check-failビルド(codegen なし)は失敗するべきuiN/A
build-passビルドは成功するべきuiN/A
build-failビルドは失敗するべきuiN/A
run-passプログラムはコード 0 で終了しなければならないuiN/A
run-failプログラムはコード 1..=127 で終了しなければならないuiN/A
run-crashプログラムはクラッシュしなければならないuiN/A
run-fail-or-crashプログラムは run-fail または run-crash でなければならないuiN/A
no-pass-override--pass フラグを無視するuiN/A
dont-check-failure-status正確な失敗ステータス(つまり 1)をチェックしないui, incrementalN/A
failure-status失敗時、コンパイラはこのステータスコードで終了しなければならない。ICE を期待するには、//@ failure-status: 101 を使用する。ui, incremental任意の u16
should-failCompiletest のセルフテストすべてN/A

出力スナップショットと正規化の制御

詳細については、正規化出力 比較、および Rustfix テストを参照してください。

ディレクティブ説明サポートされるテストスイート使用可能な値
check-run-resultsテストバイナリ run-{pass,fail} の実行結果の出力スナップショットを確認するui, crashes, incrementalN/A
error-pattern出力に特定の文字列が含まれていることを確認するui, crashes, incremental文字列
regex-error-pattern出力に正規表現パターンが含まれていることを確認するui, crashes, incremental正規表現
check-stdoutテストバイナリの実行結果の stdouterror-pattern と照合する2ui, crashes, incrementalN/A
normalize-stderr-32bitスナップショットと比較する前に、ルール "<raw>" -> "<normalized>" で実際の stderr(32 ビットプラットフォーム向け)を正規化するui, incremental"<RAW>" -> "<NORMALIZED>"<RAW>/<NORMALIZED> は正規表現のキャプチャおよび置換構文
normalize-stderr-64bitスナップショットと比較する前に、ルール "<raw>" -> "<normalized>" で実際の stderr(64 ビットプラットフォーム向け)を正規化するui, incremental"<RAW>" -> "<NORMALIZED>"<RAW>/<NORMALIZED> は正規表現のキャプチャおよび置換構文
normalize-stderrスナップショットと比較する前に、ルール "<raw>" -> "<normalized>" で実際の stderr を正規化するui, incremental"<RAW>" -> "<NORMALIZED>"<RAW>/<NORMALIZED> は正規表現のキャプチャおよび置換構文
normalize-stdoutスナップショットと比較する前に、ルール "<raw>" -> "<normalized>" で実際の stdout を正規化するui, incremental"<RAW>" -> "<NORMALIZED>"<RAW>/<NORMALIZED> は正規表現のキャプチャおよび置換構文
dont-check-compiler-stderr実際のコンパイラ stderr と stderr スナップショットを照合しないuiN/A
dont-check-compiler-stdout実際のコンパイラ stdout と stdout スナップショットを照合しないuiN/A
dont-require-annotations指定された診断種別(//~ KIND)について、行アノテーションが網羅的であることを要求しないui, incrementalERROR, WARN, NOTE, HELP, SUGGESTION
run-rustfixrustfix によってすべての提案を適用し、修正後の出力をスナップショット化し、修正後の出力がビルドできることを確認するuiN/A
rustfix-only-machine-applicablerun-rustfix と同じだが、機械的に適用可能な提案のみを対象とするuiN/A
exec-envテスト実行時に設定する環境変数ui, crashes<KEY>=<VALUE>
unset-exec-envテスト実行時に解除する環境変数ui, crashes任意の環境変数名
stderr-per-bitwidth各ビット幅ごとに stderr スナップショットを生成するuiN/A
forbid-outputコンパイル/実行の出力に特定の文字列が含まれていないことを確認するui, incremental文字列
run-flagsテスト実行可能ファイルに渡されるフラグui任意のフラグ
known-bug既知のバグのためエラーアノテーションは不要ui, crashes, incrementalIssue 番号 #123456
compare-output-by-lines出力を単一の文字列としてではなく、行単位で比較するすべてN/A

テストを実行するタイミングの制御

これらのディレクティブは、状況によってテストを無視するために使用されます。これは、 そのテストがコンパイルも実行もされないことを意味します。

  • ignore-X は、X がテストを無視する対象の詳細またはその他の条件である場合に使用します(以下を参照)
  • only-Xignore-X に似ていますが、そのターゲットまたはステージでのみテストを実行します
  • ignore-auxiliary は、1つ以上の他のメインテストファイルに関与するものの、 compiletest がそのファイル自体をビルドしようとするべきではないファイルを対象としています。 その補助ファイルを実際に使用しているメインテストへのバックリンクを付けてください。
  • ignore-test は常にテストを無視します。 これは、現在動作していないテストを一時的に無効化しつつ、 後で再有効化するためにツリー内に保持したい場合に使用できます。

ignore-X または only-X における X の例をいくつか示します。

  • 完全なターゲットトリプル: aarch64-apple-ios
  • アーキテクチャ: aarch64, arm, csky, mips, mips64, wasm32, x86_64, x86, …
  • OS: android, emscripten, freebsd, ios, linux, macos, windows, …
  • 環境(ターゲットトリプルの4番目の単語): gnu, msvc, musl
  • ポインター幅: 32bit, 64bit
  • エンディアン: endian-big
  • ステージ: stage1, stage2
  • バイナリ形式: elf
  • チャネル: stable, beta
  • クロスコンパイル時: cross-compile
  • [remote testing] が使用される場合: remote
  • 特定のデバッガーがテストされる場合: cdb, gdb, lldb
  • 特定のデバッガーバージョンに一致する場合: ignore-gdb-version
  • [parallel frontend] が有効な場合: ignore-parallel-frontend
  • 特定の [compare modes]: compare-mode-polonius, compare-mode-next-solver, compare-mode-next-solver-coherence, compare-mode-split-dwarf, compare-mode-split-dwarf-single
  • カバレッジテストで使用される2つの異なるテストモード: ignore-coverage-map, ignore-coverage-run
  • dist ツールチェーンをテストする場合: dist
    • これは COMPILETEST_ENABLE_DIST_TESTS=1 で有効にする必要があります
  • ターゲットの rustc_abi: 例: rustc_abi-x86_64-sse2

以下のディレクティブは rustc のビルド設定とターゲット設定をチェックします。

  • needs-asm-supportホストアーキテクチャが asm! の安定サポートを持たない場合に無視します。 明示的なターゲットへクロスコンパイルするテストでは、 --target を介して、 適切なバックエンドが利用可能であることを保証するために、代わりに needs-llvm-components を使用してください。
  • needs-asm-mnemonic: <MNEMONIC> — ターゲットバックエンドが指定されたアセンブリニーモニック(例: RET, NOP)を サポートしていない場合に無視します。 LLVM バックエンドでのみサポートされます。
  • needs-profiler-runtime — プロファイラーランタイムがターゲットに対して有効化されていない場合に テストを無視します(bootstrap.tomlbuild.profiler = true
  • needs-sanitizer-support — サニタイザーサポートがターゲットに対して有効化されていない場合に 無視します(bootstrap.tomlbuild.sanitizers = true
  • needs-sanitizer-{address,hwaddress,leak,memory,thread} — 対応するサニタイザーが ターゲットに対して有効化されていない場合に無視します(それぞれ AddressSanitizer、 ハードウェア支援 AddressSanitizer、LeakSanitizer、MemorySanitizer または ThreadSanitizer)
  • needs-run-enabled — 実行されるテストであり、実行が無効化されている場合に無視します。 テストの実行は、x test --run=never フラグ、または fuchsia 上で実行することによって無効化できます。
  • needs-unwind — ターゲットがアンワインドをサポートしていない場合に無視します
  • needs-rust-lld — rust lld サポートが有効化されていない場合に無視します(bootstrap.tomlrust.lld = true
  • needs-threads — ターゲットがスレッドサポートを持たない場合に無視します
  • needs-subprocess — ターゲットがサブプロセスサポートを持たない場合に無視します
  • needs-symlink — ターゲットがシンボリックリンクをサポートしていない場合に無視します。 開発者が特権付きシンボリックリンク権限を有効にしていない場合、Windows ではこれに該当することがあります。
  • ignore-std-debug-assertions — std がデバッグアサーション付きでビルドされている場合に無視します。
  • needs-std-debug-assertions — std がデバッグアサーション付きでビルドされていない場合に無視します。
  • ignore-std-remap-debuginfo — std がそのソースの再マッピング付きでビルドされている場合に無視します。
  • needs-std-remap-debugino — std がそのソースの再マッピング付きでビルドされていない場合に無視します。
  • ignore-rustc-debug-assertions — rustc がデバッグアサーション付きでビルドされている場合に無視します。
  • needs-rustc-debug-assertions — rustc がデバッグアサーション付きでビルドされていない場合に無視します。
  • needs-target-has-atomic — ターゲットが指定されたすべての アトミック幅のサポートを持たない場合に無視します。たとえば、//@ needs-target-has-atomic: 8, 16, ptr を持つテストは、カンマ区切りリストのアトミック幅をサポートしている場合にのみ実行されます。
  • needs-dynamic-linking — ターゲットが動的リンクをサポートしていない場合に無視します (これは、dylib および cdylib クレートタイプを作成できないこととは直交します)
  • needs-crate-type — ターゲットプラットフォームが、指定されたクレートタイプの カンマ区切りリストのうち1つ以上をサポートしていない場合に無視します。 たとえば、 //@ needs-crate-type: cdylib, proc-macro は、 wasm32-unknown-unknown ターゲットではターゲットが proc-macro クレートタイプをサポートしていないため、テストが無視される原因になります。
  • needs-target-std — ターゲットプラットフォームが std サポートを持たない場合に無視します。
  • ignore-backends — 空白文字で区切られた、列挙されたバックエンドを無視します。 このディレクティブは --bypass-ignore-backends=[BACKEND] コマンドライン フラグで上書きできることに注意してください。
  • needs-backends — 現在の codegen バックエンドが列挙されている場合にのみテストを実行します。
  • needs-offload — LLVM バックエンドがオフロードサポート付きでビルドされていない場合に無視します。
  • needs-enzyme — Enzyme サブモジュールがビルドされていない場合に無視します。

以下のディレクティブは LLVM サポートをチェックします。

  • exact-llvm-major-version: 19 — llvm メジャーバージョンが指定された llvm メジャーバージョンと 一致しない場合に無視します。
  • min-llvm-version: 13.0 — LLVM バージョンが指定された値より小さい場合に無視されます
  • min-system-llvm-version: 12.0 — システム LLVM を使用していて、その バージョンが指定された値より小さい場合に無視されます
  • max-llvm-major-version: 19 — LLVM メジャーバージョンが指定されたメジャーバージョンより高い場合に 無視されます
  • ignore-llvm-version: 9.0 — 特定の LLVM バージョンを無視します
  • ignore-llvm-version: 7.0 - 9.9.9 — 範囲内の LLVM バージョンを無視します(両端を含む)
  • needs-llvm-components: powerpc — 特定の LLVM コンポーネントがビルドされていない場合に無視します。 注: コンポーネントが存在しない場合、CI では(COMPILETEST_REQUIRE_ALL_LLVM_COMPONENTS が設定されていると) テストは失敗します。
  • needs-forced-clang-based-tests — 環境変数 RUSTBUILD_FORCE_CLANG_BASED_TESTS が設定されていない限りテストは無視されます。これにより LLVM と並行して clang をビルドできるようになります
    • これは2つの CI ジョブ([x86_64-gnu-debug] と [aarch64-gnu-debug])でのみ設定されており、run-make テストのサブセットのみを実行します。 このディレクティブを持つ他のテストはまったく実行されませんが、これは通常望ましいことではありません。

デバッガーを無視するためのディレクティブについては、デバッグ情報テスト も参照してください。 [remote testing]: running.md#running-tests-on-a-remote-machine [parallel frontend]: compiletest.md#parallel-frontend [compare modes]: ui.md#compare-modes [x86_64-gnu-debug]: https://github.com/rust-lang/rust/blob/ab3dba92db355b8d97db915a2dca161a117e959c/src/ci/docker/host-x86_64/x86_64-gnu-debug/Dockerfile#L32 [aarch64-gnu-debug]: https://github.com/rust-lang/rust/blob/20c909ff9cdd88d33768a4ddb8952927a675b0ad/src/ci/docker/host-aarch64/aarch64-gnu-debug/Dockerfile#L32

テストのビルド方法に影響するもの

ディレクティブ説明サポートされるテストスイート指定可能な値
compile-flagsテストまたは aux ファイルをビルドするときに rustc に渡されるフラグrun-make/run-make-cargo を除くすべて任意の有効な rustc フラグ(例: -Awarnings -Dfoo)。-Cincremental または --edition は不可
editionテストのビルドに使用されるエディションrun-make/run-make-cargo を除くすべて任意の有効な --edition の値
rustc-envrustc の実行時に設定する環境変数run-make/run-make-cargo を除くすべて<KEY>=<VALUE>
unset-rustc-envrustc の実行時に設定解除する環境変数run-make/run-make-cargo を除くすべて任意の環境変数名
incrementalインクリメンタルテストスイート外のテストに対する適切なインクリメンタルサポートui, crashesN/A
no-prefer-dynamic-C prefer-dynamic を使用せず、--crate-type=dylib プリセットフラグによる dylib としてのビルドもしないui, crashesN/A

インクリメンタルテストスイートに含まれないインクリメンタルテストを使用したいテスト(run-make/run-make-cargo 以外)は、 compile-flags 経由で -C incremental を渡してはならず、 代わりに //@ incremental ディレクティブを使用しなければなりません。

代わりに、そのテストを適切なインクリメンタルテストとして書くことを検討してください。

edition ディレクティブ

//@ edition ディレクティブには、厳密なエディション、エディションの有界範囲、 またはエディションの左有界な半開範囲を指定できます。 これは、./x test がテストを実行するために使用するエディションに影響します。

例:

  • //@ edition: 2018 ディレクティブを持つテストは、2018 エディションでのみ実行されます。
  • //@ edition: 2015..2021 ディレクティブを持つテストは、2015 エディションと 2018 エディションで実行できます。 したがって、上限は Rust の場合と同様に排他的です (上限を包含する Rust の ..= に相当するものはないことに注意してください)。 ただし、CI は範囲内の最も低いエディション(この例では 2015)でのみテストを実行します。
  • //@ edition: 2018.. ディレクティブを持つテストは、2018 エディション以上で実行されます。 ただし、CI は範囲内の最も低いエディション(この例では 2018)でのみテストを実行します。

-- --edition= 引数を渡すことで、./x test に特定のエディションを強制的に使用させることもできます。 ただし、//@ edition ディレクティブを持つテストは、引数に渡された値をクランプします。 たとえば、./x test -- --edition=2015 を実行した場合:

  • //@ edition: 2018 を持つテストは、2018 エディションで実行されます。
  • //@ edition: 2015..2021 を持つテストは、2015 エディションで実行されます。
  • //@ edition: 2018.. を持つテストは、2018 エディションで実行されます。

Rustdoc

ディレクティブ説明サポートされるテストスイート指定可能な値
doc-flagsテストまたは aux ファイルをビルドするときに rustdoc に渡されるフラグrustdoc, rustdoc-js, rustdoc-json任意の有効な rustdoc フラグ

テストスイート固有のディレクティブ

テストスイート rustdoc-htmlrustdoc-js/rustdoc-js-std、 および rustdoc-json には、それぞれ追加のディレクティブ群があります。その基本的な 構文は compiletest ディレクティブのものに似ていますが、最終的には別個のツールによって読み取られ、チェックされます。 詳細については、上にリンクされているそれぞれの章を参照してください。

プリティプリント

Pretty-printer を参照してください。

その他のディレクティブ

  • no-auto-check-cfg — 自動 check-cfg を無効化する(--check-cfg テスト専用)
  • revisions — 複数回コンパイルする
  • forbid-output — 出力に指定された文字列が含まれていないことをチェックする
  • reference — reference 内のルールにリンクする注釈
  • disable-gdb-pretty-printers — debuginfo テスト用の gdb pretty printer を無効化する

ツール固有のディレクティブ

以下のディレクティブは、それらのツールを使用するテストスイートにおいて、 特定のコマンドラインツールがどのように呼び出されるかに影響します。

  • skip-filecheck は、通常であれば出力をチェックするために LLVM の FileCheck ツールを実行するテストで、その実行を回避します。
    • codegen テスト、assembly テスト、および mir-opt テストで使用されます。
  • filecheck-flags は、LLVM の FileCheck ツールの実行時に追加のフラグを追加します。
  • llvm-cov-flags は、LLVM の llvm-cov ツールの実行時に追加のフラグを追加します。

Tidy 固有のディレクティブ

以下のディレクティブは、tidy スクリプトがテストを検証する方法を制御します。

  • ignore-tidy-target-specific-tests は、テストが(compile-flag ディレクティブ内の --target フラグによって) 特定のターゲット向けにコンパイルされる場合に、適切な LLVM コンポーネントが(needs-llvm-components ディレクティブによって) 必要とされていることのチェックを無効にします。
  • unused-revision-names - 未知のリビジョン名への言及に対する tidy チェックを抑制します。

置換

ディレクティブの値では、対応する値に置き換えられるいくつかの変数を 置換として使用できます。 たとえば、特定のファイルへのパスを含むコンパイラフラグを渡す必要がある場合は、 次のようなものが使用できます。

//@ compile-flags: --remap-path-prefix={{src-base}}=/the/src

ここで、センチネル {{src-base}} は、以下で説明する適切なパスに置き換えられます。

  • {{cwd}}: compiletest が実行されるディレクトリ。 これはチェックアウトのルートではない場合があるため、可能な限り使用を避けるべきです。
    • 例: /path/to/rust, /path/to/build/root
  • {{src-base}}: テストが定義されているディレクトリ。 これは output normalization における $DIR と同等です。
    • 例: /path/to/rust/tests/ui/error-codes
  • {{build-base}}: テストの出力先となるベースディレクトリ。 これは output normalization における $TEST_BUILD_DIR と同等です。
    • 例: /path/to/rust/build/x86_64-unknown-linux-gnu/test/ui
  • {{rust-src-base}}: libstd/libcore/… が置かれている sysroot ディレクトリ
  • {{sysroot-base}}: テストのビルドに使用される sysroot ディレクトリのパス。
    • 主に、API 経由でコンパイラを実行する ui-fulldeps テストを想定しています。
  • {{target-linker}}: このテストで -Clinker に渡されるリンカー。 リンカーのオーバーライドが有効でない場合は空です。
    • 主に、API 経由でコンパイラを実行する ui-fulldeps テストを想定しています。
  • {{target}}: テストのコンパイル対象となるターゲット
    • 例: x86_64-unknown-linux-gnu

この置換を使用するテストの例については、 tests/ui/argfile/commandline-argfile.rs を参照してください。

ディレクティブの追加

個々のテストごとに何らかのテストプロパティや振る舞いを定義する必要がある場合、 新しいディレクティブを追加します。 ディレクティブプロパティは、実行時にディレクティブのバッキングストア (コマンドの現在の値を保持するもの)として機能します。

新しいディレクティブプロパティを追加するには、次のようにします。

  1. src/tools/compiletest/src/directives.rspub struct TestProps 宣言を探し、 新しい public プロパティをその宣言の末尾に追加します。
  2. 構造体宣言の直後にある impl TestProps 実装ブロックを探し、 新しいプロパティをデフォルト値で初期化します。

新しいディレクティブパーサーの追加

compiletest がテストファイルに遭遇すると、これも src/tools/compiletest/src/directives.rs にある Config 構造体の実装ブロックで 定義されているすべてのパーサーを呼び出すことで、ファイルを 1 行ずつ解析します (Config 構造体の宣言ブロックは src/tools/compiletest/src/common.rs にあることに注意してください)。 TestPropsload_from() メソッドは、現在のテキスト行を 各パーサーに渡そうとします。各パーサーは通常、その行が //@ must-compile-successfully//@ failure-status のような 特定のコメント付き(//@)ディレクティブで始まるかどうかをチェックします。 コメントマーカーの後の空白は任意です。

パーサーは、ディレクティブに応じて、テストファイル内でディレクティブとして 指定されているだけで、またはテストファイル内でパラメータ値が指定されていることで、 指定されたディレクティブプロパティのデフォルト値を上書きします。

impl Config で定義されるパーサーは通常、parse_<directive-name> という名前です (kebab-case の <directive-command> が snake-case の <directive_command> に変換されることに注意してください)。 impl Config は、単純な有無(parse_name_directive())や directive:parameter(s)parse_name_value_directive())、 特定の cfg 属性が定義されている場合にのみ任意で解析するもの(has_cfg_prefix())など、 一般的なパターンを簡単に解析できるいくつかの「低レベル」パーサーも定義しています。 低レベルパーサーは impl Config ブロックの末尾付近にあります。不要な追加の解析コードを書かないように、 それらと、そのすぐ上にある関連パーサーに目を通して、どのように使用されているかを必ず確認してください。

具体例として、src/tools/compiletest/src/directives.rs にある parse_failure_status() パーサーの実装を次に示します。

@@ -232,6 +232,7 @@ pub struct TestProps {
     // カスタマイズされた正規化ルール
     pub normalize_stdout: Vec<(String, String)>,
     pub normalize_stderr: Vec<(String, String)>,
+    pub failure_status: i32,
 }

 impl TestProps {
@@ -260,6 +261,7 @@ impl TestProps {
             run_pass: false,
             normalize_stdout: vec![],
             normalize_stderr: vec![],
+            failure_status: 101,
         }
     }

@@ -383,6 +385,10 @@ impl TestProps {
             if let Some(rule) = config.parse_custom_normalization(ln, "normalize-stderr") {
                 self.normalize_stderr.push(rule);
             }
+
+            if let Some(code) = config.parse_failure_status(ln) {
+                self.failure_status = code;
+            }
         });

         for key in &["RUST_TEST_NOCAPTURE", "RUST_TEST_THREADS"] {
@@ -488,6 +494,13 @@ impl Config {
         self.parse_name_directive(line, "pretty-compare-only")
     }

+    fn parse_failure_status(&self, line: &str) -> Option<i32> {
+        match self.parse_name_value_directive(line, "failure-status") {
+            Some(code) => code.trim().parse::<i32>().ok(),
+            _ => None,
+        }
+    }

振る舞いの変更の実装

テストが特定のディレクティブを呼び出すと、その結果として何らかの振る舞いが 変更されることが期待されます。 どのような振る舞いかは、当然ながらディレクティブの目的によって異なります。 failure-status の場合、変更される振る舞いは、 compiletest がデフォルト値ではなく、テスト内で呼び出されたディレクティブによって 定義された失敗コードを期待するようになることです。

これは failure-status に固有のものですが(振る舞いの変更を呼び出すための実装は ディレクティブごとに異なるため)、単なる例として、1 つのケースにおける 振る舞いの変更の実装を見ると役に立つかもしれません。 failure-status を実装するために、src/tools/compiletest/src/runtest.rs にある TestCx 実装ブロック内の check_correct_failure_status() 関数は、以下のように変更されました。

@@ -295,11 +295,14 @@ impl<'test> TestCx<'test> {
     }

     fn check_correct_failure_status(&self, proc_res: &ProcRes) {
-        // Rustランタイムが失敗時に返す値
-        const RUST_ERR: i32 = 101;
-        if proc_res.status.code() != Some(RUST_ERR) {
+        let expected_status = Some(self.props.failure_status);
+        let received_status = proc_res.status.code();
+
+        if expected_status != received_status {
             self.fatal_proc_rec(
-                &format!("failure produced the wrong error: {}", proc_res.status),
+                &format!("エラー: 期待される失敗ステータス ({:?}) に対して、受け取ったステータスは {:?} でした。",
+                         expected_status,
+                         received_status),
                 proc_res,
             );
         }
@@ -320,7 +323,6 @@ impl<'test> TestCx<'test> {
         );

         let proc_res = self.exec_compiled_test();
-
         if !proc_res.status.success() {
-            self.fatal_proc_rec("test run failed!", &proc_res);
+            self.fatal_proc_rec("テストの実行に失敗しました!", &proc_res);
         }
@@ -499,7 +501,6 @@ impl<'test> TestCx<'test> {
                 expected,
                 actual
             );
-            panic!();
         }
     }

ディレクティブプロパティにアクセスするために self.props.failure_status を使用していることに注目してください。 failure status ディレクティブを指定していないテストでは、 本稿執筆時点で self.props.failure_status はデフォルト値の 101 に評価されます。 しかし、たとえば //@ failure-status: 1 のようなディレクティブを 指定したテストでは、そのテストに限って、parse_failure_status()TestProps の デフォルト値を上書きするため、self.props.failure_status は 1 に評価されます。


  1. 詳細については、compiletest の章の 補助 proc-macro セクションを参照してください。

  2. 現在 これには奇妙な癖があり、 テストバイナリの stdout と stderr が連結されてから、 error-pattern がこの結合された出力に対してマッチされる。これは ??? 控えめに言っても、少し疑問がある。

minicore テスト補助: core スタブの使用

tests/auxiliary/minicore.rs は、ui/codegen/assembly/mir-opt テストスイート用のテスト補助です。 これは、クロスコンパイルされたターゲット向けにビルドする必要があるものの、 実行する必要がない、または実行したくないテストのために core スタブを提供します。

minicorecore 項目のみを対象としており、明示的に stdalloc 項目は対象外であることに注意してください。これは、core 項目のほうがより幅広いテストに適用できるためです。

テストでは、//@ add-minicore ディレクティブを指定することで minicore を使用できます。 次に、そのテストに #![feature(no_core)] + #![no_std] + #![no_core] を付け、 extern crate minicore(edition 2015) または use minicore(edition 2018+)でそのクレートをテストにインポートします。

暗黙的に指定されるコンパイラフラグ

これらのテストは no_std + no_core であるため、//@ add-minicore は そのテストが -C panic=abort でビルドされることを暗黙的に指定し、かつ必須とします。 アンワインドするパニックはサポートされていません。

アセンブリテストで CFI ディレクティブを保持するため、テストは -C force-unwind-tables=yes でもビルドされます。

TL;DR: //@ add-minicore は 2 つのコンパイラフラグを暗黙的に指定します。

  1. -C panic=abort
  2. -C force-unwind-tables=yes

core スタブをさらに追加する

minicore スタブに core 項目が不足していることに気付いた場合、 それが使用される可能性が高い、または既に複数のテストで必要とされているなら、 テスト補助に追加することを検討してください。

core との同期を保つ

minicore の項目は core に追随して最新の状態に保つ必要があります。 coreminicore の使用時で診断出力の一貫性を保つため、あらゆる diagnostic 属性(例: on_unimplemented)は minicore に正確に複製するべきです。

minicore を使用する codegen テストの例

#![allow(unused)]
fn main() {
//@ add-minicore
//@ revisions: meow bark
//@[meow] compile-flags: --target=x86_64-unknown-linux-gnu
//@[meow] needs-llvm-components: x86
//@[bark] compile-flags: --target=wasm32-unknown-unknown
//@[bark] needs-llvm-components: webassembly

#![crate_type = "lib"]
#![feature(no_core)]
#![no_std]
#![no_core]

extern crate minicore;
use minicore::*;

struct Meow;
impl Copy for Meow {} // ここでの `Copy` は `minicore` によって提供されます

// CHECK-LABEL: meow
#[unsafe(no_mangle)]
fn meow() {}
}

エコシステムテスト

Rust は、リグレッションを検出し、言語の進化について十分な情報に基づいた意思決定を行えるように、エコシステム内の実世界のコードとの統合をテストします。

テスト方法

Crater

Crater は、何千もの公開プロジェクトでテストを実行するツールです。このツールには実行用の独立したインフラストラクチャがあり、CI の一部としては実行されません。詳細については、Crater の章を参照してください。

cargotest

cargotest は、いくつかのサンプルプロジェクト(servoripgreptokei など)で cargo test を実行する小さなツールです。これは CI の一部として実行され、重大なリグレッションがないことを確認します。

./x test src/tools/cargotest

大規模 OSS プロジェクトビルダー

CI には、CI のリグレッションテストとして使用される大規模なオープンソース Rust プロジェクトをビルドするジョブがあります。統合ジョブは次のプロジェクトをビルドします。

Crater

Crater は、crates.io 上の すべての crate(および GitHub 上の一部)をコンパイルし、 テストを実行するためのツールです。 主に、互換性を破壊する可能性のある変更を実装する際に、破壊的影響の範囲を確認したり、 beta と stable のコンパイラバージョンを比較して破壊的影響がないことを確認したりするために使用されます。

Crater を実行するタイミング

PR がコンパイラに大きな変更を加える場合、または破壊的影響を引き起こす可能性がある場合は、 Crater の実行をリクエストする必要があります。 判断に迷う場合は、PR のレビュアーに遠慮なく尋ねてください。

Crater 実行のリクエスト

Rust チームは、PR によって導入された変更に対して Crater を実行するために使用できるマシンをいくつか保守しています。 PR に Crater の実行が必要な場合は、PR スレッドでトリアージチーム宛てにコメントを残してください。 “check-only” Crater 実行、“build only” Crater 実行、または “build-and-test” Crater 実行のどれが必要かをチームに知らせてください。 違いは主に所要時間です。 よくわからない場合は、build-and-test 実行を選択してください。 コンパイル時にのみ影響する変更を行う場合 (例: 新しいトレイトを実装する場合)は、check 実行だけで十分です。

PR はトリアージチームによってキューに追加され、結果が準備でき次第投稿されます。 check 実行にはおよそ 3〜4 日かかり、他の 2 つは平均して 5〜6 日かかります。

Crater は非常に有用ですが、いくつかの注意点を認識しておくことも重要です。

  • すべてのコードが crates.io にあるわけではありません! GitHub やその他の場所にあるリポジトリにも多くのコードがあります。 また、企業は自社のコードを公開したくない場合があります。 したがって、Crater の実行が成功したからといって、 破壊的影響がまったくないという意味にはなりません。引き続き注意が必要です。

  • Crater は x86_64 上の Linux ビルドのみを実行します。したがって、他のアーキテクチャやプラットフォームはテストされません。 重要な点として、これには Windows が含まれます。

  • 多くの crate はテストされません。 これには多くの理由が考えられます。たとえば、 その crate がすでにコンパイルできない(例: 古い nightly 機能を使用している)、テストが壊れている、または不安定である、 ネットワークアクセスを必要とする、その他の理由がある、といった場合です。

  • Crater を実行する前に、@bors try が成果物のビルドに成功している必要があります。 つまり、コードがコンパイルできない場合は Crater を実行できません。

Fuchsia 統合テスト

Fuchsia は、約 200 万行の Rust コードを持つオープンソースのオペレーティングシステムです。1 過去に多数の regressions を検出しており、その後 CI に含められました。

Fuchsia ジョブが壊れた場合はどうすればよいですか?

fuchsia ping グループに連絡し、支援を依頼してください。

@rustbot ping fuchsia

CI で Fuchsia をビルドする

Fuchsia は、プルリクエストがマージされる前に実行される bors テストスイートの一部として ビルドされます。

プルリクエストによって Fuchsia ビルダーが壊れる可能性を懸念しており、 bors キューに送信する前にテストしたい場合は、Fuchsia 統合をビルドする try ジョブを実行するよう bors に依頼するだけです: @bors try jobs=x86_64-fuchsia

Fuchsia をローカルでビルドする

Fuchsia は Rust 以外の言語を使用しているため、ビルドシステムとして Cargo を 使用していません。また、ツールチェーンのビルドが 特定の 方法 で構成されている必要もあります。

Fuchsia をビルドする推奨方法は、Fuchsia のチェックアウトとビルドを代わりに実行する Docker スクリプトを使用することです。以前に Docker テストを実行したことがある場合は、 Rust チェックアウトから次のコマンドを実行するだけで、ローカルの Rust ツールチェーンを使用して Fuchsia をダウンロードしてビルドできます。

src/ci/docker/run.sh x86_64-fuchsia

Docker でジョブを実行およびデバッグする方法の詳細については、 Testing with Docker の章を参照してください。

Fuchsia のチェックアウトは大きいことに注意してください。本稿執筆時点では、チェックアウトと ビルドには 46G の容量が必要であり、想像できるとおり、完了までにしばらく 時間がかかります。

Fuchsia チェックアウトを変更する

Fuchsia をローカルでビルドしたい主な理由は、リグレッションを 調査する必要があるためです。Docker ビルドを実行した後、Rust チェックアウトの obj/fuchsia ディレクトリ内に Fuchsia のチェックアウトがあります。 build-fuchsia.sh スクリプトの KEEP_CHECKOUT 行を KEEP_CHECKOUT=1 に変更すると、 必要に応じてチェックアウトを変更し、上記のビルドコマンドを再実行できます。これにより、 以前のすべてのビルド結果が再利用されます。

Fuchsia チェックアウトをカスタマイズするためのその他のオプションは、 build-fuchsia.sh スクリプトで確認できます。

Fuchsia ビルドをカスタマイズする

Rust CI で Fuchsia をビルドするために使用されるオプションの詳細は、 build-fuchsia.sh によって呼び出される build_fuchsia_from_rust_ci.sh スクリプトで 確認できます。

Fuchsia のビルドシステムは GN を使用します。これは Ninja ファイルを生成し、その後ビルドを実行する作業を Ninja に引き渡すメタビルドシステムです。

Fuchsia 開発者は fx を使用してビルドを実行し、その他の開発タスクを行います。 このツールは Fuchsia チェックアウトの .jiri_root/bin にあります。一部のワークフローでは、 これを $PATH に追加する必要があるかもしれません。

関連する fx サブコマンドがいくつかあります。たとえば次のとおりです。

  • fx set はビルド引数を受け取り、それを out/default/args.gn に書き込み、 GN を実行します。
  • fx build は Ninja を使用して Fuchsia プロジェクトをビルドします。ビルド引数への 変更を自動的に検出して GN を再実行します。デフォルトではすべてをビルドしますが、 特定のターゲットをビルドするためのターゲットパスも受け付けます(下記参照)。
  • fx clippy は特定の Rust ターゲット(またはそのすべて)に対して Clippy を実行します。Rust CI ビルドでは、 ほとんどの Rust ターゲットで codegen を実行しないようにするため、これを使用します。内部では、 fx build と同様に Ninja を呼び出します。clippy の結果は、出力される前にビルド出力ディレクトリ内の json ファイルに保存されます。

ターゲットパス

GN は次のようなパスを使用してビルドターゲットを識別します。

//src/starnix/kernel:starnix_core

先頭の // はチェックアウトのルートを意味し、残りのスラッシュは ディレクトリ名です。: の後の文字列は、そのディレクトリの BUILD.gn ファイルで定義されたターゲットの ターゲット名 です。

ターゲット名がディレクトリ名と同じ場合は省くことができます。言い換えると、 //src/starnix/kernel//src/starnix/kernel:kernel と同じです。

これらのターゲットパスは、依存関係を参照するために BUILD.gn ファイル内で使用され、 fx build でも使用できます。

コンパイラフラグを変更する

ターゲットに追加される GN config の中にカスタムコンパイラフラグを入れることができます。 簡単な例を示します。

config("everybody_loops") {
    rustflags = [ "-Zeverybody-loops" ]
}

rustc_binary("example") {
    crate_root = "src/bin.rs"
    # ...既存のキーをここに...
    configs += [ ":everybody_loops" ]
}

これにより、example ターゲットをビルドするときに rustc に -Zeverybody-loops フラグが追加されます。あるターゲットに依存するすべてのターゲットに config を追加するために public_configs も使用できることに注意してください。

ビルド内のすべての Rust ターゲットにフラグを追加したい場合は、 rustflags を //build/config:compiler config、またはそのファイルから参照される OS 固有の config に追加できます。Rust ターゲットでは cflagsldflags は無視されることに注意してください。

ninja と rustc コマンドを直接実行する

1 レイヤー下に進むと、fx buildninja を呼び出し、さらにそれが最終的に rustc を呼び出します。すべてのビルドアクションは out ディレクトリ内で実行されます。これは通常、 Fuchsia チェックアウト内の out/default です。

ninja が呼び出す実際のコマンドを表示させるには、そのコマンドが失敗するように強制します。 たとえば、ターゲットのソースファイルの 1 つに構文エラーを追加します。 コマンドを取得したら、出力ディレクトリ内からそれを実行できます。

ツールチェーン自体を変更した後は、fx build または ninja がすべての Rust ターゲットを 再ビルドするように、out/default/args.gn 内のビルド設定 rustc_version_string を 変更する必要があります。これはテキストエディターで行うことができ、文字列の内容は、 あるビルドから次のビルドへ変化している限り重要ではありません。 build_fuchsia_from_rust_ci.sh は、ツールチェーンディレクトリをハッシュすることでこれを行います。

Fuchsia のウェブサイトには、build system のより詳細なドキュメントがあります。

その他のヒントとテクニック

build_fuchsia_from_rust_ci.sh を使用するときは、初回実行後に fx set コマンドをコメントアウトして、毎回 GN が再実行されないようにできます。これを行う場合は、 version_string 行もコメントアウトして数秒節約できます。

初回ビルド後の ninja の起動時間を短縮するには、export NINJA_PERSISTENT_MODE=1 を実行します。

Fuchsia ターゲットサポート

Fuchsia ターゲットサポートの詳細については、the rustc book の Fuchsia の章を参照してください。


  1. 2024 年 6 月時点で、Fuchsia には約 200 万行のファーストパーティ Rust コードと、ほぼ同量のサードパーティコードがありました。これは tokei によるカウントです (コメントと空行は除外)。

Rust for Linux 統合テスト

Rust for Linux (RfL) は、Linux カーネルに Rust プログラミング言語のサポートを追加する取り組みです。

Rust for Linux ジョブが壊れた場合はどうすればよいですか?

PR が Rust for Linux CI ジョブを壊した場合は、次のようにします。

  • 障害が意図しないもので、一時的なものに見える場合は、RfL に 知らせて再試行します。
    • PR が緊急で、再試行しても修正されない場合は、CI ジョブを 一時的に無効にします(src/ci/github-actions/jobs.ymlimage: x86_64-rust-for-linux ジョブをコメントアウトします)。
  • 障害が意図しないものだった場合は、その障害を解決するように PR を変更します。
  • 障害が意図したものだった場合は、RfL に知らせて、 カーネル側で何を変更する必要があるかを話し合います。
    • PR が緊急の場合は、CI ジョブを一時的に無効にします( src/ci/github-actions/jobs.ymlimage: x86_64-rust-for-linux ジョブをコメントアウトします)。
    • PR が数日待てる場合は、RfL メンテナーが必要な変更を行った 新しい Linux カーネルのコミットハッシュを提供するのを待ち、それを PR に適用します。これにより、変更が機能することを確認できます( src/ci/docker/scripts/rfl-build.shLINUX_VERSION 環境変数を更新します)。

RfL 開発者に連絡する必要がある場合は、Rust for Linux ping グループに ping して助けを求めることができます。

@rustbot ping rfl

CI で Rust for Linux をビルドする

Rust for Linux は、プルリクエストがマージされる前に実行される bors テスト群の一部として ビルドされます。

このワークフローは、Rust コンパイラの stage1 sysroot をビルドし、Linux カーネルをダウンロードして、この sysroot を使用して複数の Rust for Linux ドライバーと サンプルのコンパイルを試みます。RfL は複数の不安定なコンパイラ/言語機能を使用しているため、 このワークフローにより、特定のコンパイラ変更によってそれが壊れる場合に通知されます。

プルリクエストが Rust for Linux ビルダーを壊す可能性を懸念していて、 bors キューに送信する前にテストしたい場合は、bors に Rust for Linux 統合をビルドする try ジョブを実行するよう依頼するだけです。 @bors try jobs=x86_64-rust-for-linux

コード生成バックエンドのテスト

コード生成の章も参照してください。

主要な LLVM コード生成バックエンドに加えて、rust-lang/rust CI では、特定のテストジョブで cranelift および GCC コード生成バックエンドのテストも実行します。

関連するテストの詳細については、以下を参照してください。

Cranelift コード生成バックエンドのテスト

TODO: このページにもう少し情報を追加してください。

GCC コード生成バックエンド

このバックエンドとコンパイラーの統合を壊す可能性のある変更を見つけやすくするため、CI では GCC コード生成バックエンドを使ってコンパイラーのテストスイートの一部を実行しています。

GCC コード生成バックエンド全般についてバグや問題に遭遇した場合は、遠慮なく rustc_codegen_gcc リポジトリで issue を開いてください。

なお、このバックエンドは現在 x86_64-unknown-linux-gnu ターゲットのみをサポートしています。

GCC バックエンドの CI エラーに遭遇した場合

CI の x86_64-gnu-gcc ジョブで GCC コード生成バックエンドを使って実行されたテストに関連するエラーに遭遇した場合は、 次のコマンドを使用して、CI で起きることを再現しながら、GCC バックエンドを使って UI テストをローカルで実行できます。

./x test tests/ui \
  --set 'rust.codegen-backends = ["llvm", "gcc"]' \
  --set 'rust.debug-assertions = false' \
  --test-codegen-backend gcc

CI で別のテストスイートが失敗した場合は、tests/ui の部分を変更する必要があります。

CI ジョブ全体をローカルで再現するには、cargo run --manifest-path src/ci/citool/Cargo.toml run-local x86_64-gnu-gcc を実行できます。 詳細については、Docker によるテストを参照してください。

GCC ジョブが失敗した場合はどうすればよいですか?

GCC ジョブのテストが失敗し、その失敗が GCC バックエンドによって引き起こされた可能性があるように見える場合は、@rust-lang/wg-gcc-backend を使用して cg-gcc ワーキンググループに ping できます。

GCC バックエンドで失敗するコンパイラーテストの修正が自明でない場合は、//@ ignore-backends: gcc compiletest ディレクティブを使用して、cg_gcc で実行されるときにそのテストを無視できます。

ビルドするコード生成バックエンドの選択

rust.codegen-backends = [...] bootstrap オプションは、どのコード生成バックエンドがビルドされ、生成される rustc の sysroot に含まれるかに影響します。 GCC コード生成バックエンドを使用するには、bootstrap.toml のこの配列に "gcc" を含める必要があります。

rust.codegen-backends = ["llvm", "gcc"]

bootstrap.toml ファイルを変更したくない場合は、代わりに x コマンドを --set 'rust.codegen-backends=["llvm", "gcc"]' とともに実行できます。 例:

./x build --set 'rust.codegen-backends=["llvm", "gcc"]'

codegen-backends 配列の最初のバックエンドによって、ビルドされた rustcデフォルトバックエンドとして使用されるバックエンドが決まります。 これにより、stage 1 標準ライブラリ(または stage 2 以降でビルドされるもの)をコンパイルするために使用されるバックエンドも決まります。 したがって、デフォルトで GCC バックエンドを使用する rustc を生成するには、この配列の最初の要素として "gcc" を置くことができます。

./x build --set 'rust.codegen-backends=["gcc"]' library

テストで使用するコード生成バックエンドの選択

テスト用の Rust プログラムをビルドするために GCC コード生成バックエンドを使用してコンパイラーテストを実行するには、 --test-codegen-backend フラグを使用できます。

./x test tests/ui --test-codegen-backend gcc

これが機能するためには、テスト対象のコンパイラーの sysroot ディレクトリで GCC コード生成バックエンドが利用可能である必要があることに注意してください。

CI から GCC をダウンロードする

gcc.download-ci-gcc bootstrap オプションは、GCC(GCC コード生成バックエンドの依存関係)が CI からダウンロードされるか、ローカルでビルドされるかを制御します。 デフォルト値は true で、GCC ソースにローカルの変更がなく、指定されたホストターゲットが CI で利用可能な場合は、CI から GCC をダウンロードします。

独自の GCC を提供する

独自の libgccjit.so ファイルを提供したい場合があります。 そのような場合の 1 つは、GCC がマルチターゲットコンパイラーではないため、rustc を別のターゲット向けにクロスコンパイルしたい場合です。 このユースケースをサポートするために、gcc.libgccjit-libs-dir bootstrap オプションがあります。 このオプションは gcc.download-ci-gcc を上書きします。つまり、bootstrap によって libgccjit.so がダウンロードされたりローカルでビルドされたりすることはありません。 このディレクトリのディレクトリ構造は <host>/<target>/libgccjit.so です。たとえば、次のようになります。

.
├── m68k-unknown-linux-gnu
│   └── m68k-unknown-linux-gnu
│       └── libgccjit.so
└── x86_64-unknown-linux-gnu
    ├── m68k-unknown-linux-gnu
    │   └── libgccjit.so
    └── x86_64-unknown-linux-gnu
        └── libgccjit.so

バックエンド自体のテストの実行

GCC コード生成バックエンドを使用してコンパイラーのテストスイートを実行することに加えて、バックエンド自体のテストスイートも実行できます。

現在は、次のコマンドを使用して実行できます。

./x test rustc_codegen_gcc

これが機能するためには、バックエンドが有効化されている必要があります。

パフォーマンステスト

rustc-perf

コンパイラのパフォーマンスを改善し、パフォーマンスのリグレッションを防ぐために、 多くの作業が行われています。

rustc-perf プロジェクトは、 パフォーマンスのテストと追跡のための複数のサービスを提供しています。 これは、ベンチマークをサービスとして実行するためのホストされたインフラストラクチャを提供します。 現時点では、x86_64-unknown-linux-gnu ビルドのみが追跡されています。

“perf run” は、多数の人気のあるクレートについて、異なる構成における コンパイラのパフォーマンスを比較するために使用されます。 異なる構成には、“fresh builds”、インクリメンタルコンパイルを用いたビルドなどが含まれます。

perf run の結果は、コンパイラの 2 つのバージョン間の比較 (それぞれのコミットハッシュによるもの)です。

rustc-perf を使用して、コンパイラを ローカルで手動でベンチマークおよびプロファイルすることもできます。

自動 perf run

各 PR がマージされた後、コンパイラに対して一連のベンチマークが実行されます。 結果は https://perf.rust-lang.org/ Web サイトで時系列に追跡されます。 変更はすべて PR 上のコメントに記録されます。

手動 perf run

さらに、必要に応じて、PR がマージされる前にパフォーマンステストを実行できます。 PR がパフォーマンスに影響を与える可能性がある場合、 特に悪影響を与える可能性がある場合は、perf run をリクエストするべきです。

PR のパフォーマンスへの影響を評価するには、PR にこのコメントを書きます。

@bors try @rust-timer queue

注記: perf run の実行を許可されたユーザーのみが、このコメントを投稿できます。 それを使用できるチームは、Teams リポジトリ[permissions] セクションに perf = true の値で記録されています(bors 権限も必要です)。もしあなたが それらのチームのいずれにも属していない場合は、代わりに誰かに投稿してもらうよう 気軽に依頼してください(Zulip 上、または割り当てられたレビュー担当者に依頼してください)。

これにより、まず bors に “try” ビルドを実行するよう指示します。これは x86_64-unknown-linux-gnu 向けの完全なリリースビルドを行います。 ビルドが完了すると、それに対してパフォーマンススイートを実行するためにキューに入れられます。 パフォーマンステストが完了すると、 bot が PR に概要と完全なレポートへのリンクを含むコメントを投稿します。

すでにビルド済みのアーティファクトに対して perf run を実行したい場合 (たとえば、まだベンチマークされていない以前の try ビルドに対して)は、代わりに次を実行できます。

@rust-timer build <commit-sha>

ただし、同じアーティファクトを 2 回ベンチマークすることはできません。

利用可能な perf bot コマンドの詳細については、 こちらを参照してください。

ベンチマークプロセス自体の詳細については、perf collector documentationを参照してください。

テスト関連のその他の情報

RUSTC_BOOTSTRAP と安定性

これはブートストラップ/コンパイラの実装詳細ですが、テストにも役立つことがあります。

  • RUSTC_BOOTSTRAP=1 は「不正に」通常の安定性チェックをバイパスし、 安定版 rustc で不安定機能や CLI フラグを使用できるようにします。
  • RUSTC_BOOTSTRAP=-1 は、指定した rustc に、実際には nightly rustc であっても 安定版コンパイラであるかのように振る舞わせます。これは、コンパイラの一部の 振る舞い(例: 診断)が、コンパイラが nightly かどうかによって異なることがあるため便利です。

ui テストや //@ rustc-env をサポートするその他のテストスイートでは、次のように指定できます。

// 安定版 rustc で不安定機能を使用できるようにする
//@ rustc-env:RUSTC_BOOTSTRAP=1

// または nightly rustc に安定版 rustc であるかのように振る舞わせる
//@ rustc-env:RUSTC_BOOTSTRAP=-1

run-make/run-make-cargo テストでは、//@ rustc-env はサポートされていません。 個々の rustc 呼び出しについては、次のようなことができます。

use run_make_support::rustc;

fn main() {
    rustc()
        // 自分が非常に安定しているふりをする
        .env("RUSTC_BOOTSTRAP", "-1")
        //...
        .run();
}

コンパイラのデバッグ

この章では、コンパイラをデバッグするためのヒントをいくつか紹介します。 これらのヒントは、何に取り組んでいるかに関係なく役立つことを目指しています。 他の章の中には、 コンパイラの特定の部分に関するアドバイスがあります(例: クエリのデバッグと テストの章LLVM デバッグの 章)。

コンパイラの設定

デフォルトでは、rustc はほとんどのデバッグ情報なしでビルドされます。 デバッグ情報を有効にするには、 bootstrap.toml で rust.debug = true を設定してください。

rust.debug = true を設定すると、多くの異なるデバッグオプション(例: debug-assertionsdebug-logging など)が有効になります。必要であれば個別に調整できますが、多くの人は 単に rust.debug = true を設定します。

GDB を使用して rustc をデバッグしたい場合は、bootstrap.toml に次のオプションを設定してください。

rust.debug = true
rust.debuginfo-level = 2

注: これは大量のディスク容量を使用します ( 35GB 以上)し、 コンパイル時間も大幅に長くなります。 debuginfo-level = 1debug = true のときのデフォルト)では、 実行パスを追跡できますが、 デバッグ用のシンボル情報は失われます。

デフォルト設定では、symbol-mangling-version v0 が有効になります。 これには少なくとも GDB v10.2 が必要です。 そうでない場合は、bootstrap.toml で新しい symbol-mangling-version を無効にする必要があります。

rust.new-symbol-mangling = false

詳細については、bootstrap.example.toml のコメントを参照してください。

設定オプションを変更した後は、コンパイラをリビルドする必要があります。

ICE ファイルの抑制

デフォルトでは、rustc が Internal Compiler Error(ICE)に遭遇すると、ICE の内容を rustc-ice-<timestamp>-<pid>.txt という名前の ICE ファイルとして現在の作業ディレクトリにダンプします。 これが望ましくない場合は、RUSTC_ICE=0 を使用して ICE ファイルが作成されないようにできます。

バックトレースの取得

ICE(コンパイラ内の panic)が発生した場合、通常の Rust プログラムと同様に RUST_BACKTRACE=1 を設定して panic! のスタックトレースを取得できます。 記憶が正しければ、MinGW ではバックトレースは 機能しません。申し訳ありません。 問題がある場合やバックトレースが unknown だらけの場合は、 Linux、Mac、または Windows 上の MSVC を使用する方法を探した方がよいかもしれません。

デフォルト設定(debugtrue に設定されていない場合)では、行番号が 有効になっていないため、バックトレースは次のようになります。

stack backtrace:
   0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
   1: std::sys_common::backtrace::_print
   2: std::panicking::default_hook::{{closure}}
   3: std::panicking::default_hook
   4: std::panicking::rust_panic_with_hook
   5: std::panicking::begin_panic
   (~~~~ LINES REMOVED BY ME FOR BREVITY ~~~~)
  32: rustc_typeck::check_crate
  33: <std::thread::local::LocalKey<T>>::with
  34: <std::thread::local::LocalKey<T>>::with
  35: rustc::ty::context::TyCtxt::create_and_enter
  36: rustc_driver::driver::compile_input
  37: rustc_driver::run_compiler

debug = true を設定すると、スタックトレースに行番号が表示されます。 その場合、バックトレースは次のようになります。

stack backtrace:
   (~~~~ LINES REMOVED BY ME FOR BREVITY ~~~~)
             at /home/user/rust/compiler/rustc_typeck/src/check/cast.rs:110
   7: rustc_typeck::check::cast::CastCheck::check
             at /home/user/rust/compiler/rustc_typeck/src/check/cast.rs:572
             at /home/user/rust/compiler/rustc_typeck/src/check/cast.rs:460
             at /home/user/rust/compiler/rustc_typeck/src/check/cast.rs:370
   (~~~~ LINES REMOVED BY ME FOR BREVITY ~~~~)
  33: rustc_driver::driver::compile_input
             at /home/user/rust/compiler/rustc_driver/src/driver.rs:1010
             at /home/user/rust/compiler/rustc_driver/src/driver.rs:212
  34: rustc_driver::run_compiler
             at /home/user/rust/compiler/rustc_driver/src/lib.rs:253

-Z フラグ

コンパイラには多数の -Z * フラグがあります。 これらは nightly でのみ有効になる不安定なフラグです。 その多くはデバッグに役立ちます。 -Z フラグの完全な一覧を取得するには、-Z help を使用してください。

便利なフラグの 1 つに -Z verbose-internals があります。これは一般に、デバッグに役立つ可能性がある 情報をより多く出力できるようにします。

すぐ下に、選択したいくつかのフラグについて詳しい解説があります。

エラーのバックトレースの取得

コンパイラがエラーメッセージを出力する箇所までのバックトレースを取得したい場合は、 -Z treat-err-as-bug=n を渡すことができます。これにより、 コンパイラは n 番目のエラーで panic します。 =n を省略した場合、コンパイラは n として 1 を想定するため、最初に遭遇したエラーで panic します。

例:

cat error.rs
fn main() {
    1 + ();
}
$ rustc +stage1 error.rs
error[E0277]: cannot add `()` to `{integer}`
 --> error.rs:2:7
  |
2 |       1 + ();
  |         ^ no implementation for `{integer} + ()`
  |
  = help: the trait `Add<()>` is not implemented for `{integer}`

error: aborting due to previous error

では、上記のエラーはどこから発生しているのでしょうか?

$ RUST_BACKTRACE=1 rustc +stage1 error.rs -Z treat-err-as-bug
error[E0277]: the trait bound `{integer}: std::ops::Add<()>` is not satisfied
 --> error.rs:2:7
  |
2 |     1 + ();
  |       ^ no implementation for `{integer} + ()`
  |
  = help: the trait `std::ops::Add<()>` is not implemented for `{integer}`

error: internal compiler error: unexpected panic

note: the compiler unexpectedly panicked. this is a bug.

note: we would appreciate a bug report: https://github.com/rust-lang/rust/blob/HEAD/CONTRIBUTING.md#bug-reports

note: rustc 1.24.0-dev running on x86_64-unknown-linux-gnu

note: run with `RUST_BACKTRACE=1` for a backtrace

thread 'rustc' panicked at 'encountered error with `-Z treat_err_as_bug',
/home/user/rust/compiler/rustc_errors/src/lib.rs:411:12
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose
backtrace.
stack backtrace:
  (~~~ IRRELEVANT PART OF BACKTRACE REMOVED BY ME ~~~)
   7: rustc::traits::error_reporting::<impl rustc::infer::InferCtxt<'a, 'tcx>>
             ::report_selection_error
             at /home/user/rust/compiler/rustc_middle/src/traits/error_reporting.rs:823
   8: rustc::traits::error_reporting::<impl rustc::infer::InferCtxt<'a, 'tcx>>
             ::report_fulfillment_errors
             at /home/user/rust/compiler/rustc_middle/src/traits/error_reporting.rs:160
             at /home/user/rust/compiler/rustc_middle/src/traits/error_reporting.rs:112
   9: rustc_typeck::check::FnCtxt::select_obligations_where_possible
             at /home/user/rust/compiler/rustc_typeck/src/check/mod.rs:2192
  (~~~ IRRELEVANT PART OF BACKTRACE REMOVED BY ME ~~~)
  36: rustc_driver::run_compiler
             at /home/user/rust/compiler/rustc_driver/src/lib.rs:253

よし、これでエラーのバックトレースを取得できました!

delayed bug のデバッグ

-Z eagerly-emit-delayed-bugs オプションを使うと、delayed bug を簡単にデバッグできます。 これは delayed bug を通常のエラーに変換します。つまり、可視化します。これは -Z treat-err-as-bug と組み合わせて使用することで、特定の delayed bug で停止し、バックトレースを取得できます。

エラー作成場所を取得する

-Z track-diagnostics は、エラーがどこで出力されたかを特定するのに役立ちます。 これはそのために #[track_caller] を使用し、エラーとともにその場所を出力します。

$ RUST_BACKTRACE=1 rustc +stage1 error.rs -Z track-diagnostics
error[E0277]: cannot add `()` to `{integer}`
 --> src\error.rs:2:7
  |
2 |     1 + ();
  |       ^ no implementation for `{integer} + ()`
-Ztrack-diagnostics: created at compiler/rustc_trait_selection/src/traits/error_reporting/mod.rs:638:39
  |
  = help: the trait `Add<()>` is not implemented for `{integer}`
  = help: the following other types implement trait `Add<Rhs>`:
            <&'a f32 as Add<f32>>
            <&'a f64 as Add<f64>>
            <&'a i128 as Add<i128>>
            <&'a i16 as Add<i16>>
            <&'a i32 as Add<i32>>
            <&'a i64 as Add<i64>>
            <&'a i8 as Add<i8>>
            <&'a isize as Add<isize>>
          and 48 others

For more information about this error, try `rustc --explain E0277`.

これは -Z treat-err-as-bug と似ていますが、異なります。

  • 出力されたすべてのエラーについて場所を出力します
  • デバッグシンボル付きでビルドされたコンパイラを必要としません
  • 大きなスタックトレースを読み解く必要がありません。

ロギング出力を取得する

コンパイラはロギングに tracing クレートを使用します。

詳細については、tracing に関するガイドのセクションを参照してください

リグレッションの絞り込み(二分探索)

cargo-bisect-rustc ツールは、rustc の挙動の変化を引き起こした PR を正確に見つけるための、手早く簡単な方法として使用できます。 これは rustc の PR アーティファクトを自動的にダウンロードし、リグレッションが見つかるまで、あなたが提供したプロジェクトに対してそれらをテストします。 その後、その PR を確認して、なぜ 変更されたのかについてより多くの文脈を得ることができます。 使用方法については、このチュートリアルを参照してください。

Rust の CI からアーティファクトをダウンロードする

kennytm による rustup-toolchain-install-master ツールは、特定の SHA1 に対して Rust の CI によって生成されたアーティファクトをダウンロードするために使用できます。これは基本的には、ある PR が正常にマージされたことに対応します。そして、それらをローカルで使用できるようにセットアップします。 これは @bors try によって生成されたアーティファクトに対しても機能します。 これは、自分でビルドせずに PR の結果として生成されたビルドを調べたい場合に便利です。

#[rustc_*] TEST 属性

コンパイラは、大量の内部(永続的に unstable な)属性を定義しており、その一部はコンパイラ内部の追加情報をダンプすることでデバッグに役立ちます。 これらには rustc_ というプレフィックスが付けられており、内部 feature である rustc_attrs によってゲートされています(例: #![feature(rustc_attrs)] で有効化)。

完全かつ最新の一覧については、builtin_attrs を参照してください。 より具体的には、TEST とマークされているものです。 注目すべきものをいくつか示します。

属性説明
rustc_dump_def_parents特定の定義の DefId 親の連鎖をダンプします。
rustc_dump_def_pathアイテムの def_path_str をダンプします。
rustc_dump_hidden_type_of_opaquesクレート内の各 opaque type の隠された型をダンプします。
rustc_dump_inferred_outlivesアイテムの暗黙の境界をダンプします。より正確には、アイテムの inferred_outlives_of です。
rustc_dump_item_boundsアイテムの item_bounds をダンプします。
rustc_dump_layoutこのセクションを参照してください
rustc_dump_object_lifetime_defaultsアイテムの object lifetime defaults をダンプします。
rustc_dump_predicatesアイテムの predicates_of をダンプします。
rustc_dump_symbol_nameアイテムのマングル済みおよびデマングル済みの symbol_name をダンプします。
rustc_dump_variancesアイテムの variances をダンプします。
rustc_dump_vtableimpl、または dyn 型の型エイリアスの vtable レイアウトをダンプします。
rustc_regionsNLL クロージャのリージョン要件をダンプします。

すぐ下に、選択したいくつかについての詳しい解説があります。

Graphviz 出力(.dot ファイル)の整形

特定の機能をデバッグするためのコンパイラオプションの中には、graphviz グラフを生成するものがあります。たとえば、関数に付与された #[rustc_mir(borrowck_graphviz_postflow="suffix.dot")] 属性は、-Zdump-mir-dataflow と組み合わせることで、さまざまな borrow-checker のデータフローグラフをダンプします。

これらはすべて .dot ファイルを生成します。これらのファイルを表示するには、graphviz をインストールし(例: apt-get install graphviz)、次のコマンドを実行します。

dot -T pdf maybe_init_suffix.dot > maybe_init_suffix.pdf
firefox maybe_init_suffix.pdf # またはお好みの pdf ビューアー

型レイアウトのデバッグ

内部属性 #[rustc_dump_layout(...)] は、それが付与された型の Layout をダンプするために使用できます。 例:

#![allow(unused)]
#![feature(rustc_attrs)]

fn main() {
#[rustc_dump_layout(debug)]
type T<'a> = &'a u32;
}

次の内容が出力されます。

error: layout_of(&u32) = Layout {
           size: Size(8 bytes),
           align: AbiAlign {
               abi: Align(8 bytes),
           },
           backend_repr: Scalar(
               Initialized {
                   value: Pointer(
                       AddressSpace(
                           0,
                       ),
                   ),
                   valid_range: 1..=18446744073709551615,
               },
           ),
           fields: Primitive,
           largest_niche: Some(
               Niche {
                   offset: Size(0 bytes),
                   value: Pointer(
                       AddressSpace(
                           0,
                       ),
                   ),
                   valid_range: 1..=18446744073709551615,
               },
           ),
           uninhabited: false,
           variants: Single {
               index: 0,
           },
           max_repr_align: None,
           unadjusted_abi_align: Align(8 bytes),
           randomization_seed: 281492156579847,
       }
 --> src/lib.rs:4:1
  |
4 | type T<'a> = &'a u32;
  | ^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

rustc をデバッグするための CodeLLDB の設定

VSCode を使用しており、関心のあるコード部分についてデバッグレベル 1 または 2 を要求するように bootstrap.toml を編集している場合は、VSCode の CodeLLDB 拡張機能を使ってデバッグできるはずです。

以下は、stage 1 コンパイラをビルドされたディレクトリから直接実行するために使用している launch.json ファイルのサンプルです(“インストール“されている必要はありません)。

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
      {
        "type": "lldb",
        "request": "launch",
        "name": "Launch",
        "args": [],  // コンパイラに渡す文字列のコマンドライン引数の配列
        "program": "${workspaceFolder}/build/host/stage1/bin/rustc",
        "windows": {  // windows を使用している場合に適用可能
            "program": "${workspaceFolder}/build/host/stage1/bin/rustc.exe"
        },
        "cwd": "${workspaceFolder}",  // プログラム開始時の現在の作業ディレクトリ
        "stopOnEntry": false,
        "sourceLanguages": ["rust"]
      }
    ]
  }

tracing を使ってコンパイラをデバッグする

コンパイラには多数の debug!(または trace!)呼び出しがあり、多くの箇所でログ情報を出力します。 これらは、バグを完全に見つけるまでには至らなくても少なくとも場所を絞り込むのに非常に役立ちます。また、コンパイラが特定の処理を行っている理由を把握するのにも役立ちます。

ログを見るには、RUSTC_LOG 環境変数をログフィルターに設定する必要があります。 ログフィルターの完全な構文は、tracing-subscriber の rustdocにあります。

環境変数

これは、rustc が tracing 出力をカスタマイズするために受け付ける環境変数の概要です。 これらの定義は、ほとんど compiler/rustc_log/src/lib.rs にあります。

名前用途
RUSTC_LOGtracing フィルター(このページの残りを参照)
RUSTC_LOG_COLORalwaysnever、または auto
RUSTC_LOG_ENTRY_EXIT設定されていて ‘0’ でない場合、スパンの entry/exit をログに記録します。
RUSTC_LOG_THREAD_IDS設定されていて ‘1’ と等しい場合、スレッド ID もログに記録します。
RUSTC_LOG_BACKTRACE指定された文字列値に一致する target で trace が出力された場合、バックトレースをキャプチャして出力します。
RUSTC_LOG_LINES1 の場合、深さに基づいてログ行をインデントします。
RUSTC_LOG_FORMAT_JSON1 の場合、ログを JSON として出力します。正確なパラメーターは rustc_log/src/lib.rs にありますが、形式は不安定です。
RUSTC_LOG_OUTPUT_TARGET指定された場合、ログは stderr ではなく指定されたファイル名に出力されます。

関数レベルのフィルター

rustc の多くの関数には、次のような注釈が付けられています。

#[instrument(level = "debug", skip(self))]
fn foo(&self, bar: Type) {}

これにより、次のように使用できます。

RUSTC_LOG=[foo]

これにより、次のことをすべて一度に行えます。

  • foo へのすべての関数呼び出しをログに記録する
  • 引数をログに記録する(skip リスト内のものを除く)
  • 関数が戻るまで、(コンパイラ内の他の場所からのものを含めて)すべてをログに記録する

すべては不要な場合

関数のスコープによっては、その本体内のすべてをログに記録したくない場合があります。 例として、do_mir_borrowck 関数は、借用チェックされる些細なコードに対しても数百行をダンプします。

すべてのフィルターを組み合わせることができるため、たとえば次のように crate/module パスを追加できます。

RUSTC_LOG=rustc_borrowck[do_mir_borrowck]

すべての呼び出しは不要な場合

libcore をコンパイルしている場合、すべての borrowck ダンプはおそらく不要で、特定の関数に対するものだけが必要でしょう。 関数呼び出しは、引数を正規表現で指定することでフィルタリングできます。

RUSTC_LOG=[do_mir_borrowck{id=\.\*from_utf8_unchecked\.\*}]

これにより、from_utf8_unchecked を借用チェックするログだけが得られます。 無視された do_mir_borrowck ごとに短いメッセージは引き続き表示されますが、それらの呼び出し内の内容は表示されないことに注意してください。 これは、発生している呼び出しを調べるのに役立ち、正規表現を入力し間違えた場合に調整する助けになります。

クエリレベルのフィルター

すべてのクエリには、自動的にロギングスパンがタグ付けされるため、 クエリの実行中のすべてのログメッセージを表示できます。 たとえば、型チェック中のすべてをログに記録したい場合は、次のようにします。

RUSTC_LOG=[typeck]

クエリ引数は tracing フィールドとして含まれます。つまり、引数のデバッグ表示に対してフィルタリングできます。 たとえば、typeck クエリには、何がチェックされているかを示す引数 key: LocalDefId があります。 その LocalDefId に一致する正規表現を使うことで、特定の関数に対する型チェックをログに記録できます。

RUSTC_LOG=[typeck{key=.*name_of_item.*}]

クエリによって引数は異なります。 クエリとその引数の一覧は、rustc_middle/src/queries.rs にあります。

広範なモジュールレベルのフィルター

log crate のフィルターに似たフィルターを使うこともでき、特定のモジュール内のすべてを有効化できます。 これはしばしば冗長すぎ、構造化も不十分になるため、関数レベルのフィルターを使うことを推奨します。

ログフィルターは、すべての debug! 出力以上(たとえば info! も含まれます)を得るために単に debug とすることもできますし、特定のモジュールからのすべての出力(trace! も含まれます)を得るために path::to::module とすることもできます。また、特定のモジュールからの debug! 出力以上を得るために path::to::module=debug とすることもできます。

たとえば、特定のモジュールについて debug! 出力以上を得るには、RUSTC_LOG=path::to::module=debug rustc my-file.rs でコンパイラを実行できます。 すると、すべての debug! 出力が標準エラー出力に表示されます。

部分的なパスを使ってもフィルターは機能することに注意してください。 たとえば、rustdoc::passes::collect_intra_doc_links からの info! 出力だけを見たい場合は、 RUSTDOC_LOG=rustdoc::passes::collect_intra_doc_links=info を使うことも、RUSTDOC_LOG=rustdoc::passes::collect_intra=info を使うこともできます。

rustdoc を開発している場合は、代わりに RUSTDOC_LOG を使ってください。 Miri を開発している場合は、代わりに MIRI_LOG を使ってください。 要領はお分かりでしょう :)

使用できる完全な構文については、tracing crate のドキュメント、特に debug! のドキュメントを参照してください。 (注: コンパイラとは異なり、tracing crate とその例では RUSTC_LOG 環境変数を使用します。 rustc、rustdoc、およびその他のツールはカスタム環境変数を設定します。)

非常に厳密なフィルターを使わない限り、ロガーは大量の出力を生成するため、可能な限り最も具体的なモジュール(複数ある場合はカンマ区切り)を使用してください。 通常は、標準エラー出力をファイルにパイプし、テキストエディターでログ出力を見るのがよいでしょう。

では、まとめると次のようになります。

# これは `rustc_middle/src/traits` 内のすべての debug 呼び出しの出力を
# 標準エラーに送ります。コンソールのスクロールバックを埋め尽くす可能性があります。
$ RUSTC_LOG=rustc_middle::traits=debug rustc +stage1 my-file.rs

# これは `rustc_middle/src/traits` 内のすべての debug 呼び出しの出力を
# `traits-log` に送るので、あとでテキストエディターで確認できます。
$ RUSTC_LOG=rustc_middle::traits=debug rustc +stage1 my-file.rs 2>traits-log

# 推奨しません!これは Rust コンパイラ内のすべての `debug!` 呼び出しの
# 出力を表示します。その数は*非常に多い*ため、何かを見つけるのは
# 難しくなります。
$ RUSTC_LOG=debug rustc +stage1 my-file.rs 2>all-log

# これは `rustc_codegen_ssa` 内のすべての `info!` 呼び出しの出力を表示します。
#
# `codegen_instance` には、codegen されたすべての関数を出力する
# `info!` 文があります。これは、どの関数が LLVM アサーションを
# 引き起こすかを調べるのに役立ちます。また、これは `debug!` ログではなく
# `info!` ログなので、公式コンパイラでも動作します。
$ RUSTC_LOG=rustc_codegen_ssa=info rustc +stage1 my-file.rs

# これは `rustc_codegen_ssa` と `rustc_resolve` 内のすべてのログを表示します。
$ RUSTC_LOG=rustc_codegen_ssa,rustc_resolve rustc +stage1 my-file.rs

# これは rustdoc またはそれが呼び出すすべての rustc ライブラリによる
# すべての `info!` 呼び出しの出力を表示します。
$ RUSTDOC_LOG=info rustdoc +stage1 my-file.rs

# これは rustdoc が直接行った `debug!` 呼び出しのみを表示し、
# `rustc*` クレートによるものは表示しません。
$ RUSTDOC_LOG=rustdoc=debug rustdoc +stage1 my-file.rs

ログの色

デフォルトでは、rustc(および rustdoc や Miri などの他のツール)は、ログ出力で ANSI カラーをいつ使うべきかを賢く判断します。 端末に出力している場合は色を使い、ファイルに出力している場合やどこか別の場所へパイプしている場合は色を使いません。 しかし、非常に厳密なフィルターを設定していない限り、端末でログ出力を読むのは難しいため、less のようなページャーに出力をパイプしたくなることがあります。 しかし、そうすると色がなくなってしまい、探しているものを見つけにくくなります!

ログ出力で色を使うかどうかは、RUSTC_LOG_COLOR 環境変数(rustdoc の場合は RUSTDOC_LOG_COLOR、Miri の場合は MIRI_LOG_COLOR など)で上書きできます。選択肢は 3 つあります: auto(デフォルト)、alwaysnever です。 したがって、less にパイプするときに色を有効にしたい場合は、次のようなコマンドを使います:

# `-R` スイッチは、ANSI カラーをエスケープせずに表示するよう less に指示します。
$ RUSTC_LOG=debug RUSTC_LOG_COLOR=always rustc +stage1 ... | less -R

MIRI_LOG_COLOR は Miri から来るログにのみ色を付け、Miri が呼び出す rustc 関数からのログには色を付けないことに注意してください。 rustc からのログに色を付けるには RUSTC_LOG_COLOR を使ってください。

生成されるバイナリから debug!trace! の呼び出しを保持または削除する方法

error!warn!info! の呼び出しはコンパイラのすべてのビルドに含まれますが、 debug!trace! の呼び出しは、bootstrap.toml で rust.debug-logging=true が有効になっている場合にのみプログラムに含まれます(デフォルトでは無効です)。そのため、DEBUG ログが表示されない場合、特に RUSTC_LOG=rustc rustc some.rs でコンパイラを実行して INFO ログしか表示されない場合は、bootstrap.toml で rust.debug-logging=true が有効になっていることを確認してください。

ロギングのエチケットと慣習

debug! の呼び出しはデフォルトで削除されるため、ほとんどの場合、「不要な」debug! 呼び出しを追加してコミットするコードに残しておくことによる性能への影響を心配する必要はありません。それらが出荷するものの性能を低下させることはありません。

とはいえ、特に近くにある他の呼び出しやここから呼ばれる関数内の呼び出しと重複している場合は、トレース呼び出しが過剰になることもあります。 ここで達成すべき完璧なバランスはなく、debug! 文を残すことを許可するか、マージ前に削除するよう求めるかは、レビュアーの裁量に委ねられます。

非常に冗長なログには、debug! よりも trace! を使う方が望ましい場合があります。

大まかに守られている慣習として、関数 foo の先頭では debug!("foo(...)") よりも #[instrument(level = "debug")]属性のドキュメントも参照) を使います。 関数内では、debug!("xyz = {:?}", variable.field) よりも debug!(?variable.field) を、debug!("bar = {:?}", var.method(arg)) よりも debug!(bar = ?var.method(arg)) を好んで使ってください。 この構文のドキュメントはこちらにあります。

注意すべきことの 1 つは、ログ内のコストの高い操作です。

モジュール rustc::foo 内に次の文があるとします。

debug!(x = ?random_operation(tcx));

この場合、誰かが RUSTC_LOG=rustc::foo でデバッグ版の rustc を実行すると、 random_operation() が実行されます。 この debug 文を有効にしない RUSTC_LOG フィルターでは、random_operation は実行されません。

つまり、コストが高すぎるものやクラッシュしそうなものをそこに置くべきではありません。それは、そのモジュールのロギングを使いたい人を困らせることになります。 誰かが別のバグを見つけるためにロギングを使おうとするまで、誰にも分かりません。

コンパイラのプロファイリング

このセクションでは、コンパイラをプロファイルし、どこで時間を費やしているかを調べる方法について説明します。

何を計測したいかに応じて、いくつかの異なるアプローチがあります。

  • PR がコンパイラのパフォーマンスを改善するか、低下させるかを確認したい場合は、 ベンチマーク実行を依頼する方法について rustc-perf の章を参照してください。

  • rustc がどこで時間を費やしているかについて中〜高レベルの概要を把握したい場合:

    • -Z self-profile フラグと measureme ツールは、プロファイリングへのクエリベースのアプローチを提供します。 詳細については、それらのドキュメントを参照してください。
  • 関数レベルのパフォーマンスデータ、または単に上記のアプローチよりも詳しい情報が欲しい場合:

    • perf のようなネイティブコードプロファイラの使用を検討してください
    • または、ナノ秒精度でフル機能のグラフィカルインターフェイスを備えた tracy を使用してください。
  • クレートグラフのコンパイル時間を見やすく視覚的に表示したい場合は、 cargo の --timings フラグを使用できます。 例: cargo build --timingsCARGOFLAGS="--timings" ./x build を使って、このフラグをコンパイラ自体に対して使用できます

  • メモリ使用量をプロファイルしたい場合は、使用しているオペレーティングシステムに応じて さまざまなツールを使用できます。

    • Windows の場合は、WPA ガイドを読んでください。

cargo-llvm-lines による rustc のブートストラップ時間の最適化

cargo-llvm-lines を使用すると、ジェネリック関数の すべてのインスタンス化にわたる LLVM IR の行数を数えることができます。 rustc のコンパイル時間の大部分は LLVM で費やされるため、LLVM に渡すコード量を 減らすことで rustc のコンパイルが速くなる、という考え方です。

やや独自の rustc ビルドプロセスと一緒に cargo-llvm-lines を使用するには、必要な LLVM IR を取得するために -C save-temps を使用できます。 このオプションは、コンパイル中に作成された一時的な作業生成物を保持します。 その中には、最適化パイプラインへの入力を表す LLVM IR が含まれており、 私たちの目的に理想的です。 これは LLVM ビットコード形式で *.no-opt.bc 拡張子のファイルに保存されます。

使用例:

cargo install cargo-llvm-lines
# 通常のクレートではここで `cargo llvm-lines` を実行できますが、`x` は普通ではありません :P

# 前回の実行結果が混ざらないように、毎回実行前にクリーンします。
./x clean
env RUSTFLAGS=-Csave-temps ./x build --stage 0 compiler/rustc

# 単一クレート、例: rustc_middle。(シェルの glob サポートに依存します。)
# 未最適化の LLVM ビットコードを、cargo-llvm-lines が受け付ける人間が読める LLVM アセンブリに変換します。
for f in build/x86_64-unknown-linux-gnu/stage0-rustc/x86_64-unknown-linux-gnu/release/deps/rustc_middle-*.no-opt.bc; do
  ./build/x86_64-unknown-linux-gnu/llvm/bin/llvm-dis "$f"
done
cargo llvm-lines --files ./build/x86_64-unknown-linux-gnu/stage0-rustc/x86_64-unknown-linux-gnu/release/deps/rustc_middle-*.ll > llvm-lines-middle.txt

# コンパイラのすべてのクレートを指定します。
for f in build/x86_64-unknown-linux-gnu/stage0-rustc/x86_64-unknown-linux-gnu/release/deps/*.no-opt.bc; do
  ./build/x86_64-unknown-linux-gnu/llvm/bin/llvm-dis "$f"
done
cargo llvm-lines --files ./build/x86_64-unknown-linux-gnu/stage0-rustc/x86_64-unknown-linux-gnu/release/deps/*.ll > llvm-lines.txt

コンパイラでの出力例:

  Lines            Copies          Function name
  -----            ------          -------------
  45207720 (100%)  1583774 (100%)  (TOTAL)
   2102350 (4.7%)   146650 (9.3%)  core::ptr::drop_in_place
    615080 (1.4%)     8392 (0.5%)  std::thread::local::LocalKey<T>::try_with
    594296 (1.3%)     1780 (0.1%)  hashbrown::raw::RawTable<T>::rehash_in_place
    592071 (1.3%)     9691 (0.6%)  core::option::Option<T>::map
    528172 (1.2%)     5741 (0.4%)  core::alloc::layout::Layout::array
    466854 (1.0%)     8863 (0.6%)  core::ptr::swap_nonoverlapping_one
    412736 (0.9%)     1780 (0.1%)  hashbrown::raw::RawTable<T>::resize
    367776 (0.8%)     2554 (0.2%)  alloc::raw_vec::RawVec<T,A>::grow_amortized
    367507 (0.8%)      643 (0.0%)  rustc_query_system::dep_graph::graph::DepGraph<K>::with_task_impl
    355882 (0.8%)     6332 (0.4%)  alloc::alloc::box_free
    354556 (0.8%)    14213 (0.9%)  core::ptr::write
    354361 (0.8%)     3590 (0.2%)  core::iter::traits::iterator::Iterator::fold
    347761 (0.8%)     3873 (0.2%)  rustc_middle::ty::context::tls::set_tlv
    337534 (0.7%)     2377 (0.2%)  alloc::raw_vec::RawVec<T,A>::allocate_in
    331690 (0.7%)     3192 (0.2%)  hashbrown::raw::RawTable<T>::find
    328756 (0.7%)     3978 (0.3%)  rustc_middle::ty::context::tls::with_context_opt
    326903 (0.7%)      642 (0.0%)  rustc_query_system::query::plumbing::try_execute_query

これはインクリメンタルコンパイルや ./x check では動作しないようなので、 rustc を 何度も コンパイルすることになります。 耐えられるようにするため、bootstrap.toml でいくつかの設定を変更することをお勧めします:

# 私のマシンではデバッグビルドの時間は _3 分の 1_ ですが、
# stage0 rustc より先をコンパイルすると耐えがたいほど遅くなります。
rust.optimize = false

# どうせ incremental は使えないので、少し速度を上げるために無効化します。
rust.incremental = false
# 実行はしないので、デバッグチェックをコンパイルしても意味がありません。
rust.debug = false

# 単一の codegen unit を使用すると出力は少なくなりますが、コンパイルは遅くなります。
rust.codegen-units = 0  # num_cpus

llvm-lines の出力はいくつかのオプションの影響を受けます。 rust.optimize = false にすると 2.1GB から 3.5GB に増え、codegen-units = 0 にすると 4.1GB になります。

MIR 最適化の影響は小さいです。 デフォルトの RUSTFLAGS="-Z mir-opt-level=1" と比較すると、レベル 0 では 0.3GB 増え、レベル 2 では 0.2GB 減ります。

2022 年 7 月時点では、

インライン化は LLVM と GCC のコード生成バックエンドで行われており、 Cranelift のものだけが欠けています。

perf を使ったプロファイリング

これは、perf を使って rustc をプロファイリングする方法のガイドです。

初期手順

  • rust-lang/rust のクリーンなチェックアウトを取得する
  • bootstrap.toml に次の設定を行う:
    • rust.debuginfo-level = 1 - 行デバッグ情報を有効にする
    • rust.jemalloc = false - valgrind を使ったメモリ使用量のプロファイリングを可能にする
    • それ以外はすべてデフォルトのままにする
  • ./x build を実行して完全なビルドを取得する
  • その結果を指す rustup ツールチェーンを作成する

perf プロファイルの収集

perf は Linux 上の優れたツールであり、あらゆる種類の情報を収集して 分析するために使用できます。主に、プログラムがどこで時間を費やしているかを 把握するために使われます。ただし、キャッシュミスなど、他の種類のイベントにも 使用できます。

基本

基本的な perf コマンドは次のとおりです:

perf record -F99 --call-graph dwarf XXX

-F99 は、perf に 99 Hz でサンプリングするよう指示します。これにより、 長い実行で大量のデータが生成されるのを避けられます(なぜ 99 Hz なのか、ですか? 他の周期的な活動と同期しにくいため、よく選ばれます)。--call-graph dwarf は、 正確なデバッグ情報からコールグラフ情報を取得するよう perf に指示します。XXX は プロファイリングしたいコマンドです。したがって、たとえば次のようにできます:

perf record -F99 --call-graph dwarf cargo +<toolchain> rustc

これは cargo を実行します – ここで <toolchain> は、最初に作成したツールチェーンの 名前である必要があります。ただし、注意すべき点がいくつかあります:

  • 依存関係のビルドに費やされる時間は、おそらくプロファイリングしたくないでしょう。 そのため、cargo build; cargo clean -p $C のようなものが役立つ場合があります (ここで $C は crate 名です)
    • ただし、通常は代わりに touch src/lib.rs して再ビルドするだけです。=)
  • インクリメンタルビルドがプロファイルに干渉するのは、おそらく望ましくないでしょう。 そのため、CARGO_INCREMENTAL=0 のようなものが役立ちます。

cargo から収集したデータを読み取るときに addr2line xxx/elf: could not read first record の問題を回避するには、 最新版の addr2line を使用する必要があるかもしれません:

cargo install addr2line --features="bin"

perf.rust-lang.org のテストから perf プロファイルを収集する

多くの場合、perf.rust-lang.org の特定のテストを分析したいことがあります。 それを行う最も簡単な方法は、rustc-perf ベンチマークスイートを 使用することです。この方法はこちらで説明されています。

ベンチマークスイートの CLI を使用する代わりに、ベンチマークを手動でプロファイリングすることもできます。まず、 rustc-perf リポジトリをクローンする必要があります:

$ git clone https://github.com/rust-lang/rustc-perf

次に、プロファイリングしたいテストのソースコードを見つけます。テストのソースは collector/compile-benchmarks ディレクトリcollector/runtime-benchmarks ディレクトリにあります。それでは、 特定のテストのディレクトリに移動しましょう。ここでは clap-rs を例として使用します:

cd collector/compile-benchmarks/clap-3.1.6

この場合、cargo check のパフォーマンスをプロファイリングしたいとします。 その場合、私はまず依存関係をビルドするためにいくつかの基本的なコマンドを実行します:

# セットアップ: まず古い結果をすべて削除し、依存関係をビルドします:
cargo +<toolchain> clean
CARGO_INCREMENTAL=0 cargo +<toolchain> check

(繰り返しになりますが、<toolchain> は最初の手順で作成したツールチェーンの名前に 置き換える必要があります。)

次に、cargo check を実行して、ちょうど clap-rs crate の実行時間を記録したいとします。 私はこれに cargo rustc を使うことが多いです。後で行うように、明示的なフラグを追加することも できるためです。

touch src/lib.rs
CARGO_INCREMENTAL=0 perf record -F99 --call-graph dwarf cargo rustc --profile check --lib

最後のコマンドに注意してください。これはかなり大がかりです!これは cargo rustc コマンドを使用しており、追加のオプションを付けて(可能性として)rustc を実行します。 --profile check--lib オプションは、cargo check の実行を行っていること、 そしてこれがライブラリ(バイナリではない)であることを指定します。

この時点で、perf ツールを使用して結果を分析できます。たとえば:

perf report

これは対話型の TUI プログラムを開きます。単純なケースでは、これが役立つことがあります。 より詳細な調査には、perf-focus ツールが役立つ場合があります。これについては後述します。

注意事項。 rustc-perf の各テストは、それぞれ独自の特殊ケースです。 特に、その一部はライブラリではありません。その場合は touch src/main.rs を行い、 --lib を渡さないようにする必要があります。正直なところ、どのテストがどちらなのかを 見分ける最善の方法はよく分かりません。

NLL データの収集

NLL の実行をプロファイリングしたい場合は、次のように cargo rustc コマンドに 追加のオプションを渡すだけです:

touch src/lib.rs
CARGO_INCREMENTAL=0 perf record -F99 --call-graph dwarf cargo rustc --profile check --lib -- -Z borrowck=mir

perf focus による perf プロファイルの分析

perf プロファイルを収集したら、それに関する情報を取得したいところです。 このために、私は個人的に perf focus を使用しています。これはシンプルながら便利な ツールの一種で、次のようなクエリに答えることができます:

  • 「関数 F でどれだけの時間が費やされたか」(どこから呼び出されたかに関係なく)
  • 「関数 G から呼び出されたとき、関数 F でどれだけの時間が費やされたか」
  • 「関数 G で費やされた時間を除外して、関数 F でどれだけの時間が費やされたか」
  • 「F はどの関数を呼び出し、それらでどれだけの時間を費やしているか」

これがどのように機能するかを理解するには、perf について少しだけ知っておく必要があります。 基本的に、perf は定期的に(または何らかのイベントが発生したときに)プロセスを サンプリングすることで動作します。各サンプルについて、perf はバックトレースを収集します。 perf focus を使うと、そのバックトレースにどの関数が現れるかをテストする正規表現を 書くことができ、その正規表現に一致するバックトレースを持つサンプルが何パーセントあったかを 教えてくれます。おそらく、私が NLL パフォーマンスをどのように分析するかを順を追って説明するのが 最も分かりやすいでしょう。

perf-focus のインストール

cargo install を使って perf-focus をインストールできます:

cargo install perf-focus

例: MIR borrowck ではどれだけの時間が費やされているか?

あるテストについて NLL データを収集したとします。MIR 借用チェッカーで どれだけの時間が費やされているかを知りたいとします。MIR borrowck の「メイン」 関数は do_mir_borrowck と呼ばれているため、次のコマンドを実行できます:

$ perf focus '{do_mir_borrowck}'
Matcher    : {do_mir_borrowck}
Matches    : 228
Not Matches: 542
Percentage : 29%

'{do_mir_borrowck}' 引数は matcher と呼ばれます。これは、バックトレースに適用されるテストを指定します。この場合、{X} は、正規表現 X を満たす関数がバックトレース上に 何らか 存在しなければならないことを示します。この場合、その正規表現は、欲しい関数の名前そのものです(実際には、名前の一部です。完全な名前には、モジュールパスなど、ほかにも多くのものが含まれます)。このモードでは、perf-focus は、do_mir_borrowck がスタック上にあったサンプルの割合だけを出力します。この場合は 29% です。

c++filt に関する注意。 perf からデータを取得するために、perf focus は現在 perf script を実行しています(もっと良い方法があるかもしれません…)。ときどき、perf script が C++ のマングルされた名前を出力することがあります。これは厄介です。自分で perf script | head を実行すれば分かります。rustc::middle ではなく 5rustc6middle のような名前が見える場合、同じ問題が起きています。これは次のようにして解決できます。

perf script | c++filt | perf focus --from-stdin ...

これにより、perf script からの出力が c++filt にパイプされ、それらの名前の大半がより扱いやすい形式に変換されるはずです。perf focus--from-stdin フラグは、perf focus を実行するのではなく、stdin からデータを取得するように指示します。これはもっと便利にすべきです(最悪の場合でも、perf focusc++filt オプションを追加するか、単に常にそれを使うようにするかもしれません。かなり無害です)。

例: MIR borrowck は trait の解決にどれくらいの時間を費やしているか?

MIR borrowck が trait checker でどれくらいの時間を費やしているかを知りたいかもしれません。より複雑な正規表現を使って、これを問い合わせることができます。

$ perf focus '{do_mir_borrowck}..{^rustc::traits}'
Matcher    : {do_mir_borrowck},..{^rustc::traits}
Matches    : 12
Not Matches: 1311
Percentage : 0%

ここでは、.. 演算子を使って「スタック上に do_mir_borrowck があり、その後ろに、名前が rustc::traits で始まる何らかの関数がある頻度はどれくらいか?」を尋ねています(基本的には、そのモジュール内のコードです)。結果として、答えは「ほとんどない」です。その記述に当てはまるサンプルは 12 個だけです(サンプルが まったく 見つからない場合、多くの場合、クエリがおかしくなっていることを示します)。

興味があれば、--print-match オプションを使うことで、どのサンプルなのかを正確に調べることができます。これにより、各サンプルの完全なバックトレースが出力されます。行の先頭にある | は、正規表現がマッチした部分を示します。

例: MIR borrowck はどこで時間を費やしているか?

多くの場合、より「探索的」なクエリを実行したくなります。たとえば、MIR borrowck が時間の 29% を占めていることは分かっていますが、その時間はどこで費やされているのでしょうか?そのためには、--tree-callees オプションが最適なツールであることが多いです。通常は --tree-min-percent または --tree-max-depth も指定したくなります。結果は次のようになります。

$ perf focus '{do_mir_borrowck}' --tree-callees --tree-min-percent 3
Matcher    : {do_mir_borrowck}
Matches    : 577
Not Matches: 746
Percentage : 43%

Tree
| matched `{do_mir_borrowck}` (43% total, 0% self)
: | rustc_borrowck::nll::compute_regions (20% total, 0% self)
: : | rustc_borrowck::nll::type_check::type_check_internal (13% total, 0% self)
: : : | core::ops::function::FnOnce::call_once (5% total, 0% self)
: : : : | rustc_borrowck::nll::type_check::liveness::generate (5% total, 3% self)
: : : | <rustc_borrowck::nll::type_check::TypeVerifier<'a, 'b, 'tcx> as rustc::mir::visit::Visitor<'tcx>>::visit_mir (3% total, 0% self)
: | rustc::mir::visit::Visitor::visit_mir (8% total, 6% self)
: | <rustc_borrowck::MirBorrowckCtxt<'cx, 'tcx> as rustc_mir_dataflow::DataflowResultsConsumer<'cx, 'tcx>>::visit_statement_entry (5% total, 0% self)
: | rustc_mir_dataflow::do_dataflow (3% total, 0% self)

--tree-callees で起きることは次のとおりです。

  • 正規表現にマッチする各サンプルを見つける
  • 正規表現のマッチの に現れるコードを見て、コールツリーを構築しようとする

--tree-min-percent 3 オプションは、「時間の 3% より多くを占めるものだけを表示してほしい」という意味です。これがないと、ツリーはしばしば非常にノイズが多くなり、malloc の内部のようなランダムなものが含まれます。--tree-max-depth も便利な場合があります。これは、出力する階層数を制限するだけです。

各行について、その関数全体での時間の割合(“total”)と、その関数だけに費やされ、その関数の何らかの callee には費やされていない時間の割合(self)を表示します。通常は “total” のほうが興味深い数値ですが、常にそうとは限りません。

相対的な割合

デフォルトでは、perf-focus におけるすべての割合は プログラム全体の実行 に対する相対値です。これは視点を保つのに役立ちます。多くの場合、ホットスポットを見つけるために掘り下げていくと、プログラム全体の実行という観点では、この「ホットスポット」は実際には重要ではないという事実を見失うことがあります。また、異なるクエリ間の割合を互いに簡単に比較できることも保証されます。

とはいえ、相対的な割合を取得すると便利な場合もあるため、perf focus--relative オプションを提供しています。この場合、割合は、すべてのサンプルではなく、マッチしたサンプルだけについて表示されます。したがって、たとえば次のようにして、borrowck 自体に対する相対的な割合を取得できます。

$ perf focus '{do_mir_borrowck}' --tree-callees --relative --tree-max-depth 1 --tree-min-percent 5
Matcher    : {do_mir_borrowck}
Matches    : 577
Not Matches: 746
Percentage : 100%

Tree
| matched `{do_mir_borrowck}` (100% total, 0% self)
: | rustc_borrowck::nll::compute_regions (47% total, 0% self) [...]
: | rustc::mir::visit::Visitor::visit_mir (19% total, 15% self) [...]
: | <rustc_borrowck::MirBorrowckCtxt<'cx, 'tcx> as rustc_mir_dataflow::DataflowResultsConsumer<'cx, 'tcx>>::visit_statement_entry (13% total, 0% self) [...]
: | rustc_mir_dataflow::do_dataflow (8% total, 1% self) [...]

ここでは、compute_regions が “47% total” として現れていることが分かります。これは、do_mir_borrowck の 47% がその関数で費やされていることを意味します。以前は 20% でした。これは、do_mir_borrowck 自体が合計時間の 43% にすぎないためです(そして .47 * .43 = .20)。

Windows でのプロファイリング

WPR と WPA の紹介

高レベルのパフォーマンス分析(メモリ使用量を含む)は、Windows Performance Recorder (WPR) と Windows Performance Analyzer (WPA) を使って実行できます。 名前が示すとおり、WPR はシステム統計情報(イベント トレース ログ、別名 ETL ファイルの形式)を記録するためのものであり、WPA はこれらの ETL ファイルを分析するためのものです。

WPR はシステム全体の統計情報を収集するため、rustc に関連するものだけでなく、 そのマシン上で実行されている他のすべてのものも記録します。 分析中に、関心のあるものだけにフィルタリングできます。

これらのツールは非常に強力ですが、Rust コンパイラをうまくプロファイリングできるようになるには、 少し学習も必要です。

ここでは、Rust コンパイラを分析するために WPR と WPA を使用する方法を見ていくとともに、 rustc の分析を容易にするよう特別に設計された有用な「プロファイル」(つまり、WPR と WPA のデフォルトを調整する設定ファイル)へのリンクも提供します。

WPR と WPA のインストール

WPR と WPA は、Windows Assessment and Deployment Kit (ADK) のダウンロード時に選択できる Windows Performance Toolkit の一部としてインストールできます。 ADK インストーラーは こちらからダウンロードできます。 Windows Performance Toolkit を必ず選択してください(他には何も選択する必要はありません)。

記録

システム分析を実行するには、まず WPR でシステムを記録する必要があります。 WPR を開き、ウィンドウの下部で記録したいものの「プロファイル」を選択します。 rustc のブートストラップ プロセスのメモリ使用量を調べる場合は、以下の項目を選択します。

  • CPU 使用率
  • VirtualAlloc 使用状況

「Heap usage」も記録したくなるかもしれませんが、これはヒープ割り当てを 1 つ残らず記録するため、 非常に、非常に高コストになる可能性があります。 高レベルの分析では、これはオフのままにしておくのが最善かもしれません。

次に、記録するためのセットアップを準備する必要があります。 メモリ使用量の分析では、デバッグ シンボル付きの stage 1 コンパイラ ビルドを使って stage 2 コンパイラ ビルドを記録するのが最善です。 rustc のビルドに使用しているコンパイラにシンボルがあると、WPA が Rust のシンボルを正しく解決できるため、 分析に大いに役立ちます。 残念ながら、stage 0 コンパイラではシンボルが有効になっていないため、 stage 1 コンパイラをビルドし、 続いて stage 2 コンパイラを自分でビルドする必要があります。

これを行うには、bootstrap.toml ファイルで rust.debuginfo-level = 1 を設定していることを確認してください。 これにより、ブートストラップ時にスタック フレームを含むデバッグ情報を生成するよう rustc に指示します。

これで stage 1 コンパイラをビルドできます: x build --stage 1 -i library、または stage 1 コンパイラをビルドしたい他の任意の方法でビルドしてください。

stage 1 コンパイラがビルドできたので、stage 2 ビルドを記録できます。 WPR に戻り、 「start」ボタンをクリックして stage 2 コンパイラをビルドします(例: x build --stage=2 -i library)。 このプロセスが完了したら、記録を停止します。

Save ボタンをクリックし、そのプロセスが完了したら、表示される「Open in WPA」ボタンをクリックします。

注: トレース ファイルはかなり大きいため、WPA がファイルを開き終えるまでに少し時間がかかることがあります。

分析

ETL ファイルが WPA で開かれたので、結果を分析できます。 まず、rustc のブートストラップ分析に適した状態に WPA を設定する、 事前作成済みの「プロファイル」を適用します。 プロファイルはこちらからダウンロードしてください。 上部の「Profiles」メニューを選択し、次に「apply」を選択して、ダウンロードしたプロファイルを選択します。

次のようなものが表示されるはずです。

プロファイルが適用された WPA

次に、Rust のスタック トレースを適切にデマングルできるように、デバッグ シンボルを読み込んで処理するよう WPA に指示する必要があります。 これを行うには、「Trace」をクリックしてから「Load Symbols」を選択します。 この手順には時間がかかることがあります。

WPA が rustc のシンボルを読み込んだら、rustc.exe ノードを展開し、割り当てが最も大きいスタックを掘り下げていけます。

そのためには、「Commit Stack」列の [Root] ノードを展開し、興味深いスタック フレームが見つかるまで 展開を続けます。

ヒント: 展開したいノードを選択した後、右矢印キーを押します。これによりそのノードが展開され、 展開された集合内で次に大きいノードに選択が移動します。 興味深いフレームに到達するまで、右矢印キーを押し続けることができます。

展開されたスタックを表示した WPA

このサンプルでは、このプロファイル全体を通して、codegen 経由の呼び出しが合計で約 30GB のメモリを割り当てていることがわかります。

その他の分析タブ

このプロファイルには、役立つ可能性のある他のタブもいくつか含まれています。

  • System Configuration
    • キャプチャが記録されたシステムに関する一般情報。
  • rustc Build Processes
    • rustc.exe、cargo.exe、link.exe などの関連プロセスのフラットな一覧。
    • 各プロセスには、そのコマンドライン引数が表示されます。
    • 特定の rustc プロセスが何に取り組んでいたかを把握するのに役立ちます。
  • rustc Build Process Tree
    • プロセスがいつ開始し終了したかを示すタイムライン。
  • rustc CPU Analysis
    • rustc のホットスポットを表示するように事前設定されたチャートが含まれています。
    • これらのチャートは、rustc がどこで時間を費やしているかを分析することを支援するよう設計されています。
  • rustc Memory Analysis
    • rustc がどこでメモリを割り当てているかを表示するように事前設定されたチャートが含まれています。

rustc-perf によるプロファイリング

Rust benchmark suite は、Rust コンパイラをプロファイリングおよびベンチマークするための包括的な方法を提供します。 このスイートの使用方法については、そのマニュアルに手順があります。

ただし、このスイートを手動で使用するのは少し面倒な場合があります。 rustc コントリビューターがこれをより簡単に使えるようにするため、 コンパイラビルドシステム(bootstrap)もベンチマークスイートとの組み込み連携を提供しており、 スイートのダウンロードとビルド、ローカルのコンパイラツールチェーンのビルドを行い、簡略化されたコマンドラインインターフェイスを使用してそれをプロファイリングできるようにします。

この連携を使用するには、./x perf <command> [options] コマンドを使用できます。

このコマンドには、たとえば --stage 1--stage 2 などの通常の bootstrap フラグを使用して、作成される sysroot のステージを変更できます。また、プロファイリングをより適切にサポートするために bootstrap.toml を設定すると役立つ場合があります。たとえば、ビルドされたコンパイラにソース行情報を追加するには rust.debuginfo-level = 1 を設定します。

x perf は現在、次のコマンドをサポートしています。

  • benchmark <id>: コンパイラのベンチマークを実行し、結果を渡された id の下に保存します。
  • compare <baseline> <modified>: 渡された 2 つの id を持つ 2 つのコンパイラのベンチマーク結果を比較します。
  • eprintln: コンパイラを実行し、その stderr 出力をキャプチャするだけです。 コンパイラは通常、stderr に何も出力しないことに注意してください。 そのため、何らかの出力を得るには eprintln! 呼び出しをいくつか追加するとよいかもしれません。
  • samply: samply サンプリングプロファイラを使用してコンパイラをプロファイリングします。
  • cachegrind: Cachegrind を使用して、コンパイラ実行の詳細なシミュレーショントレースを生成します。

プロファイラのより詳細な説明は、rustc-perf マニュアルにあります。

x perf コマンドには、次のオプションを使用できます。これらは、このスイートで使用できる profile_local コマンドおよび bench_local コマンドの対応するオプションを反映しています。

  • --include: プロファイリングまたはベンチマークすべきベンチマークを選択します。
  • --profiles: プロファイリングまたはベンチマークすべきプロファイル(Check, Debug, Opt, Doc)を選択します。
  • --scenarios: プロファイリングまたはベンチマークすべきシナリオ(Full, IncrFull, IncrPatched, IncrUnchanged)を選択します。

外部クレート向けプロファイリング差分の例

外部クレートに対して、コンパイラの 2 つのコミットのローカル差分を生成することに関心があるかもしれません。 まず、rustc-perf リポジトリで、Rust コンパイラのベンチマークを実行する collector を次のようにビルドします。

cargo build --release -p collector

その後、collector バイナリを指定して、cargo を使用して collector を実行できます。 これは次の引数を想定しています。

  • <PROFILE>: パフォーマンスをどのように測定すべきかを表すプロファイラの選択。 この例では、Cachegrind を使用します。
  • <RUSTC>: ベンチマーク対象の Rust コンパイラのリビジョンで、rust-lang/rust のコミット SHA として指定します。 任意引数により、上記のようにプロファイルとシナリオを実行できます。 必須引数および任意引数に関する詳細は、rustc-perf-readme-profilers にあります。

次に、rust-lang/rust リポジトリの 2 つのコミット <SHA1><SHA2> について、クレート serve_derive-1.0.136 のプロファイル差分を生成する場合は、 rustc-perf リポジトリで次を実行します。

cargo run --release --bin collector profile_local cachegrind +<SHA1> --rustc2 +<SHA2> --exact-match serde_derive-1.0.136 --profiles Check --scenarios IncrUnchanged

crates.io の依存関係

Rust コンパイラは、crates.io から取得した一部の依存関係を用いたビルドをサポートしています。

Rust Forge には、新しい依存関係を精査するための公式ポリシーがあります。

許可されている依存関係

tidy ツールには、許可されているクレートのリストがあります。 コンパイラにまだ含まれていない依存関係を追加するには、そのリストに追加する必要があります。

コントリビューション手順

バグ報告

バグは残念なものですが、ソフトウェアにおける現実です。 私たちは把握していないものを修正することはできないため、どうぞ積極的に報告してください。 何かがバグかどうか確信が持てない場合でも、遠慮なく issue を開いてください。

バグを公開で報告することが Rust ユーザーに対するセキュリティリスクになると考えられる場合は、 セキュリティ脆弱性を報告するための手順に従ってください

nightly チャンネルを使用している場合は、バグを報告する前に、そのバグが 最新のツールチェーンにも存在するか確認してください。 すでに修正されている可能性があります。

可能であれば、バグを報告する前に [search existing issues] を行ってください。 他の誰かがすでにそのエラーを報告している可能性があるためです。 これは常にうまくいくとは限らず、何を検索すればよいか分かりにくい場合もあるため、 追加で行うとよいこと程度に考えてください。 誤って重複した報告を作成してしまっても問題ありません。

同様に、そのバグに遭遇した他の人があなたの issue を見つけやすくするために、 そのバグに固有である可能性のある情報を含む、説明的なタイトルで issue を作成することを検討してください。 それは、使用している言語機能またはコンパイラ機能、 バグを引き起こす条件、またはエラーメッセージがある場合はその一部などです。 例としては、戻り位置の impl Trait のライフタイム推論における “impossible case reached” が考えられます。

issue を開くには、[このリンク][create an issue]をたどり、提供されている適切なテンプレート内の フィールドに記入するだけです。

バグ修正または「通常の」コード変更

ほとんどの PR では、特別な手順は必要ありません。 単に open a PR すれば、それがレビューされ、承認され、マージされます。 これには、ほとんどのバグ修正、リファクタリング、その他のユーザーから見えない変更が含まれます。 次のいくつかのセクションでは、このルールの例外について説明します。

また、WIP PR や GitHub の Draft PRs を開くことはまったく問題ないことにも注意してください。 進めながらフィードバックを得たり、 共同作業者とコードを共有したりするために、これを好む人もいます。 また、PR のビルドとテストに CI を利用できるようにするためにこれを行う人もいます(たとえば、遅いマシンで開発している場合)。

新機能

Rust には強力な後方互換性の保証があります。 したがって、新機能を stable Rust に直接実装することはできません。 代わりに、stable、beta、nightly という 3 つのリリースチャンネルがあります。 Rust のトレインリリースモデルの詳細については、The Rust Book を参照してください。

  • Stable: 一般的な利用向けの最新の安定版リリースです。
  • Beta: 次のリリースです(6 週間以内に stable になります)。
  • Nightly: リポジトリの main ブランチに従います。 これは、不安定な機能の利用が想定されている唯一のチャンネルであり、 その利用はオプトインの feature gate を通じて行われます。

詳細については、新機能の実装に関するこの章を参照してください。

破壊的変更

破壊的変更には、dev-guide 内に[専用のセクション][Breaking Changes]があります。

大規模な変更

コンパイラチームには、破壊的変更を引き起こすかどうかにかかわらず、大きな変更のための特別なプロセスがあります。 このプロセスは Major Change Proposal (MCP) と呼ばれます。 MCP は、コンパイラに対する大きな変更についてフィードバックを得るための、 比較的軽量な仕組みです(完全な RFC や、チームとの設計ミーティングとは対照的なものです)。

MCP が必要となる可能性のあるものの例には、大規模なリファクタリング、 重要な型への変更、コンパイラが何かを行う方法への重要な変更、 または小規模なユーザー向け変更が含まれます。

迷った場合は、on Zulip で質問してください。 最終的にマージされない PR に多くの労力を費やすのは 残念なことです! MCP の詳細については、このドキュメントを参照してください。

パフォーマンス

コンパイラのパフォーマンスは重要です。 私たちはここ数年、それを徐々に改善することに多くの労力を注いできました。

あなたの変更がパフォーマンスの低下(または改善)を引き起こす可能性があると考えられる場合は、 “perf run” をリクエストできます(また、レビュアーも承認前にこれをリクエストする場合があります)。 これは、あなたの変更を含むコンパイラで ベンチマーク群をコンパイルする、さらに別のボットです。 数値はこちらに報告され、 あなたの変更を最新の main と比較して確認できます。

rustc 開発にも役立つ、一般的な Rust コードのパフォーマンス入門については、 The Rust Performance Book を参照してください。

プルリクエスト

プルリクエスト(略して PR)は、Rust を変更するために私たちが使用する主要な仕組みです。 GitHub 自体にも、Pull Request 機能の使用方法に関する優れたドキュメントがあります。 私たちは “fork and pull” モデルを使用しています。 このモデルでは、コントリビューターが自分の個人用 fork に変更を push し、プルリクエストを作成して それらの変更をソースリポジトリに取り込みます。 Rust へのコントリビューション時に Git を使用する方法についてのがあります。

潜在的に大規模、複雑、横断的、かつ/または非常にドメイン固有な変更に関する助言

ローテーション中のコンパイラレビュアーは通常、それぞれがよく知っているコンパイラの領域を持っていますが、 あまり詳しくない領域もあります。あなたの PR に大規模、複雑、横断的、かつ/または非常にドメイン固有な変更が含まれている場合、 そのような PR のすべての変更を安心してレビューできる適切なレビュアーを見つけることは非常に困難になります。 これは、変更がコンパイラ固有のものだけでなく、標準ライブラリチームのような他チームのレビュアーの管轄に属する変更も含む場合にも当てはまります。 変更されたファイルに基づいて、関連するチームに通知し、特定のアラートを設定している人に ping する bot があります。

そのような変更を行う前に、提案する変更について事前にコンパイラチームと話し合うこと(および、その変更に承認が必要となる他のチームとも話し合うこと)を強く推奨します。また、コンパイラチームと協力して、潜在的にレビュー不能な大規模 PR を、個別にレビューしやすいより小さな PR の連続に分割できるかを検討してください。

提案する変更について話し合うには、Zulip に #t-compiler スレッドを作成してコンパイラチームと連絡を取ることができます。

事前にコンパイラチームと連絡を取ることは、いくつかの点で役立ちます。

  1. PR が適時にレビューされる可能性が高まります。
    • 実際の PR を開くに適切なレビュアーを特定する手助けをしたり、変更手続きを進めるためのアドバイザーやリエゾンを見つける手助けをしたり、必要に応じて try-job、perf 実行、crater 実行を行う手助けをしたりできます。
  2. コンパイラチームがあなたの変更を追跡しやすくなります。
  3. コンパイラチームは、変更の方向性がコンパイラチームの望む方向と一致しているかを確認するために、早い段階から頻繁にあなたの変更の感触を確認できます。
  4. コンパイラチームが受け入れる意思のない大規模な変更に多大な時間と労力を費やしてしまう状況や、変更がコンパイラチームの同意しない方向であることに非常に遅い段階で気付く状況を避けるのに役立ちます。

ブランチを最新に保つ

rust-lang/rust の CI は、あなたのパッチを、ブランチの基点となっているコミットではなく、現在の main に対して直接適用します。 これにより、明示的なマージコンフリクトがない場合でも、 ブランチが古くなっていると予期しない失敗につながることがあります。

ブランチを更新するのは必要な場合だけにしてください。つまり、マージコンフリクトがある場合、上流の CI が壊れていてグリーンな PR を妨げている場合、またはメンテナーから要求された場合です。 必要がない限り、レビュー中のすでにグリーンな PR を更新することは避けてください。 レビュー中は、フィードバックに対応するためにインクリメンタルなコミットを作成してください。 squash や rebase は最後に行うか、レビュアーから要求された場合に行うことを推奨します。

更新する際は、git push --force-with-lease を使用し、何が変わったかを説明する簡潔なコメントを残してください。 一部のリポジトリでは、rebase の代わりに upstream/main からのマージを好みます。 プロジェクトの慣例に従ってください。 詳細な手順については、最新状態を保つを参照してください。

rebase 後は、CI が実行される前に問題を検出するために、関連するテストをローカルで実行することを推奨します。

r?

すべてのプルリクエストは別の人によってレビューされます。 @rustbot という bot があり、変更したファイルに基づいて、あなたのリクエストをレビューするランダムな人を自動的に割り当てます。

特定の人にプルリクエストのレビューを依頼したい場合は、 プルリクエストの説明またはコメントに r? を追加できます。 たとえば、@awesome-reviewer にレビューを依頼したい場合は、 プルリクエストの説明の末尾に次の内容を追加します。

r? @awesome-reviewer

すると、@rustbot はランダムな人ではなく、そのレビュアーに PR を割り当てます。 これは完全に任意です。

r? rust-lang/groupname と書くことで、特定のチームからランダムなレビュアーを割り当てることもできます。 例として、診断の変更を行っている場合は、 次の内容を追加することで診断チームからレビュアーを得ることができます。

r? rust-lang/diagnostics

指定可能な groupname の完全な一覧については、 [triagebot.toml 設定ファイル]の adhoc_groups セクション、 または [rust-lang チームデータベース]のチーム一覧を確認してください。

レビューを待つ

注記

プルリクエストのレビュアーは、多くの場合、作業量が上限に達しており、 その多くはボランティアとして貢献しています。 レビューの遅延を最小限に抑えるため、 プルリクエストの作成者と割り当てられたレビュアーは、レビューラベル (S-waiting-on-reviewS-waiting-on-author)が最新に保たれるようにし、 適切なタイミングで次のコマンドを呼び出すべきです。

  • @rustbot author: レビューは完了しており、 PR 作成者はコメントを確認し、それに応じて対応するべきです。

  • @rustbot review: 作成者はレビューの準備ができており、 この PR はレビュアーのキューに再度入れられます。

レビュアーは人間であり、そのほとんどは空き時間に rustc に取り組んでいることに注意してください。 つまり、応答して PR をレビューするまでに時間がかかることがあります。 また、レビュアーが割り当てられた一部の PR を見逃す可能性もあるということです。

PR を前進させるために、Triage WG はレビュー待ちで、少なくとも 2 週間議論されていないすべての PR を定期的に確認しています。 2 週間以内にレビューを受けられない場合は、Zulip の #t-release/triage で Triage WG に遠慮なく尋ねてください。 彼らは、いつ ping すべきか、誰が休暇中かもしれないか、などを把握しています。

レビュアーは GitHub のコードレビューインターフェイスを使用して、いくつかの変更を要求することがあります。 一部の PR については、特別な手続きを要求することもあります。 そのような手続きの例については、Crater と [Breaking Changes] の章を参照してください。

CI

人間によるレビューに加えて、プルリクエストは継続的インテグレーション(CI)によって自動的にテストされます。 基本的に、プルリクエストを開いたり更新したりするたびに、 CI はコンパイラをビルドし、[compiler test suite] に対してテストし、さらにプルリクエストが Rust のスタイルガイドラインに準拠していることの確認など、他のテストも実行します。

継続的インテグレーションテストを実行することで、PR 作成者は最初のレビューサイクルを経ることなく早期にミスを発見でき、またレビュアーが特定のプルリクエストの状態を把握し続ける助けにもなります。 Rust には十分な CI キャパシティがあり、変更を push するたびに 計算リソースを無駄にすることを心配する必要はまったくありません。 また、生産性の向上に役立つのであれば、変更のテストに CI を使うことも まったく問題ありません(むしろ推奨されています!)。 特に、完全な ./x test スイートをローカルで実行することは推奨していません。 実行に非常に長い時間がかかるためです。 Rust の CI を使って変更をテストする方法については、Testing with CI の章を参照してください。

r+

誰かがあなたのプルリクエストをレビューした後、その人はプルリクエストに r+ という注釈を残します。 これは次のような見た目です。

@bors r+

これは、私たちの頼れるインテグレーションボットである @bors に、 あなたのプルリクエストが承認されたことを伝えます。 その後 PR は [merge queue] に入り、そこで @bors が 私たちのサポートするすべてのプラットフォームですべてのテストを実行します。 すべてうまくいけば、@bors はあなたのコードを main にマージし、プルリクエストを閉じます。

変更の規模によっては、少し異なる形式の r+ を目にすることがあります。

@bors r+ rollup

追加の rollup は、この変更を常に「ロールアップ」するべきであることを @bors に伝えます。 ロールアップされる変更は、処理を高速化するために、他の PR と一緒にテストおよびマージされます。 通常、互いに競合しないと見込まれる小さな変更だけが 「常にロールアップ」としてマークされます。

辛抱強く待ってください。 これには時間がかかる場合があり、キューが長くなることもあります。 また、PR が手動でマージされることは決してない点にも注意してください。

Opening a PR

これでプルリクエスト(PR)を提出する準備ができましたか? すばらしいです! 知っておくべき点がいくつかあります。

すべてのプルリクエストは、別のブランチを対象にすべきだと確実に分かっている場合を除き、 main ブランチに対して提出する必要があります。

PR を送信する前に、いくつかのスタイルチェックを実行してください。

./x test tidy --bless

このチェックは、すべてのプルリクエストの前に(またプルリクエスト内のすべての新しいコミットの前にも) 実行することを推奨します。 このチェックを忘れないように、push の前に [git hooks] を追加できます。 CI でも tidy が実行され、tidy が失敗すると CI も失敗します。

Rust は no merge-commit policy に従っています。 これは、マージコンフリクトに遭遇した場合、 マージではなく常にリベースすることが求められるという意味です。 たとえば、main ブランチの最新の変更を自分の feature ブランチに取り込むときは、 常に rebase を使用してください。 PR にマージコミットが含まれている場合、has-merge-commits としてマークされます。 たとえばインタラクティブリベースによってマージコミットを削除したら、再度そのラベルを 削除する必要があります。

@rustbot label -has-merge-commits

詳細については、この章を参照してください。

マージコンフリクトに遭遇した場合、またはレビュー担当者から何らかの 変更を求められた場合、PR は S-waiting-on-author としてマークされます。 それらを解決したら、@rustbot を使って S-waiting-on-review としてマークする必要があります。

@rustbot ready

GitHub では キーワードを使って issue を閉じることができます。 この機能は issue トラッカーを整然と保つために使用するべきです。 ただし、一般的には、 “closes #123” というテキストはコミットメッセージではなく PR の説明に入れることが好まれます。 特にリベース中に、コミット内で issue 番号を引用すると、該当する issue に 「スパム」を送ることになり得ます。

ただし、PR が stable-to-beta または stable-to-stable のリグレッションを修正し、 beta および/または stable へのバックポートが承認されている場合(つまり beta-accepted および/または stable-accepted としてマークされている場合)は、そのようなキーワードを 使用しないでください。修正が main に入った時点で、対応する issue が自動的に閉じられることを 望まないためです。 issue についてはどこかで言及したまま、PR の説明を更新してください。 たとえば、Fixes (after beta backport) #NNN. と書くことができます。

追加の対応として、タイトルが [beta] または [stable] で始まり、 対象の PR をバックポートする PR に十分注意してください。 その PR がマージされたら、関連する issue を閉じることができます。 閉じるコメントでは、関係したすべての PR に言及する必要があります。 issue を閉じる権限がない場合は、 元の PR にコメントを残し、レビュー担当者に代わりに閉じてもらうよう依頼してください。

Reverting a PR

PR が誤コンパイル、重大なパフォーマンスリグレッション、またはその他の重大な問題を引き起こす場合、 その PR をリグレッションテストケースとともに revert したいことがあります。 Forge docs の revert policy も確認できます (主にレビュー担当者向けですが、PR 作成者にとっても有用な情報が含まれています)。

PR に巨大な変更が含まれている場合、revert が難しくなり、その後の更新で インクリメンタルな修正をレビューすることが難しくなる場合があります。 また、その PR 内の特定のコードが後続の PR に大きく依存されている場合、 それを revert することが難しくなることがあります。

そのような場合は、#128271 に示されているように、問題のあるコードを特定し、一部の入力に対して無効化できます。

MIR 最適化については、#132356 に示されているように、 -Zunsound-mir-opt オプションを使って mir-opt をゲートすることもできます。

External dependencies

このセクションは “Using External Repositories” に移動しました。

Writing documentation

ドキュメントの改善は大歓迎です。 doc.rust-lang.org のソースは ツリー内の src/doc にあり、標準 API ドキュメントは ソースコード自体から生成されます(例: library/std/src/lib.rs)。ドキュメントのプルリクエストは、 他のプルリクエストと同じように機能します。

ドキュメント関連の issue を見つけるには、[A-docs label] を使用してください。

ドキュメントのスタイルガイドラインは [RFC 1574] にあります。

標準ライブラリのドキュメントをビルドするには、x doc --stage 1 library --open を使用します。 書籍(例: unstable book)のドキュメントをビルドするには、x doc src/doc/unstable-book. を使用します。 結果は build/host/doc に表示され、同時にデフォルトブラウザーで自動的に開かれるはずです。 詳細については、Building Documentation を参照してください。

小さな修正を確認するために、rustdoc を直接使用することもできます。 たとえば、rustdoc src/doc/reference.md は reference を doc/reference.html にレンダリングします。 CSS は乱れるかもしれませんが、HTML が正しいことは確認できます。

内部ドキュメントに対するタイポグラフィやスペルチェックの修正は受け付けていないことに注意してください。 通常、それは変更の churn やレビュー時間に見合わないためです。 内部ドキュメントの例としては、コードコメントや rustc api docs があります。 ただし、同じ PR 内の他の改善に伴う場合は、それらを修正しても構いません。

Contributing to rustc-dev-guide

Contributions to the [rustc-dev-guide] はいつでも歓迎されており、 [rust-lang/rustc-dev-guide リポジトリ][rdgrepo]で直接行うことができます。 そのリポジトリの issue tracker も、やるべきことを見つけるのに最適な方法です。 初心者向けの issue も、高度なコンパイラ開発者向けの issue もあります!

覚えておいてほしいことがいくつかあります:

  • 過度に長い行はできるだけ避け、意味的な改行(各文の後で行を改行すること)を使用してください。 行の長さに厳密な制限はありません。 文または文の一部が、同じ行で自然な終わりまで続くようにしてください。

    これを支援するために、ci/sembr にあるツールを使用できます。 そのヘルプ出力は、次のコマンドで確認できます:

    cargo run --manifest-path ci/sembr/Cargo.toml -- --help
    
  • ガイドにテキストをコントリビュートする場合は、何らかの期間 および/または理由を添えて情報に文脈を与え、読者がその情報をどの程度信頼すべきか分かるようにしてください。 適切な量の文脈を提供することを目指してください。たとえば、以下を含めることが考えられますが、これらに限定されません:

    • テキストが古くなっている可能性がある理由として、「変更」以外のもの。 変更はプロジェクト全体で常に起こるものだからです。

    • コメントが追加された日付。たとえば、「現在、…」「現時点では、…」 と書く代わりに、次のいずれかの形式で日付を追加することを検討してください:

      • Jan 2021
      • January 2021
      • jan 2021
      • january 2021

      .github/workflows/date-check.yml には CI アクションがあり、 6 か月を超えているものを示す月次レポートを生成します ()。

      アクションが日付を拾えるようにするには、日付を指定する前に特別なアノテーションを追加します:

      <!-- date-check --> Nov 2025
      

      例:

      As of <!-- date-check --> Nov 2025, the foo did the bar.
      

      日付を表示されるレンダリング結果の一部にすべきでない場合は、 代わりに次を使用してください:

      <!-- date-check: Nov 2025 -->
      
    • 関連する WG、tracking issue、rustc rustdoc ページ、または類似のものへのリンク。 変更プロセスの詳しい説明や、その情報が古くなっていないことを検証する方法を提供している可能性があります。

  • テキストがかなり長くなった場合(数回ページスクロールする量を超える)や、複雑になった場合(4 つを超える サブセクションがある場合)は、先頭に目次を置くとよいかもしれません。 先頭に <!-- toc --> マーカーを含めることで自動生成できます。

⚠️ 注: rustc-dev-guide の変更をどこにコントリビュートするか

rustc-dev-guide の変更をどこにコントリビュートするか、およびそうすることの利点についての詳細は、 rustc-dev-guide チームのドキュメントを参照してください。

Issue トリアージ

https://forge.rust-lang.org/release/issue-triaging.html を参照してください。

rfcbot ラベル

rfcbot は、変更を承認または却下するなど、非同期の意思決定を調整するプロセスを追跡するために 独自のラベルを使用します。 これは RFCs、issue、pull request に使用されます。

ラベル説明
proposed-final-comment-period グレー現在、final comment period に入るために、すべてのチームメンバーによる承認を待っています。
disposition-merge 緑変更をマージする意図があることを示します。
disposition-close 赤変更を受け入れず、クローズする意図があることを示します。
disposition-postpone グレー現時点では変更を受け入れず、後日に延期する意図があることを示します。
final-comment-period 青現在、マージまたはクローズする前に最終コメントを募っています。
finished-final-comment-period 薄い黄色final comment period は終了しており、issue はマージまたはクローズされます。
postponed 黄色issue は延期されました。
closed 赤issue は却下されました。
to-announce グレーfinal-comment-period を終え、公開告知されるべき issue。注: rust-lang/rust リポジトリでは、トリアージミーティングで issue を告知するために、このラベルを異なる用途で使用しています。

役立つリンクと情報

このセクションは [“このガイドについて”] の章に移動しました。 [“About this guide”]: about-this-guide.md#other-places-to-find-information [search existing issues]: https://github.com/rust-lang/rust/issues?q=is%3Aissue [Breaking Changes]: bug-fix-procedure.md [triagebot.toml config file]: https://github.com/rust-lang/rust/blob/HEAD/triagebot.toml [rust-lang teams database]: https://github.com/rust-lang/team/tree/HEAD/teams [compiler test suite]: tests/intro.md [merge queue]: https://bors.rust-lang.org/queue/rust [git hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks [A-docs label]: https://github.com/rust-lang/rust/issues?q=is%3Aopen%20is%3Aissue%20label%3AA-docs [RFC 1574]: https://github.com/rust-lang/rfcs/blob/master/text/1574-more-api-documentation-conventions.md#appendix-a-full-conventions-text [rustc-dev-guide]: https://rustc-dev-guide.rust-lang.org/ [rdgrepo]: https://github.com/rust-lang/rustc-dev-guide [create an issue]: https://github.com/rust-lang/rust/issues/new/choose

コンパイラーチームについて

注記: チームについての詳細は Forge 上 に多く存在しており、以下の大半は古くなっています。

rustc は Rust コンパイラーチームによって保守されています。 このチームに所属する人々は、共同でリグレッションの追跡や新機能の実装に取り組んでいます。 Rust コンパイラーチームのメンバーは、rustc とその設計に対して重要な貢献を行ってきた人々です。

議論

現在、コンパイラーチームは Zulip でチャットしています。

  • チームチャットは Zulip インスタンス上の t-compiler ストリームで行われます
  • 関連する Zulip チャンネルも他にいくつかあります。 たとえば t-compiler/help では rustc 開発について助けを求めることができ、 t-compiler/meetings ではチームが週次のトリアージミーティングと運営ミーティングを行っています。

レビュアー

コンパイラーの特定の部分について誰が質問に答えられるのかを知りたい場合や、単に誰が何に取り組んでいるのかを知りたい場合は、 triagebot.toml の assign セクションを確認してください。 そこには、コンパイラーのさまざまな部分と、それぞれの部分のレビュアーである人々の一覧が含まれています。

Rust コンパイラーミーティング

コンパイラーチームには週次ミーティングがあり、そこでトリアージを行い、 一般に新しいバグやリグレッションを把握し、重要な事柄全般について議論するよう努めています。 ミーティングは Zulip で開催されます。 おおよそ次のように進行します。

  • お知らせ、MCP/FCP、WG チェックイン: チームの残りのメンバーに対して、 全員に知っておいてほしい重要な事柄についていくつかのお知らせを共有します。 また、MCP と FCP の状況も共有し、 いくつかの WG から作業状況の更新を聞く機会としても使います。
  • beta および stable へのノミネーションの確認: これらは、それぞれ beta と stable に バックポートするもののノミネーションです。 その後、実際に使われている、以前は動作していたコードをコンパイラーが壊した新しいケースを探します。 リグレッションは修正すべき重要な問題であるため、 P-critical または P-high としてタグ付けされている可能性があります。主な例外は バグ修正です(ただしその場合でも、私たちは多くの場合 まず警告を出すことを目指します)。
  • P-critical および P-high のバグのレビュー: P-critical および P-high のバグは、 私たちが進捗を能動的に追跡するのに十分重要なものです。 P-critical および P-high のバグには、理想的には常に担当者がいるべきです。
  • S-waiting-on-t-compiler および I-compiler-nominated issue の確認: これらは、 チームからのフィードバックが求められている issue です。
  • パフォーマンストリアージレポートの確認: パフォーマンスを悪化させた PR を確認し、 パフォーマンスリグレッションを revert する価値があるのか、それとも そのリグレッションを将来の PR で対処できるのかを判断しようとします。

現在、ミーティングは木曜日のボストン時間午前 10 時に行われています (通常は UTC-4 ですが、サマータイムによって複雑になることがあります)。

チームメンバーシップ

Rust チームのメンバーシップは通常、ある人がしばらくの間 コンパイラーに対して重要な貢献を行ってきたときに提供されます。 メンバーシップは承認であると同時に義務でもあります。 コンパイラーチームのメンバーには、一般にレビューやその他の作業を行うだけでなく、 維持管理を手助けすることも期待されます。

コンパイラーチームのメンバーになることに関心がある場合、最初に行うべきことは、 いくつかのバグ修正を始めるか、ワーキンググループに参加することです。 バグを見つけるよい方法の 1 つは、 E-easy がタグ付けされた未解決の issue または E-mentor を探すことです。

また、非アクティブのため close された PR の墓場を掘り起こすこともできます。 その中には、まだ有用な作業が含まれているものがあるかもしれません。関連する issue があれば参照してください。 そして、元の作者に時間がなかったために、仕上げだけが必要な場合もあります。

r+ 権限

rustc に対して個別の PR をいくつか作成した後、私たちはしばしば r+ 権限を提供します。 これは、PR をマージするように 「bors」(どの PR を rustc に取り込むかを管理するロボット)へ指示する権利があることを意味します (bors とやり取りする方法についての説明はこちら)。

レビュアー向けのガイドラインは次のとおりです。

  • 誰に割り当てられているかに関係なく、どの PR でもいつでもレビューしてかまいません。 ただし、次の場合を除き、PR に r+ しないでください。
    • コードのその部分に自信がある。
    • 他の誰も先にレビューしたいと思っていないと確信している。
      • たとえば、特にデリケートな部分のコードに触れているなどの理由で、 PR が取り込まれる前にレビューしたいという希望を表明する人がいることがあります。
  • レビューするときは常に礼儀正しくしてください。あなたは Rust プロジェクトの代表者であるため、行動規範に関しては 期待を上回る対応をすることが求められます。

レビュアーローテーション

r+ 権限を持つと、レビュアーローテーションに追加されることもできます。 triagebot は、受信した PR をレビュアーに自動的に割り当てる bot です。 追加された場合、PR のレビュー担当としてランダムに選ばれます。 自分に割り当てられた PR をレビューすることに不安がある場合は、 r? @so-and-so のようなコメントを残して、他の誰かに割り当てることもできます。 誰に依頼すればよいかわからない場合は、単に r? @nikomatsakis for reassignment と書けば、@nikomatsakis があなたのために誰かを選びます。

レビュアーローテーションに加わることは、私たち全員のレビュー負担を軽減するため、とてもありがたいことです。 ただし、PR に対してタイムリーなフィードバックを人々に返す時間がない場合は、 リストに加わらないほうがよいかもしれません。

Git の使用

Rust プロジェクトでは、ソースコードの管理に Git を使用しています。 コントリビュートするには、自分の変更をコンパイラに取り込めるようにするため、 Git の機能にある程度慣れている必要があります。

このページの目的は、新しいコントリビューターが直面しがちな、より一般的な質問や 問題をいくつか取り上げることです。 ここでも Git の基本をいくつか扱いますが、 それでも少し進みが速いと感じる場合は、まず Atlassian のこのチュートリアルの 「Beginner」や「Getting started」セクションなど、Git の入門記事を読むとよいでしょう。 GitHub も初心者向けのドキュメントガイドを提供しています。 また、より詳しい Git の書籍を参照することもできます。

このガイドは完全ではありません。 このページでは解決できない git の問題に遭遇した場合は、 修正方法を文書化できるように、issue を開いてください

前提条件

Git をインストールし、rust-lang/rust をフォークし、フォークしたリポジトリを 自分の PC にクローン済みであるものとします。 Git とのやり取りにはコマンドラインインターフェイスを使用します。 一般的には同じことができる GUI や IDE 連携も数多くあります。

フォークをクローンしている場合、ローカルリポジトリではそれを origin として参照できます。 次のようにして、公式の rust-lang/rust リポジトリ用のリモートも設定しておくと便利かもしれません。

git remote add upstream https://github.com/rust-lang/rust.git

HTTPS を使用している場合、または

git remote add upstream git@github.com:rust-lang/rust.git

SSH を使用している場合です。

注: このページは rust-lang/rust 向けのワークフローを扱っていますが、 Rust プロジェクト内の他のリポジトリへコントリビュートする際にも役立つ可能性があります。

標準的な手順

以下は、ほとんどの小さな変更や PR で使用することになる通常の手順です。

  1. main を基点に変更を加えていることを確認します: git checkout main
  2. Rust リポジトリから最新の変更を取得します: git pull upstream main --ff-only。 (これについての詳細はマージ禁止ポリシーを参照してください)。
  3. 変更用の新しいブランチを作成します: git checkout -b issue-12345-fix
  4. リポジトリに変更を加え、テストします。
  5. git add src/changed/file.rs src/another/change.rs で変更をステージし、 その後 git commit でコミットします。 もちろん、中間コミットを作成するのもよい考えです。 git add . は避けてください。サブモジュールの更新など、 コミットすべきでない変更を意図せずコミットしてしまいやすくなるためです。 ステージし忘れたファイルがないか確認するには、git status を使用できます。
  6. 変更を自分のフォークへプッシュします: git push --set-upstream origin issue-12345-fix (コミットを追加した後は git push を使用でき、リベースまたは pull-and-rebase の後は git push --force-with-lease を使用できます)。
  7. 自分のフォークから rust-lang/rustmain ブランチへ PR を開きます

最終的にリベースが必要になり、コンフリクトに遭遇している場合は、リベースを参照してください。 長期間続く機能や issue に取り組んでいる間に upstream を追跡したい場合は、 最新の状態に保つを参照してください。

レビュー担当者から変更を求められた場合、その変更の手順もほぼ同じですが、 いくつかの手順は省かれます。

  1. 自分のコードの最新バージョンに対して変更を加えていることを確認します: git checkout issue-12345-fix
  2. 以前と同じように、追加の変更を行い、ステージし、コミットします。
  3. それらの変更を自分のフォークへプッシュします: git push

git の問題のトラブルシューティング

rust-lang/rust が古くなっていても、最初からクローンし直す必要はありません! 修復できないほど壊してしまったと思っていても、リポジトリ全体を再度ダウンロードせずに git の状態を修正する方法があります。 遭遇する可能性のある一般的な問題をいくつか示します。

誤ってマージコミットを作成してしまいました。

Git には、ブランチを最新の変更で更新する方法が 2 つあります。マージとリベースです。 Rust はリベースを使用しています。 マージコミットを作成してしまっても、修正はそれほど難しくありません: git rebase -i upstream/main

リベースの詳細については、リベースを参照してください。

GitHub 上の自分のフォークを削除してしまいました!

これは git の観点では問題ではありません。 git remote -v を実行すると、 次のような内容が表示されます。

$ git remote -v
origin  git@github.com:jyn514/rust.git (fetch)
origin  git@github.com:jyn514/rust.git (push)
upstream        https://github.com/rust-lang/rust (fetch)
upstream        https://github.com/rust-lang/rust (fetch)

フォークの名前を変更した場合は、次のように URL を変更できます。

git remote set-url origin <URL>

ここで <URL> は新しいフォークです。

誤ってサブモジュールを変更してしまいました

通常、これは cargo が変更されたと rustbot が GitHub にコメントを投稿したときに気づきます。

rustbot のサブモジュールコメント

Web UI でコンフリクトに気づく場合もあります。

src/tools/cargo のコンフリクト

最も一般的な原因は、変更後にリベースし、サブモジュールを更新するための x を先に実行せずに git add . を実行したことです。 あるいは、x fmt ではなく cargo fmt を実行し、 サブモジュール内のファイルを変更してから、その変更をコミットした可能性もあります。

修正するには、次のことを行います(cargo 以外のサブモジュールを変更した場合は、 src/tools/cargo をそのサブモジュールへのパスに置き換えてください)。

  1. 誤った変更が含まれているコミットを確認します: git log --stat -n1 src/tools/cargo
  2. そのコミットに対する変更を取り消します: git checkout <my-commit>~ src/tools/cargo~ はそのまま入力しますが、<my-commit> は手順 1 の出力に置き換えてください。
  3. git に変更をコミットするよう指示します: git commit --fixup <my-commit>
  4. 変更したすべてのサブモジュールについて、手順 1〜3 を繰り返します。
    • 複数の異なるコミットでサブモジュールを変更した場合は、変更した各コミットについて 手順 1〜3 を繰り返す必要があります。 git log コマンドが自分の作成者ではないコミットを表示したときに、 そこで止めればよいことが分かります。
  5. 変更を既存のコミットに squash します: git rebase --autosquash -i upstream/main
  6. 変更をプッシュします

リベースしようとすると “error: cannot rebase” が表示されます

リベース時によく見られるエラーは次の 2 つです。

error: cannot rebase: Your index contains uncommitted changes.
error: Please commit or stash them.
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.

(この 2 つの違いについては https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F#_the_three_states を参照してください。) これは、前回コミットした時点から変更を加えていることを意味します。 リベースできるようにするには、 変更をコミットするか、リベースを完了したときにそれらをまだコミットされていない状態にしておくために “stash” と呼ばれる一時的なコミットを作成します。 git がこの “stash” を自動的に作成するように設定したい場合があります。これにより、 ほぼすべての場合で “cannot rebase” エラーを防げます。

git config --global rebase.autostash true

stash の詳細については、https://git-scm.com/book/en/v2/Git-Tools-Stashing-and-Cleaning を参照してください。

‘Untracked Files: src/stdarch’ が表示されますか?

これは library/ ディレクトリへの移動の名残です。 残念ながら、git rebase はサブモジュールのリネームを追跡しないため、 自分でディレクトリを削除する必要があります。

rm -r src/stdarch

<<< HEAD が表示されますか?

おそらく、リベースまたはマージ競合の途中だったのでしょう。 競合を修正する方法については、競合 を参照してください。 変更内容を気にせず、 リポジトリのクリーンなコピーを取り戻したいだけであれば、git reset を使用できます。

# 警告: これにより、あなたが行ったローカルの変更はすべて破棄されます! 代わりに競合を解決することを検討してください。
git reset --hard main

failed to push some refs

git push は正しく動作せず、次のようなことを言います。

 ! [rejected]        issue-xxxxx -> issue-xxxxx (non-fast-forward)
error: failed to push some refs to 'https://github.com/username/rust.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

ここで示されるアドバイスは正しくありません! Rust の “no-merge” ポリシー により、git pull によって作成されるマージコミットは 最終的な PR では許可されませんし、そもそもリベースの目的も損なってしまいます! 代わりに git push --force-with-lease を使用してください。

Git が自分で書いていないコミットをリベースしようとしていますか?

リベースリストに多数のコミット、マージコミット、または自分で 書いていない他の人のコミットが表示される場合、おそらく間違ったブランチを対象にリベースしようとしています。 たとえば、rust-lang/rust のリモート upstream があるのに、git rebase upstream/main ではなく git rebase origin/main を実行したのかもしれません。 修正方法は、リベースを中止し、代わりに正しいブランチを使用することです。

git rebase --abort
git rebase --interactive upstream/main
間違ったブランチを対象にリベースした例を見るには、ここをクリックしてください

間違ったブランチを対象にしたインタラクティブリベース

サブモジュールに関する簡単な注意

git pull でローカルリポジトリを更新すると、自分が一度も編集していないファイルを 変更したと Git が言うことに気づく場合があります。 たとえば、 git status を実行すると次のような出力になります(new commits という言及に注意してください)。

On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   src/llvm-project (new commits)
	modified:   src/tools/cargo (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

これらの変更はファイルへの変更ではなく、サブモジュールへの変更です(これについては後ほど詳しく説明します)。 これらを取り除くには、次を実行します。

git submodule update

一部のサブモジュールは実際には必要ありません。たとえば、download-ci-llvm を使用している場合、 src/llvm-project をチェックアウトする必要はありません。 その履歴を取得し続けなくて済むようにするには、 git submodule deinit -f src/llvm-project を使用できます。これにより、再び変更済みとして表示されることも避けられます。

リベースと競合

ローカルでコードを編集するとき、フィーチャーブランチを作成した時点で存在していた rust-lang/rust のバージョンに変更を加えています。 そのため、PR を提出すると、それ以降に rust-lang/rust に加えられた変更の一部が、 あなたの加えた変更と競合する可能性があります。 これが発生した場合、変更をマージできるようにする前に競合を解決する必要があります。 そのためには、自分の作業を rust-lang/rust の上にリベースする必要があります。

リベース

フィーチャーブランチを rust-lang/rust の main ブランチの最新バージョンの上に リベースするには、自分のブランチをチェックアウトしてから、次のコマンドを実行します。

git pull --rebase https://github.com/rust-lang/rust.git main

次のエラーが表示された場合:

error: cannot pull with rebase: Your index contains uncommitted changes.
error: please commit or stash them.

これは、ワーキングツリーに未コミットの作業があることを意味します。その 場合は、リベースする前に git stash を実行し、リベースしてすべての競合を 修正した後に git stash pop を実行してください。

ブランチを main にリベースすると、そのブランチ上のすべての変更が main の最新バージョンに再適用されます。 言い換えると、Git は、 古いバージョンの main に対して行った変更が、代わりに新しいバージョンの main に 対して行われたかのように見せようとします。 この処理中には、少なくとも 1 つの「リベース競合」に遭遇することを想定しておくべきです。 これは、あなたの変更が他の加えられた変更と競合したために、 Git による変更の再適用が失敗したときに発生します。 次のような行が出力に表示されるため、これが発生したことがわかります。

CONFLICT (content): Merge conflict in file.rs

これらのファイルを開くと、次の形式のセクションが表示されます。

<<<<<<< HEAD
Original code
=======
Your code
>>>>>>> 8fbf656... Commit fixes 12345

これは、Git がどのようにリベースすればよいか判断できなかったファイル内の行を表しています。 <<<<<<< HEAD======= の間のセクションには main のコードがあり、反対側にはあなたのバージョンのコードがあります。 競合にどう対処するかを決める必要があります。 自分の変更を残したい場合もあれば、 main 上の変更を残したい場合、またはその 2 つを組み合わせたい場合もあります。

一般に、競合の解決は 2 つの手順で構成されます。まず、特定の競合を修正します。 ファイルを編集して望む変更を加え、その過程で <<<<<<<=======>>>>>>> の各行を削除します。 次に、周辺のコードを確認します。 競合があった場合、そこには論理的なエラーも潜んでいる可能性があります! ここで x check を実行して、明らかなエラーがないことを確認するのはよい考えです。

競合の修正がすべて完了したら、競合があったファイルを git add で ステージする必要があります。 その後、git rebase --continue を実行して、 競合を解決したのでリベースを完了してよいことを Git に知らせます。

リベースが成功したら、フォーク上の関連するブランチを git push --force-with-lease で更新します。

最新の状態に保つ

上記のセクションは、作業をリベースし、マージ競合に対処するための具体的な ガイドです。 ローカルリポジトリをアップストリームの変更に追従させて最新の状態に保つ方法について、一般的なアドバイスを以下に示します。 ローカルの main ブランチ上で定期的に git pull upstream main を使用すると、最新の状態を保てます。 フィーチャーブランチも最新の状態に保つ必要があります。 pull した後、フィーチャーブランチをチェックアウトしてリベースできます。

git checkout main
git pull upstream main --ff-only # マージコミットがないことを確実にするため
git rebase main feature_branch
git push --force-with-lease # (origin をローカルと同じ状態に設定する)

No-Merge Policy に従ってマージを避けるには、 git config pull.ff only を使用するとよいでしょう(これにより、設定はローカルリポジトリにのみ適用されます)。 これにより、毎回 --ff-only--rebase を渡さなくても、 git pull 時に Git がマージコミットを作成しないようにできます。

また、main から git push --force-with-lease を実行することで、 フィーチャーブランチが GitHub 側の状態と同期していることを再確認することもできます。

高度なリベース

コミットを squash する

コミットを互いに「squash」すると、それらは 1 つのコミットに統合されます。 これの利点でも欠点でもあるのは、履歴が単純化されることです。 一方では、変更が行われた手順を追跡できなくなりますが、 履歴は扱いやすくなります。

rust-lang/rust リポジトリの PR でコミットを squash する最も簡単な方法は、PR のコメントで @bors squash コマンドを使用することです。 デフォルトでは、bors は PR のすべてのコミットメッセージを squash 後のコミットメッセージに結合します。 コミットメッセージをカスタマイズするには、@bors squash msg="<commit message>" を使用します。 たとえば、@bors squash msg="Improve diagnostics for missing lifetime parameter" です。

ローカルの git 操作を使用してコミットを squash したい場合は、以下を読み進めてください。

コンフリクトがなく、履歴を整理するためだけに squash する場合は、 git rebase --interactive --keep-base main を使用します。 これにより、PR の分岐点が同じまま保たれるため、 リベースをまたいで何が起きたかの diff をレビューしやすくなります。

squash は、コンフリクト解決の一環としても役立つことがあります。 ブランチに同じコードの連続した複数の書き換えが含まれている場合、または リベースのコンフリクトが非常に深刻な場合は、 git rebase --interactive main を使用してプロセスをより細かく制御できます。 これにより、コミットをスキップするか、スキップしないコミットを編集するか、 適用される順序を変更するか、またはそれらを互いに「squash」するかを選択できます。

あるいは、次のようにしてコミット履歴を犠牲にすることもできます。

# すべての変更を 1 つのコミットに squash し、コンフリクトを 1 回だけ気にすればよいようにする
git rebase --interactive --keep-base main  # その過程ですべての変更を squash する
git rebase main
# すべてのマージコンフリクトを修正する
git rebase --continue

場合によっては、最後の数個のコミットだけをまとめて squash したいこともあります。 それらが実際の変更ではなく「fixup」だけを表している場合などです。 たとえば、 git rebase --interactive HEAD~2 を使用すると、2 つのコミットだけを編集できます。

git range-diff

リベースを完了した後、変更を push する前に、 古いブランチと新しいブランチの間の変更をレビューしたい場合があります。 これは git range-diff main @{upstream} HEAD で行えます。

range-diff の最初の引数、この場合は main は、 古いブランチと新しいブランチを比較する基準となるリビジョンです。 2 番目の引数は ブランチの古いバージョンです。この場合、@upstream は GitHub に push 済みのバージョンを意味し、これはプルリクエストで他の人が見るものと同じです。 最後に、range-diff の 3 番目の引数は ブランチの新しいバージョンです。この場合は HEAD であり、これは現在 ローカルリポジトリでチェックアウトされているコミットです。

同等の省略形である git range-diff main @{u} HEAD も使用できることに注意してください。

通常の Git diff とは異なり、range-diff の出力では、別の - または + の隣に - または + が表示されます。 左側のマーカーは 古いブランチと新しいブランチの間の変更を示し、右側のマーカーはコミットした変更を示します。 したがって、range-diff は「diff の diff」と考えることができます。 古い diff と新しい diff の間の違いを示すためです。

以下は git range-diff の出力例です(Git のドキュメントから取得):

-:  ------- > 1:  0ddba11 Prepare for the inevitable!
1:  c0debee = 2:  cab005e Add a helpful message at the start
2:  f00dbal ! 3:  decafe1 Describe a bug
    @@ -1,3 +1,3 @@
     Author: A U Thor <author@example.com>

    -TODO: Describe a bug
    +Describe a bug
    @@ -324,5 +324,6
      This is expected.

    -+What is unexpected is that it will also crash.
    ++Unexpectedly, it also crashes. This is a bug, and the jury is
    ++still out there how to fix it best. See ticket #314 for details.

      Contact
3:  bedead < -:  ------- TO-UNDO

(端末での git range-diff の出力は色が付くため、おそらくこの例よりも 読みやすいことに注意してください。)

git range-diff のもう 1 つの機能は、git diff とは異なり、コミットメッセージの diff も表示することです。 この機能は、複数のコミット メッセージを修正する際に、正しい箇所を変更したことを確認できるため便利です。

git range-diff は非常に便利なコマンドですが、その出力形式に慣れるには多少時間がかかることに注意してください。 また、このコマンドに関する Git のドキュメント、 特に “Examples” セクションも役立つかもしれません。

No-Merge Policy

rust-lang/rust リポジトリでは、「リベースワークフロー」として知られるものを使用しています。 これは、PR 内のマージコミットが受け入れられないことを意味します。 そのため、ローカルで git merge を実行している場合は、代わりにリベースすべき可能性が高いです。 もちろん、これは常に正しいわけではありません。git pull が通常行うマージのように、 マージが単なる fast-forward である場合、 マージコミットは作成されないため、心配する必要はありません。 一度 git config merge.ff only を実行すると(これにより、設定はローカルリポジトリに適用されます)、 実行するすべてのマージがこの種類であることが保証されるため、 ミスをすることができなくなります。

この決定にはいくつかの理由があり、他のすべてのものと同様に、これはトレードオフです。 主な利点は、一般的に線形なコミット履歴です。 これにより bisect が大幅に単純化され、履歴とコミットログがはるかに 追いやすく理解しやすくなります。

レビューのヒント

NOTE: このセクションは PR を作成するためではなく、レビューするためのものです。

空白を非表示にする

GitHub には、空白の変更を無効にするための便利なボタンがあります。 ローカルで変更を表示するには、git diff -w origin/main を使用することもできます。

空白を非表示にする

PR を取得する

PR をローカルにチェックアウトするには、git fetch upstream pull/NNNNN/head && git checkout FETCH_HEAD を使用できます。

github の CLI ツールを使用することもできます。 GitHub は PR 上にボタンを表示し、ローカルにチェックアウトするためのコマンドをコピー & ペーストできます。 詳細については https://cli.github.com/ を参照してください。

gh の提案

GitHub dev の使用

GitHub Web UI の代替として、GitHub Dev はリポジトリや PR を閲覧するための Web ベースのエディタを提供します。 URL の github.comgithub.dev に置き換えるか、 GitHub ページで . を押すことで開けます。 詳細については、github.dev エディタのドキュメント を参照してください。

大きなコードセクションの移動

ファイル内部での大きな移動に対する Git と GitHub のデフォルトの diff 表示はかなり貧弱です。各 行が削除済み、各行が追加済みとして表示されるため、各行を自分で比較する必要があります。 Git には、移動された行を別の色で表示するオプションがあります。

git log -p --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change

詳細については、--color-moved のドキュメントを参照してください。

range-diff

PR 作成者向けの関連セクションを参照してください。 これは、予期しない変更がないことを確認するために、 force-push されたコードを比較する際に役立ちます。

特定のファイルへの変更を無視する

リポジトリ内の多くの大きなファイルは自動生成されています。 それらのファイルへの変更を無視した diff を表示するには、 次の構文を使用できます(例: Cargo.lock)。

git log -p ':!Cargo.lock'

任意のパターンがサポートされています(例: :!compiler/*)。パターンは .gitignore と同じ構文を使用し、パターンであることを示すために先頭に : を付けます。

Git サブモジュール

注記: サブモジュールについて知っておくのは良いことですが、rustc にコントリビュートするための 絶対的な前提条件ではありません。 Git を初めて使用する場合は、 このセクションを読む前に Git の主要な概念に慣れておくとよいでしょう。

rust-lang/rust リポジトリでは、rust リポジトリ内から他の Rust プロジェクトを利用する方法として Git submodules を使用しています。 例として、Rust の llvm-project のフォーク、cargostdarchbacktrace のようなライブラリがあります。

これらのプロジェクトは別の Git(および GitHub)リポジトリで開発・保守されており、 それぞれ独自の Git 履歴/コミット、issue トラッカー、PR を持っています。 サブモジュールを使うと、rust リポジトリ内にある種の埋め込みサブリポジトリを作成し、 それらを rust リポジトリ内のディレクトリであるかのように使用できます。

llvm-project を例に考えてみましょう。 llvm-projectrust-lang/llvm-project リポジトリで保守されていますが、 rust-lang/rust ではコンパイラによるコード生成と最適化のために使用されています。 これを rust にはサブモジュールとして、src/llvm-project フォルダに取り込んでいます。

サブモジュールの内容は Git によって無視されます。サブモジュールはある意味で、 リポジトリの他の部分から分離されています。 しかし、cd src/llvm-project を実行してから git status を実行してみると、次のようになります。

HEAD detached at 9567f08afc943
nothing to commit, working tree clean

git から見ると、あなたはもはや rust リポジトリ内ではなく、llvm-project リポジトリ内にいます。 ここでは “detached HEAD” 状態、すなわちブランチ上ではなく 特定のコミット上にいることに気づくでしょう。

これは、あらゆる依存関係と同様に、使用するバージョンを制御できるようにしたいためです。 サブモジュールを使うと、まさにそれが可能になります。各サブモジュールは特定の コミットに「ピン留め」されており、手動で変更しない限り変わりません。 llvm-project ディレクトリで git checkout <commit> を使用し、 rust ディレクトリに戻ると、たとえば git add src/llvm-project を実行することで、この変更を 他の変更と同じようにステージできます。(なお、コミットするためにその変更をステージしない場合、 x を実行したときに、サブモジュールが自動的に「更新」される際に以前のコミットへ戻され、 変更が取り消されてしまうリスクがあります。)

このバージョン選択は通常、プロジェクトのメンテナーによって行われ、 こちらのようになります。

Git サブモジュールに慣れるには少し時間がかかるため、まだ完全に明確でなくても心配しないでください。 直接使わなければならないことはめったにありませんし、繰り返しになりますが、 Rust にコントリビュートするためにサブモジュールについてすべてを知っている必要はありません。 それらが存在し、Git がうまく、そしてかなり便利に扱ってくれる、ある種の埋め込みサブリポジトリ依存関係に 対応していることだけを知っておいてください。

サブモジュールを hard-reset する

ときどき(git status を実行したときに)次のような状況に遭遇することがあります。

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
  (commit or discard the untracked or modified content in submodules)
        modified:   src/llvm-project (new commits, modified content)

そして git submodule update を実行しようとすると、次のようなエラーでひどく壊れることがあります。

error: RPC failed; curl 92 HTTP/2 stream 7 was not closed cleanly: CANCEL (err 8)
error: 2782 bytes of body are still expected
fetch-pack: unexpected disconnect while reading sideband packet
fatal: early EOF
fatal: fetch-pack: invalid index-pack output
fatal: Fetched in submodule path 'src/llvm-project', but it did not contain 5a5152f653959d14d68613a3a8a033fb65eec021. Direct fetching of that commit failed.

(new commits, modified content) が表示される場合は、次を実行できます。

git submodule foreach git reset --hard

その後、もう一度 git submodule update を試してください。

git サブモジュールを deinit する

それでもうまくいかない場合は、すべての git サブモジュールを deinit してみることができます…

git submodule deinit -f --all

残念ながら、何らかの理由でローカルの git サブモジュール設定が 完全におかしくなってしまうことがあります。

fatal: not a git repository: <submodule>/../../.git/modules/<submodule> を克服する

ときどき、何か不運な理由で、次のような状況に遭遇することがあります。

fatal: not a git repository: src/gcc/../../.git/modules/src/gcc

この状況では、指定されたサブモジュールパス、すなわちこの例では <submodule_path> = src/gcc について、次を行う必要があります。

  1. rm -rf <submodule_path>/.git
  2. rm -rf .git/modules/<submodule_path>/config
  3. 何らかの理由で .gitconfig のロックが孤立している場合は、rm -rf .gitconfig.lock

その後、./x fmt のようなものを実行して、bootstrap にサブモジュールのチェックアウトを管理させます。

git blame 中にコミットを無視する

一部のコミットには、機能を変更しない大規模な再フォーマット変更が含まれています。 これらは、.git-blame-ignore-revs を通じて git blame に無視するよう指示できます。

  1. 無視するコミットの一覧として .git-blame-ignore-revs を使用するように git blame を設定します: git config blame.ignorerevsfile .git-blame-ignore-revs
  2. git blame に無視させたい適切なコミットを追加します。

.git-blame-ignore-revs に追加するコミットにはコメントを含めてください。そうすれば、そのコミットが なぜ無視されているのかを他の人が簡単に理解できます。

@rustbot を使いこなす

@rustbottriagebot としても知られています)は、主に任意のコントリビューターが、 通常であれば rust-lang organization の GitHub メンバーシップを必要とする特定のタスクを 実行できるようにするために使われるユーティリティロボットです。rustc への コントリビューターにとって特に興味深い機能は、Issue の引き受けとラベル変更です。

Issue の引き受け

@rustbot は、誰でも Issue を自分に割り当てることができるコマンドを公開しています。 作業したい Issue を見つけた場合は、対象の Issue にコメントとして次のメッセージを 送信できます。

@rustbot claim

これにより、まだ担当者がいない場合、@rustbot はその Issue をあなたに割り当てます。 いくつかの GitHub の制約により、間接的に割り当てられる場合があることに注意してください。 つまり、@rustbot はプレースホルダーとして自身を割り当て、先頭のコメントを編集して、 その Issue が現在あなたに割り当てられていることを反映します。

Issue から自分の割り当てを解除したい場合、@rustbot には別のコマンドがあります。

@rustbot release-assignment

Issue のラベル変更

Issue や PR のラベルを変更することも、通常は organization のメンバーに限られています。 しかし、@rustbot を使うと、いくつかの制限はあるものの、自分で Issue のラベルを 変更できます。これは主に次の2つのケースで役立ちます。

Issue トリアージの支援: Rust の Issue トラッカーには、執筆時点で 5,000 件を超える 未解決の Issue があるため、ラベルは可能な限り整理された状態を保つために私たちが持つ 最も強力なツールです。Issue をトリアージするために Issue トラッカーで何時間も費やす 必要はありませんが、Issue を開いた場合、自分で行うことに不安がなければ、自由に ラベルを付けてかまいません。

PR のステータスの更新: 私たちは PR のステータスを反映するために「ステータスラベル」を 使用しています。たとえば、あなたの PR にマージコンフリクトがある場合、自動的に S-waiting-on-author が割り当てられ、PR をリベースするまでレビュー担当者はレビューしない 可能性があります。ブランチをリベースしたら、S-waiting-on-author ラベルを削除し、 S-waiting-on-review を追加し直すために、自分でラベルを変更する必要があります。 この場合、@rustbot コマンドは次のようになります。

@rustbot label -S-waiting-on-author +S-waiting-on-review

このコマンドの構文はかなり緩やかなので、このコマンド呼び出しには他にもバリエーションが あります。ラベルを更新するためのショートカットもあり、たとえば @rustbot ready は 上記のコマンドと同じことを行います。 詳細については、ラベル付けに関するドキュメントページショートカットを参照してください。

その他のコマンド

@rustbot に何ができるのかを知りたい場合は、そのドキュメントを確認してください。 これは bot のリファレンスとして意図されており、bot がアップグレードされるたびに最新の状態に 保たれるべきものです。

@rustbot は Release チームによってメンテナンスされています。既存のコマンドに関する フィードバックや新しいコマンドの提案がある場合は、遠慮なく Zulip で連絡するか、triagebot リポジトリに Issue を作成してください。

ウォークスルー: 典型的なコントリビューション

バグの修正、パフォーマンスの改善、機能設計の支援、既存機能へのフィードバックの提供など、Rust コンパイラにコントリビュートする方法は_非常にたくさん_あります。 この章は、その表面をなぞるものですらありません。 代わりに、新機能の設計と実装を順を追って説明します。 ここで説明する手順やプロセスのすべてが、すべてのコントリビューションに必要なわけではありません。 必要に応じて、その点も指摘するようにします。

一般に、コントリビューションに関心があるものの、どこから始めればよいかわからない場合は、遠慮なく質問してください!

概要

この章で説明する機能は、マクロ用の ? Kleene 演算子です。 基本的には、次のようなものを書けるようにしたいと考えています。

macro_rules! foo {
    ($arg:ident $(, $optional_arg:ident)?) => {
        println!("{}", $arg);

        $(
            println!("{}", $optional_arg);
        )?
    }
}

fn main() {
    let x = 0;
    foo!(x); // OK! "0" を出力する
    foo!(x, x); // OK! "0 0" を出力する
}

つまり基本的に、マクロ内の $(pat)? マッチャーは、他の正規表現構文と同様に、「このパターンは 0 回または 1 回出現できる」という意味です。

アイデアから安定版 Rust の機能に至るまでには、いくつもの手順がありました。 以下は簡単な一覧です。 これらについては、以下で順番に見ていきます。 先ほど述べたように、これらすべてがあらゆる種類のコントリビューションに必要なわけではありません。

  • アイデアの議論/Pre-RFC Pre-RFC は、機能の初期ドラフトまたは設計に関する議論です。 この段階は、設計空間を少し具体化し、 アイデアに関するさまざまな利点や問題点を把握することを目的としています。 より広い読者に提示する前に、自分のアイデアについて早期のフィードバックを得る優れた方法です。 元の議論はこちらで確認できます。
  • RFC これは、検討のために自分のアイデアをコミュニティへ正式に提示する段階です。 RFC はこちらで確認できます。
  • 実装 自分のアイデアをコンパイラに不安定な形で実装します。 元の実装はこちらで確認できます。
  • 場合によっては反復/改良 コミュニティが nightly コンパイラや std であなたの 機能を使って経験を積むにつれて、調整が必要になる可能性のある設計上の選択について 追加のフィードバックが寄せられることがあります。 この特定の機能は、いくつもの反復経ました
  • 安定化 機能が十分に成熟すると、Rust チームメンバーが 安定化を提案することがあります。 合意があれば、それで完了です。
  • ひと休み あなたの機能は、安定版 Rust の機能になりました!

Pre-RFC と RFC

注: 一般に、Rust やそのエコシステムに対する_新しい_機能や実質的な 変更を提案するのでなければ、RFC プロセスに従う必要はありません。 代わりに、実装へ直接進むことができます。

RFC を開くべきタイミングに関する公式ガイドラインはこちらで確認できます。

RFC は、提案する機能や変更を詳細に説明する文書です。 誰でも RFC を書くことができます。 プロセスは Rust チームメンバーを含め、全員に対して同じです。

RFC を開くには、GitHub 上の rust-lang/rfcs リポジトリで PR を開きます。 詳細な手順は README で確認できます。

RFC を開く前に、自分のアイデアを「具体化」するための調査を行うべきです。 性急に提案された RFC は、受け入れられない傾向があります。 一般には、動機、影響、欠点、および他の機能との潜在的な相互作用について、十分な説明があるべきです。

それが大変そうに聞こえるなら、実際に大変だからです。 でも心配はいりません! コンパイラハッカーでなくても、pre-RFC を行うことで有益なフィードバックを得ることができます。 これは、そのアイデアに関する_非公式な_議論です。 これを行うのに最適な場所は internals.rust-lang.org です。 投稿は特定の構成に従う必要はありません。 まとまったアイデアである必要すらありません。 通常は大量のフィードバックが得られ、それを取り入れて優れた RFC を作成できます。

(もう 1 つの実践的なヒント: 関連する過去のアイデアについて、RFC リポジトリと internals を検索してみてください。 多くの場合、アイデアはすでに検討されており、 却下されているか、後で再試行するために延期されています。 これにより、あなた自身と他の全員の時間を節約できます)

この例の場合、pre-RFC スレッドの参加者が 構文上の曖昧さと潜在的な解決策を指摘しました。 また、全体的なフィードバックは好意的に見えました。 このケースでは議論はかなり早く収束しましたが、 アイデアによっては、はるかに多くの議論が行われることもあります(たとえば、なんと 684 件ものコメントを受け取ったこの RFC を参照してください!)。 そのような場合でも、落胆しないでください。 それはコミュニティがあなたのアイデアに関心を持っているという意味ですが、おそらく何らかの調整が必要なのです。

私たちの ? マクロ機能に関する RFC も、RFC スレッドでいくらか議論されました。 ほとんどの RFC と同様に、議論だけでは答えられない質問がいくつかありました。 判断するには、その機能を使用した経験が必要だったのです。 そのような質問は、RFC の「未解決の質問」セクションに記載されています。 また、RFC の議論の過程では、議論の流れを反映するために、RFC 文書自体を更新したくなることもおそらくあるでしょう(たとえば、新しい代替案や過去の取り組みを追加したり、提案自体の一部を変更することにしたりする場合があります)。

最終的に、議論が合意に達し、少し落ち着いてきたように見えると、 Rust チームメンバーが、3 つの可能な処分のいずれかを伴って「最終コメント期間」(FCP)へ進むことを提案する場合があります。 これは、その人が、適切なチームの他のメンバーに RFC をレビューしてコメントしてほしいと考えていることを意味します。 さらに議論が続く場合があり、その結果、さらなる変更や未解決の質問が追加されることがあります。 ある時点で全員が満足すると、RFC は FCP に入ります。これは、 人々が異議を提起できる最後の機会です。 FCP が終了すると、その処分が採択されます。 3 つの可能な処分は次のとおりです。

  • Merge: 機能を受け入れる。 こちらが、私たちの ? マクロ機能をマージする提案です。
  • Close: この機能は、現在の形では Rust に適していない。 あなたの RFC がこのようになっても落胆しないでください。また、個人的に受け止めないでください。 これはあなた自身への評価ではなく、Rust が 別の方向へ進むというコミュニティの決定です。
  • Postpone: この方向に進むことへの関心はあるが、現時点では行わない。 これは多くの場合、該当する Rust チームに、その機能をプロセスを通じて 安定化まで導く余力がないために起こります。 多くの場合、その機能がチームのロードマップに合わない場合がこれに該当します。 延期されたアイデアは、後で再検討されることがあります。

RFC がマージされると、PR は RFCs リポジトリにマージされます。 新しい tracking issuerust-lang/rust リポジトリに作成され、その機能の進捗を追跡し、 未解決の質問、実装の進捗、ブロッカーなどについて議論します。 こちらが、私たちの ? マクロ機能に関する tracking issue です。

Experimental RFC (eRFC)

eRFC は RFC プロセスの一種で、高レベルの必要性は明確だが、 設計空間が大きすぎて詳細な仕様を最初から確定できない複雑な機能に使われます。 最終的な設計を提供する代わりに、eRFC は一定期間の積極的な実験を認可するための 高レベルな戦略を概説します。 これにより、チームは feature gate の背後で機能を実装し、 実践的なデータを収集できます。そのデータは、その後の安定化に向けた正式な RFC に反映されます。 このプロセスは coroutines のような主要機能で使われましたが(RFC 2033 を参照)、 明示的な “eRFC” ラベルは今日ではほとんど使われていません。 現在このプロジェクトでは一般に、初期バージョンに対して標準の RFC を承認し、最終的な安定化の前に nightly channel を通じて反復することが好まれています。

実装

コンパイラに変更を加えるには、rust-lang/rust リポジトリに対して PR を開きます。

機能、変更、バグ修正、改善によっては、実装は 比較的簡単な場合もあれば、大きな取り組みになる場合もあります。 経験豊富なコンパイラ開発者に、いつでも助けやメンターシップを求めることができます。 また、あなたの機能を実装するのがあなた自身である必要はありません。 ただし、自分で実装しない場合、誰か他の人が実装するまでにしばらくかかる可能性があることは覚えておいてください。

? マクロ機能については、私はコンパイラにおける マクロ展開の関連部分を理解する必要がありました。 個人的には、コード内のコメントを改善することは、自分がそれを理解しているかを 確認するうえで役立つ方法だと思っていますが、 望まないのであればそうする必要はありません。

その後、RFC に記述されているとおりに、元の機能を実装しました。 新しい機能が実装されると、それは feature gate の背後に置かれます。つまり、 その機能を使うには #![feature(my_feature_name)] を使用する必要があります。 feature gate は、その機能が安定化されると削除されます。

ほとんどのバグ修正や改善には feature gate は必要ありません。 単に変更や改善を行うことができます。

rust-lang/rust で PR を開くと、bot があなたの PR をレビュー担当者に割り当てます。 一緒に作業している特定の Rust チームメンバーがいる場合は、 スレッドに r? @reviewer-github-id(例: r? @eddyb)というコメントを残すことで、そのレビュー担当者をリクエストできます。誰をリクエストすればよいかわからない場合は、 誰もリクエストしないでください。 bot が、変更したファイルに基づいて自動的に誰かを割り当てます。

レビュー担当者は、あなたの PR を承認する前に変更を求めることがあります。また、コメントを残した後に PR に “S-waiting-on-author” ラベルを付けることがあります。これは、要求された変更をあなたが行うまで PR がブロックされていることを意味します。 変更の反復作業が完了したら、@rustbot ready というコメントを残すことで、 PR を再び S-waiting-on-review としてマークできます。これにより、 S-waiting-on-author ラベルが削除され、S-waiting-on-review ラベルが追加されます。

質問したり、理解できないことや同意できないことについて議論したりしてかまいません。 ただし、Rust チームの誰かが承認しない限り、PR はマージされないことを認識してください。 レビュー担当者が r=me after fixing ... のようなコメントを残した場合、それはそのレビュー担当者が PR を承認しており、 軽微な問題を修正した後に @bors r=reviewer-github-id(例: @bors r=eddyb)というコメントでマージできることを意味します。 r=someone には権限が必要であり、r=someone とコメントしたときに bors が “🔑 Insufficient privileges…” のように言う場合があることに注意してください。 その場合は、レビュー担当者にあなたの PR を再確認してもらうよう依頼する必要があります。

レビュー担当者が PR を承認すると、それは @bors というさらに別の bot のキューに入ります。 @bors は CI のビルドおよびマージキューを管理します。 あなたの PR が @bors キューの先頭に到達すると、@bors は GitHub Actions 上であなたの PR に対して すべてのテストを実行し、マージを試します。 これが完了するには多くの時間がかかります。 すべてのテストに合格すると、PR はマージされ、次の nightly コンパイラの一部になります!

レビュー過程で一部の PR に起こり得ることがいくつかあります

  • 変更が十分に大きい場合、レビュー担当者は PR に対して FCP を要求することがあります。 これにより、該当するチームのすべてのメンバーが変更をレビューする機会を得ます。
  • 変更が破壊的変更を引き起こす可能性がある場合、レビュー担当者は crater の実行を要求することがあります。 これは、あなたの変更を含むコンパイラをコンパイルし、その変更済みコンパイラで crates.io 上のすべての crate のコンパイルを試みます。 これは、エコシステムの大きな部分に影響するコンパイラの挙動変更を導入したかどうかを 確認するための優れたスモークテストです。
  • あなたの PR の diff が大きい場合やレビュー担当者が忙しい場合、先にマージされてしまった他の PR と マージコンフリクトが発生することがあります。 通常の git の手順を使って、これらのマージコンフリクトを修正する必要があります。

新機能やそれに類するものを行っていない場合(たとえば バグを修正している場合)は、これで終わりです! ご貢献ありがとうございます :)

実装を洗練する

nightly で新機能を使う経験が蓄積されるにつれて、軽微な変更が 提案されたり、未解決の質問が解決されたりすることがあります。 更新や変更は、上で説明したように、他の変更を実装する場合と同じプロセスを経ます (つまり、PR を提出し、レビューを受け、@bors を待つ、などです)。

一部の変更は、FCP と Rust チームメンバーによるレビューを必要とするほど大きい場合があります。

? マクロ機能については、元の実装後にいくつかの異なる反復を経ました: 1, 2, 3. その過程で、? は区切り記号を取るべきではないと判断しました。これは以前、RFC に未解決の問題として記載されていたものです。 また、曖昧性解消の戦略も変更しました。? を他の繰り返し演算子(例: +*)の区切りトークンとして使用できる機能を削除することにしました。ただし、これは破壊的変更であるため、エディション境界を越えて行うことにしました。 したがって、この新機能は 2018 エディションでのみ有効にできます。元の RFC からのこれらの差異には、別の FCP が必要でした。

安定化

最後に、この機能が nightly でしばらく試用された後、言語チームのメンバーが これを安定化する動議を提出しました

安定化レポート を作成する必要があり、そこには以下を含めます。

  • 振る舞いの簡単な説明と RFC からの差異
  • どのエディションが影響を受けるか、およびその影響
  • 興味深い側面を示すいくつかのテストへのリンク

この機能の安定化レポートはこちらです。

その後、機能ゲートを削除し、機能をデフォルトで(2018 エディションで)有効にするために、PR が作成されます。 この機能についての注記が Release notes に追加されます。

機能を安定化する手順は、機能の安定化で確認できます。

新しい言語機能の実装

コンパイラに新しい重要な機能を実装したい場合は、 すべてが円滑に進むように、このプロセスを通る必要があります。

注: このセクションは言語機能を対象としており、別のプロセスを使用するライブラリ機能は対象外です。

言語への変更を提案する方法については、Rust Language Design Team の手順も参照してください。

@rfcbot FCP プロセス

変更が小さく、議論の余地がなく、破壊的変更ではなく、 stable の言語に対してユーザーが観測できる形で影響せず、新しい不安定機能も追加しない場合は、 単に PR を書き、そのコード部分に詳しい人から r+ を得るだけで実施できます。 しかし、そうでない場合は、さらに行うべきことがあります。 コンパイラ内部の作業であっても、 チームの他のメンバーから合意を得ずに議論のある変更を押し通すのはよくありません (「分散システム」という意味では、自分の知らないものを壊さないようにするためであり、 社会的な意味では、PR 上の争いを避けるためです)。

チームの合意が必要な変更については、 最終コメント期間(FCP)を提案するプロセスを使用します。 関連するチームに所属していない場合(したがって @rfcbot の権限がない場合)は、 所属している人に開始を依頼してください。 その人自身に懸念がない限り、開始してくれるはずです。

FCP プロセスが必要なのは、合意が必要な場合だけです – 変更に対して合意を要求するプロセスがなく、 誰も問題にしないと思うなら、r+ だけに頼っても問題ありません。 たとえば、 コンパイラ開発や標準ライブラリでの使用のために、 予約済みのコンパイラ内部用 rustc_ 名前空間にある不安定なコマンドラインフラグや 属性を追加または変更することは、 nightly エコシステムで広く使われることが想定されない限り、FCP なしで問題ありません。 一部のチームは、このようなシナリオで使用する、より軽量なプロセスを持っています。 たとえば、 コンパイラチームは、完全な合意を必要とせずに支持とフィードバックを得る軽量な方法として、 Major Change Proposal(MCP)を提出することを推奨しています。

FCP を提案するために、実装が r+ を得られるほど完全に準備できている必要はありませんが、 少なくとも概念実証があるのは、一般的によい考えです。 そうすることで、人々があなたの話している内容を確認できます。

FCP が提案されると、チームの全メンバーがその FCP を承認する必要があります。 全員が承認した後、 10 日間の「最終コメント期間」(名前の由来です)があり、その間は誰でもコメントできます。 懸念が提起されなければ、その PR/issue は FCP の承認を得ます。

機能を書く際の実務

機能を動作する形で実装するために、 いくつかの「実務上の」手続きを通る必要があるかもしれません。

警告サイクル

場合によっては、機能やバグ修正が、いくつかのエッジケースで既存のプログラムを壊す可能性があります。 その場合は、 影響を評価するために crater run を実施し、必要に応じて将来互換性 lint を追加することになるでしょう。 これは、エディションで制限された lint で使われるものと似ています。

安定性

私たちは Rust の安定性を重視しています。 stable で動作し実行できるコードは、(ほとんどの場合)壊れるべきではありません。 そのため、 チームの合意とコードレビューだけで機能を世に出したくはありません - nightly でその機能を使用する実際の経験を得たいと考えていますし、 その経験に基づいて機能を変更したい場合もあります。

それを可能にするために、 ユーザーが誤ってその新機能に依存しないようにする必要があります - そうしなければ、 特に実験に時間がかかったり遅延したりして、その機能がリリース列車に乗って stable に到達した場合、 事実上 stable になってしまい、 人々のコードを壊さずに変更できなくなってしまいます。

そのための方法として、すべての新機能がフィーチャーゲートされていることを保証します - フィーチャーゲート(#[feature(foo)])を有効にしなければ使用できず、 これは stable/beta コンパイラでは行えません。 技術的な詳細については、コードにおける安定性セクションを参照してください。

最終的に、その機能を使用して十分な経験を得て、必要な変更を行い、 満足できる状態になったら、こちらで説明されている安定化プロセスを使用して世に公開します。 それまでは、その機能は確定したものではありません。 機能のあらゆる部分は変更される可能性があり、機能が完全に書き直されたり削除されたりする可能性もあります。 機能は、不安定なまま長期間変更されなかったというだけで、既得権を得ることはありません。

追跡 issue

不安定機能の状態、 nightly で使用する中で得られた経験、 そして安定化を妨げている懸念を追跡するために、 すべてのフィーチャーゲートには追跡 issue が必要です。 その機能に関連する issue や PR を作成するときは、この追跡 issue を参照し、 機能の進捗に関する更新があるときは、それらを追跡 issue に投稿してください。

承認済み RFC または承認済みの lang experiment の一部である機能については、 その追跡 issue を使用してください。

その他の機能については、その機能の追跡 issue を作成してください。 issue のタイトルは “Tracking issue for YOUR FEATURE” にする必要があります。 “Tracking Issue” issue テンプレートを使用してください。

Lang experiment

コンパイラに取り込まれるためには、 言語に対してユーザーから見える影響を持つ機能(不安定なものを含む)は、 承認済み RFC の一部であるか、承認済みの lang experiment でなければなりません。

新しい lang experiment を提案するには、 動機と意図した解決策を説明する issue を rust-lang/rust に開いてください。 受け入れられた場合、この issue はその実験の追跡 issue になるため、 これらの他の詳細も含めつつ、追跡 issue のテンプレートを使用してください。 その issue を lang チームに nominate し、@rust-lang/lang@rust-lang/lang-advisors を CC してください。 実験が承認されると、追跡 issue は B-experimental としてマークされます。

lang experiment に関連するフィーチャーフラグは、 その機能の RFC が承認されるまで incomplete としてマークされなければなりません。

コードにおける安定性

新しい不安定機能を実装するためには、以下の手順に従う必要があります。

  1. トラッキングイシューを開くか、特定します。 受理済み RFC または承認済みの lang experiment の一部であるフィーチャーについては、 そのトラッキングイシューを使用します。

    トラッキングイシューに C-tracking-issue と関連する F-feature_name ラベルを付けます (必要であればそのラベルを追加します)。

  2. フィーチャーゲートの名前を選びます(RFC の場合は、RFC 内の名前を使用します)。

  3. rustc_span/src/symbol.rsSymbols {...} ブロックにフィーチャー名を追加します。

    このブロックはアルファベット順でなければならないことに注意してください。

  4. rustc_feature/src/unstable.rs の unstable な declare_features ブロックにフィーチャーゲート宣言を追加します。

    /// フィーチャーの説明
    (unstable, $feature_name, "CURRENT_RUSTC_VERSION", Some($tracking_issue_number))

    まだトラッキングイシューを開いていない場合 (たとえば、そのフィーチャーが受け入れられそうかどうかについて最初のフィードバックを得たい場合)は、 一時的に None を使用できます。ただし、PR がマージされる前に必ず更新してください!

    例:

    /// ASCII を超える識別子の定義を許可します。
    (unstable, non_ascii_idents, "CURRENT_RUSTC_VERSION", Some(55467)),

    フィーチャーは incomplete としてマークできます。 その type を incomplete に設定することで、 デフォルトで警告となる incomplete_features lint を発生させます。

    /// deref パターンを許可します。
    (incomplete, deref_patterns, "CURRENT_RUSTC_VERSION", Some(87121)),

    lang experiment に関連するフィーチャーフラグは、 そのフィーチャーの RFC が受理されるまで incomplete としてマークしなければなりません。

    セマンティックなマージコンフリクトを避けるため、 1.70 や別の明示的なバージョン番号ではなく CURRENT_RUSTC_VERSION を使用します。

  5. フィーチャーゲートが設定されていない限り、新しいフィーチャーを使用できないようにします。 コンパイラ内のほとんどの場所では、 式 tcx.features().$feature_name() を使用して確認できます。

    フィーチャーゲートが設定されていない場合は、 状況に応じて、フィーチャー導入前の挙動を維持するか、エラーを発生させるべきです。 エラーには通常 rustc_session::errors::feature_err を使用するべきです。 エラーを追加する例については、#81015 を参照してください。

    新しい構文を導入するフィーチャーでは、代わりに展開前ゲーティングを使用するべきです。 パース中に新しい構文がパースされたとき、 self.psess.gated_spans.gate(sym::my_feature, span) によって、 その symbol を現在のクレートの GatedSpans に挿入しなければなりません。

    gated spans に挿入した後、 実際にフィーチャーを拒否する rustc_ast_passes::feature_gate::check_crate 関数で その span をチェックしなければなりません。 正確にどのようにゲートするかはフィーチャーの正確な種類によりますが、 ほとんどの場合は gate_all!() マクロを使用することになるでしょう。

  6. フィーチャーゲートなしではそのフィーチャーを使用できないことを保証するテストを、 tests/ui/feature-gates/feature-gate-$feature_name.rs を作成して追加します。 対応する .stderr ファイルは、 ./x test tests/ui/feature-gates/ --bless を実行して生成できます。

  7. unstable book にセクションを追加します。 場所は src/doc/unstable-book/src/language-features/$feature_name.md です。

  8. 新しいフィーチャーについて多くのテストを書きます。できれば tests/ui/$feature_name/ に置きます。 テストのない PR は受け入れられません!

  9. PR のレビューを受け、マージします。 これで Rust にフィーチャーを実装することに成功しました!

テストの呼びかけ

実装が完了すると、 そのフィーチャーは nightly ユーザーが利用できるようになりますが、まだ stable Rust の一部ではありません。 これは、Rust のメインブログにブログ記事を書き、 「テストの呼びかけ」を行うのに適したタイミングです。

過去のそのようなブログ記事には次のものがあります。

  1. GAT 安定化に向けた取り組み
  2. Rust 2024 における impl Trait の変更
  3. Async Closures MVP: テストの呼びかけ!

あるいは、This Week in Rust にはこのためのセクションがあります。 これが使用された例の 1 つは次のとおりです。

どの選択肢を選ぶかは、その言語変更がどれほど重要かに依存するかもしれません。 ただし、This Week in Rust のセクションは、 Rust のメインブログでの専用投稿よりも目立ちにくい可能性があることに注意してください。

仕上げ

ユーザーに洗練された体験を提供するということは、単に rustc にフィーチャーを実装する以上のことを意味します。 私たちは、提供しているすべてのツールとリソースについて考える必要があります。 この作業には次が含まれます。

  • 言語フィーチャーを Rust Reference に文書化する。
  • 新しい構文がある場合は、それをフォーマットするように rustfmt を拡張する(該当する場合)。
  • rust-analyzer を拡張する(該当する場合)。 この作業の範囲は言語フィーチャーの性質によって異なる場合があります。 一部のフィーチャーは、完全な サポートを待つ必要がないためです。
    • 言語フィーチャーが rust-analyzer でサポートが実装される前に存在するだけで ユーザー体験を悪化させる場合、 lang チームがブロッキングな懸念を提起する可能性があります。
    • そのような例には、rust-analyzer がパースできない新しい構文や、 誤った診断につながる、rust-analyzer が理解できない型推論の変更などが含まれるかもしれません。

安定化

フィーチャーライフサイクルの最終ステップは安定化です。 これは、そのフィーチャーがすべての Rust ユーザーに利用可能になるときです。 この時点では、 後方互換性のない変更は一般的に許可されなくなります (詳細については、lang チームの定義済み semver ポリシーを参照してください)。 安定化についてさらに詳しく知るには、安定化ガイドを参照してください。

安定性保証

このページでは、私たちの安定性保証の概要を説明します。

RFC

ブログ記事

rustc-dev-guide のリンク

適用除外

私たちのインフラストラクチャの一部が他者によって使用できる場合でも、それは依然として 内部的なものと見なされ、安定性保証はありません。以下は、安定性保証のない コンポーネントの網羅的ではないリストです。

  • remote-test-client / remote-test-server によって使用される CLI と環境変数

安定性属性

このセクションでは、rustc の標準ライブラリで、安定した API が内部的に不安定な API を使用できるようにする安定性属性と仕組みについて説明します。

NOTE: このセクションは ライブラリ 機能についてのものであり、言語 機能についてのものではありません。 言語機能を安定化する手順については、Stabilizing Features を参照してください。

unstable

#[unstable(feature = "foo", issue = "1234", reason = "lorem ipsum")] 属性は、アイテムを不安定であると明示的にマークします。 “unstable” としてマークされたアイテムは、nightly コンパイラであっても、そのクレートに対応する #![feature] 属性がなければ使用できません。 この制限はクレート境界を越える場合にのみ適用され、不安定なアイテムはそれを定義しているクレート内では使用できます。

issue フィールドは、関連する GitHub の issue number を指定します。 このフィールドは必須であり、すべての不安定機能には関連する追跡 issue があるべきです。 適切な値がないまれなケースでは、issue = "none" が使用されます。

unstable 属性はすべてのサブアイテムに波及し、その属性を再適用する必要はありません。 したがって、これをモジュールに適用すると、そのモジュール内のすべてのアイテムが不安定になります。

機能名を変更する場合は、有用なエラーメッセージを生成するために old_name = "old_name" を追加できます。

特定のサブアイテムを安定にするには、それらに #[stable] 属性を使用します。 安定性の仕組みは、pub の動作と似ています。 非公開モジュールの公開関数を持つことができるのと同様に、不安定なモジュール内に安定した関数を持つことも、その逆も可能です。

以前は、rustc bug により、不安定なモジュール内の安定したアイテムが、その場所では安定版コードから利用可能でした。

2024年9月時点で、[accidentally stabilized paths] を持つアイテムには、それらのパスに依存するコードが壊れるのを防ぐため、`#[rustc_allowed_through_unstable_modules]` 属性が付けられています。

破壊的変更を避けるために必要な場合を除き、この属性をこれ以上のアイテムに追加してはいけません

unstable 属性には soft 値を持たせることもでき、これによりハードエラーではなく、デフォルトで deny となる将来非互換 lint になります。 これは、過去に誤って受け入れられていた bench 属性で使用されています。 これにより、Cargo の lint capping を利用して依存関係を壊すことを防ぎます。

stable

#[stable(feature = "foo", since = "1.420.69")] 属性は、アイテムが安定化済みであることを明示的にマークします。 安定した関数は、その本体内で不安定なものを使用できることに注意してください。

rustc_const_unstable

#[rustc_const_unstable(feature = "foo", issue = "1234", reason = "lorem ipsum")] は、unstable 属性と同じインターフェイスを持ちます。 これは、const fn の const 性が不安定であることをマークするために使用されます。 これはまれなケースでのみ必要です。

  • const fn が不安定な言語機能または intrinsic を使用している場合。 (これに該当する場合、コンパイラがこの属性を追加するよう指示します。)
  • const fn#[stable] であるが、まだ const-stable にすることが意図されていない場合。
  • const-unstable な intrinsic を呼び出すために必要な feature gate を変更する場合。

const 安定性は通常の安定性とは異なり、再帰的です。#[rustc_const_unstable(...)] 関数は、安定版コードから間接的に呼び出すことすらできません。 これは、不安定なコンパイラ実装の成果物が誤って安定版コードに漏れ出したり、不完全な実装の偶発的な癖に縛られたりすることを避けるためです。 このチェックを細かく調整する方法については、下記の rustc_const_stable_indirect 属性と rustc_allow_const_fn_unstable 属性を参照してください。

rustc_const_stable

#[rustc_const_stable(feature = "foo", since = "1.420.69")] 属性は、const fn の const 性が stable であることを明示的にマークします。

rustc_const_stable_indirect

#[rustc_const_stable_indirect] 属性は、#[rustc_const_unstable(...)] 関数に追加することで、#[rustc_const_stable(...)] 関数から呼び出せるようにできます。 これは、その関数が実装の観点では安定版に対応する準備ができていること(つまり、不安定なコンパイラ機能を使用していないこと)を示します。その関数がまだ const-stable でない唯一の理由は API 上の懸念です。

これは、コンパイラ内で const 呼び出しが合成される lang item にも追加するべきです。これにより、それらの呼び出しが再帰的な const 安定性ルールを回避しないようにします。

rustc_intrinsic_const_stable_indirect

intrinsic において、この属性はその intrinsic を「公開された安定関数によって使用される準備ができている」とマークします。 その intrinsic に rustc_const_unstable 属性がある場合は、削除するべきです。 intrinsic にこの属性を追加するには、t-lang と wg-const-eval の承認が必要です!

rustc_default_body_unstable

#[rustc_default_body_unstable(feature = "foo", issue = "1234", reason = "lorem ipsum")] 属性は、unstable 属性と同じインターフェイスを持ちます。 これは、trait 内のアイテムのデフォルト実装を不安定としてマークするために使用されます。 default-body-unstable なアイテムを持つ trait は、そのようなアイテムに明示的な本体を提供することで安定的に実装できます。または、対応する #![feature] を有効にすることでデフォルト本体を使用できます。

ライブラリ機能の安定化

機能を安定化するには、次の手順に従います。

  1. @T-libs-api メンバーに、追跡 issue で FCP を開始するよう依頼し、FCP が完了するまで待ちます(disposition-merge を伴う)。
  2. #[unstable(...)]#[stable(since = "CURRENT_RUSTC_VERSION")] に変更します。
  3. この API のテストまたは doc-test から #![feature(...)] を削除します。 その機能がコンパイラまたはツールで使用されている場合は、そこからも削除します。
  4. これが const fn の場合は、#[rustc_const_stable(since = "CURRENT_RUSTC_VERSION")] を追加します。 あるいは、これをまだ const 安定化する想定ではない場合は、新しい feature gate(新しい追跡 issue を伴う)用に #[rustc_const_unstable(...)] を追加します。
  5. rust-lang/rust に対して PR を開きます。
    • 適切なラベルを追加します: @rustbot modify labels: +T-libs-api
    • 追跡 issue にリンクし、“Closes #XXXXX” と記載します。

機能を安定化する例としては、tracking issue #81656 with FCP と、関連する implementation PR #84642 を参照できます。

allow_internal_unstable

マクロとコンパイラの糖衣構文展開は、その本体を呼び出し元に公開します。 標準ライブラリのマクロ内で不安定なものを使用できない問題を回避するために、指定された機能を安定したマクロ内で使用できるようにする #[allow_internal_unstable(feature1, feature2)] 属性があります。

マクロが const コンテキストで使用され、#[rustc_const_unstable(...)] 関数への呼び出しを生成する場合、allow_internal_unstable があっても、それは依然として拒否されることに注意してください。 そのマクロが再帰的な const 安定性チェックを誤って回避できないようにするため、関数に #[rustc_const_stable_indirect] を追加してください。

rustc_allow_const_fn_unstable

上で説明したように、安定した const fn の内部では、不安定な const 機能は間接的であっても許可されません。 しかし、ときには、ある機能が安定化されることは分かっているが、それがいつなのかは分からない、あるいは安定した(ただし、たとえば実行時には遅い)回避策があるため、不安定機能を破棄した場合でも常に何らかの安定版にフォールバックできる、という場合があります。 そのような場合、[rustc_allow_const_fn_unstable(feature1, feature2)] 属性を使用して、安定した(または間接的に安定した)const fn の本体内で一部の不安定機能を許可できます。

また、const fn には、実行時に呼び出した場合とコンパイル時に呼び出した場合で同じように振る舞う必要があるという不変条件があり、それを維持するよう注意する必要があります(こちらのブログ記事も参照)。 これは、たとえばメモリアドレスを整数に変換するような const fn を作成してはならないことを意味します。 なぜなら、もののアドレスは非決定的であり、コンパイル時には不明であることが多いからです。

いずれかの const fnrustc_allow_const_fn_unstable 属性を追加する場合は、必ず @rust-lang/wg-const-eval に通知してください。

staged_api

stable または unstable 属性を使用するクレートは、そのクレートに #![feature(staged_api)] 属性を含めなければなりません。

deprecated

標準ライブラリにおける非推奨化は、ユーザーコードにおける非推奨化とほぼ同じです。 項目に #[deprecated] を使用する場合は、stable または unstable 属性も付与されていなければなりません。

deprecated は次の形式です。

#[deprecated(
    since = "1.38.0",
    note = "explanation for deprecation",
    suggestion = "other_function"
)]

suggestion フィールドは任意です。 指定する場合は、警告を修正するための機械的に適用可能な提案として使用できる文字列であるべきです。 これは通常、識別子の名前が変更されたものの、その他の大きな変更が不要な場合に使用されます。 suggestion フィールドを使用する場合は、クレートルートに #![feature(deprecated_suggestion)] が必要です。

ユーザーコードとのもう 1 つの違いは、since フィールドが実際に現在の rustc のバージョンに対してチェックされることです。 since が将来のバージョンである場合、deprecated_in_future lint がトリガーされます。これはデフォルトでは allow ですが、標準ライブラリの大部分では #![warn(deprecated_in_future)] によって警告に引き上げています。

unstable_feature_bound

#[unstable_feature_bound(foo)] 属性は、#[unstable] 属性と併用して、安定した型および安定したトレイトの impl を不安定としてマークするために使用できます。 std/core では、#[unstable_feature_bound(foo)] が注釈された項目は、同じく #[unstable_feature_bound(foo)] が注釈された別の項目からのみ使用できます。 std/core の外部では、#[unstable_feature_bound(foo)] を持つ項目を使用するには、クレートに #![feature(foo)] 属性を指定してその機能を有効化する必要があります。

現在、#[unstable_feature_bound] を注釈できる項目は次のとおりです。

  • impl
  • 自由関数
  • トレイト

名前変更された機能と削除された機能

不安定機能は名前変更および削除されることがあります。 機能の名前を変更する場合は、#[unstable] 属性に old_name = "old_name" を追加できます。 機能を削除する場合は、削除された機能のユーザーに分かりやすいエラーメッセージを生成するために、#!unstable_removed(feature = "foo", reason = "brief description", link = "link", since = "1.90.0") 属性を使用するべきです。

link フィールドは、GitHub issue、コメント、PR など、その機能の削除に関する最も関連性の高い情報へリンクするために使用できます。

安定化のリクエスト

注記: このページは言語機能の安定化について説明しています。 ライブラリ機能の安定化については、ライブラリ機能の安定化を参照してください。

不安定機能が十分にテストされ、未解決の懸念がなくなったら、誰でもその安定化を推進できますが、その機能に携わってきた人々を関与させるのが賢明です。 以下の手順に従ってください。

必要であれば RFC を書く

その機能が lang experiment の一部であった場合、lang チームは一般に、安定化の前にまず RFC を受理したいと考えます。

ドキュメント PR

その機能は Unstable Book に文書化されている可能性があり、これは src/doc/unstable-book にあります。 feature gate のページが存在する場合は削除してください。 そのドキュメントの有用な部分を他の場所に統合してください。

ドキュメントの更新が必要になる可能性がある場所は次のとおりです。

  • The Reference: これは完全な詳細を含めて更新しなければならず、安定化をマージできるようになる前に、lang-docs チームのメンバーが PR をレビューして承認しなければなりません。
  • 判断に迷う場合は、このリポジトリで issue を開いてください。そこで議論できます。
  • 標準ライブラリドキュメント: これは必要に応じて更新されます。 言語機能では多くの場合これは不要ですが、? が言語に追加されたときのように、慣用的な例の書き方を変える機能であれば、ライブラリドキュメント内のこれらを更新することが重要です。 また、標準ライブラリ内のキーワードドキュメントと ABI ドキュメントも確認してください。これらは言語の変更に応じて更新が必要になる場合があります。

上記のリポジトリについて、この新機能に関するドキュメントを更新する PR を準備してください。 これらのリポジトリのメンテナーは、安定化プロセス全体が完了するまで、これらの PR をオープンのままにしておきます。 その間に、次の手順に進むことができます。

安定化レポートを書く

このリポジトリにあるテンプレートを使用して安定化レポートを作成してください。

安定化レポートは以下を要約します。

  • RFC が受理されてからの主要な設計上の決定と逸脱。これには、FCP された、または言語チームによって他の形で受理された決定と、lang チームに初めて提示されるものの両方が含まれます。
    • 多くの場合、最終的に安定化される言語機能には、元の RFC から大きな設計上の逸脱があります。 それは問題ありませんが、これらの逸脱は明確に示し、慎重に説明しなければなりません。
  • RFC が受理されてから行われた作業。言語機能の前進を支えた主要なコントリビューターを明記します。

Stabilization Template には、この機能と lang のサブチーム(例: types、opsem、lang-docs など)との関連を明らかにし、見落とされがちな項目を特定することを目的とした一連の質問が含まれています。

安定化レポートは通常、安定化 PR のメインコメントとして投稿されます(次のセクションを参照)。

安定化 PR

機能はそれぞれ異なり、このガイドで説明している内容を超える手順が必要になる場合もあります。

lang チームが安定化を検討する前に、その機能を説明する The Reference への完全な PR が存在していなければならず、安定化 PR がマージされる前に、その PR は lang-docs チームによってレビューされ、承認されていなければなりません。

feature-gate 一覧の更新

不安定な feature-gate の中央一覧は compiler/rustc_feature/src/unstable.rs にあります。 declare_features! マクロを検索してください。 安定化しようとしている機能のエントリがあるはずです。 たとえば次のようなものです(rust-lang/rust#32409 から引用)。

// pub(restricted) 可視性 (RFC 1422)
(unstable, pub_restricted, "CURRENT_RUSTC_VERSION", Some(32409)),

上記の行は compiler/rustc_feature/src/accepted.rs に移動する必要があります。 declare_features! 呼び出し内のエントリはソートされているため、正しい場所を見つけてください。 完了すると、次のようになります。

// pub(restricted) 可視性 (RFC 1422)
(accepted, pub_restricted, "CURRENT_RUSTC_VERSION", Some(32409)),
// これを変更したことに注意

(過去の変更のファイル内でバージョン番号を見かけることになりますが、安定化が行われると予想する rustc バージョンを入れるべきではありません。代わりに CURRENT_RUSTC_VERSION を使用してください。)

feature-gate の既存の使用箇所の削除

次に、コードベース内で機能文字列(この場合は pub_restricted)を検索し、それが出現する場所を見つけます。 std および任意の rustc クレート内の #![feature(XXX)] の使用箇所を (これには library/ および compiler/ 配下のテストフォルダーが含まれますが、トップレベルの tests/ は含まれません) #![cfg_attr(bootstrap, feature(XXX))] に変更してください。 これにより、feature-gate は stage1 にのみ含まれます。stage1 は現在のベータを使用してビルドされます(これは、その機能が現在のベータではまだ不安定であるために必要です)。

また、テスト(例: tests/ 配下)からそれらの文字列を削除してください。feature-gate を特に対象とするテスト(つまり、その機能を使うには feature-gate が必要であることだけをテストし、それ以外は何もテストしないもの)がある場合は、そのテストを単に削除してください。

その機能の使用に feature-gate を要求しない

最も重要なのは、feature-gate が存在しない場合にエラーを示すコードを削除することです(その機能は現在では安定していると見なされるためです)。 その機能が何らかの新しい構文を使用するために検出できる場合、そのコードの一般的な場所は compiler/rustc_ast_passes/src/feature_gate.rs です。 たとえば、次のようなコードが見つかるかもしれません。

gate_all!(pub_restricted, "`pub(restricted)` 構文は実験的です");

gate_all! マクロは、pub_restricted 機能が有効になっていない場合にエラーを報告します。 pub(restricted) が安定した現在では、これは不要です。

より微妙な機能については、次のようなコードが見つかる場合があります。

if self.tcx.features().async_fn_in_dyn_trait() { /* XXX */ }

この pub_restricted フィールド(機能にちなんで名付けられたもの)は、通常、feature flag が存在しない場合は false、存在する場合は true になります。 そのため、そのフィールドが true であると仮定するようにコードを変換してください。 この場合、それは if を削除し、/* XXX */ だけを残すことを意味します。

if self.tcx.sess.features.borrow().pub_restricted { /* XXX */ }
これは次のようになります
/* XXX */

if self.tcx.sess.features.borrow().pub_restricted && something { /* XXX */ }
 これは次のようになります
if something { /* XXX */ }

チームのノミネート

安定化 PR を開くときは、lang チームとそのアドバイザー(@rust-lang/lang @rust-lang/lang-advisors)、およびその機能に関連するその他のチームを CC してください。例:

  • @rust-lang/types: 型システムとの相互作用について。
  • @rust-lang/opsem: unsafe コードとの相互作用について。
  • @rust-lang/compiler: 実装の堅牢性について。
  • @rust-lang/libs-api: 標準ライブラリ API またはその保証に対する変更について。
  • @rust-lang/lang-docs: これを Reference でどのように文書化すべきかに関する質問について。

安定化レポートとともに安定化 PR が開かれたら、すぐに寄せられるコメントを少し待ってください。 そのようなコメントが「落ち着いて」、その PR を lang チームが検討する準備ができたと感じたら、今後の lang ミーティングで検討される議題に載せるために、PR をノミネートしてください。

あなたが rust-lang organization のメンバーでない場合は、割り当てられたレビュアーに、あなたに代わって関連チームを CC するよう依頼できます。

PR で FCP を提案する

lang チームやその他の関連チームが安定化をレビューし、彼らからの質問があればそれに回答した後、いずれかのチームのメンバーが次のコメントをすることで、安定化の受け入れを提案できます。

@rfcbot fcp merge

十分な数のチームメンバーがレビューすると、その PR は「最終コメント期間」(FCP)に移行します。 新たな懸念が提起されなければ、この期間は完了し、その PR は通常どおり実装レビューを経てマージできます。

安定化のレビューとマージ

安定化において、r+ を与える前に、その PR が次を満たしていることを確認してください。

  • チームが安定化対象として提案した内容、および Reference PR に文書化されている内容と一致していること。
  • 懸念を解決または回避するために、チームが途中で要求すると決定した変更をすべて含んでいること。
  • それ以外の点では、安定化レポート、および関連する RFC や過去の lang FCP に記載されている内容と完全に一致していること。
  • 指定され、安定化が受け入れられ、Reference に文書化されたもの以外の挙動を stable で公開していないこと。
  • これらを説得力をもって示すための十分なテストがあること。
  • lang-docs のメンバーによってレビューおよび承認された Reference への PR が添えられていること。

特に、PR をレビューするときは、lang チームが検討および指定し損ねたユーザーから見える詳細がないか注意してください。 そのようなものを見つけた場合は、それを説明し、lang チームに対して PR をノミネートしてください。

安定化レポートテンプレート

これは何か?

これは、言語機能安定化レポート用のテンプレートです。各質問は、最も頻繁に必要とされる詳細を引き出すことを目的としています。これらの詳細は、レビュー担当者が潜在的な問題を事前に特定するのに役立ちます。テンプレートのすべての部分が、すべての安定化に当てはまるわけではありません。質問が当てはまらない場合は、その理由を簡潔に説明してください。

区切り線より後ろをすべてコピーし、Markdownとして編集してください。各TODOをあなたの回答に置き換えてください。


安定化レポート

概要

この機能が何であり、どのような価値を提供するのかを思い出させてください。この安定化に至るまでの経緯を説明してください。

例として、以下を参照してください。

TODO

追跡:

  • TODO(追跡issueへのリンク。)

Reference PR:

  • TODO(Reference PRへのリンク。)

cc @rust-lang/lang @rust-lang/lang-advisors

安定化されるもの

安定化される各挙動を説明し、今後受理されるようになるコードの短い例を示してください。

#![allow(unused)]
fn main() {
todo!()
}

安定化されないもの

安定化されない機能の部分を説明してください。今後何を行いたい可能性があるのか、またそのためにどのような扉を開いたままにしているのかについて述べてください。安定化しない内容がユーザーに驚きをもたらす可能性がある場合は、特にその点について述べてください。

設計

Reference

Referenceにどのような更新が必要ですか?各PRにリンクしてください。Referenceにこの機能を説明するために必要な内容が欠けている場合は、その点を議論してください。

  • TODO

RFCの履歴

この機能について、どのRFCが受理されていますか?

  • TODO

未解決の質問への回答

RFCによって未解決のまま残された質問は何ですか?それらにはどのように回答されましたか?関連するlangの決定があればリンクしてください。

TODO

RFC後の変更

RFCが受理されて以降、その他にどのようなユーザーから見える変更が発生しましたか?langチームが受理した変更(およびそれらの決定へのリンク)と、この安定化レポートで初めてチームに提示される変更の両方を説明してください。

TODO

重要なポイント

どの決定が最も困難で、安定化される挙動のうちどれが最も議論を呼びましたか?すべての立場における主要な主張を要約し、以前の文書や議論にリンクしてください。

TODO

Nightly拡張

この機能には、まだ不安定なまま残っている拡張がありますか?私たちがそれらに誤ってコミットしていないことを、どのように確認していますか?

TODO

閉じられる扉

この安定化によって、言語に対する将来の変更について、どのような扉が閉じられますか?たとえば、この安定化によって、他のRFC、lang実験、または進行中であることが知られている提案を後で実施することがより困難または不可能になりますか?

フィードバック

テストの呼びかけ

「テストの呼びかけ」は行われましたか?行われた場合、どのようなフィードバックが得られましたか?

TODO

Nightlyでの使用

既知のnightlyユーザーはこの機能を使用していますか?GitHub上でgrepを使って#![feature(FEATURE_NAME)]のインスタンス数を数えると参考になるかもしれません。

TODO

実装

主要部分

実装の主要部分を要約し、コード内の該当箇所や関連するPRへのリンクを示してください。

例として、async closuresの主要部分についての次の内訳を参照してください。

TODO

カバレッジ

この機能のテストカバレッジを要約してください。

この機能の「境界」がどこにあるかを考慮してください。私たちは特に、近接するもののうち正確に何を安定化しないのかについて確信を与えるテストを見ることに関心があります。もちろん、テストはこの機能が機能することを包括的に実証するべきです。よくある間違いが行われた場合や機能が誤って使用された場合に見られる診断を実証することも考えてください。

各テスト内では、テストの目的と、それが実証しようとする不変条件の集合を説明するコメントを先頭に含めてください。これは私たちのレビューにとって大いに助けになります。

テストカバレッジにおける既知または意図的なギャップを説明してください。

テストフォルダおよび個々のテストへのリンクを示し、文脈を説明してください。

TODO

未解決のバグ

この機能に関係する未解決のバグは何ですか?それらを列挙してください。そのいずれかが安定化をブロックするべきですか?その理由、またはそうでない理由を議論してください。

TODO

  • TODO
  • TODO
  • TODO

未解決のFIXME

その機能に関して、コード内にまだどのようなFIXMEが残っており、それらを残しておいて問題ないのはなぜですか?

TODO

ツールの変更

この機能をサポートするために、他のツールにどのような変更を加える必要がありますか。この作業は完了していますか?関連するPRやissueがあればリンクしてください。

  • rustfmt
    • TODO
  • rust-analyzer
    • TODO
  • rustdoc(JSONとHTMLの両方)
    • TODO
  • cargo
    • TODO
  • clippy
    • TODO
  • rustup
    • TODO
  • docs.rs
    • TODO

TODO

破壊的変更

この安定化が既知の破壊的変更を表す場合は、craterレポート、craterレポートの分析、およびこの破壊的変更の影響を受けるエコシステムプロジェクトに対して私たちが作成したすべてのPRにリンクしてください。把握または修正できることの制限について議論してください。

TODO

Craterレポート:

  • TODO

Crater分析:

  • TODO

影響を受けるcrateへのPR:

  • TODO
  • TODO
  • TODO

型システム、opsem

コンパイル時チェック

未定義動作を防ぐために必要な、どのようなコンパイル時チェックが行われますか?

これらのチェックが行われていることを実証するテストにリンクしてください。

TODO

  • TODO
  • TODO
  • TODO

型システムのルール

この機能ではどのような型システムのルールが強制され、それぞれの目的は何ですか?

TODO

デフォルトで健全か?

この機能の実装は、UBを防ぐために特定のチェックを必要としますか?それともデフォルトで健全であり、危険な操作やunsafeな操作を実行するには特定のオプトインが必要ですか?デフォルトで健全でない場合、その根拠は何ですか?

TODO

AMを破るか?

ユーザーはこの機能を使用して未定義動作を導入できますか?または、この機能を使用してRustの抽象化を破り、基礎となるアセンブリレベルの実装を露出させることができますか?該当する場合はその点を説明してください。

TODO

一般的な相互作用

一時値

この機能は、一時値を生成できる新しい式を導入しますか?それらの一時値のスコープは何ですか?

TODO

Drop順序

この機能は、値をどの順序でdropすべきかについて疑問を生じさせますか?ここで行われた決定と、それらが以前の決定とどのように一貫しているかについて述べてください。

TODO

展開前 / 展開後

この機能は、展開前(例: #[cfg(false)]で覆われたコード内)に何を受理すべきかと、展開後に何を受理すべきかについて疑問を生じさせますか?これについてどのような決定が行われましたか?

TODO

Edition hygiene

この機能がeditionでゲートされている場合、トークンのedition hygieneの文脈で、コードを受理するか拒否するかをどのように判断しますか?たとえば、判断にはどのトークンを使用しますか?

TODO

SemVerへの影響

この機能は、ライブラリ作者がマイナーバージョンリリースを行う際に、下流を壊さないよう注意しなければならない新たな方法を生み出しますか?それらを説明してください。これらの新たな危険性は、RFC 1105 に照らして「メジャー」ですか、それとも「マイナー」ですか?

TODO

他の機能の露出

この機能によって、他の不安定な機能の挙動が何らかの形で露出する可能性はありますか?その中で最もリスクが高い機能は何ですか?

TODO

履歴

ここに至った経緯を理解するうえで重要な issue と PR を列挙してください。

  • TODO
  • TODO
  • TODO

謝辞

表彰のため、またそれらの人々に安定化について通知されるように、この機能への貢献者を名前で要約してください。この作業に関わった人の中で、これを今すぐ安定化すべきではないと考えている人はいますか?もしそうであれば、その点について知らせてください。

TODO

未解決項目

まだ完了しておらず、これが安定化される前に完了すべき既知の項目を列挙してください。

  • TODO
  • TODO
  • TODO

機能ゲート

この章は、機能ゲートの追加、削除、変更に関する基本的な支援を提供することを目的としています。

rustc がコンパイラパイプライン内で機能ゲートをどのように適用し、チェックするかについては、 機能ゲートチェックを参照してください。

これは言語機能ゲートに固有の内容であることに注意してください。ライブラリ機能ゲートでは別の 仕組みを使用します。

機能ゲートを追加する

手順については、「新機能を実装する」セクションの“コード内の安定性”を参照してください。

機能ゲートを削除する

機能ゲートを削除するには、次の手順に従ってください。

  1. rustc_feature/src/unstable.rs 内の機能ゲート宣言を削除します。 次のようなものです。

    /// 機能の説明
    (unstable, $feature_name, "$version", Some($tracking_issue_number))
  2. 削除したばかりの機能ゲート宣言を変更したバージョンを rustc_feature/src/removed.rs に追加します。

    /// 機能の説明
    (removed, $old_feature_name, "$version", Some($tracking_issue_number),
     Some("$why_it_was_removed"))

機能ゲートの名前を変更する

機能ゲートの名前を変更するには、次の手順に従ってください(最初の 2 つは、機能ゲートを削除するときに 従う手順と同じです)。

  1. rustc_feature/src/unstable.rs 内の古い機能ゲート宣言を削除します。 次のようなものです。

    /// 機能の説明
    (unstable, $old_feature_name, "$version", Some($tracking_issue_number))
  2. 削除したばかりの古い機能ゲート宣言を変更したバージョンを rustc_feature/src/removed.rs に追加します。

    /// 機能の説明
    /// `$new_feature_name` に名前変更済み
    (removed, $old_feature_name, "$version", Some($tracking_issue_number),
     Some("renamed to `$new_feature_name`"))
  3. 新しい名前の機能ゲート宣言を rustc_feature/src/unstable.rs に追加します。 これは古い宣言と非常によく似たものになるはずです。

    /// 機能の説明
    (unstable, $new_feature_name, "$version", Some($tracking_issue_number))

機能を安定化する

手順については、「機能の安定化」章の“機能ゲート一覧の更新”を参照してください。 単に宣言を更新するだけでなく、追加で行う必要がある手順があります!

コーディング規約

この章では、フォーマット正しさのためのコーディングcrates.io のクレートの使用、およびレビューしやすい PR の構成に関するヒントを扱います。

フォーマットと tidy スクリプト

rustc は Rust の標準コーディングスタイルに移行しつつあります。

ただし、現時点では stable rustfmt は使用していません。特別な設定を持つ 固定されたバージョンを使用しているため、通常の rustfmt とは異なるスタイルになる可能性があります。 したがって、このリポジトリを cargo fmt でフォーマットすることは推奨されません。

代わりに、フォーマットは ./x fmt を使って行うべきです。 後でコンフリクトが減るため、各コミットの前に ./x fmt を実行する習慣をつけるとよいでしょう。

フォーマットは tidy スクリプトによってチェックされます。 これは ./x test を実行したときに自動的に実行され、./x fmt --check で単独でも実行できます。

注: フォーマットとテストスイート

tests/ ディレクトリ配下のほとんどの Rust ソースファイルは、空白に敏感であること、 スナップショットテストの性質、位置に敏感なコメントなどの理由によりフォーマットされていません。

どのテストファイルがフォーマットされないかについては、 https://github.com/rust-lang/rust/blob/main/rustfmt.tomlignore エントリを参照してください。

エディターで保存時フォーマットを使用したい場合、固定されたバージョンの rustfmtbuild/<target>/stage0/bin/rustfmt 配下にビルドされています。

C++ コードのフォーマット

コンパイラには、安定した C API を持たない LLVM の一部と連携するための C++ コードが含まれています。 そのコードを変更する場合は、次のコマンドを使ってフォーマットしてください。

./x test tidy --extra-checks cpp:fmt --bless

これは、ローカル環境に依存しないように、固定されたバージョンの clang-format を使用します。

Python コードのフォーマットと lint

Rust リポジトリにはかなり多くの Python コードが含まれています。 私たちは ruff ツールによって、それらが lint され、フォーマットされた状態を保つようにしています。

Python コードを変更する場合は、次のコマンドを使ってフォーマットしてください。

./x test tidy --extra-checks py:fmt --bless

そして、lint を実行するには次のコマンドを使用します。

./x test tidy --extra-checks py:lint

これらは、ローカル環境に依存しないように、固定されたバージョンの ruff を使用します。

著作権表示

以前は、ファイルは著作権とライセンスの表示で始まっていました。 標準の条件(MIT OR Apache-2.0)でライセンスされる新しいファイルでは、この表示を省略してください。

著作権表示は現在ではすべてなくなっているはずですが、rust-lang/rust リポジトリで 見かけた場合は、それを削除する PR を遠慮なく開いてください。

行の長さ

行は最大 100 文字にするべきです。 80 文字以内に収められればなおよいです。

場合によっては、特にテストでは、この制限から除外する必要があることがあります。 その場合は、次のようにファイルの先頭付近にコメントを追加できます。

#![allow(unused)]
fn main() {
// ignore-tidy-linelength
}

タブとスペース

4 スペースのインデントを優先してください。

正しさのためのコーディング

フォーマット以外にも、従う価値のあるヒントがいくつかあります。

網羅的な match を優先する

match で _ を使用するのは便利ですが、enum に新しいバリアントが追加されたとき、 それらが正しく処理されない可能性があることを意味します。 自問してみてください。この enum に新しいバリアントが追加された場合、それが _ のコードを使いたい可能性と、別の処理を必要とする可能性はどちらが高いでしょうか。 答えが「低い」でない限り、網羅的な match を優先してください。

同じ助言は if letwhile let にも当てはまります。 これらは実質的に単一のバリアントに対するテストです。

忘れたくないことには “TODO” コメントを使う

自分自身にとって便利なツールとして、PR をマージする前に戻って対応したいことに // TODO コメントを挿入できます。

fn do_something() {
    if something_else {
        unimplemented!(); // TODO これを書く
    }
}

tidy スクリプトは // TODO コメントに対してエラーを報告するため、この コードは TODO が修正される(または削除される)までマージできません。

これは PR 内で、あるコミットではバグを残しておき、後のコミットで修正することを 示す方法としても有用です。

if foo {
    return true; // TODO 誤りだが、後のコミットで修正される
}

コードベースにメモを残したい場合は、代わりに // FIXME を使用してください。

crates.io のクレートの使用

crates.io の依存関係セクションを参照してください。

PR の構成方法

PR のコミットをどのように準備するかは、レビュー担当者にとって大きな違いを生みます。 以下にいくつかのヒントを示します。

「純粋なリファクタリング」は独立したコミットに分離してください。 たとえば、 メソッドの名前を変更する場合は、その名前変更を、そのすべての使用箇所の名前変更とともに、 独立したコミットに入れてください。

通常、コミットは多いほうがよいです。 大きな変更を行っている場合、 独立して理解できる小さなステップに分割するほうが、ほとんど常に望ましいです。 注意すべき点が 1 つあります。ある戦略に従ってコードを導入した後で、後のコミットで それを大幅に変更する(追加するのではなく)場合、その「行ったり来たり」は混乱を招く可能性があります。

積極的にフォーマットしてください。 PR の最後のコミットだけが正しく フォーマットされている必要がありますが、各コミットを ./x fmt で個別にフォーマットするほうが、 レビューしやすく、ノイズも少なくなります。

マージ禁止。 bors によるものを除き、私たちは履歴へのマージコミットを許可していません。 マージコンフリクトが発生した場合は、代わりに git rebase --interactive rust-lang/main のようなコマンドで rebase してください (remote に rust-lang という名前を使用していると仮定しています)。

個々のコミットはビルドできなくてもかまいません(ただし、できると望ましいです)。 私たちは すべての中間コミットが正常にビルドできることを要求していません。PR レベルで bisect できることだけを期待しています。 ただし、個々のコミットをビルド可能にできるなら、それは常に助けになります。

命名規則

通常の Rust のスタイルや命名規則に加えて、コンパイラ固有のものもいくつかあります。

  • cx は “context” の短縮形であることが多く、サフィックスとしてよく使われます。 たとえば、tcxTyping Context の一般的な名前です。

  • 'tcx は Typing Context のライフタイム名として使用されます。

  • crate はキーワードであるため、クレート関連の何かを表す変数が必要な場合、 多くの場合、綴りを krate に変更します。

破壊的変更の手順

このページでは、既存のコードがコンパイルできなくなる可能性のある、コンパイラにおけるバグ修正や健全性の修正を行うためのベストプラクティスの手順を定義します。このテキストは RFC 1589 に基づいています。

動機

時折、既存のコードがコンパイルできなくなる原因となる、コンパイラのバグ修正、健全性の修正、またはその他の変更を行う必要が生じます。このような場合、Rust のユーザーが円滑に移行できるような方法で変更を扱うことが重要です。避けたいのは、既存のプログラムが不透明なエラーメッセージによって突然コンパイルできなくなることです。むしろ、何が問題なのか、どのように修正するのか、なぜその変更が行われたのかについて明確なガイダンスを伴う、段階的な警告期間を設けることが望ましいです。この RFC では、そのような円滑な移行を実現することを目的として、破壊的変更を扱うために私たちが開発してきた手順について説明します。

このポリシーの要点の 1 つは、(a) 可能な限り、最初からハードエラーではなく警告を発行すべきであること、および (b) 既存のコードがコンパイルできなくなるすべての変更には、関連する追跡 issue が用意されることです。この issue は、その変更の結果に関するフィードバックを集める場を提供します。変更が予想外に大きな影響を及ぼすこともあれば、検討されていなかった方法で変更を避けられる場合もあります。そのような場合には、方針を変更して変更をロールバックするか、別の解決策を見つけることを決定する場合があります(警告が使われている場合、これは特に容易です)。

何がバグ修正に該当するのか?

この RFC は、破壊的変更がいつ許可されるかを定義しようとするものではないことに注意してください。それについてはすでに RFC 1122 で扱われています。この文書では、行われる変更がそれらのポリシーに従っていることを前提としています。以下は RFC 1122 の条件の要約です。

  • 健全性に関する変更: 型システムで見つかった穴の修正。
  • コンパイラのバグ: RFC または lang-team の決定に記載された仕様上のセマンティクスを、コンパイラが実装していない箇所。
  • 仕様が不十分な言語セマンティクス: コンパイラの振る舞いが一貫しておらず、正式な振る舞いが以前に決定されていなかったグレーゾーンの明確化。

詳細については RFC を参照してください!

詳細設計

破壊的変更を行う手順は次のとおりです(各手順については以下でさらに詳しく説明します)。

  1. 変更の影響を評価するために crater run を行う。
  2. その変更専用の 特別な追跡 issue を作成する。
  3. すぐにエラーを報告しない。代わりに、前方互換性 lint 警告を発行する。
    • これが単純ではない場合もあります。過去に採用したさまざまな手法については、以下のテキストを参照してください。
    • 警告が実現困難な場合:
      • エラーを報告するが、ユーザーを追跡 issue に導く、的を絞ったエラーメッセージを出すよう最大限努力する
      • 問題を修正する PR を、影響を受ける既知のすべての crate に送る
        • または、少なくとも、それらの crate の所有者に問題を知らせ、追跡 issue に案内する
  4. 変更が少なくとも 1 サイクルの間公開された状態になったら、変更を安定化し、それらの警告をエラーに変換できる。

最後に、プラグインに影響する rustc_ast への変更については、一般的なポリシーとして、これらの変更をまとめて行います。これについては以下でさらに詳しく説明します。

追跡 issue

すべての破壊的変更には、その変更専用の 追跡 issue を付随させるべきです。この issue の本文では、ユーザーがコードを修正するために何をしなければならないかに焦点を当てて、行われる変更を説明するべきです。issue は親しみやすく実用的であるべきです。詳細全体については RFC や他の issue にユーザーを案内するのが適切な場合もあります。この issue は、ユーザーが質問やその他の懸念事項をコメントできる場としても機能します。

これらの破壊的変更の追跡 issue 用テンプレートは こちら にあります。そのような issue がどのようなものになるべきかの例は、こちら にあります。

将来互換性警告の発行

破壊的変更を扱う最良の方法は、将来互換性警告を発行することから始めることです。これは lint 警告の特別なカテゴリです。新しい将来互換性警告は、次のように追加できます。

#![allow(unused)]
fn main() {
// 1. lint を `compiler/rustc_lint/src/builtin.rs` で定義し、 
//    将来非互換性のメタデータを追加します:
declare_lint! {
    pub YOUR_LINT_HERE,
    Warn,
    "illegal use of foo bar baz"
    @future_incompatible = FutureIncompatibleInfo {
        reason: fcw!(FutureReleaseError #1234) // ここに追跡 issue を記述します!
    },
}

// 2. それ専用の lint pass を追加します。
//    既存の pass の一部として lint を発行する場合、この手順は省略できます。

#[derive(Default)]
pub struct MyLintPass {
    ...
}

impl {Early,Late}LintPass for MyLintPass { 
    ...
}

impl_lint_pass!(MyLintPass => [YOUR_LINT_HERE]);

// 3. lint pass 内のどこかで lint を発行します:
cx.emit_span_lint(
    YOUR_LINT_HERE,
    pat.span,
    // 何らかの診断 struct
    MyDiagnostic {
        ...
    },
);

}

最後に、compiler/rustc_lint/src/lib.rs で lint を登録します。 そのファイルには、すでにその方法を示す例が多数あります。

役立つ手法

新しい警告を、以前から存在する古いエラーから除外するのが難しい場合がよくあります。過去に使われた手法の 1 つは、古いコードを変更せずに実行し、それが報告したであろうエラーを収集することです。そうすれば、その元の集合に含まれない、あなたが出すことになる任意のエラーに対して警告を発行できます。別の選択肢は、元のコードが完了した後にエラーが報告されていればコンパイルを中止することです。そうすれば、新しいコードは以前にエラーがなかった場合にのみ実行されることがわかります。

Crater と crates.io

Crater は、あなたの変更を含むコンパイラを使って、すべての crates.io crate と多くの公開 GitHub リポジトリをコンパイルする bot です。その後、あなたの変更によってコンパイルできなくなった、またはコンパイルできるようになった crate を含むレポートが生成されます。Crater run が完了するまでには数日かかる場合があります。

影響を評価するために、常に crater run を行うべきです。影響を受ける crate の作者に、少なくとも破壊的変更を通知することは丁寧で思いやりのある対応です。問題を修正する PR を送れるなら、それに越したことはありません。

直接エラーを発行することが許容される場合はあるか?

影響が無視できるほど小さいと考えられる変更は、直接エラーの発行に進むことができます。目安の 1 つは crates.io で確認することです。影響を受けるプロジェクトが 合計 10 個未満(ルートエラーではない)であれば、そのままエラーに進めます。このような場合でも、これまでと同様に「破壊的変更」ページを作成し、エラーがユーザーをこのページに誘導するようにする必要があります。言い換えると、ユーザーが警告ではなくエラーを受け取る点を除き、すべては同じであるべきです。さらに、影響を受けるプロジェクトに PR を送るべきです(理想的には、その変更を実装する PR が rustc に取り込まれる前に)。

影響が無視できるほど小さいとは考えられない場合(たとえば、10 個を超えるクレートが影響を受ける場合)、警告が必要です(ただし、特定のケースでコンパイラチームが特別な例外を認めることに同意した場合を除きます)。警告の実装が現実的でない場合は、影響を受けるクレートの数を減らすために、変更を取り込む前にクレートを移行する積極的な戦略を取るべきです。このシナリオに取り組むための手法をいくつか示します。

  1. 問題の一部に対して警告を発行し、新しいエラーは可能な限り最小のケース集合に限定します。
  2. 問題の修正方法を示し、ユーザーを追跡 issue に誘導する、非常に正確なエラーメッセージを出すようにします。
  3. 修正を段階的に行うことも理にかなっている場合があります。
    • まず、可能なところに警告を追加し、それらが取り込まれてからエラーの発行に進みます。
    • 修正が取り込まれる_前_に修正版が利用可能になるよう、影響を受けるクレートの作者と協力し、下流のユーザーがそれらを使用できるようにします。

安定化

変更が行われた後、その変更は不安定機能に使用しているのと同じプロセスで安定化します。

  • 新しいリリースが行われた後、破壊的変更に対応する未解決の追跡 issue を確認し、その一部を最終コメント期間(FCP)に推薦します。

  • このような issue の FCP は 1 サイクル続きます。サイクルの最後の 1、2 週間でコメントを確認し、最終判断を行います。

    • エラーに変換: その変更をハードエラーにするべきです。
    • 差し戻し: 警告を削除し、古いコードのコンパイルを引き続き許可するべきです。
    • 延期: まだ判断できない、もう少し待つ、または他の戦略を試します。

理想的には、破壊的変更は最終化される前に、コンパイラの stable ブランチに取り込まれているべきです。

リントの削除

「future warning」をハードエラーにすることを決定したら、カスタムリントを削除する PR が必要です。例として、overlapping_inherent_impls 互換性リントを削除するために必要な手順を示します。まず、リント名を大文字(OVERLAPPING_INHERENT_IMPLS)に変換し、その文字列をソース全体で ripgrep します。基本的には、このリント名が言及されている各場所を変換します(コンパイラでは大文字の名前を使用し、マクロが自動的に小文字の文字列を生成します。そのため、overlapping_inherent_impls を検索しても多くは見つかりません)。

NOTE: これらの正確なファイルはもう存在しませんが、手順は今も同じです。

リントを削除します。

最初に見つかる可能性が高い参照は、これに似た rustc_session/src/lint/builtin.rs 内のリント定義です :

#![allow(unused)]
fn main() {
declare_lint! {
    pub OVERLAPPING_INHERENT_IMPLS,
    Deny, // これは Warning と書かれている場合もあります
    "two overlapping inherent impls define an item with the same name were erroneously allowed",
    @future_incompatible = FutureIncompatibleInfo {
        reason: fcw!(FutureReleaseError #1234), // ここに追跡 issue を記述してください!
    },
}
}

この declare_lint! マクロは、関連するデータ構造を作成します。これを削除します。また、ファイルの後半で OVERLAPPING_INHERENT_IMPLS への言及が lint_array! の一部として見つかるので、それも削除します。

削除されたリントの一覧にリントを追加します。

compiler/rustc_lint/src/lib.rs には、「名前変更および削除されたリント」の一覧があります。このリントを一覧に追加できます。

#![allow(unused)]
fn main() {
store.register_removed("overlapping_inherent_impls", "converted into hard error, see #36889");
}

ここで #36889 はあなたのリントの追跡 issue です。

リントを発行する箇所を更新する

最後に見つかる参照の種類は、実際にリント自体をトリガーする箇所(つまり、警告が表示される原因)です。これらは削除したくありません。代わりに、エラーへ変換したいはずです。この場合、add_lint 呼び出しは次のようになります。

#![allow(unused)]
fn main() {
self.tcx.sess.add_lint(lint::builtin::OVERLAPPING_INHERENT_IMPLS,
                       node_id,
                       self.tcx.span_of_impl(item1).unwrap(),
                       msg);
}

この用途では node_span_lint が使われていることもよくあります。

これをエラーに変換したいです。場合によっては、このシナリオに対する既存のエラーがあるかもしれません。それ以外の場合は、新しい診断コードを割り当てる必要があります。新しい診断コードを割り当てる手順はこちらにあります。 拡張説明の中で、この点に関するコンパイラの動作が変更されたことに触れ、その変更の追跡 issue への参照を含めるとよいでしょう。

E0592 をコードとして採用したとしましょう。その場合、上記の add_lint() 呼び出しを次のようなものに変更できます。

#![allow(unused)]
fn main() {
struct_span_code_err!(self.dcx(), self.tcx.span_of_impl(item1).unwrap(), E0592, msg)
    .emit();
}

または、より良い方法として、次のような構造化診断を使います。

#![allow(unused)]
fn main() {
#[derive(Diagnostic)]
struct MyDiagnostic {
    #[label]
    span: Span,
    ...
}
}

テストを更新する

最後に、テストスイートを実行します。これらの中には、以前 overlapping_inherent_impls リントを参照していたテストがあるはずで、それらを更新する必要があります。一般的には、テストに #[deny(overlapping_inherent_impls)] があった場合、それは単に削除できます。

./x test

すべて完了です!

PR を開きます。=)

外部リポジトリの使用

rust-lang/rust git リポジトリは、rust-lang organization 内のいくつかの他のリポジトリに依存しています。 依存関係の利用方法には、主に 3 つあります。

  1. crates.io 経由の Cargo 依存関係として(例: rustc-rayon
  2. git(例: clippy)または josh(例: miri)のサブツリーとして
  3. git サブモジュールとして(例: cargo

一般的なルールとして:

  • エコシステム内の他の人にとっても有用になり得るライブラリには crates.io を使用する
  • コンパイラ内部に依存し、破壊的変更がある場合に更新が必要なツールにはサブツリーを使用する
  • コンパイラから独立しているツールにはサブモジュールを使用する

外部依存関係(サブツリー)

以下の外部プロジェクトは、何らかの形式の subtree を使用して管理されています。

submodule 依存関係とは対照的に (それらについては以下を参照)、subtree 依存関係は通常のファイルとディレクトリにすぎず、 ツリー内で更新できます。ただし、可能であれば、これらのツールに固有の機能強化、バグ修正などは、 各ツールのそれぞれの upstream リポジトリに直接報告するべきです。 例外として、新しいツール機能やテストを実装するために rustc の変更が必要な場合は、 それを 1 つのまとまった rustc PR で行うべきです。

subtree 依存関係は現在、2 つの異なるアプローチで管理されています。

Josh サブツリー

josh ツールは git サブツリーの代替であり、git 履歴を異なる方法で管理し、大規模なリポジトリに対してよりよくスケールします。 josh を扱うには専用のツールが必要です。 同期を支援するためのヘルパーツール rustc-josh-sync を提供しており、これについては以下で説明します。

Josh サブツリーの同期

Josh サブツリーの更新を実行するために、rustc-josh-sync という専用ツールを使用します。 以下のコマンドはすべての Josh サブツリーで使用できますが、miri については、 pull 中にいくつかの追加手順を実行する必要がある点に注意してください。

以下のコマンドでツールをインストールできます。

cargo install --locked --git https://github.com/rust-lang/josh-sync

pull(rust-lang/rust からサブツリーへ変更を同期する)と push(サブツリーから rust-lang/rust へ変更を同期する)は、どちらもサブツリーのリポジトリから実行します(したがって、まず ターミナルでそのリポジトリのチェックアウトディレクトリへ移動してください)。

pull の実行

  1. サブツリーへ PR を作成するために使用する新しいブランチをチェックアウトする
  2. pull コマンドを実行する
    rustc-josh-sync pull
    
  3. ブランチを自分の fork に push し、サブツリーリポジトリへの PR を作成する
    • gh CLI がインストールされている場合、rustc-josh-sync が PR を作成できます。

push の実行

注: 続行する前に、Git に関連するいくつかのガイダンスを [josh-sync README 上で]確認してください。

  1. push コマンドを実行し、<gh-username> アカウント配下の rustc fork に <branch-name> という名前のブランチを作成する
    rustc-josh-sync push <branch-name> <gh-username>
    
  2. <branch-name> から rust-lang/rust への PR を作成する

新しい Josh サブツリー依存関係の作成

リポジトリ依存関係を git subtree または git submodule から josh へ移行したい場合は、このガイドを確認できます。

git サブツリーの同期

サブツリーベースの依存関係に対して行われた変更は、定期的にこの リポジトリと upstream ツールリポジトリの間で同期する必要があります。

サブツリー同期は通常、それぞれのツールメンテナーによって処理されます。 他のユーザーも 同期 PR を提出できますが、そのためにはローカルの git インストールを変更し、 非常に正確な一連の手順に従う必要があります。 これらの手順は、いくつかの有用なヒントやコツとともに、Clippy の Contributing ガイド内の syncing subtree changes セクションに文書化されています。 この手順は、任意のサブツリーベースのツールで使用できますが、必ず 対応する正しいサブツリーディレクトリとリモートリポジトリを使用してください。

同期プロセスには、subtree pushsubtree pull の 2 つの方向があります。

subtree push は、このリポジトリ内のコピーに対して発生したすべての変更を取り込み、 ローカルの変更に対応するコミットをリモートリポジトリ上に作成します。 サブツリーに触れたすべてのローカルコミットはリモートリポジトリ上のコミットを発生させますが、 指定されたディレクトリからツールリポジトリのルートへファイルを移動するように変更されます。

subtree pull は、最後の subtree pull 以降のすべての変更を ツールリポジトリから取り込み、ツールの変更を Rust リポジトリ内の指定されたディレクトリへ移動する マージコミットとともに、これらのコミットを rustc リポジトリに追加します。

常に最初に push を行い、それをツールのデフォルトブランチへマージしてもらうことを推奨します。 その後 pull を行うと、マージは競合なしで機能します。 pull 中に競合を解決することはもちろん可能ですが、PR が十分速くマージされず新しい競合が発生した場合、 競合解決をやり直さなければならないことがあります。 git subtree pull の結果を rebase しようとしないでください。一般に、マージコミットを rebase するのは悪い考えです。

サブツリーディレクトリと対応するリモート リポジトリには、常に -P プレフィックスを指定する必要があります。 誤ったディレクトリまたはリポジトリを指定すると、 誤ったディレクトリを誤ったリモートリポジトリへ push しようとする、とても愉快なマージが発生します。 幸い、rustc 内の pull されたコミットまたはリモート上の push されたブランチのいずれかを破棄することで、 何の影響もなくこれを中止してやり直すことができます。 同期されようとしているコミットが突然数千個表示されるため、 通常、この状況が発生していることはかなり明らかです。

新しい subtree 依存関係の作成

既存のリポジトリから新しい subtree 依存関係を作成したい場合は、(この リポジトリのルートディレクトリから!)次を実行してください。

git subtree add -P src/tools/clippy https://github.com/rust-lang/rust-clippy.git master

これにより新しいコミットが作成されます。このコミットは、いかなる場合でもリベースしてはなりません! リベースする必要がある場合は、そのコミットを削除して操作をやり直してください。

これで完了です。src/tools/clippy ディレクトリは、Clippy が rustc モノレポの一部であるかのように振る舞うため、実際に git subtree を使う必要があるのは、 あなた(または subtree を同期する他の人)だけです。

外部依存関係(サブモジュール)

Rust のビルドでは、git submodules を使って追跡される外部 Git リポジトリも使用します。 完全な一覧は .gitmodules ファイルで確認できます。 これらのプロジェクトの一部は必須(標準ライブラリ向けの stdarch など)であり、 一部は任意(src/doc/book など)です。

サブモジュールの使用方法については、Git の使用に関する章でさらに詳しく説明されています。

一部のサブモジュールは、ビルドできない、またはテストが通らない「broken」状態になることが許容されています。たとえば、 The Rust Reference のようなドキュメントブックです。 これらのプロジェクトのメンテナーには、 プロジェクトが broken 状態になっているときに通知され、できるだけ早く修正する必要があります。 現在の状態は toolstate website で追跡されています。 詳細は Forge の Toolstate chapter で確認できます。 実際には、ドキュメントが broken toolstate になることは非常にまれです。

beta および stable チャンネルでは破損は許可されておらず、PR がマージされる前に対処されなければなりません。 また、beta cut までの 1 週間は、main で broken であることも許可されません。

ファジング

このガイドにおける ファジング とは、rustc のバグを見つける目的で、多種多様なプログラムをコンパイルすることを伴う任意のテスト手法を指します。 ファジングは、内部コンパイラエラー(ICE)を見つけるためによく使用されます。 ファジングは、ユーザーが遭遇する前にバグを発見できるため有益です。 また、バグの追跡を容易にする、小さく自己完結したプログラムも提供します。 しかし、よくあるいくつかの誤りによって、ファジングの有用性が低下し、結果としてコントリビューターの負担が増えることがあります。 Rust プロジェクトに対する好影響を最大化するため、ファザーで生成されたバグを報告する前に、このガイドを読んでください!

ガイドライン

要約

やってください:

  • バグが最新の nightly rustc でもまだ存在することを確認する
  • バグ報告には、合理的に最小化されたスタンドアロンの例を含める
  • バグ報告テンプレートで要求されているすべての情報を含める
  • 同じメッセージとクエリスタックを持つ既存の報告を検索する
  • テストケースを rustfmt でフォーマットする
  • そのバグがファジングによって見つかったことを示す

やらないでください:

  • custom_mirlang_itemsno_corerustc_attrs などを含むがこれらに限定されない、内部機能を使用するバグを大量に報告しないでください。
  • rustc をクラッシュさせることが既知の入力をファザーのシードにしないでください(詳細は後述)。

議論

ある ICE が既に報告済みのものと重複しているかどうか確信が持てない場合は、報告したうえで、関連している可能性があると思う issue にリンクしてください。 一般に、同じ行で発生している ICE でも、クエリスタック が異なる場合は、通常は別個のバグです。 たとえば、#109020#109129 には似たエラーメッセージがありました。

error: internal compiler error: compiler/rustc_middle/src/ty/normalize_erasing_regions.rs:195:90: Failed to normalize <[closure@src/main.rs:36:25: 36:28] as std::ops::FnOnce<(Emplacable<()>,)>>::Output, maybe try to call `try_normalize_erasing_regions` instead
error: internal compiler error: compiler/rustc_middle/src/ty/normalize_erasing_regions.rs:195:90: Failed to normalize <() as Project>::Assoc, maybe try to call `try_normalize_erasing_regions` instead

しかし、それらは異なるクエリスタックを持っています。

query stack during panic:
#0 [fn_abi_of_instance] computing call ABI of `<[closure@src/main.rs:36:25: 36:28] as core::ops::function::FnOnce<(Emplacable<()>,)>>::call_once - shim(vtable)`
end of query stack
query stack during panic:
#0 [check_mod_attrs] checking attributes in top-level module
#1 [analysis] running analysis passes on this crate
end of query stack

コーパスの構築

コーパスを構築するときは、rustc をクラッシュさせることが既にわかっているテストを収集しないようにしてください。 そのようなテストをシードにしたファザーは、同じ根本原因を持つバグを生成する可能性が高くなります。 これを避ける最も簡単な方法は、コーパス内の各ファイルをループし、それが ICE を引き起こすかどうかを確認し、引き起こす場合は削除することです。

コーパスを構築するには、以下を使用するとよいでしょう。

  • rustc/rust-analyzer/clippy のテストスイート(あるいはソースコード)— ただし、既に失敗を引き起こすことがわかっているテストは避けてください。そうしたテストは、しばしば //@ failure-status: 101//@ known-bug: #NNN のようなコメントで始まります。
  • アーカイブされた Glacier リポジトリ内の、既に修正済みの ICE — ただし、ices/ 内の未修正のものは避けてください!

追加でできること

ICE を報告した後に Rust プロジェクトを支援するためにできることがいくつかあります。

  • バグがいつ導入されたかを把握するために、バグを bisect する。 リグレッションを引き起こした PR / コミットを見つけた場合は、その issue に S-has-bisection ラベルを付けることができます。 見つからない場合は、代わりに E-needs-bisection を適用することを検討してください。
  • 「気を散らす要素」を修正する: 構文エラーや borrow-checking エラーなど、ICE の発生に寄与しないテストケース上の問題
  • テストケースを最小化する(下記参照)。 成功した場合は、その issue に S-has-mcve ラベルを付けることができます。 そうでなければ、E-needs-mcve を適用できます。
  • 最小化されたテストケースを crash test として rust-lang/rust リポジトリに追加する。 その際、自分の PR に他の「未追跡」のクラッシュを含めることも検討してください。 PR がマージされたら、関連するすべての issue に S-bug-has-test を付けることを忘れないでください。

ラベルの適用と削除 も参照してください。

最小化

ファザーで生成された入力を注意深く 最小化 することは有用です。 最小化するときは、元のエラーを保持し、構文、型チェック、または borrow-checking エラーなどの気を散らす問題を導入しないように注意してください。

最小化に役立つツールがいくつかあります。 これらのツールを使用している間に、構文、型、borrow-checking エラーを導入しないようにする方法がわからない場合は、完全なテストケースと最小化されたテストケースの両方を投稿してください。 一般に、構文を認識する ツールは、最も短い時間で最良の結果をもたらします。 treereduce-rustpicireny は構文を認識します。 halfempty はそうではありませんが、一般に高品質なツールです。

効果的なファジング

rustc をファジングするときは、機械語コードの生成を避けるとよいでしょう。これは主に LLVM によって行われるためです。 代わりに --emit=mir を試してください。

さまざまなコンパイラフラグによって、異なる問題を発見できます。 -Zmir-opt-level=4 は、デフォルトでは実行されない MIR 最適化パスを有効にし、興味深いバグを発見する可能性があります。 -Zvalidate-mir は、そのようなバグの発見に役立ちます。

自分でビルドしたコンパイラをファジングしている場合は、1 秒あたりの実行回数をもう少し絞り出すために、-C target-cpu=native、または PGO/BOLT でビルドするとよいでしょう。 もちろん、複数のビルド構成を試し、実際にどれがより優れたスループットをもたらすかを確認するのが最善です。

追加のバグを見つけるために、デバッグアサーションを有効にしてソースから rustc をビルドするとよいでしょう。ただし、これにより各実行で追加の作業が必要になり、ファジングが遅くなる可能性があります。 デバッグアサーションを有効にするには、rustc をコンパイルするときに bootstrap.toml に以下を追加します。

rust.debug-assertions = true

再現にデバッグアサーションが必要な ICE には、 requires-debug-assertions タグを付ける必要があります。

既存のプロジェクト

  • fuzz-rustc は、libfuzzer で rustc をファジングする方法を示しています
  • icemaker は、多数のソースファイルに対してさまざまなフラグで rustc やその他のツールを実行し、ICE を検出します
  • tree-splicer は、正しい構文を維持しながら既存のソースファイルを組み合わせることで、新しいソースファイルを生成します

通知グループ

通知グループは、より大きなプロジェクトにコミットすることなく、 「小分け」の形で rustc を手助けするための簡単な方法です。 通知グループには 簡単に参加 でき(PR を提出するだけです!)、 参加しても特定のコミットメントが生じるわけではありません。

通知グループに参加すると、その通知グループの条件に一致する 新しい issue が見つかるたびに、GitHub 上で ping を受け取る リストに追加されます。 興味があれば、その後で issue を引き受け、作業を始めることができます。

もちろん、新しい issue にタグが付けられるのを待つ必要はありません! 望む場合は、通知グループの GitHub ラベルを使って、 まだ引き受けられていない既存の issue を検索できます。

通知グループの一覧

通知グループの一覧は次のとおりです。

通知グループに適した issue とは?

通知グループは、独立したバグ、 特に中程度の優先度のバグについて ping される傾向があります。

  • 独立したとは、そのバグを修正するために大規模なリファクタリングが 必要になるとは想定していない、という意味です。
  • 中程度の優先度とは、そのバグが修正されることは望んでいるものの、 それを修正するために他のすべてを中断するほど差し迫った問題ではない、 という意味です。 もちろん、このようなバグの危険性は、時間の経過とともに 蓄積していく可能性があることであり、通知グループの役割は それを防ごうとすることです!

通知グループへの参加

通知グループに参加するには、Rust team リポジトリ内の適切なファイルに 自分の GitHub ユーザー名を追加する PR を開くだけです。 具体的なイメージをつかみ、編集すべきファイルを特定するには、以下の「PR の例」を参照してください。

また、まだ Rust team のメンバーではない場合は、ファイルに自分の名前を追加することに加えて、 リポジトリをチェックアウトし、次のコマンドを実行する必要があります。

cargo run add-person $your_user_name

PR の例:

通知グループ用に issue にタグを付ける

通知グループに適しているものとして issue にタグを付けるには、通知グループ名を指定して rustbotping コマンドを与えます。 例:

@rustbot ping apple
@rustbot ping arm
@rustbot ping emscripten
@rustbot ping risc-v
@rustbot ping wasi
@rustbot ping wasm
@rustbot ping windows

一部のコマンドを短く、覚えやすくするために、エイリアスがあります。 これらは triagebot.toml ファイルで定義されています。 たとえば、以下のコマンドはすべて同等であり、Apple グループに ping します。

@rustbot ping apple
@rustbot ping macos
@rustbot ping ios

これらのエイリアスは、人間にとって扱いやすくすることを意図したものである点に注意してください。 変更される可能性があります。 コマンドが常に有効であることを保証する必要がある場合は、 エイリアスよりも完全な呼び出しを優先してください。

ただし、これは compiler team のメンバーまたはコントリビューターのみが行うべきであり、 通常は compiler team のトリアージの一環として行われることに注意してください。

Apple 通知グループ

GitHub ラベル: O-macos, O-ios, O-tvos, O-watchos および O-visionos
Ping コマンド: @rustbot ping apple

このリストは、Apple 関連の問題の診断とテストの両方で支援を求めるため、また私たちの macOS/iOS/tvOS/watchOS/visionOS サポートに関する興味深い問題をどう解決するかについて提案を求めるために使われます。

このグループが何を行うのかをよりよく理解するために、最善の対応方針を決定する際に助言を求めるため、このグループに連絡していたであろう質問の種類の例をいくつか示します。

  • サポート対象の最低バージョンの引き上げ(例: #104385
  • 追加の Apple ターゲット(例: #121419
  • 分かりにくい Xcode リンカーの詳細(例: #121430

デプロイメントターゲット

Apple プラットフォームには「デプロイメントターゲット」という概念があり、*_DEPLOYMENT_TARGET 環境変数で制御され、バイナリが実行される最低 OS バージョンを指定します。

標準ライブラリで、rustc が使用するデフォルトより新しい OS バージョンの API を使用すると、静的リンカーエラーまたは動的リンカーエラーのいずれかが発生します。このため、extern "C" API については、その API がどの OS バージョンで導入されたかをドキュメント化するよう提案し、それが rustc で使用されている現在のデフォルトより新しい場合は、weak linking を使用するよう提案してみてください。

App Store とプライベート API

Apple は文書化されていない API の使用に非常に厳格であるため、変更が新しい関数を使用する場合は常に、それらが実際に公開 API であることを確認することが重要です。というのも、文書化されていない API にバイナリ内で言及しているだけでも(それを呼び出していなくても)、App Store から却下される可能性があるためです。

たとえば、Darwin / XNU カーネルには実際には futex システムコールがありますが、それらは公開 API ではないため、std では使用できません。

一般に、Apple によって API が公開されていると見なされるには、次の条件を満たす必要があります。

  • 公開ヘッダーに現れること(すなわち、Xcode とともに配布され、xcrun --show-sdk-path --sdk $SDK の下で特定のプラットフォーム向けに見つかるもの)。
  • それに availability 属性があること(__API_AVAILABLEAPI_AVAILABLE、または類似のものなど)。

ARM 通知グループ

GitHub ラベル: O-ARM
Ping コマンド: @rustbot ping arm

このリストは、ARM 関連の問題の診断とテストの両方、および 私たちの ARM サポートに関する興味深い疑問を解決する方法についての 提案を求めるために使用されます。

このグループには、関連する Zulip チャンネル (#t-compiler/arm) もあり、 そこでは質問を投げかけたり、ARM 固有のトピックについて議論したりできます。

そのため、参加に興味がある場合は、ぜひ ARM グループに登録してください! 登録するには、rust-lang/team リポジトリに対して PR を開いてください。 この例 に従うだけでかまいませんが、ユーザー名はご自身のものに変更してください!

Emscripten 通知グループ

GitHub ラベル: O-emscripten
Ping コマンド: @rustbot ping emscripten

このリストは、Emscripten 関連の問題の診断とテストの両方で支援を求めるため、また、私たちの Emscripten サポートに関する興味深い質問を解決する方法について提案を求めるために使用されます。

このグループには、関連する Zulip チャンネル(#t-compiler/wasm)もあり、そこで質問を投げかけたり、Emscripten 固有のトピックについて議論したりできます。

したがって、参加に興味がある場合は、ぜひ Emscripten グループにサインアップしてください! そのためには、rust-lang/team リポジトリに対して PR を開いてください。 このに従うだけで構いませんが、ユーザー名は自分のものに変更してください!

Fuchsia 通知グループ

GitHub ラベル: O-fuchsia
Ping コマンド: @rustbot ping fuchsia

このリストは、コンパイラまたは標準ライブラリの変更によって Fuchsia 統合が壊れる場合に、 Fuchsia メンテナーへ通知するために使用されます。

LoongArch 通知グループ

GitHub ラベル: O-loongarch
Ping コマンド: @rustbot ping loongarch

このリストは、LoongArch 関連の問題の診断とテストの両方について助けを求めたり、私たちの LoongArch サポートに関する興味深い疑問を解決する方法について提案を求めたりするために使用されます。

このグループには、関連する Zulip チャンネル(#t-compiler/loong-arch)もあり、そこで質問したり、LoongArch 固有のトピックについて議論したりできます。

したがって、参加に興味がある場合は、ぜひ LoongArch グループに登録してください! そのためには、rust-lang/team リポジトリに対して PR を開いてください。 このに従うだけでかまいませんが、ユーザー名は自分のものに変更してください!

RISC-V 通知グループ

GitHub ラベル: O-riscv
Ping コマンド: @rustbot ping risc-v

このリストは、RISC-V 関連の問題の診断とテストの両方について支援を求めるため、また RISC-V サポートに関する興味深い疑問を解決する方法について提案を求めるために使用されます。

このグループには、関連する Zulip チャンネル(#t-compiler/risc-v)もあり、質問を投稿したり、RISC-V 固有のトピックについて議論したりできます。

参加に興味がある場合は、ぜひ RISC-V グループにサインアップしてください! そのためには、rust-lang/team リポジトリに対して PR を開いてください。 このに従い、ユーザー名を自分のものに変更してください!

Rust for Linux 通知グループ

GitHub ラベル: A-rust-for-linux
Ping コマンド: @rustbot ping rfl

このリストは、コンパイラまたは標準ライブラリの変更が Rust for Linux を壊すような形で行われた場合に、Rust for Linux (RfL) のメンテナーへ通知するために使用されます。 これは、Rust for Linux が複数の unstable なフラグと機能に依存しているためです。 その場合、RfL のメンテナーは理想的には、その破損を解決するための支援を提供するか、 一時的にその破損を受け入れ、RfL の CI ジョブを一時的に削除することで CI のブロックを解除するかを決定する必要があります。

このグループには関連する Zulip チャンネル(#rust-for-linux)もあり、 Rust for Linux に関する質問やトピックの議論を行うことができます。

参加に興味がある場合は、Zulip の Rust for Linux グループに登録してください!

WASI 通知グループ

GitHub ラベル: O-wasi
Ping コマンド: @rustbot ping wasi

このリストは、WASI 関連の問題の診断とテスト、および私たちの WASI サポートに関する興味深い疑問をどのように解決するかについての提案の両方で、助けを求めるために使用されます。

このグループには、関連する Zulip チャンネル(#t-compiler/wasm)もあり、人々はそこで質問をしたり、WASI 固有のトピックについて議論したりできます。

したがって、参加に興味がある場合は、WASI グループに登録してください! そのためには、rust-lang/team リポジトリに対して PR を開いてください。 この例に従うだけで構いませんが、ユーザー名は自分のものに変更してください!

WebAssembly (WASM) 通知グループ

GitHub ラベル: O-wasm
Ping コマンド: @rustbot ping wasm

このリストは、WebAssembly 関連の問題の診断とテストの両方について支援を求めるため、また私たちの WASM サポートに関する興味深い課題を解決する方法についての提案を求めるために使用されます。

このグループには関連する Zulip チャンネル(#t-compiler/wasm)もあり、そこで質問をしたり、WASM 固有のトピックについて議論したりできます。

そのため、参加に興味がある場合は、WASM グループに参加登録してください!そのためには、rust-lang/team リポジトリに対して PR を開いてください。この例 に従うだけでかまいませんが、ユーザー名は自分のものに変更してください!

Windows 通知グループ

GitHub ラベル: O-Windows
Ping コマンド: @rustbot ping windows

このリストは、Windows 関連の問題の診断とテストに関する支援を求めるため、また Windows サポートに関する興味深い問題を解決する方法について提案を求めるために使用されます。

このグループには、関連する Zulip チャンネル(#t-compiler/windows)もあり、質問をしたり Windows 固有のトピックについて議論したりできます。

このグループが何を行うのかをよりよく理解できるように、最適な対応方針を判断するうえで、このグループに助言を求めていたであろう質問の例をいくつか示します。

  • どのバージョンの MinGW をサポートすべきか?
  • 従来の InnoSetup GUI インストーラーを削除すべきか? #72569
  • Windows 上の静的ライブラリにはどのような名前を使用すべきか? #29520

そのため、参加に関心がある場合は、ぜひ Windows グループに登録してください! 登録するには、rust-lang/team リポジトリに対して PR を開いてください。 この例に従い、ユーザー名を自分のものに変更するだけです!

GPU ターゲット通知グループ

Github ラベル: なし
Ping コマンド: @rustbot ping gpu-target

この通知グループは、リンカー関連の問題と、コンパイラ内でのそれらの統合を扱います。

このグループには、関連する Zulip ストリーム(#t-compiler/gpgpu-backend)もあり、GPU 関連のトピックや問題について質問したり議論したりできます。

参加に興味がある場合は、ぜひこのグループに登録してください。そのためには、rust-lang/team リポジトリに対して PR を開き、GitHub ユーザーをこのファイルに追加してください。

rust-lang/rust ライセンス

rustc コンパイラのソースと標準ライブラリは、特に明記されていない限り、Apache License v2.0MIT License のデュアルライセンスです。

詳細なライセンス情報は、rust-lang/rust リポジトリの COPYRIGHT ドキュメント で入手できます。

レビュアー向けガイドライン

一般に、レビュアーはコントリビューションのコード品質だけでなく、それらが適切にライセンスされているかにも 目を向ける必要があります。 レビュー時に注意すべき点について、以下にいくつかのヒントを示します。ただし、あるコードが適切に ライセンスされているかどうか少しでも不安に感じた場合は、安全側に倒してください — フィードバックを得るために Council または Compiler Team Leads に連絡してください!

注意すべき点:

  • PR の作成者が、他のソースからコードをコピー、移植、または改変したと述べている。
  • コード内に、Web ページを指すコメントや、そのアルゴリズムがどこから取られたかを説明するコメントがある。
  • アルゴリズムやコードパターンが、どこか他の場所からコピーされた可能性が高いように見える。
  • 新しい依存関係を追加する場合は、その依存関係のライセンスを再確認する。

これらすべての場合において、そのソースが Rust のライセンスと互換性のある形で ライセンスされていることを確認する必要があります。

  • GNU binutils のような GPL プロジェクトから C コードを移植することは許可されません。それには Rust 自体を GPL の下でライセンスする必要が生じます。
  • アルゴリズムの教科書からコードをコピーすることは許可される場合がありますが、一部のアルゴリズムは特許で保護されています。

移植

rustc へのコントリビューション、特にプラットフォームやコンパイラ組み込み関数に関するものには、 他のプロジェクト、主に LLVM や GCC から作業を移植することがよく含まれます。

いくつかの一般的なルールが適用されます:

  • 作業をコピーする場合は、元のライセンスに従う必要があります
    • これは直接のコピー&ペーストに適用されます
    • これは、あなたが参照して移植したコードにも適用されます

一般に、他のコードベースから着想を得ることは問題ありませんが、コードを 移植する際には注意を払ってください。

完全なライブラリ(例: LLVM に同梱されている C ライブラリ)の移植は、元の ライブラリのライセンスを維持しなければなりません。

エディション

この章では、rustc におけるエディションサポートの仕組みの概要を説明します。 ここでは、エディションが何であるかを理解していることを前提とします(Edition Guide を参照)。

エディションの定義

--edition CLI フラグは、クレートで使用するエディションを指定します。 これは Session::edition からアクセスできます。 クレートのエディションを確認するための Session::at_least_rust_2021 のような便利な関数がありますが、 グローバルセッションを確認するのか、スパンを確認するのかについては注意する必要があります。下記の Edition hygiene を参照してください。

at_least_rust_20xx 便利メソッドの代替として、Edition 型は span.edition() >= Edition::Edition2021 のような範囲チェックを行うための比較もサポートしています。

新しいエディションの追加

新しいエディションを追加するには、主に Edition enum にバリアントを追加し、その後 壊れたものをすべて修正します。例については #94461 を参照してください。

フィーチャーとエディションの安定性

Edition enum は、エディションが安定しているかどうかを定義します。 安定していない場合、それを有効にするには -Zunstable-options CLI オプションを渡す必要があります。

新しいフィーチャーを追加するとき、将来のエディションにおける安定性の扱い方として、次の 2 つの選択肢があります。

  • span.at_least_rust_20xx() のようにスパンのエディションを確認する(Edition hygiene を参照)か、 Session::edition を確認するだけにする。これは、そのフィーチャーが利用可能であることを示すために、 エディション自体の安定性に暗黙的に依存します。
  • 新しい挙動を feature gate の背後に置く。

比較的単純な変更であれば、現在のエディションだけを確認すれば十分な場合があります。 しかし、より大きな言語変更については、フィーチャーゲートの作成を検討すべきです。 フィーチャーゲートを使用することには、いくつかの利点があります。

  • フィーチャーゲートにより、新しいフィーチャーの作業や実験がしやすくなります。
  • #![feature(…)] 属性が使用されたときに、新しいフィーチャーが有効化されているという意図が明確になります。
  • まだ完成していないフィーチャーが、完成して準備ができているエディション固有のフィーチャーのテストを妨げないようにできるため、エディションのテストが容易になります。
  • フィーチャーをエディションから切り離すことで、そのフィーチャーの準備ができたときに、次のエディションに追加すべきかどうかをチームが意図的に判断しやすくなります。

フィーチャーが完成して準備ができたら、フィーチャーゲートを削除できます(そして、そのコードは有効かどうかを判断するために スパンまたは Session のエディションを確認するだけにすべきです)。

フィーチャーチェックを行うには、いくつかの異なる選択肢があります。

  • エディションに関与するかもしれないし、しないかもしれない非常に実験的なフィーチャーについては、 tcx.features().my_feature のような通常のフィーチャーゲートを実装し、当面はエディションを無視できます。

  • エディションに関与する可能性がある実験的なフィーチャーについては、 tcx.features().my_feature && span.at_least_rust_20xx() を使ってゲートを実装すべきです。 これにより、ユーザーは引き続き #![feature(my_feature)] を指定する必要があり、 エディション内で準備ができて受け入れられている他のエディションフィーチャーのテストを妨げることを避けられます。

  • エディションの一部であることが確定する段階に進んだ実験的なフィーチャーについては、 tcx.features().my_feature || span.at_least_rust_20xx() を使ってゲートを実装するか、 フィーチャーチェックを完全に削除して span.at_least_rust_20xx() だけを確認すべきです。

複数の場所でフィーチャーゲーティングを行う必要がある場合は、更新箇所が 1 つだけになるように、 チェックを 1 つの関数に置くことを検討してください。例:

// Edition 2021 の disjoint closure captures からの例。

fn enable_precise_capture(tcx: TyCtxt<'_>, span: Span) -> bool {
    tcx.features().capture_disjoint_fields || span.rust_2021()
}

Lint が安定性をどのように扱うかについての詳細は、下記の Lint と安定性 を参照してください。

エディションのパース

ほとんどの場合、字句解析器はエディション非依存です。 Lexer 内では、トークンをエディション固有の挙動に基づいて変更できます。 たとえば、c"foo" のような C 文字列リテラルは、2021 より前のエディションでは複数のトークンに分割されます。 2021 エディションの予約済みプレフィックスなども、ここで処理されます。

エディション固有のパースは比較的まれです。1 つの例は async fn で、 トークンのスパンを確認して 2015 エディションかどうかを判断し、その場合はエラーを出力します。 これは、その構文がすでに無効であった場合にのみ実行できます。

パーサー内でエディションチェックを行う必要がある場合、通常はトークンのエディションを見ることになります。 Edition hygiene を参照してください。 まれなケースでは、代わりに ParseSess::edition からグローバルエディションを確認する必要がある場合があります。

ほとんどのエディション固有のパース挙動は、パーサー内ではなく migration lints によって処理されます。 これは、(新しい構文ではなく)構文の変更がある場合に適しています。 これにより、古い構文は以前のエディションで引き続き動作できます。 その後、lint が挙動の変更を確認します。 古いエディションでは、lint パスは新しいエディションへの移行を支援するために移行 lint を出力すべきです。 新しいエディションでは、代わりにコードが emit_err でハードエラーを出力すべきです。 たとえば、非推奨の start...end パターン構文は、2021 より前のエディションでは ellipsis_inclusive_range_patterns lint を出力し、2021 では emit_err メソッドを介したハードエラーになります。

キーワード

新しいキーワードは、エディションの境界を越えて導入できます。 これは [Symbol::is_used_keyword_conditional] のような関数によって実装されており、 キーワードが定義される順序に依存しています。

新しいキーワードが導入されるときは、そのキーワードを識別子として使用している可能性のあるコードを 自動移行で移行できるように、keyword_idents lint を更新すべきです([KeywordIdents] を参照)。 検討すべき代替案として、そのキーワードが使用される位置だけで区別するのに十分であれば、 そのキーワードを弱いキーワードとして実装することがあります。

検討すべき追加の選択肢として、[RFC 3101] で導入された k# プレフィックスがあります。 これにより、キーワードが導入されるエディションより前のエディションでキーワードを使用できます。 これは現在実装されていません。 [Symbol::is_used_keyword_conditional]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_span/symbol/struct.Symbol.html#method.is_used_keyword_conditional keyword_idents: https://doc.rust-lang.org/nightly/rustc/lints/listing/allowed-by-default.html#keyword-idents [KeywordIdents]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint/builtin/struct.KeywordIdents.html [RFC 3101]: https://rust-lang.github.io/rfcs/3101-reserved_prefixes.html

エディションの衛生性

スパンには、そのスパンの由来となったクレートのエディションがマークされています。 これが何を意味するのかについて、ユーザー中心の説明は、Edition Guide のマクロハイジーンを参照してください。

通常は、グローバルな Session のエディションを見るのではなく、トークンのスパンから エディションを使用するべきです。 たとえば、sess.at_least_rust_2021() の代わりに span.edition().at_least_rust_2021() を使用します。 これは、マクロがクレートをまたいで使用されたときに正しく動作することを保証するのに役立ちます。

lint

lint には、エディションと相互作用するためのいくつかの異なるオプションがあります。 lint は 将来非互換のエディション移行lint にできます。これは、新しいエディションへの 移行をサポートするために使用されます。 あるいは、lint はエディション固有にできます。この場合、特定のエディションから 既定レベルが変更されます。

移行lint

移行lint は、プロジェクトをあるエディションから次のエディションへ移行するために使用されます。 これらは MachineApplicable提案で実装されており、 コードを書き換え、前のエディションと次のエディションの両方で正常にコンパイルされるようにします。 たとえば、keyword_idents lint は、新しいキーワードと衝突する識別子を対象に、 衝突を避けるため raw identifier 構文を使用するようにします(たとえば asyncr#async に変更します)。

移行lint は、lint 宣言内で FutureIncompatibilityReason::EditionError または FutureIncompatibilityReason::EditionSemanticsChange将来非互換 オプションを使って宣言しなければなりません。

declare_lint! {
    pub KEYWORD_IDENTS,
    Allow,
    "エディションのキーワードが識別子として使用されていることを検出します",
    @future_incompatible = FutureIncompatibleInfo {
        reason: fcw!(EditionError 2018 "slug-of-edition-guide-page")
    };
}

このように宣言すると、その lint は適切な rust-20xx-compatibility lint グループに自動的に追加されます。 ユーザーが cargo fix --edition を実行すると、cargo は --force-warn rust-20xx-compatibility フラグを渡し、エディション移行中にこれらすべての lint が表示されるよう強制します。 Cargo はさらに --cap-lints=allow も渡すため、他の lint がエディション移行を妨げることはありません。

サンプルコードが正しいエディションを設定していることを確認してください。サンプルは以前のエディションを示し、移行警告がどのように見えるかを示すべきです。たとえば、2024 移行用のこの lint では、2021 のサンプルを示しています。

declare_lint! {
    /// `keyword_idents_2024` lint は ... を検出します
    ///
    /// ### 例
    ///
    /// ```rust,edition2021
    /// #![warn(keyword_idents_2024)]
    /// fn gen() {}
    /// ```
    ///
    /// {{produces}}
}

移行lint の既定値は Allow または Warn のいずれかにできます。 Allow の場合、ユーザーは通常、手動でエディション移行を行っている場合や、 移行中に問題が発生した場合を除き、この警告を見ることはありません。 ほとんどの移行lint は Allow です。

既定で Warn の場合、すべてのエディションのユーザーにこの警告が表示されます。 Warn は、その変更を全員が認識すること、およびすべてのエディションでコードの更新を促すことが 重要だと考える場合にのみ使用してください。 多くのプロジェクトに影響する新しいデフォルト警告lintは、ユーザーに大きな混乱と不満をもたらす可能性が あることに注意してください。 エディションが安定化してから数年後に AllowWarn に切り替えることを検討してもよいでしょう。 これは、新しいエディションに更新していない比較的少数のユーザーにのみ表示されます。

エディション固有のlint

lint は、特定のエディションから異なるレベルを持つようにマークできます。 lint 宣言では、@edition マーカーを使用します。

declare_lint! {
    pub SOME_LINT_NAME,
    Allow,
    "自分のlintの説明",
    @edition Edition2024 => Warn;
}

ここでは、SOME_LINT_NAME は 2024 より前のすべてのエディションで既定値が Allow で、その後は Warn になります。

他の選択肢があるため、これは一般に控えめに使用するべきです。

  • エディションに関係しない影響の小さいスタイル上の変更では、単にすべての エディションで lint を Warn にできます。別の書き方を採用してもらいたいのであれば、思い切って すべてのプロジェクトに表示されるようにすることを決めてください。

    新しいデフォルト警告lintが多くのプロジェクトに影響すると、ユーザーに大きな混乱と 不満をもたらす可能性があることに注意してください。

  • 新しいスタイルを新しいエディションでハードエラーに変更し、移行lintを使って プロジェクトを新しいスタイルへ自動変換します。たとえば、 ellipsis_inclusive_range_patterns は 2021 ではハードエラーであり、それ以前のすべてのエディションでは警告します。

    これらはエディションの安定化後には追加できないことに注意してください。

  • 移行lintも時間とともに変更できます。 たとえば、移行lint は最初は既定で Allow にできます。 移行を行うユーザーは、自動的に新しいコードへ更新されます。 その後、数年が経ってから、以前のエディションで lint を Warn にできます。

    たとえば anonymous_parameters は 2018 Edition の移行lint(かつ 2018 ではハードエラー)であり、 以前のエディションでは既定で Allow でした。 その後、3年後に、それ以前のすべてのエディションで Warn に変更され、すべてのユーザーに そのスタイルが段階的に廃止されつつあるという警告が表示されるようになりました。 これが最初から警告だった場合、多くのプロジェクトに影響し、大きな混乱を招いていたでしょう。 エディションの一部にすることで、ほとんどのユーザーは最終的に新しいエディションへ更新し、 移行によって処理されました。 Warn への切り替えは、更新しなかった少数のユーザーにのみ影響しました。

lint と安定性

リントには不安定であるというマークを付けることができます。これは、新しいエディション機能を開発していて、 移行リントを試したい場合に役立ちます。 機能ゲートは、次のようにリントの宣言で指定できます。

declare_lint! {
    pub SOME_LINT_NAME,
    Allow,
    "my cool lint",
    @feature_gate = sym::my_feature_name;
}

すると、そのリントはユーザーが適切な #![feature(my_feature_name)] を持っている場合にのみ発火します。 ただし、移行をテストする crater 実行の時期になったら、その機能ゲートを削除する必要があることに 注意してください。

あるいは、今後の不安定なエディション向けに、機能ゲートなしでデフォルトで許可される 移行リントを実装することもできます。 技術的には、エディションが安定化される前にユーザーがそのリントを有効にできる可能性はありますが、ほとんどのユーザーは 新しいリントの存在に気付かず、何かを妨げたり破壊的変更を引き起こしたりすることはないはずです。

イディオムリント

2018 エディションでは、rust-2018-idioms リントグループの下に「イディオムリント」という概念がありました。 この概念は、rust-2018-compatibility リントグループの下にある強制的な移行とは別のリントグループに、 新しいイディオム的なスタイルを置くことで、特定のエディション変更にどのようにオプトインするかについて ある程度の柔軟性を与えるものでした。

全体として、このアプローチはあまりうまく機能しなかったようであり、 今後イディオムグループを使用する可能性は低いです。

標準ライブラリの変更

プレリュード

各エディションには、標準ライブラリの特定のプレリュードがあります。 これらは core::preludestd::prelude の通常のモジュールとして実装されています。 プレリュードには新しい項目を追加できますが、これはユーザーの既存コードと競合する可能性があることに注意してください。 通常は、競合を避けるために既存のコードを移行する 移行リントを使用するべきです。 たとえば、rust_2021_prelude_collisions は、2021 の新しいトレイトとの競合を処理するために使用されます。

カスタマイズされた言語の挙動

通常、標準ライブラリに破壊的変更を加えることはできません。 まれな場合には、チームがこのルールを破ってでも挙動の変更が十分に重要だと判断することがあります。 欠点は、古いシグネチャや挙動と新しいシグネチャや挙動のどちらを使用すべきかを区別できるようにするため、 コンパイラで特別な処理が必要になることです。

一例は、配列の into_iter() のメソッド解決の変更です。 これは IntoIterator トレイトに #[rustc_skip_array_during_method_dispatch] 属性を付けることで実装され、 それによってコンパイラに対して、エディションに基づく代替のトレイト解決の選択肢を考慮するよう伝えます。

もう 1 つの例は、panic! マクロの変更です。 これには複数の panic マクロを定義し、組み込みの panic マクロ実装がそれを展開する適切な方法を 判断する必要がありました。 これには、古いコードを新しい形式に調整するための non_fmt_panics 移行リントも含まれており、 panic マクロの使用を検出するために rustc_diagnostic_item 属性が必要でした。

一般に、非常に価値の高い状況を除き、これらの特殊なケースは避けることをお勧めします。

標準ライブラリのエディションを移行する

標準ライブラリ自体のエディションを更新するには、おおまかに次のプロセスが含まれます。

  • 新しく安定化されたエディションが beta に到達し、ブートストラップコンパイラが更新されるまで待ちます。
  • 移行リントを適用します。一部のコードは外部サブモジュール1にあり、標準ライブラリは条件付きコンパイルを多用しているため、これは込み入ったプロセスになる可能性があります。また、標準ライブラリ自体に対して cargo fix --edition を実行するのは実用的でない場合があります。1 つの方法は、各リントについて各クレートの先頭に個別に #![warn(...)] を追加し、./x check library を実行し、移行を適用し、#![warn(...)] を削除して、各移行を個別にコミットすることです。完全なカバレッジを得るには、多くの異なるターゲットに対して --target 付きで ./x check を実行する必要があるでしょう(そうしないと、おそらく CI を通すのに何日も、あるいは何週間も費やすことになります)2。さらにヒントについては、高度な移行ガイドも参照してください。
    • backtrace-rs に移行を適用します。2024 の例。これはクレート自体のエディションを更新しないことに注意してください。なぜなら、そのクレートは crates.io で独立して公開されており、そうすると最小 Rust バージョンを制限してしまうためです。そのエディションが更新されるまでリグレッションを避けるために、いくつかの #![deny()] 属性を追加することを検討してください。
    • stdarch に移行を適用し、そのエディションとフォーマットを更新します。2024 の例
    • backtrace と stdarch のサブモジュールを更新する PR を投稿し、それらがマージされるまで待ちます。
    • 標準ライブラリのクレートに移行リントを適用し、それらのエディションを更新します。core から始めて、1 度に 1 つのクレートに取り組むことをお勧めします。2024 の例

エディションを安定化する

エディションチームがゴーサインを出した後、エディションを安定化するプロセスはおおまかに次のとおりです。

  • LATEST_STABLE_EDITION を更新します。
  • Edition::is_stable を更新します。
  • エディションを番号で参照しているドキュメントを探し出して更新します。
  • //@ edition ヘッダーを使用しているテストを整理して、-Zunstable-options フラグを削除し、それらが実際に安定版であることを確認します。注: 理想的には、これは自動化されるべきです。#133582 を参照してください。
  • 変更されたテストを bless します。
  • lint-docs を更新して、新しいエディションをデフォルトにします。

2024 の例を参照してください。


  1. 将来的には、これらのサブモジュールを rust-lang/rust に取り込むように変わることが期待されます。

  2. また、多くの異なるターゲットで大量のテストを行う必要がある可能性が高く、そのような場合に docker テスト が役立ちます。

コンパイラのブートストラップ

ブートストラップとは、コンパイラを使ってそのコンパイラ自身をコンパイルするプロセスです。 より正確には、古いコンパイラを使って、同じコンパイラの新しいバージョンをコンパイルすることを意味します。

これにより、鶏と卵のパラドックスが生じます。最初のコンパイラはどこから来たのでしょうか? それは別の言語で書かれていたに違いありません。Rust の場合、それは OCaml で書かれていました。しかし、それはかなり昔に放棄されており、 現代的なバージョンの rustc をビルドする唯一の方法は、少しだけ古いバージョンを使うことです。

これはまさに x.py の動作そのものです。現在のベータリリースの rustc をダウンロードし、それを使って新しいコンパイラをコンパイルします。

このセクションでは、まず Bootstrap が何をするのかについて大まかに概説し、その後で Bootstrap がそれをどのように行うのかについて大まかに紹介します。

さらに、デバッグ方法について学ぶには、bootstrap のデバッグを参照してください。

ブートストラップが行うこと

ブートストラップとは、コンパイラを使ってそのコンパイラ自身をコンパイルするプロセスです。 より正確には、古いコンパイラを使って、同じコンパイラの新しいバージョンをコンパイルすることを意味します。

これは「鶏が先か卵が先か」というパラドックスを生みます。最初のコンパイラはどこから来たのでしょうか? それは別の言語で書かれていたに違いありません。 Rust の場合、それは OCaml で書かれていました。 しかし、それはずっと昔に放棄されており、現代のバージョンの rustc をビルドする 唯一の方法は、少しだけ古いバージョンを使うことです。

これこそが ./x.py の動作です。現在のベータリリースの rustc をダウンロードし、それを使って新しいコンパイラをコンパイルします。

このドキュメントは、主にユーザー向けの情報を扱っていることに注意してください。 bootstrap の内部について読むには、bootstrap/README.md を参照してください。

ブートストラップのステージ

概要

  • Stage 0: 事前にコンパイルされたコンパイラと標準ライブラリ
  • Stage 1: 現在のコードから、以前のコンパイラによって作られたもの
  • Stage 2: 真に現在のコンパイラ
  • Stage 3: 同一結果のテスト

rustc のコンパイルはステージごとに行われます。 以下は、RustConf 2022 での Jynn Nelson の ブートストラップに関する講演 をもとにした図で、 その下に詳細な説明があります。

ABCD は、ブートストラップのステージの順序を示しています。 のノードは ダウンロードされ、黄色 の ノードは stage0 コンパイラでビルドされ、 のノードは stage1 コンパイラでビルドされます。

graph TD
    s0c["stage0 compiler (1.86.0-beta.1)"]:::downloaded -->|A| s0l("stage0 std (1.86.0-beta.1)"):::downloaded;
    s0c & s0l --- stepb[ ]:::empty;
    stepb -->|B| s0ca["stage0 compiler artifacts (1.87.0-dev)"]:::with-s0c;
    s0ca -->|copy| s1c["stage1 compiler (1.87.0-dev)"]:::with-s0c;
    s1c -->|C| s1l("stage1 std (1.87.0-dev)"):::with-s1c;
    s1c & s1l --- stepd[ ]:::empty;
    stepd -->|D| s1ca["stage1 compiler artifacts (1.87.0-dev)"]:::with-s1c;
    s1ca -->|copy| s2c["stage2 compiler"]:::with-s1c;

    classDef empty width:0px,height:0px;
    classDef downloaded fill: lightblue;
    classDef with-s0c fill: yellow;
    classDef with-s1c fill: lightgreen;

Stage 0: 事前にコンパイルされたコンパイラ

stage0 コンパイラは、デフォルトではごく最近の beta rustc コンパイラと、それに 関連付けられた動的ライブラリであり、./x.py が自動的にダウンロードします。 (./x.py を設定して、stage0 を別のものに変更することもできます。)

事前コンパイル済みの stage0 コンパイラは、その後、事前コンパイル済みの stage0 std とともに src/bootstrapcompiler/rustc をコンパイルするためだけに使われます。

stage1 コンパイラをビルドするために、事前コンパイル済みの stage0 コンパイラと std を使うことに注意してください。 したがって、ツリーから新しくビルドされた std を持つコンパイラを使うには、 stage2 コンパイラをビルドする必要があります。

ここでは 2 つの概念が関係しています。コンパイラ(その依存関係一式を含む)と、その 「ターゲット」または「オブジェクト」ライブラリ(stdrustc)です。 どちらもステージ化されていますが、段階をずらした形で行われます。

Stage 1: 現在のコードから、以前のコンパイラによって作られたもの

次に、rustc のソースコードは stage0 コンパイラでコンパイルされ、stage1 コンパイラを生成します。

Stage 2: 真に現在のコンパイラ

次に、ツリー内の std とともに stage1 コンパイラを使ってコンパイラを再ビルドし、stage2 コンパイラを生成します。

stage1 コンパイラ自体は、事前コンパイル済みの stage0 コンパイラと std によってビルドされたものであり、 したがって作業ディレクトリ内のソースによってビルドされたものではありません。 これは、stage0 コンパイラによって生成された ABI が、stage1 コンパイラによって 作られるはずだった ABI と一致しない可能性があることを意味し、その結果、動的ライブラリ、テスト、 および rustc_private を使うツールで問題が発生する可能性があります。

proc_macro クレートは、proc_macro::bridge と呼ばれる C FFI レイヤーによって この問題を回避しており、stage1 で使えるようになっていることに注意してください。

stage2 コンパイラは、rustup およびその他すべてのインストール方法で配布されるものです。 しかし、ビルドには非常に長い時間がかかります。なぜなら、まず 古いコンパイラで新しいコンパイラをビルドし、次にそれを使って新しい コンパイラをそれ自身でビルドする必要があるためです。

開発では、通常は --stage 1 フラグだけを使って物をビルドしたいはずです。 コンパイラのビルド を参照してください。

Stage 3: 同一結果のテスト

Stage 3 は任意です。 新しいコンパイラの健全性を確認するために、stage2 コンパイラでライブラリをビルドできます。 何かが壊れていない限り、結果は以前と同一であるはずです。

ステージのビルド

スクリプト ./x は、各サブコマンドについて、あなたが最も意図していそうなステージを選んでくれるようにします。 以下は、デフォルトのステージを持ついくつかの x コマンドです。

  • check: --stage 1
  • clippy: --stage 1
  • doc: --stage 1
  • build: --stage 1
  • test: --stage 1
  • dist: --stage 2
  • install: --stage 2
  • bench: --stage 2

--stage N を明示的に渡すことで、いつでもステージを上書きできます。

ステージについての詳細は、以下を参照してください

ブートストラップの複雑さ

ビルドシステムは現在のベータコンパイラを使って stage1 ブートストラップ用コンパイラをビルドするため、コンパイラのソースコードは、 一部の機能がベータに到達するまでそれらを使うことができません (そうでなければベータコンパイラがそれらをサポートしていないためです)。 一方で、コンパイラ組み込み関数 や内部機能については、それらの 機能を使わなければなりません。 さらに、コンパイラは nightly 機能(#![feature(...)])を多用します。 この問題をどのように解決できるでしょうか?

使われている方法は 2 つあります。

  1. ビルドシステムは stage0 でビルドするときに --cfg bootstrap を設定するため、 cfg(not(bootstrap)) を使って、stage1 でビルドされるときだけ機能を使うことができます。 この方法で --cfg bootstrap を設定することは、ちょうど安定化されたばかりの機能に使われます。 それらの機能は stage0 でビルドされるときには #![feature(...)] が必要ですが、stage1 では不要です。
  2. ビルドシステムは RUSTC_BOOTSTRAP=1 を設定します。 この特殊な変数は、Rust の 安定性の保証を破る ことを意味します。つまり、nightly ではないコンパイラで #![feature(...)] を使えるようにします。 RUSTC_BOOTSTRAP=1 の設定は、コンパイラをブートストラップする場合を除いて、決して使うべきではありません。

bootstrap のステージを理解する

概要

これは、個別の bootstrap ステージを詳細に見たものです。

./x が使う規約は次のとおりです。

  • --stage N フラグは、stage N コンパイラ(stageN/rustc)を実行することを意味します。
  • “stage N artifact” とは、stage N コンパイラによって_生成_されるビルドアーティファクトです。
  • stage N+1 コンパイラは、stage N アーティファクトから組み立てられます。このプロセスは _昇格_と呼ばれます。

ビルドアーティファクト

./x でビルドできるものはすべて_ビルドアーティファクト_です。 ビルドアーティファクトには、以下が含まれますが、これらに限定されません。

  • stage0-rustc/rustc-main のようなバイナリ
  • stage0-sysroot/rustlib/libstd-6fae108520cf72fe.so のような共有オブジェクト
  • stage0-sysroot/rustlib/libstd-6fae108520cf72fe.rlib のような rlib ファイル
  • doc/std のような、rustdoc によって生成された HTML ファイル

  • ./x test tests/ui は、stage1 コンパイラをビルドし、その上で compiletest を実行することを意味します。 コンパイラに取り組んでいる場合、通常はこれが使用したいテストコマンドです。
  • ./x test --stage 0 library/std は、ソースから rustc をビルドせずに 標準ライブラリでテストを実行することを意味します(「stage0 でビルドし、その後アーティファクトをテストする」)。 標準ライブラリに取り組んでいる場合、通常はこれが使用したいテストコマンドです。
  • ./x build --stage 0 は、stage0 rustc でビルドすることを意味します。
  • ./x doc --stage 1 は、stage0 rustdoc を使用してドキュメントを生成することを意味します。

やってはいけないことの例

  • ./x test --stage 0 tests/ui は有用ではありません。これは beta コンパイラでテストを実行し、ソースから rustc をビルドしません。 代わりに test tests/ui を使用してください。 これはソースから stage1 をビルドします。
  • ./x test --stage 0 compiler/rustc はコンパイラをビルドしますが、テストは実行しません。 これは cargo test -p rustc を実行していますが、cargo は Rust のテストを理解しません。 これを使用する必要はないはずです。代わりに(引数なしで)test を使用してください。
  • ./x build --stage 0 compiler/rustc はコンパイラをビルドしますが、 libstdlibcore さえもビルドしません。 ほとんどの場合、代わりに ./x build library を使用したいはずです。 これにより、lang item を定義する必要なくプログラムをコンパイルできるようになります。

ビルドと実行

要するに、stage 0 は stage0 コンパイラを使用して stage0 アーティファクトを作成し、 それらは後で stage1 コンパイラへ昇格されます

0 以外の各ステージでは、2 つの主要なステップが実行されます。

  1. std が stage N コンパイラによってコンパイルされます。
  2. その std が、stage N コンパイラによってビルドされたプログラムにリンクされます。これには stage N アーティファクト(stage N+1 コンパイラ)が含まれます。

stage N アーティファクトを、stage N コンパイラでビルドしている「単なる」 別のプログラムだと考えると、これはある程度直感的です。build --stage N compiler/rustc は、stage N アーティファクトを、stage N コンパイラによってビルドされた std にリンクしています。

ステージと std

ここでは、2 つの std ライブラリが関係していることに注意してください。

  1. stageN/rustc に_リンク_されるライブラリ。これは stage N-1 によってビルドされたものです(stage N-1 std
  2. stageN/rustc で_プログラムをコンパイルするために使用される_ライブラリ。これは stage N によってビルドされたものです(stage N std)。

stage N std は、stage N コンパイラで有用な作業を行うためにはほぼ必須です。 これがないと、#![no_core] を使ったプログラムしかコンパイルできません。これはあまり役に立ちません!

これらが異なっている必要がある理由は、必ずしも ABI 互換ではないからです。nightly には、beta には存在しない新しいレイアウト最適化、MIR への変更、または Rust メタデータへのその他の変更が含まれている可能性があります。

ここで --keep-stage 1 library/std も関係してきます。 コンパイラへの変更のほとんどは実際には ABI を変更しないため、いったん stage1std を生成すれば、おそらく別のコンパイラでそのまま再利用できます。 ABI が変更されていなければ、問題ありません。その std を再コンパイルする時間を費やす必要はありません。 --keep-stage フラグは単に、前回のコンパイルが問題ないと仮定するようビルドスクリプトに指示し、 それらのアーティファクトを適切な場所にコピーして、 cargo の呼び出しをスキップします。

rustc のクロスコンパイル

クロスコンパイルとは、別のアーキテクチャで実行されるコードをコンパイルするプロセスです。 たとえば、x86 マシンを使用して rustc の ARM 版をビルドしたい場合があります。 クロスコンパイルしている場合、stage2 std のビルドは異なります。

これは、./x が次のロジックを使用するためです。HOSTTARGET が 同じ場合、stage2 では stage1 std を再利用します! これは妥当です。なぜなら、stage1 stdstage1 コンパイラ、つまり現在チェックアウトしているソース コードを使用するコンパイラでコンパイルされたものだからです。 したがって、それは stage2/rustc がコンパイルする std と同一である(したがって ABI 互換である)はずです。

しかし、クロスコンパイル時には、stage1 std はホスト上でしか実行されません。 そのため、stage2 コンパイラはターゲット向けに std を再コンパイルする必要があります。

(表で、stage2 が非ホストの std ターゲットのみをビルドすることを確認してください)。

‘sysroot’ とは何ですか?

cargo でプロジェクトをビルドする場合、依存関係のビルドアーティファクトは 通常 target/debug/deps に保存されます。 ここには cargo が 把握している依存関係だけが含まれます。特に、標準ライブラリは含まれていません。 stdproc_macro はどこから来るのでしょうか? それらは sysroot から来ます。これは、コンパイラが実行時にビルドアーティファクトをロードする 複数のディレクトリのルートです。 ただし、sysroot は標準ライブラリを保存するだけではありません。実行時に ロードする必要があるものはすべて含まれます。 これには以下が含まれますが、これらに限定されません。

  • ライブラリ libstd/libtest/libproc_macro
  • rustc_private を使用する場合の、コンパイラクレート自体。 ツリー内では、これらは常に存在します。ツリー外では、rustuprustc-dev をインストールする必要があります。
  • LLVM プロジェクト用の共有オブジェクトファイル libLLVM.so。 ツリー内では、これはソースからビルドされるか CI からダウンロードされます。ツリー外では、 rustupllvm-tools-preview をインストールする必要があります。

ここまでに挙げたアーティファクトはすべて、コンパイラのランタイム依存関係です。 これらは rustc --print sysroot で確認できます。

$ ls $(rustc --print sysroot)/lib
libchalk_derive-0685d79833dc9b2b.so  libstd-25c6acf8063a3802.so
libLLVM-11-rust-1.50.0-nightly.so    libtest-57470d2aa8f7aa83.so
librustc_driver-4f0cc9f50e53f0ba.so  libtracing_attributes-e4be92c35ab2a33b.so
librustc_macros-5f0ec4a119c6ac86.so  rustlib

標準ライブラリにもランタイム依存関係があります! これらは lib/ 直下ではなく、lib/rustlib/ にあります。

$ ls $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/lib | head -n 5
libaddr2line-6c8e02b8fedc1e5f.rlib
libadler-9ef2480568df55af.rlib
liballoc-9c4002b5f79ba0e1.rlib
libcfg_if-512eb53291f6de7e.rlib
libcompiler_builtins-ef2408da76957905.rlib

ディレクトリ lib/rustlib/ には、hashbrowncfg_if のようなライブラリが含まれています。これらは 標準ライブラリの公開 API には含まれませんが、その実装に使用されています。 また、lib/rustlib/ はリンカーの検索パスに含まれますが、 lib は検索パスに含まれることはありません。

-Z force-unstable-if-unmarked

lib/rustlib/ は検索パスに含まれるため、そこに どのクレートを含めるかについて注意する必要があります。 特に、 標準ライブラリを除くすべてのクレートは -Z force-unstable-if-unmarked フラグ付きでビルドされます。これは、 それをロードするには #![feature(rustc_private)] を使用する必要があることを意味します( 常に利用可能な標準ライブラリとは対照的です)。 -Z force-unstable-if-unmarked フラグには、正しいクレートが unstable としてマークされていることを強制するのに役立つさまざまな目的があります。 これは主に、rustc と標準ライブラリが、staged_api を使用していない crates.io 上の任意のクレートへリンクできるようにするために導入されました。 rustc もこのフラグに依存しており、各クレートを慎重に unstable としてマークする必要がないように、すべてのクレートを rustc_private feature 付きの unstable としてマークします。

このフラグは、bootstrap スクリプトによって rustc 全体と標準ライブラリ全体に自動的に適用されます。 これは、コンパイラとそのすべての依存関係が、すべてのユーザーに対して sysroot 内で提供されるため必要です。

このフラグには次の効果があります。

  • クレート自体が stable または unstable としてマークされていない場合、そのクレートを rustc_private feature 付きの “unstable” としてマークします。
  • これらのクレートが、属性を必要とせずに他の強制的に unstable にされたクレートへアクセスできるようにします。 通常、クレートが他の unstable クレートを使用するには、#![feature(rustc_private)] 属性が必要です。 しかし、そうすると crates.io のクレートが自身の依存関係へアクセスできなくなります。なぜなら、そのクレートには feature(rustc_private) 属性がない一方で、すべて-Z force-unstable-if-unmarked 付きでコンパイルされるためです。

-Z force-unstable-if-unmarked を使用しないコードでは、これらの強制的に unstable にされたクレートへアクセスするために、#![feature(rustc_private)] クレート属性を含める必要があります。 これは、Miriclippy など、rustc 自体にリンクするものに必要です。

sysroot についての詳細な議論は次で確認できます。

bootstrap によって呼び出されるコマンドへフラグを渡す

便利なことに、./x では bootstrapping 時にステージ固有のフラグを rustccargo に渡せます。 RUSTFLAGS_BOOTSTRAP 環境変数は、bootstrap ステージ(stage0)に RUSTFLAGS として渡され、RUSTFLAGS_NOT_BOOTSTRAP は後続ステージ用のアーティファクトをビルドするときに渡されます。 RUSTFLAGS も機能しますが、bootstrap 自体のビルドにも影響するため、これを使いたいことはまれです。 最後に、MAGIC_EXTRA_RUSTFLAGScargo のキャッシュを迂回して、すべての依存関係を再コンパイルせずに rustc へフラグを渡します。

  • RUSTDOCFLAGSRUSTDOCFLAGS_BOOTSTRAPRUSTDOCFLAGS_NOT_BOOTSTRAPRUSTFLAGS と同様ですが、rustdoc 用です。
  • CARGOFLAGS は cargo 自体に引数を渡します(例: --timings)。 CARGOFLAGS_BOOTSTRAPCARGOFLAGS_NOT_BOOTSTRAPRUSTFLAGS_BOOTSTRAP と同様に機能します。
  • --test-args は引数をテストランナーへ渡します。 tests/ui の場合、これは compiletest です。 ユニットテストと doc test の場合、これは libtest ランナーです。

ほとんどのテストランナーは --help を受け付けるため、ランナーが受け付けるオプションを調べるために使用できます。

環境変数

bootstrapping 中には、多数のコンパイラ内部用の環境変数が使用されます。 rustc の中間バージョンを実行しようとしている場合、これらの環境変数の一部を手動で設定する必要があることがあります。 そうしないと、次のようなエラーが発生します。

thread 'main' panicked at 'RUSTC_STAGE was not set: NotPresent', library/core/src/result.rs:1165:5

./stageN/bin/rustc が環境変数に関するエラーを出す場合、それは通常、何かがかなりおかしいことを意味します。たとえば、rustcstd、または環境変数に依存する何かをコンパイルしようとしている場合などです。 そのような状況で本当に rustc を呼び出す必要があるというまれなケースでは、x コマンドに -vvv を追加することで、bootstrap shim にすべての env 変数を表示させることができます。

最後に、bootstrap は cc-rs crate を利用しています。この crate には、環境変数を通じて C コンパイラと C フラグを構成するための独自の方法があります。

ビルドコマンドの stdout の明確化

この部分では、実際の動作におけるビルドコマンドの stdout を調査します (上のトピックと似ていますが、より詳細で完全なドキュメントです)。 x build --dry-run コマンドを実行すると、ビルド出力は次のようになります。

Building stage0 library artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage0 library from stage0 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Building stage0 compiler artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage0 rustc from stage0 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Assembling stage1 compiler (x86_64-unknown-linux-gnu)
Building stage1 library artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage1 library from stage1 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Building stage1 tool rust-analyzer-proc-macro-srv (x86_64-unknown-linux-gnu)
Building rustdoc for stage1 (x86_64-unknown-linux-gnu)

stage0 の {std,compiler} アーティファクトのビルド

これらのステップでは、提供された(通常はダウンロードされた)コンパイラを使用して、ローカルの Rust ソースを、使用可能なライブラリへコンパイルします。

stage0 {std,rustc} のコピー

これは、ライブラリとコンパイラのアーティファクトを cargo から stage0-sysroot/lib/rustlib/{target-triple}/lib へコピーします。

stage1 コンパイラの組み立て

これは、「stage0 の … アーティファクトのビルド」でビルドしたライブラリを、stage1 コンパイラの lib/ ディレクトリへコピーします。 これらは、コンパイラ自体が実行時に使用するホストライブラリです。 これらは、新しいコンパイラが生成するアーティファクトでは実際には使用されません。 このステップでは、生成した rustcrustdoc のバイナリも build/$HOST/stage/bin へコピーします。

stage1/bin/rustc は、stage0(事前コンパイル済み)コンパイラと std でビルドされた、完全に機能するコンパイラです。 ツリー内のコンパイラと std を使って完全にソースからビルドされたコンパイラを使用するには、stage2 コンパイラをビルドする必要があります。これは stage1(ツリー内)コンパイラと std を使ってコンパイルされます。

Bootstrap の仕組み

Bootstrap の中核概念はビルド Step であり、これらは Builder::ensure によって連結されます。Builder::ensureStep を入力として受け取り、 その Step がまだ実行されていない場合に限り、その Step を実行します。 Step をさらに詳しく見てみましょう。

Step の概要

Step は、何らかのアーティファクトを生成するプロセスに関与するアクションの粒度の細かい集合を表します。 Makefile におけるルールのようなものと考えることができます。 Step トレイトは次のように定義されています。

pub trait Step: 'static + Clone + Debug + PartialEq + Eq + Hash {
    type Output: Clone;

    const DEFAULT: bool = false;
    const ONLY_HOSTS: bool = false;

    // 必須メソッド
    fn run(self, builder: &Builder<'_>) -> Self::Output;
    fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_>;

    // 提供されるメソッド
    fn make_run(_run: RunConfig<'_>) { ... }
}
  • run は実際の作業を行う責任を持つ関数です。 Builder::ensurerun を呼び出します。
  • should_run はコマンドラインインターフェイスであり、x build foo のような呼び出しによって 指定された Step を実行すべきかどうかを決定します。パスが指定されていない「デフォルト」のコンテキストでは、 make_run が直接呼び出されます。
  • make_run は、CLI 経由で直接要求されたものに対してのみ呼び出され、 他のステップの依存関係であるステップに対しては呼び出されません。

エントリポイント

中核となる Bootstrap コードに到達する前に、いくつかの準備段階があります。

  1. シェルスクリプトまたは make: ./x または ./x.ps1 または make
  2. 便宜的なラッパースクリプト: x.py
  3. src/bootstrap/bootstrap.py
  4. src/bootstrap/src/bin/main.rs

実装の詳細について、より具体的な説明は src/bootstrap/README.md を参照してください。

Bootstrap でツールを書く

bootstrap で書けるツールには 3 種類あります。

  • Mode::ToolBootstrap

    ツリー内コンパイラから何も必要とせず、stage0 の rustc で実行できるツールにはこれを使用します。 出力は “bootstrap-tools” ディレクトリに配置されます。 このモードは、ターゲットライブラリを含め、stage0 コンパイラだけでビルドされる汎用ツール向けで、 stage 0 でのみ機能します。

  • Mode::ToolStd

    ローカルでビルドされた std に依存するツールにはこれを使用します。 出力は “stageN-tools” ディレクトリに入ります。 このモードはほとんど使われず、主に libtest を必要とする compiletest 用です。

  • Mode::ToolRustcPrivate

    rustc_private メカニズムを使用するツール、 つまりローカルでビルドされた rustc とその rlib アーティファクトに依存するツールにはこれを使用します。 これは他のモードよりも複雑です。 なぜなら、そのツールは rustc に使用されたものと同じコンパイラでビルドされ、 “stageN-tools” ディレクトリに配置されなければならないためです。 Mode::ToolRustcPrivate を選択すると、 ToolBuild の実装がこれを自動的に処理します。 何か特定の用途で builder のコンパイラを使用する必要がある場合は、 ツールの Step から返される ToolBuildResult から取得できます。

ツールの種類にかかわらず、 ツールの Step 実装から ToolBuildResult を返し、 その中で ToolBuild を使用する必要があります。

bootstrap のデバッグ

デバッグ(および bootstrap のプロファイリング)には、主に 2 つの方法があります。1 つ目は println ロギングによる方法で、2 つ目は tracing 機能による方法です。

println ロギング

Bootstrap には、構造化されていない広範なロギングがあります。その大半は --verbose フラグの背後にゲートされています(さらに詳細を得るには -vv を渡します)。

実行された Cargo コマンドの詳細な出力や、その他の種類の詳細ログを確認したい場合は、bootstrap を呼び出すときに -v または -vv を渡してください。ログは構造化されておらず、圧倒される量になる可能性があることに注意してください。

$ ./x dist rustc --dry-run -vv
learning about cargo
running: RUSTC_BOOTSTRAP="1" "/home/jyn/src/rust2/build/x86_64-unknown-linux-gnu/stage0/bin/cargo" "metadata" "--format-version" "1" "--no-deps" "--manifest-path" "/home/jyn/src/rust2/Cargo.toml" (failure_mode=Exit) (created at src/bootstrap/src/core/metadata.rs:81:25, executed at src/bootstrap/src/core/metadata.rs:92:50)
running: RUSTC_BOOTSTRAP="1" "/home/jyn/src/rust2/build/x86_64-unknown-linux-gnu/stage0/bin/cargo" "metadata" "--format-version" "1" "--no-deps" "--manifest-path" "/home/jyn/src/rust2/library/Cargo.toml" (failure_mode=Exit) (created at src/bootstrap/src/core/metadata.rs:81:25, executed at src/bootstrap/src/core/metadata.rs:92:50)
...

bootstrap における tracing

Bootstrap には条件付きの tracing 機能があり、次の機能を提供します。

  • tracing のイベントと span を使用した構造化ロギングを有効にします。
  • 実行されたステップとコマンドの階層と所要時間を可視化するために使用できる Chrome trace file を生成します。
    • 生成された chrome-trace.json ファイルは、Chrome の chrome://tracing タブで開けます。または、たとえば Perfetto を使用して開けます。
  • 実行されたステップ間の依存関係を可視化する GraphViz グラフを生成します。
    • 生成された step-graph-*.dot ファイルは、たとえば xdot を使用して開いてステップグラフを可視化できます。または、たとえば dot -Tsvg を使用して GraphViz ファイルを SVG ファイルに変換できます。
  • コマンド実行サマリーを生成します。これには、どのコマンドが実行されたか、それらの実行のうちいくつがキャッシュされたか、実行に最も時間がかかったコマンドはどれかが示されます。
    • 生成された command-stats.txt ファイルは、シンプルで人間が読みやすい形式です。

構造化ログは標準エラー出力(stderr)に書き込まれます。一方、その他の出力は <build-dir>/bootstrap-trace/<pid> ディレクトリ内のファイルに保存されます。利便性のため、bootstrap は最後に生成されたトレース出力ディレクトリへのシンボリックリンクも <build-dir>/bootstrap-trace/latest に作成します。

--dry-run で bootstrap を実行すると、tracing 出力ディレクトリが変わる可能性があることに注意してください。Bootstrap は、実行の最後に tracing 出力ファイルが保存されたパスを常に表示します。

tracing 出力の有効化

条件付きの tracing 機能を有効にするには、BOOTSTRAP_TRACING 環境変数を指定して bootstrap を実行します。

$ BOOTSTRAP_TRACING=trace ./x build library --stage 1

出力例1:

$ BOOTSTRAP_TRACING=trace ./x build library --stage 1 --dry-run
Building bootstrap
    Finished `dev` profile [unoptimized] target(s) in 0.05s
15:56:52.477  INFO > tool::LibcxxVersionTool {target: x86_64-unknown-linux-gnu} (builder/mod.rs:1715)
15:56:52.575  INFO > compile::Assemble {target_compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }} (builder/mod.rs:1715)
15:56:52.575  INFO > tool::Compiletest {compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu} (builder/mod.rs:1715)
15:56:52.576  INFO  > tool::ToolBuild {build_compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu, tool: "compiletest", path: "src/tools/compiletest", mode: ToolBootstrap, source_type: InTree, extra_features: [], allow_features: "internal_output_capture", cargo_args: [], artifact_kind: Binary} (builder/mod.rs:1715)
15:56:52.576  INFO   > builder::Libdir {compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu} (builder/mod.rs:1715)
15:56:52.576  INFO    > compile::Sysroot {compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, force_recompile: false} (builder/mod.rs:1715)
15:56:52.578  INFO > compile::Assemble {target_compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }} (builder/mod.rs:1715)
15:56:52.578  INFO > tool::Compiletest {compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu} (builder/mod.rs:1715)
15:56:52.578  INFO  > tool::ToolBuild {build_compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu, tool: "compiletest", path: "src/tools/compiletest", mode: ToolBootstrap, source_type: InTree, extra_features: [], allow_features: "internal_output_capture", cargo_args: [], artifact_kind: Binary} (builder/mod.rs:1715)
15:56:52.578  INFO   > builder::Libdir {compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu} (builder/mod.rs:1715)
15:56:52.578  INFO    > compile::Sysroot {compiler: Compiler { stage: 0, host: x86_64-unknown-linux-gnu, forced_compiler: false }, force_recompile: false} (builder/mod.rs:1715)
    Finished `release` profile [optimized] target(s) in 0.11s
Tracing/profiling output has been written to <src-root>/build/bootstrap-trace/latest
Build completed successfully in 0:00:00

tracing 出力の制御

環境変数 BOOTSTRAP_TRACING は、tracing_subscriber フィルター を受け付けます。BOOTSTRAP_TRACING=trace を設定すると、すべてのログが有効になりますが、圧倒される量になる可能性があります。そのため、フィルターを使用してログに記録されるデータ量を減らすことができます。

どの種類の tracing ログを必要とするかを制御するには、互いに直交する 2 つの方法があります。

  1. ログのレベルを指定できます。例: debug または trace
    • レベルを選択すると、同等またはより高い優先度レベルを持つすべてのイベント/スパンが表示されます。
  2. ログのターゲットも制御できます。例: bootstrapbootstrap::core::config、または CONFIG_HANDLINGSTEP のようなカスタムターゲット。
    • カスタムターゲットは、関心のあるスパンの種類を限定するために使用されます。BOOTSTRAP_TRACING=trace の出力は非常に冗長になる可能性があるためです。現在、以下のカスタムターゲットを使用できます。
      • CONFIG_HANDLING: config 処理に関連するスパンを表示します。
      • STEP: 実行されたすべてのステップを表示します。実行されたコマンドは info イベントレベルを持ちます。
      • COMMAND: 実行されたすべてのコマンドを表示します。実行されたコマンドは trace イベントレベルを持ちます。
      • IO: 実行された I/O 操作を表示します。実行されたコマンドは trace イベントレベルを持ちます。
        • 現在、多くの I/O はトレースされていないことに注意してください。

もちろん、これらを組み合わせることもできます(カスタムターゲットログは通常、追加で TRACE ログレベルの背後で制御されます)。

$ BOOTSTRAP_TRACING=CONFIG_HANDLING=trace,STEP=info,COMMAND=trace ./x build library --stage 1

BOOTSTRAP_TRACING を使用して指定するレベルは、Chrome トレースファイルに記録されるスパンにも影響することに注意してください。

FIXME(#96176): compiler()compiler_for() の個別の tracing

追加のターゲット COMPILERCOMPILER_FOR は、 builder.compiler()builder.compiler_for() が何を行うかをトレースするのに役立ちます。 #96176 が解決された場合、これらは削除されるべきです。

bootstrap で tracing を使用する

tracing::* マクロと tracing::instrument proc-macro 属性の両方を、tracing feature の背後で制御する必要があります。例:

#[cfg(feature = "tracing")]
use tracing::instrument;

struct Foo;

impl Step for Foo {
    type Output = ();

    #[cfg_attr(feature = "tracing", instrument(level = "trace", name = "Foo::should_run", skip_all))]
    fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
        trace!(?run, "entered Foo::should_run");

        todo!()
    }

    fn run(self, builder: &Builder<'_>) -> Self::Output {
        trace!(?run, "entered Foo::run");

        todo!()
    }    
}

#[instrument] については、以下が推奨されます。

  • 細かい粒度では trace レベルの背後で制御し、コア関数では場合によって debug レベルの背後で制御します。
  • name = ".." を通じて instrumentation 名を明示的に選択し、たとえば異なるステップの run を区別します。
  • tracing によって挙動の分岐を引き起こさないよう注意してください。たとえば、tracing 基盤が有効な場合にのみ追加のものをビルドする、といったことです。

rust-analyzer 統合?

残念ながら、bootstrap は rust-analyzer.linkedProjects であるため、https://github.com/rust-lang/rust-analyzer/issues/8521 で説明されているようにサポートが不足しており、関連する補完を得るために tracing feature を有効にして bootstrap 自体をチェック/ビルドするよう r-a に依頼することはできません。


  1. この出力は常に今後変更される可能性があります。

コンパイラの依存関係における cfg(bootstrap)

Rustコンパイラは、コンパイラ自身との間で巡回依存に陥る可能性がある外部クレートをいくつか使用しています。つまり、コンパイラはビルドするために更新されたクレートを必要としますが、そのクレートは更新されたコンパイラを必要とします。このページでは、この循環を断ち切るために #[cfg(bootstrap)] をどのように使用できるかを説明します。

#[cfg(bootstrap)] の有効化

通常、外部クレートで #[cfg(bootstrap)] を使用すると警告が発生します。

warning: unexpected `cfg` condition name: `bootstrap`
 --> src/main.rs:1:7
  |
1 | #[cfg(bootstrap)]
  |       ^^^^^^^^^
  |
  = help: expected names are: `docsrs`, `feature`, and `test` and 31 more
  = help: consider using a Cargo feature instead
  = help: or consider adding in `Cargo.toml` the `check-cfg` lint config for the lint:
           [lints.rust]
           unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bootstrap)'] }
  = help: or consider adding `println!("cargo::rustc-check-cfg=cfg(bootstrap)");` to the top of the `build.rs`
  = note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
  = note: `#[warn(unexpected_cfgs)]` on by default

この警告は、プロジェクトの Cargo.toml に次の行を追加することで抑制できます。

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bootstrap)'] }

これで、コンパイラ内で使用できるのと同じように、クレート内で #[cfg(bootstrap)] を使用できます。ブートストラップコンパイラが使用されている場合は #[cfg(bootstrap)] が付与されたコードがコンパイルされ、それ以外の場合は #[cfg(not(bootstrap))] が付与されたコードがコンパイルされます。

更新の一連の手順

具体例として、#[naked] 属性を unsafe 属性に変更したことで、compiler-builtins クレートとの巡回依存が発生した変更を取り上げます。

ステップ 1: コンパイラで新しい動作を受け入れる(#139797

この例では、エラーを無効にすることで、古い動作と新しい動作の両方を同時に受け入れることができます。

ステップ 2: クレートを更新する(#821

次にクレート内で、古い動作を使用するために #[cfg(bootstrap)] を使用するか、新しい動作を使用するために #[cfg(not(bootstrap))] を使用します。

ステップ 3: コンパイラで使用するクレートのバージョンを更新する(#139934

compiler-builtins の場合、これはバージョンの引き上げを意味しました。他の場合では、gitサブモジュールの更新になることもあります。

ステップ 4: コンパイラから古い動作を削除する(#139753

これで更新されたクレートを使用できます。この例では、古い動作を削除できることを意味しました。

高レベルのコンパイラアーキテクチャ

このガイドの残りのパートでは、コンパイラがどのように動作するかを説明します。コンパイラの高レベルな構造から、コンパイルの各段階がどのように機能するかまで、あらゆる内容を扱います。コンパイルのエンドツーエンドのプロセスに関心がある読者にも、貢献したい特定のシステムについて学びたい読者にも読みやすい内容になっているはずです。不明な点があれば、rustc-dev-guide リポジトリで issue を作成するか、パート 1 のこの章で詳しく説明されているように、コンパイラチームに連絡してください。

このパートでは、コンパイラの高レベルなアーキテクチャを見ていきます。特に、コンパイラ全体に影響を与える 3 つの包括的な設計上の選択、すなわちクエリシステム、インクリメンタルコンパイル、インターン化について見ていきます。

コンパイラの概要

この章では、プログラムをコンパイルする全体的なプロセス、つまりすべてがどのように組み合わさっているかについて説明します。

Rust コンパイラは 2 つの点で特別です。ほかのコンパイラが行わないことを コードに対して行う点(例: borrow checking)と、通常とは異なる実装上の選択が 多い点(例: queries)です。 この章ではこれらについて順に説明し、ガイドの残りの部分では、 個々の構成要素をより詳しく見ていきます。

コンパイラがコードに対して行うこと

まず、コンパイラがコードに対して行うことを見ていきましょう。 現時点では、必要な場合を除き、コンパイラがこれらのステップをどのように実装しているかには触れません。

呼び出し

コンパイルは、ユーザーが Rust ソースプログラムをテキストで記述し、それに対して rustc コンパイラを呼び出すところから始まります。 コンパイラが実行する必要のある作業は、コマンドラインオプションによって定義されます。 たとえば、nightly features(-Z flags)を有効にしたり、check のみのビルドを実行したり、実行可能な機械語コードではなく LLVM Intermediate Representation(LLVM-IR)を出力したりすることができます。 rustc 実行ファイルの呼び出しは、cargo の使用を通じて間接的に行われる場合があります。

コマンドライン引数の解析は [rustc_driver] で行われます。 このクレートは、ユーザーが要求したコンパイル設定を定義し、それを [rustc_interface::Config] としてコンパイルプロセスの残りの部分に渡します。

字句解析と構文解析

生の Rust ソーステキストは、[rustc_lexer] にある低レベルの lexer によって解析されます。 この段階で、ソーステキストは tokens と呼ばれる 原子的なソースコード単位のストリームに変換されます。 lexer は Unicode 文字エンコーディングをサポートしています。

トークンストリームは、コンパイルプロセスの次の段階に備えるために、 [rustc_parse] にあるより高レベルの lexer を通過します。 この段階では、[Lexer] struct が一連の検証を実行し、 文字列をインターン化されたシンボルに変換するために使用されます(interning については後述します)。 [String interning] は、個別の文字列値ごとに不変のコピーを 1 つだけ保存する方法です。

lexer は小さなインターフェイスを持ち、rustc の診断 インフラストラクチャには直接依存しません。 代わりに、診断をプレーンなデータとして提供し、それらは [rustc_parse::lexer] で実際の診断として出力されます。 lexer は、IDE と手続き型マクロ (“proc-macros” と呼ばれることもあります)の両方に対して、完全な忠実度の情報を保持します。

parser は、[lexer からのトークンストリームを抽象構文木 (AST)に変換します][parser]。 構文解析には、再帰下降(トップダウン)アプローチを使用します。 parser のクレートエントリポイントは、 [rustc_parse::parser::Parser] にある [Parser::parse_crate_mod][parse_crate_mod] および [Parser::parse_mod][parse_mod] メソッドです。 外部モジュール解析の エントリポイントは [rustc_expand::module::parse_external_mod][parse_external_mod] です。 また、macro-parser のエントリポイントは [Parser::parse_nonterminal][parse_nonterminal] です。

構文解析は、[bump]、 [check]、[eat]、[expect]、[look_ahead] を含む一連の [parser] ユーティリティメソッドを使用して実行されます。

構文解析は意味的な構成要素ごとに整理されています。 個別の parse_* メソッドは [rustc_parse][rustc_parse_parser_dir] ディレクトリ内にあります。 ソースファイル名は構成要素名に従います。 たとえば、parser には次のファイルがあります。

この命名方式は、多くのコンパイラ段階で使用されています。 構文解析、lowering、型 検査、[Typed High-level Intermediate Representation(THIR)][thir] の lowering、および [Mid-level Intermediate Representation(MIR)][mir] の構築ソースの各所で、同じ名前のファイルまたはディレクトリが見つかります。

マクロ展開、AST 検証、名前解決、および early linting も、 字句解析と構文解析の段階で行われます。

[rustc_ast::ast]::{[Crate], [Expr], [Pat], …} AST ノードは parser から返され、エラー処理には標準の [Diag] API が使用されます。 一般に Rust のコンパイラは、Rust の文法のスーパーセットを解析することで エラーからの回復を試みると同時に、エラー型も出力します。

AST lowering

次に、AST は [High-Level Intermediate Representation (HIR)][hir] に変換されます。これは、AST よりもコンパイラにとって扱いやすい表現です。 このプロセスは「lowering」と呼ばれ、ループや async fn のようなものに対して、多くの desugaring(短縮または省略された構文構成要素の展開と 形式化)を伴います。

その後、HIR を使用して、[type inference](式の型を自動的に 検出するプロセス)、[trait solving](trait への各参照に impl を対応付けるプロセス)、および [type checking] を行います。 型検査とは、ユーザーが記述した内容を表す HIR 内の型([hir::Ty])を、 コンパイラが使用する内部表現([Ty<'tcx>])へ変換するプロセスです。 型検査と呼ばれるのは、その情報が、プログラム内で使用される型の 型安全性、正しさ、および一貫性を検証するために使用されるためです。

MIR lowering

HIR はさらに MIR に lowering されます ([borrow checking] に使用されます)。これは、THIR(パターンと網羅性検査に使用される、さらに desugar された HIR)を構築し、それを MIR に変換することで行われます。

MIR は汎用的であり、それによって後続のコード生成とコンパイル速度が向上するため、 [MIR に対して多くの最適化を行います][mir-opt]。 一部の最適化は、LLVM-IR レベルよりも MIR レベルで行う方が簡単です。 たとえば LLVM は、 [simplify_try] MIR-opt が探すパターンを最適化できないようです。

Rust コードはコード生成中に monomorphized もされます。これは、 型パラメーターを具体的な型に置き換えたジェネリックコードのコピーをすべて作成することを意味します。 これを行うには、どの具体的な型に対してコードを生成するかのリストを収集する必要があります。 これは monomorphization collection と呼ばれ、MIR レベルで行われます。

コード生成

次に、単に コード生成 または codegen と呼ばれるものを開始します。 [コード生成段階][codegen] は、ソースの高レベル表現が 実行可能バイナリに変換される段階です。 rustc はコード生成に LLVM を使用するため、 最初のステップは MIRLLVM-IR に変換することです。 ここで MIR が実際に monomorphize されます。 LLVM-IR は LLVM に渡され、LLVM はそれに対してさらに多くの 最適化を行い、追加の低レベル型とアノテーションが付加された、基本的にはアセンブリコードである機械語コード (例: ELF オブジェクトまたは WASM)を出力します。 その後、異なるライブラリやバイナリがリンクされ、最終的なバイナリが生成されます。 [trait solving]: traits/resolution.md [type checking]: hir-typeck/summary.md [type inference]: type-inference.md [bump]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html#method.bump [check]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html#method.check [Crate]: https://doc.rust-lang.org/beta/nightly-rustc/rustc_ast/ast/struct.Crate.html [diag]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_errors/struct.Diag.html [eat]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html#method.eat [expect]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html#method.expect [Expr]: https://doc.rust-lang.org/beta/nightly-rustc/rustc_ast/ast/struct.Expr.html [hir::Ty]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/struct.Ty.html [look_ahead]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html#method.look_ahead [Parser]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html [Pat]: https://doc.rust-lang.org/beta/nightly-rustc/rustc_ast/ast/struct.Pat.html [rustc_ast::ast]: https://doc.rust-lang.org/beta/nightly-rustc/rustc_ast/index.html [rustc_driver]: rustc-driver/intro.md [rustc_interface::Config]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_interface/interface/struct.Config.html [rustc_lexer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lexer/index.html [rustc_parse::lexer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/lexer/index.html [rustc_parse::parser::Parser]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html [rustc_parse]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/index.html [simplify_try]: https://github.com/rust-lang/rust/pull/66282 [Lexer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/lexer/struct.Lexer.html [Ty<'tcx>]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.Ty.html [borrow checking]: borrow-check.md [codegen]: backend/codegen.md [hir]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/index.html [lex]: the-parser.md [mir-opt]: mir/optimizations.md [mir]: mir/index.md [parse_crate_mod]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html#method.parse_crate_mod [parse_external_mod]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_expand/module/fn.parse_external_mod.html [parse_mod]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html#method.parse_mod [parse_nonterminal]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/parser/struct.Parser.html#method.parse_nonterminal [parser]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse/index.html [rustc_parse_parser_dir]: https://github.com/rust-lang/rust/tree/HEAD/compiler/rustc_parse/src/parser [String interning]: https://en.wikipedia.org/wiki/String_interning [thir]: ./thir.md

どのように行うのか

コンパイラがあなたのコードに対して何を行うのかを大まかに把握したところで、 次は、それらすべてを_どのように_行うのかを大まかに見ていきましょう。 コンパイラが満たし、最適化する必要のある制約や相反する目標は数多くあります。 たとえば、

  • コンパイル速度: プログラムのコンパイルはどれくらい速いか? コンパイル時の解析をより多く、またはより良く行うと、多くの場合コンパイルは遅くなります。
    • また、インクリメンタルコンパイルをサポートしたいので、その点も考慮する必要があります。 ユーザーがプログラムを変更した場合に、どの作業をやり直す必要があり、 どの作業を再利用できるかを、どのように追跡できるでしょうか?
      • また、インクリメンタルキャッシュにあまり多くのものを保存することはできません。 ディスクから読み込むのに長い時間がかかり、ユーザーのシステム上で多くの 容量を占める可能性があるためです…
  • コンパイラのメモリ使用量: プログラムをコンパイルしている間、必要以上のメモリを使用したくありません。
  • プログラムの速度: コンパイルされたプログラムはどれくらい速いか? コンパイル時の解析をより多く、またはより良く行うと、多くの場合コンパイラはより優れた最適化を行えます。
  • プログラムのサイズ: コンパイルされたバイナリはどれくらい大きいか? 前の項目と似ています。
  • コンパイラのコンパイル速度: コンパイラをコンパイルするのにどれくらい時間がかかるか? これはコントリビューターやコンパイラの保守に影響します。
  • 実装の複雑さ: コンパイラの構築は、人やグループが取り組める中でも最も難しい ことの 1 つであり、Rust はそれほど単純な言語ではありません。では、どのようにして コンパイラのコードベースを管理しやすくするのでしょうか?
  • コンパイラの正しさ: コンパイラが生成するバイナリは、入力プログラムが行うと 記述していることを実行するべきであり、絶えず起きている膨大な変更にもかかわらず、 そうし続けるべきです。
  • 統合: cargoclippyMiri など、さまざまな方法でコンパイラを使用する必要がある 他のツールが多数あり、それらをサポートしなければなりません。
  • コンパイラの安定性: コンパイラは stable チャンネルでクラッシュしたり、不格好に失敗したりするべきではありません。
  • Rust の安定性: コンパイラは、常に行われている実装への多くの変更にもかかわらず、 以前はコンパイルできていたプログラムを壊さないことで、Rust の安定性保証を尊重しなければなりません。
  • 他のツールの制限: rustc はバックエンドで LLVM を使用しており、LLVM には 活用できる強みがある一方で、回避策が必要な側面もあります。

したがって、このガイドの残りを読み進める際には、これらの点を念頭に置いてください。 これらは、私たちが下す判断にしばしば影響します。

中間表現

ほとんどのコンパイラと同様に、rustc は計算を容易にするためにいくつかの中間表現(IR)を使用します。 一般に、ソースコードを直接扱うのは非常に不便で、エラーが発生しやすいものです。 ソースコードは人間にとって扱いやすく、同時に曖昧さがないように設計されていますが、 たとえば型チェックのようなことを行うにはあまり便利ではありません。

代わりに、rustc を含むほとんどのコンパイラは、解析しやすい何らかの IR を ソースコードから構築します。 rustc にはいくつかの IR があり、それぞれ異なる目的に最適化されています:

  • トークンストリーム: レキサーはソースコードから直接トークンのストリームを生成します。 このトークンのストリームは、生のテキストよりもパーサーが扱いやすいものです。
  • 抽象構文木(AST): 抽象構文木は、レキサーによって生成されたトークンのストリームから構築されます。 これは、ユーザーが書いたものをほぼそのまま表します。 これは、構文上の健全性チェック (例: ユーザーが型を書いた場所で型が期待されているかのチェック)を行うのに役立ちます。
  • 高水準 IR(HIR): これは、一種の脱糖された AST です。 構文的にはユーザーが書いたものにまだ近いですが、省略されたライフタイムなどの暗黙的なものもいくつか含みます。 この IR は型チェックに適しています。
  • 型付き HIR(THIR)以前は High-level Abstract IR(HAIR): これは HIR と MIR の中間表現です。 HIR に似ていますが、完全に型付けされており、 さらに少し脱糖されています(例: メソッド呼び出しや暗黙の参照外しが 完全に明示されます)。 その結果、HIR からよりも THIR から MIR に下げる方が容易です。
  • 中水準 IR(MIR): この IR は基本的に制御フローグラフ(CFG)です。 CFG は、プログラムの基本ブロックと、それらの間で制御フローがどのように移動できるかを示す図の一種です。 同様に、MIR にも多数の基本ブロックがあり、 その中に単純な型付き文(例: 代入、単純な計算、 など)と、他の基本ブロックへの制御フローエッジ(例: 呼び出し、値の ドロップ)があります。 MIR は借用チェックや、 未初期化の値のチェックなど、その他の重要なデータフローベースのチェックに使用されます。 また、一連の最適化や定数評価(Miri 経由)にも使用されます。 MIR はまだジェネリックであるため、単相化後よりも ここで多くの解析を効率的に行うことができます。
  • LLVM-IR: これは LLVM コンパイラへのすべての入力の標準形式です。 LLVM-IR は、多数のアノテーションを備えた、一種の型付きアセンブリ言語です。 これは LLVM を使用するすべてのコンパイラで使われる標準形式です(例: clang C コンパイラも LLVM-IR を出力します)。 LLVM-IR は、他の コンパイラが出力しやすく、また LLVM がその上で多数の最適化を実行するのに十分な表現力を持つように設計されています。

もう 1 つ注意すべき点は、コンパイラ内の多くの値が インターン化 されることです。 これは、値を arena と呼ばれる特殊なアロケータに割り当てる、 性能とメモリの最適化です。 そして、arena に割り当てられた値への参照を受け渡します。 これにより、 同一の値(例: プログラム内の型)が一度だけ割り当てられるようにし、 ポインタを比較することで低コストに比較できるようになります。 中間表現の多くはインターン化されます。

クエリ

最初の大きな実装上の選択は、Rust がコンパイラで クエリ システムを使用していることです。 Rust コンパイラは、コードに対して順番に実行される一連のパスとして構成されて_いません_。 Rust コンパイラがこれを行うのは、 インクリメンタルコンパイルを可能にするためです。つまり、ユーザーが プログラムに変更を加えて再コンパイルした場合、新しいバイナリを出力するために できるだけ冗長な作業を少なくしたいのです。

rustc では、上記の主要なステップはすべて、互いに呼び出し合う多数のクエリとして構成されています。 たとえば、あるものの型を問い合わせるクエリや、 関数の最適化済み MIR を問い合わせるクエリがあります。 これらのクエリは互いに呼び出すことができ、すべてクエリシステムを通じて追跡されます。 クエリの結果はディスクにキャッシュされるため、コンパイラは前回のコンパイルからどのクエリ結果が 変わったかを判断し、それらだけをやり直すことができます。 これがインクリメンタルコンパイルの仕組みです。

原則として、クエリ化されたステップについては、上記の各処理を各アイテムごとに個別に行います。 たとえば、ある関数の HIR を取り、その HIR に対する LLVM-IR を クエリで問い合わせます。 これが最適化済み MIR の生成を駆動し、それが借用チェッカーを駆動し、それが MIR の生成を駆動する、という具合です。

…ただし、これは非常に過度に単純化した説明です。 実際には、一部のクエリは ディスクにキャッシュされず、またコンパイラの一部は、たとえコードがデッドコードであっても、 正当性のためにすべてのコードに対して実行される必要があります(例: 借用チェッカー)。たとえば、 現在、mir_borrowck クエリはクレートのすべての関数に対して最初に実行されます。 その後、コード生成バックエンドが collect_and_partition_mono_items クエリを呼び出します。このクエリはまず、到達可能なすべての関数について optimized_mir を再帰的に要求し、それが今度はその関数について mir_borrowck を実行してから コード生成ユニットを作成します。 この種の分割は、到達不能な関数であってもそのエラーが出力されることを保証するために、 維持される必要があります。

さらに、コンパイラはもともとクエリシステムを使用するように構築されたわけではありません。クエリ システムはコンパイラに後付けされたため、まだクエリ化されていない部分があります。 また、LLVM は私たちのコードではないため、これもクエリ化されていません。 最終的には前のセクションに列挙したすべてのステップをクエリ化する計画ですが、

2022年11月時点では、`HIR` から

LLVM-IR までのステップだけがクエリ化されています。 つまり、字句解析、構文解析、名前解決、マクロ 展開は、プログラム全体に対して一度に行われます。

ここでもう 1 つ触れておくべきことは、非常に重要な「型付けコンテキスト」である TyCtxt です。これは、すべての中心にある巨大な構造体です。 (この名前はほとんど歴史的なものだという点に注意してください。 これは、型理論における ΓΔ の意味での「型付けコンテキスト」では_ありません_。 この名前が残っているのは、それがソースコード内の構造体の名前だからです。)すべての クエリは TyCtxt 型のメソッドとして定義され、メモリ内のクエリ キャッシュもそこに格納されます。 コード内には通常、型付けコンテキストへのハンドルである tcx という変数があります。 また、'tcx という 名前のライフタイムも目にするでしょう。これは、何かが TyCtxt のライフタイムに結び付いていることを意味します(通常、それはそこに格納またはインターン化されています)。

コンパイラ内のクエリの詳細については、クエリの章を参照してください。

ty::Ty

型は Rust において非常に重要であり、多くのコンパイラ解析の中核を成しています。 (コンパイラ内で)型(ユーザーの プログラム内の型)を表す主要な型は rustc_middle::ty::Ty です。 これは非常に重要なので、ty::Ty については丸ごと 1 章を 割いていますが、ここではひとまず、それが存在し、rustc が型を表現する方法であることだけを述べておきます!

また、rustc_middle::ty モジュールが、前述の TyCtxt 構造体を定義していることにも注意してください。

並列性

コンパイラの性能は、私たちが改善したいと考えている(そして常に取り組んでいる)問題です。 その一側面が、rustc 自体の並列化です。 現在、rustc のうちデフォルトで並列化されているのは、ただ1つの部分だけです: コード生成

しかし、コンパイラのそれ以外の部分は、まだ並列化されていません。 これには多くの取り組みが費やされてきましたが、一般に難しい問題です。 現在のアプローチは、RefCellMutex に変えることです。つまり、スレッドセーフな内部可変性へ 切り替えます。 しかし、ロック競合、並行実行下でのクエリシステムの不変条件の維持、 そしてコードベースの複雑さについて、継続的な課題があります。 現在の作業は、bootstrap.toml で並列コンパイルを有効にすることで試せます。 まだ初期段階ですが、 すでに有望なパフォーマンス改善がいくつか見られています。

ブートストラップ

rustc 自体は Rust で書かれています。 では、コンパイラをどのようにコンパイルするのでしょうか? より古いコンパイラを使って、より新しいコンパイラをコンパイルします。 これは ブートストラップ と呼ばれます。

ブートストラップには興味深い含意がたくさんあります。 たとえば、Rust の主要なユーザーの1つが Rust コンパイラであることを意味するため、私たちは 常に自分たちのソフトウェアをテストしています(「自分のドッグフードを食べる」)。

ブートストラップの詳細については、ガイドのブートストラップのセクションを参照してください。

参考資料

コンパイラソースの高レベルな概要

コンパイラが何をするのかを見たので、 rustc のソースコードが置かれている rust-lang/rust リポジトリの構造を見てみましょう。

この章の前に、コンパイラがどのように動作するかを紹介している 「コンパイラの概要」の章を読むと役に立つかもしれません。

ワークスペースの構造

rust-lang/rust リポジトリは、単一の大きな Cargo ワークスペースで構成されており、 コンパイラ、標準ライブラリ(coreallocstdproc_macroetc)、rustdoc に加えて、ビルドシステムや、 完全な Rust ディストリビューションをビルドするための多数のツールとサブモジュールを含んでいます。

リポジトリは 3 つの主要なディレクトリで構成されています。

  • compiler/ には rustc のソースコードが含まれています。これは多数のクレートで構成されており、 それらが合わさってコンパイラを形成しています。

  • library/ には標準ライブラリ(coreallocstdproc_macrotest)と、Rust ランタイム(backtracertstartuplang_start)が含まれています。

  • tests/ にはコンパイラテストが含まれています。

  • src/ には rustdocclippycargo、ビルドシステム、 言語ドキュメントなどのソースコードが含まれています。

コンパイラ

コンパイラはさまざまな compiler/ クレートで実装されています。 compiler/ クレートはすべて rustc_* で始まる名前を持ちます。これらは、 小さなものから巨大なものまで、およそ 50 個の相互依存するクレートの集合です。 また、実際のバイナリ(つまり main 関数)である rustc クレートもあります。 これは実際には、他のクレート内のコンパイルのさまざまな部分を駆動する rustc_driver クレートを呼び出す以外には何もしません。

これらのクレートの依存関係の順序は複雑ですが、おおよそ次のようなものです。

  1. rustc(バイナリ)が rustc_driver::main を呼び出します。
  2. rustc_driver は多くの他のクレートに依存していますが、主なものは rustc_interface です。
  3. rustc_interface は、他のほとんどのコンパイラクレートに依存しています。これは、 コンパイル全体を駆動するためのかなり汎用的なインターフェイスです。
  4. 他のほとんどの rustc_* クレートは rustc_middle に依存しており、これは コンパイラ内の多くの中核的なデータ構造を定義しています。
  5. rustc_middle と他のほとんどのクレートは、コンパイラの初期部分(たとえばパーサー)、 基本的なデータ構造(たとえば Span)、またはエラー報告を表すいくつかのクレートに依存しています。 rustc_data_structuresrustc_spanrustc_errors などです。

他の Rust パッケージの場合と同じように、cargo tree を実行することで 正確な依存関係を確認できます。

cargo tree --package rustc_driver

最後にもう 1 つ、src/llvm-project は、私たちの LLVM フォーク用のサブモジュールです。 ブートストラップ中に LLVM がビルドされ、compiler/rustc_llvm クレートには LLVM(C++ で書かれています)を包む Rust ラッパーが含まれているため、 コンパイラは LLVM とインターフェイスできます。

この本の大部分はコンパイラについて扱っているため、ここではこれらのクレートについて これ以上説明しません。

全体像

コンパイラの依存関係構造は、主に 2 つの要因の影響を受けています。

  1. 構成。コンパイラは 巨大な コードベースです。1 つのクレートとしては、 ありえないほど大きくなってしまうでしょう。依存関係構造は、ある程度、 コンパイラのコード構造を反映しています。
  2. コンパイル時間。コンパイラを複数のクレートに分割することで、Cargo を使った インクリメンタル/並列コンパイルをより有効に活用できます。特に、1 つを変更したときに 再ビルドしなければならないクレートの数を増やさないよう、クレート間の依存関係を できるだけ少なくするよう努めています。

依存関係ツリーの一番下には、コンパイラ全体で使われるいくつかのクレート (たとえば rustc_span)があります。コンパイルプロセスの非常に初期の部分 (たとえばパースと抽象構文木(AST)は、これらのみに依存しています。

AST が構築され、他の初期解析が完了すると、コンパイラの クエリシステム がセットアップされます。クエリシステムは、関数ポインタを使った 巧妙な方法でセットアップされます。これにより、クレート間の依存関係を切り離し、 より多くの並列コンパイルを可能にしています。クエリシステムは rustc_middle で定義されているため、 コンパイラの後続部分のほぼすべてがこのクレートに依存しています。これは本当に大きなクレートであり、 コンパイル時間が長くなる原因になっています。さまざまな成果の差はありますが、 そこからものを移動する取り組みも行われてきました。もう 1 つの副作用として、 関連する機能が異なるクレートに分散してしまうことがあります。たとえば、lint 機能は、 クレートの初期部分、rustc_lintrustc_middle、その他の場所にまたがって存在します。

理想的には、より少数でより凝集度の高いクレートがあり、インクリメンタルコンパイルと 並列コンパイルによってコンパイル時間が妥当な範囲に保たれるべきです。しかし、 インクリメンタルコンパイルと並列コンパイルはまだそのために十分な水準には達していないため、 現時点では、物事を別々のクレートに分割することが私たちの解決策となっています。 依存関係ツリーの最上位には rustc_driverrustc_interface があり、 rustc_interface はコンパイルのさまざまな段階を進行させるのに役立つ、クエリシステムを包む不安定なラッパーです。 コンパイラの他の利用者は、このインターフェイスを別の方法で使用する場合があります(例: rustdoc、あるいは最終的には rust-analyzer など)。 rustc_driver クレートは、まずコマンドライン引数を解析し、その後 rustc_interface を使用してコンパイルを完了まで進めます。

rustdoc

rustdoc の大部分は librustdoc にあります。ただし、rustdoc バイナリ 自体は src/tools/rustdoc であり、rustdoc::main を呼び出す以外のことは何もしません。

ドキュメント用の JavaScriptCSSsrc/tools/rustdoc-js および src/tools/rustdoc-themes にあります。--output-format=json の型定義は、src/rustdoc-json-types にある別のクレートにあります。

rustdoc について詳しくは、この章を読むことができます。

テスト

上記すべてのテストスイートは tests/ にあります。テストスイートについて詳しくは この章を読むことができます。

テストハーネスは src/tools/compiletest/ にあります。

ビルドシステム

リポジトリには、コンパイラ、標準ライブラリ、rustdoc などのビルドや、 テスト、完全な Rust ディストリビューションのビルドなどのためだけに用意された多数のツールがあります。

主要なツールの 1 つが src/bootstrap/ です。ブートストラップについて詳しくは この章を読むことができます。このプロセスでは、tidy/compiletest/ など、src/tools/ の他のツールも使用する場合があります。

標準ライブラリ

このコードは、他のほとんどの Rust クレートとかなり似ていますが、不安定な(nightly) 機能を使用できるため、特別な方法でビルドする必要があります。 標準ライブラリは、libstd or the "standard facade" と呼ばれることもあります。

その他

rust-lang/rust リポジトリには、完全な Rust ディストリビューションのビルドに関連する 他のものが多数あります。ほとんどの場合、それらについて心配する必要はありません。

これらには以下が含まれます。

  • src/ci: CI 構成。多くのプラットフォームで多数のテストを実行するため、これは実際にはかなり大規模です。
  • ほかにもあります…

クエリ: デマンド駆動コンパイル

コンパイラの概要で説明したように、Rust コンパイラは ( 2021 年 7 月時点で)従来の「パスベース」の構成から 「デマンド駆動」のシステムへ移行している最中です。 コンパイラクエリシステムは、rustc のデマンド駆動構成の鍵となるものです。 考え方は非常にシンプルです。 完全に独立したパス (構文解析、型チェックなど)の代わりに、関数のような一連のクエリが 入力ソースに関する情報を計算します。 たとえば、 type_of というクエリがあり、これは何らかのアイテムの DefId を 与えると、そのアイテムの型を計算して返します。

クエリの実行はメモ化されます。最初にクエリを呼び出したときは 計算を行いますが、次回以降は結果がハッシュテーブルから返されます。 さらに、クエリの実行は インクリメンタル計算にうまく適合します。大まかな考え方としては、 クエリを呼び出したときに、ディスクから保存済みデータを読み込むことで 結果が返される場合がある、というものです。1

最終的には、コンパイラの制御フロー全体をクエリ駆動にしたいと考えています。 実質的には、1 つのトップレベルクエリ(compile)があり、クレートに対してコンパイルを実行します。 これは次に、そのクレートに関する情報を、終端から順に要求していきます。

たとえば次のようになります。

  • compile クエリは、codegen-units のリストを取得することを要求するかもしれません (つまり、LLVM によってコンパイルされる必要があるモジュール)。
  • しかし codegen-units のリストを計算するには、Rust ソースで定義されている すべてのモジュールのリストを返す何らかのサブクエリを呼び出すことになります。
  • そのクエリは次に、HIR を要求する何かを呼び出します。
  • これがさらに遡っていき、最終的に実際の構文解析を行うところに到達します。

この構想はまだ完全には実現されていませんが、コンパイラの大きな部分 (たとえば MIR の生成)は現在、まさにこのように動作しています。

クエリの呼び出し

クエリの呼び出しは簡単です。 TyCtxt(「型コンテキスト」)構造体は、定義済みの各クエリに対応するメソッドを提供します。 たとえば、type_of クエリを呼び出すには、単に次のようにします。

let ty = tcx.type_of(some_def_id);

コンパイラがクエリを実行する方法

では、クエリメソッドを呼び出すと何が起こるのか疑問に思うかもしれません。 答えは、コンパイラが各クエリについて キャッシュを保持している、というものです。あなたのクエリがすでに実行済みであれば、答えは 簡単です。キャッシュから戻り値を clone して返します (したがって、クエリの戻り値の型は 低コストで clone できるようにしておくべきです。必要であれば Rc を挿入してください)。

プロバイダー

しかし、クエリがキャッシュにない場合、コンパイラは対応する プロバイダー関数を呼び出します。 プロバイダーは、特定のモジュールで実装され、コンパイラの初期化時に (ローカルクレートのクエリについては)Providers 構造体、または (外部クレートのクエリについては)ExternProviders 構造体のいずれかに 手動で登録される関数です。 マクロシステムは両方の構造体を生成します。 これらはすべてのクエリ実装のための関数テーブルとして機能し、各 フィールドは実際のプロバイダーへの関数ポインターです。

注: ProvidersExternProviders の両方の構造体はマクロによって生成され、すべてのクエリ実装のための関数テーブルとして機能します。 これらは Rust のトレイトではなく、関数ポインターフィールドを持つ単なる構造体です。

プロバイダーはクレートごとに定義されます。 コンパイラは内部的に、 少なくとも概念上は、すべてのクレートについてプロバイダーのテーブルを保持しています。 プロバイダーには 2 つの集合があります。

  • ローカルクレート(つまり、コンパイル中のクレート)に関するクエリのための Providers 構造体
  • 外部クレート(つまり、 ローカルクレートの依存関係)に関するクエリのための ExternProviders 構造体

クエリが対象とするクレートを決定するのは、クエリの種類ではなく、キーであることに注意してください。 たとえば、tcx.type_of(def_id) を呼び出したとき、それは def_id がどのクレートを参照しているかに応じて、ローカルクエリにも外部クエリにもなり得ます (これがどのように動作するかの詳細については、self::keys::QueryKey トレイトを参照してください)。

プロバイダーは常に同じシグネチャを持ちます。

fn provider<'tcx>(
    tcx: TyCtxt<'tcx>,
    key: QUERY_KEY,
) -> QUERY_RESULT {
    ...
}

プロバイダーは 2 つの引数、すなわち tcx とクエリキーを受け取ります。 そしてクエリの結果を返します。

N.B. rustc_* クレートのほとんどは ローカルプロバイダーのみを提供します。 ほぼすべての 外部プロバイダーは、最終的に rustc_metadata クレートを経由します。これはクレートメタデータから情報を読み込みます。 ただし場合によっては、 ローカルクレートと外部クレートの両方に対してクエリを提供するクレートもあり、その場合は provide 関数と provide_extern 関数の両方を定義し、 wasm_import_module_map を通じて、rustc_driver がそれらを呼び出せるようにします。

プロバイダーの設定方法

tcx が作成されるとき、その作成者によって、rustc_middle::utilProviders 構造体を使って、ローカルプロバイダーと外部プロバイダーの両方が与えられます。 この構造体には、ローカルプロバイダーと外部プロバイダーの両方が含まれています。

pub struct Providers {
    pub queries: crate::query::Providers,        // ローカルクレートのプロバイダー
    pub extern_queries: crate::query::ExternProviders,  // 外部クレートのプロバイダー
    pub hooks: crate::hooks::Providers,
}

これらの各プロバイダー構造体はマクロによって生成され、それぞれのクエリに対応する関数ポインターを含んでいます。

プロバイダーはどのように登録されるか?

util::Providers 構造体は、コンパイラ初期化時に、rustc_interface クレートによって DEFAULT_QUERY_PROVIDERS static から埋められます。 実際のプロバイダー関数は、さまざまな rustc_* クレート(rustc_middlerustc_hir_analysis など)にわたって定義されています。

プロバイダーを登録するために、各クレートは次のような provide 関数を公開します。

pub fn provide(providers: &mut query::Providers) {
    *providers = query::Providers {
        type_of,
        // ... ここにさらにプロバイダーを追加する
        ..*providers
    };
}

この関数は util::Providers ではなく query::Providers を受け取ることに注意してください。 単に query::Providers を受け取るだけではない provide 関数が必要になることは極めてまれです。 util::Providersqueries フィールド以外も更新する場合は、代わりに util::Providers を受け取ることができます:

pub fn provide(providers: &mut rustc_middle::util::Providers) {
    providers.queries.type_of = type_of;
    // ... ここにローカルプロバイダーをさらに追加します

    providers.extern_queries.type_of = extern_type_of;
    // ... ここに外部プロバイダーをさらに追加します

    providers.hooks.some_hook = some_hook;
    // ... ここにフックをさらに追加します
}

新しいプロバイダーを追加する

fubar という新しいクエリを追加したいとします。 このセクションではプロバイダーの接続に焦点を当てます。大きな rustc_queries! マクロ内でクエリ自体を宣言する方法については、以下の 新しいクエリを追加する を参照してください。

実際には通常、次のようにします:

  1. どのクレートがそのクエリを「所有」するかを決めます(たとえば rustc_hir_analysisrustc_mir_build、または別の rustc_* クレート)。
  2. そのクレートで、既存の provide 関数を探します:
    pub fn provide(providers: &mut query::Providers) {
        // 既存の割り当て
    }
    存在する場合は、新しいクエリ用のフィールドを設定するようにそれを拡張します。 そのクレートにまだ provide 関数がない場合は、追加したうえで、初期化中に実際に呼び出されるように rustc_interface クレート内の DEFAULT_QUERY_PROVIDERS に含められていることを確認してください(上記の説明を参照)。
  3. プロバイダー関数自体を実装します:
    fn fubar<'tcx>(tcx: TyCtxt<'tcx>, key: LocalDefId) -> Fubar<'tcx> { ... }
  4. クレートの provide 関数に登録します:
    pub fn provide(providers: &mut query::Providers) {
        *providers = query::Providers {
            fubar,
            ..*providers
        };
    }

クエリが外部クレートのメタデータとどのようにやり取りするか

外部クレート(つまり依存関係)に対してクエリが行われると、クエリシステムはそのクレートのメタデータから情報を読み込む必要があります。 これは rustc_metadata クレート によって処理されます。このクレートは、.rmeta ファイルに格納された情報のデコードと提供を担当します。

処理の流れは次のとおりです:

  1. クエリが行われると、クエリシステムはまず def_id.krate == LOCAL_CRATE かどうかを確認して、DefId がローカルクレートを指しているのか外部クレートを指しているのかを判定します。 これにより、Providers のローカルプロバイダーを使用するか、ExternProviders の外部プロバイダーを使用するかが決まります。

  2. 外部クレートの場合、クエリシステムは ExternProviders 構造体内のプロバイダーを探します。 rustc_metadata クレートは、rustc_metadata/src/rmeta/decoder/cstore_impl.rs 内の provide_extern 関数を通じて、これらの外部プロバイダーを登録します。 たとえば次のようになります:

    #![allow(unused)]
    fn main() {
    pub fn provide_extern(providers: &mut ExternProviders) {
        providers.foo = |tcx, def_id| {
            // 外部クレートのメタデータを読み込み、デコードする
            let cdata = CStore::from_tcx(tcx).get_crate_data(def_id.krate);
            cdata.foo(def_id.index)
        };
        // 他の外部プロバイダーを登録する...
    }
    }
  3. メタデータは .rmeta ファイル内にバイナリ形式で格納され、外部クレートに関する事前計算済みの情報(型、関数シグネチャ、トレイト実装、コンパイラが必要とするその他の情報など)を含みます。 外部クエリが行われると、rustc_metadata クレートは次のことを行います:

    • 外部クレートの .rmeta ファイルを読み込む
    • Decodable トレイトを使用してメタデータをデコードする
    • デコードされた情報をクエリシステムに返す

このアプローチにより、外部クレートの再コンパイルを避け、依存クレートのコンパイルを高速化し、インクリメンタルコンパイルをクレート境界を越えて機能させることができます。 簡略化した例を示します。外部クレートで定義された型に対して tcx.type_of(def_id) を呼び出すと、クエリシステムは次のように動作します:

  1. def_id.krate != LOCAL_CRATE を確認して、def_id が外部クレートを指していることを検出する
  2. rustc_metadata によって登録された ExternProviders から適切なプロバイダーを呼び出す
  3. プロバイダーは、外部クレートのメタデータから型情報を読み込み、デコードする
  4. デコードされた型を呼び出し元に返す

ほとんどの rustc_* クレートがローカルプロバイダーだけを提供すればよいのはこのためです。外部プロバイダーはメタデータシステムによって処理されます。 唯一の例外は、クレートが外部クエリに対して特別な処理を提供する必要がある場合で、その場合はローカルプロバイダーと外部プロバイダーの両方を実装することになります。

クレートをまたいで機能するべき新しいクエリを定義しても、rustc_queries! に列挙されているというだけで、自動的にクロスクレートになるわけではありません。 通常は次のことを行う必要があります:

  • 適切な修飾子(たとえばディスクにキャッシュされるかどうか)を付けて、クエリを rustc_queries! に追加する。
  • 所有するクレートでローカルプロバイダーを実装し、そのクレートの provide 関数を通じて登録する。
  • provide_extern を通じて rustc_metadata に外部プロバイダーを追加し、クエリの結果がクレートメタデータにエンコードおよびデコードされるようにする。

このようなクロスクレートクエリを導入する例は、rust-lang/rust リポジトリのコミット 996a185 で確認できます。


新しいクエリを追加する

新しいクエリはどのように追加するのでしょうか? クエリの定義は2つの手順で行われます:

  1. クエリ名、引数、説明を宣言する。
  2. 必要な場所にクエリプロバイダーを用意する。

クエリ名と引数を宣言するには、compiler/rustc_middle/src/query/mod.rs にある大きなマクロ呼び出しにエントリを追加するだけです。 次に、それに 内部向け の説明を含むドキュメントコメントを追加する必要があります。 その後、クエリの ユーザー向け の説明を含む desc 属性を指定します。 desc 属性は、クエリサイクル内でユーザーに表示されます。

これは次のようになります:

rustc_queries! {
    /// すべてのアイテムの型を記録する。
    query type_of(key: DefId) -> Ty<'tcx> {
        desc { |tcx| "`{}` の型を計算中", tcx.def_path_str(key) }
        cache_on_disk
        separate_provide_extern
    }
    ...
}

クエリ定義は次の形式を取ります:

query type_of(key: DefId) -> Ty<'tcx> { ... }
^^^^^ ^^^^^^^      ^^^^^     ^^^^^^^^   ^^^
|     |            |         |          |
|     |            |         |          クエリ修飾子
|     |            |         結果型
|     |            クエリキー型
|     クエリ名
query キーワード

これらの要素を1つずつ見ていきましょう:

  • クエリキーワード: クエリ定義の開始を示します。
  • クエリ名: クエリメソッドの名前です(tcx.type_of(..))。 このクエリを表すために生成される構造体(ty::queries::type_of)の名前としても使用されます。
  • クエリキー型: このクエリへの引数の型です。 この型は ty::query::keys::QueryKey トレイトを実装している必要があります。このトレイトは、たとえばそれをクレートに対応付ける方法などを定義します。
  • クエリの結果型: このクエリによって生成される型です。 この型は (a) RefCell やその他の内部可変性を使用せず、(b) 低コストでクローン可能であるべきです。 自明でないデータ型には、インターン化するか、Rc または Arc を使用することが推奨されます。2
  • クエリ修飾子: クエリの処理方法をカスタマイズするさまざまなフラグやオプションです(主にインクリメンタルコンパイルに関するもの)。

したがって、クエリを追加するには次のようにします。

  • 上記の形式を使用して、rustc_queries! にエントリを追加します。
  • 適切な provide メソッドを変更してプロバイダーをリンクします。 または、必要に応じて新しいものを追加し、rustc_driver がそれを呼び出していることを確認します。

外部リンク

関連する設計案と追跡 issue:

さらなる議論と issue:


  1. インクリメンタルコンパイルの詳細の章では、クエリとは何か、 またそれらがどのように動作するかについて、より詳細に説明しています。 自分でクエリを書くつもりがあるなら、読む価値があります。

  2. これらの規則の唯一の例外は ty::steal::Steal 型です。 これは、MIR を低コストでインプレースに変更するために使用されます。 詳細については、Steal の定義を参照してください。 @rust-lang/compiler に知らせずに、Steal の新しい使用を追加するべきではありません

クエリ評価モデルの詳細

この章では、クエリの基盤となる抽象モデルをより深く掘り下げます。 実装の詳細には踏み込みませんが、背後にあるロジックを説明しようとします。 そのため、ここでの例は簡略化されており、コンパイラの内部APIを直接反映しているわけではありません。

クエリとは何か?

抽象的には、あるクレートについてのコンパイラの知識を「データベース」と見なし、 クエリはそれについてコンパイラに質問する方法、つまり事実を得るためにコンパイラの「データベース」に「クエリ」を行う方法です。

しかし、このコンパイラデータベースには特別な点があります。最初は空であり、 クエリが実行されるとオンデマンドで埋められていきます。したがって、クエリは、 データベースにまだ結果が含まれていない場合に、自分の結果を計算する方法を知っていなければなりません。 そのために、クエリは他のクエリや、データベースの作成時に事前に格納されている特定の入力値にアクセスできます。

したがって、クエリは次のものから構成されます。

  • クエリを識別する名前
  • 何を検索したいかを指定する「キー」
  • どのような種類の結果を返すかを指定する結果型
  • 結果がまだデータベースに存在しない場合に、それをどのように計算するかを指定する関数である「プロバイダー」

例として、type_of クエリの名前は type_of であり、そのクエリキーは型を知りたい項目を識別する DefId、結果型は Ty<'tcx>、そしてプロバイダーは、クエリキーとデータベースの残りの部分へのアクセスを与えられると、 そのキーによって識別される項目の型を計算できる関数です。

したがって、ある意味では、クエリは単にクエリキーを対応する結果へ写像する関数にすぎません。 しかし、これが健全であるためには、いくつかの制約を課す必要があります。

  • キーと結果はイミュータブルな値でなければなりません。
  • プロバイダー関数は、同じキーに対して常に同じ結果を返さなければならないという意味で、純粋関数でなければなりません。
  • プロバイダー関数が受け取る唯一のパラメーターは、キーと「クエリコンテキスト」への参照です(これは「データベース」の残りの部分へのアクセスを提供します)。

データベースは、クエリを呼び出すことで遅延的に構築されます。クエリプロバイダーは他のクエリを呼び出し、 その結果はすでにキャッシュされているか、別のクエリプロバイダーを呼び出すことによって計算されます。 これらのクエリプロバイダー呼び出しは、概念的には有向非巡回グラフ(DAG)を形成し、 その葉には、クエリコンテキストが作成された時点ですでに分かっている入力値があります。

キャッシュ化/メモ化

クエリ呼び出しの結果は「メモ化」されます。これは、クエリコンテキストが内部テーブルに結果をキャッシュし、 同じクエリキーで再びクエリが呼び出されたときには、プロバイダーを再度実行する代わりに、 キャッシュから結果を返すことを意味します。

このキャッシュ化は、クエリエンジンを効率的にするために不可欠です。 メモ化がなくてもシステムは健全です(つまり、同じ結果を返します)が、 同じ計算が何度も繰り返されることになります。

メモ化は、クエリプロバイダーが純粋関数でなければならない主な理由の1つです。 プロバイダー関数を呼び出すたびに異なる結果が返される可能性がある場合(何らかのグローバルなミュータブル状態にアクセスするため)、 結果をメモ化することはできません。

入力データ

クエリコンテキストが作成された時点では、それはまだ空です。実行されたクエリはなく、キャッシュされた結果もありません。 しかし、コンテキストはすでに「入力」データ、つまりコンテキストが作成される前に計算され、 クエリが計算を行うためにアクセスできるイミュータブルなデータ片へのアクセスを提供しています。

2021年1月時点では、この入力データは主に

HIRマップ、上流クレートのメタデータ、そしてコンパイラが呼び出されたときのコマンドラインオプションで構成されています。 しかし将来的には、入力はコマンドラインオプションとソースファイルのリストだけで構成されるようになります。 HIRマップ自体は、これらのソースファイルを処理するクエリによって提供されるようになります。

入力がなければ、クエリは結果を計算するための材料を何も持たない空虚の中に存在することになります (クエリプロバイダーは、他のクエリとコンテキストにしかアクセスできず、その他の外部状態や情報にはアクセスできないことを思い出してください)。

クエリプロバイダーにとって、入力データと他のクエリの結果はまったく同じに見えます。 プロバイダーはコンテキストに「X の値をください」と伝えるだけです。入力データはイミュータブルであるため、 プロバイダーは、クエリ結果の場合と同じように、それが異なるクエリ呼び出し間で同じであることに依存できます。

いくつかのクエリの実行トレース例

このクエリ呼び出しのDAGはどのようにして生まれるのでしょうか?ある時点で、 コンパイラドライバーは、まだ空のクエリコンテキストを作成します。その後、 クエリシステムの外側から、自分のタスクを実行するために必要なクエリを呼び出します。 これはおおよそ次のようになります。

fn compile_crate() {
    let cli_options = ...;
    let hir_map = ...;

    // クエリコンテキスト `tcx` を作成する
    let tcx = TyCtxt::new(cli_options, hir_map);

    // 型チェッククエリを呼び出して型チェックを行う
    tcx.type_check_crate();
}

type_check_crate クエリプロバイダーは、おおよそ次のようになります。

fn type_check_crate_provider(tcx, _key: ()) {
    let list_of_hir_items = tcx.hir_map.list_of_items();

    for item_def_id in list_of_hir_items {
        tcx.type_check_item(item_def_id);
    }
}

type_check_crate クエリが入力データ (tcx.hir_map.list_of_items())にアクセスし、他のクエリ (type_check_item)を呼び出していることが分かります。type_check_item の呼び出しは、それ自体が入力データにアクセスしたり、他のクエリを呼び出したりします。 その結果、最終的には、最初に実行されたノードから逆向きに、クエリ呼び出しのDAGが構築されます。

         (2)                                                 (1)
  list_of_all_hir_items <----------------------------- type_check_crate()
                                                               |
    (5)             (4)                  (3)                   |
  Hir(foo) <--- type_of(foo) <--- type_check_item(foo) <-------+
                                      |                        |
                    +-----------------+                        |
                    |                                          |
    (7)             v  (6)                  (8)                |
  Hir(bar) <--- type_of(bar) <--- type_check_item(bar) <-------+

// (x) は呼び出し順を表す

また、クエリ結果はしばしばキャッシュから読み取れることも分かります。 type_of(bar)type_check_item(foo) のために計算されているため、 type_check_item(bar) がそれを必要としたときには、すでにキャッシュ内にあります。

クエリ結果は、コンテキストが生存している限り、クエリコンテキスト内にキャッシュされたままになります。 したがって、コンパイラドライバーが後で別のクエリを呼び出した場合でも、上記のグラフはまだ存在しており、 すでに実行されたクエリをやり直す必要はありません。

サイクル

先ほど、クエリ呼び出しはDAGを形成すると述べました。しかし、たとえば次のようなクエリプロバイダーを用意すると、 巡回グラフを簡単に形成できてしまいます。

fn cyclic_query_provider(tcx, key) -> u32 {
  // 同じキーで同じクエリを再び呼び出す
  tcx.cyclic_query(key)
}

クエリプロバイダーは通常の関数なので、これはほぼ期待どおりに振る舞います。 評価は無限再帰に陥って停止します。このようなクエリは、いずれにしても あまり有用ではありません。しかし、ときには特定の種類の不正なユーザー入力により、 クエリが循環的に呼び出されることがあります。クエリエンジンには、 同じ入力引数によるクエリの循環呼び出しをチェックする仕組みが含まれています。 そして、サイクルは回復不能なエラーであるため、人間が読めることを意図した “cycle error” メッセージとともに実行を中止します。

ある時点で、コンパイラーには「サイクルリカバリー」という概念がありました。つまり、 クエリの実行を「試行」し、それがサイクルを引き起こした場合には、別の方法で 処理を続行できるというものです。しかし、これは後に削除されました。というのも、 これが理論的にどのような結果をもたらすのか、特にインクリメンタルコンパイルに関して 完全には明らかではないためです。

“Steal” クエリ

一部のクエリは、その結果が Steal<T> 構造体でラップされています。これらのクエリは 1つの例外を除いて通常のクエリとまったく同じように振る舞います。その結果は、 ある時点でキャッシュから「盗まれる」ことが期待されています。つまり、プログラムの 別の部分がその所有権を取得し、その結果にはそれ以降アクセスできなくなります。

この盗用メカニズムは純粋にパフォーマンス最適化として存在します。というのも、 一部の結果値はクローンするにはコストが高すぎるためです(例: 関数の MIR)。結果を 盗むことは、クエリ結果が不変でなければならないという条件に違反するように見えます (結局のところ、結果値をキャッシュの外へ移動しているためです)が、その変更が 観測可能でない限り問題ありません。これは次の2つによって実現されます。

  • 結果が盗まれる前に、その結果を読み取る必要が生じうるすべてのクエリを積極的に 実行しておくようにします。これは、それらのクエリを呼び出すことで手動で行う必要があります。
  • クエリが盗まれた結果にアクセスしようとするたびに、ICE (内部コンパイラーエラー)を発生させ、そのような状態が見過ごされないようにします。

これは手動での介入が必要になるため理想的な構成ではありません。そのため、 使用は控えめにし、どのクエリが特定の結果にアクセスしうるかが十分に分かっている場合にのみ 使用すべきです。しかし実際には、盗用が大きな保守負担になることはありませんでした。

まとめると、「Steal クエリ」は制御された方法で一部のルールを破ります。 何かが気付かれないまま誤って進行することがないようにするためのチェックが用意されています。

インクリメンタルコンパイル

インクリメンタルコンパイルの仕組みは、本質的にはクエリシステム全体に対する 驚くほど単純な拡張です。まず、実際のものを少し単純化した変種、 すなわち「基本アルゴリズム」について説明し、その後で考えられる改善点を いくつか説明します。

基本アルゴリズム

基本アルゴリズムは 赤緑アルゴリズム1と呼ばれます。大まかな考え方は、 コンパイラを実行するたびに、実行したすべてのクエリの結果と クエリDAGを保存するというものです。 クエリDAGは、どのクエリがどの他のクエリを実行したかを 索引付けするDAGです。たとえば、Q1を計算するためにQ2を 計算する必要があった場合、クエリQ1から別のクエリQ2への エッジが存在します(クエリは自分自身に依存できないため、 これは一般的なグラフではなくDAGになります)。

NOTE: クエリとは単にクエリの定義のことだと考えるかもしれません。 関数のように呼び出すことができ、 キャッシュされた結果を返すか、実際にコードを実行するものです。

クエリをそのように考える場合、 以下の文章ではクエリに色があると言われることを知っておくとよいでしょう。 ただし、ここでのクエリという語は、特定の入力に対するクエリの ある呼び出しも指していることに注意してください。後で読むように、 クエリはその引数に基づいてフィンガープリント化されます。ある引数を 渡したときにはクエリの結果が変わって赤に色付けされる一方で、 別の引数では同じままであり、そのため緑になることがあります。

要するに、ここでのクエリという語は、単にクエリの定義を意味するためだけに 使われているのではなく、与えられた引数を持つそのクエリの特定のインスタンスも 指しています。

次回コンパイラを実行するときには、クエリを再実行しないようにするために、 これらのクエリ結果を再利用できる場合があります。これは、すべてのクエリに を割り当てることで行います。

  • クエリがに色付けされている場合、それはこのコンパイル中の結果が 前回のコンパイルから変化したことを意味します。
  • クエリがに色付けされている場合、それはその結果が 前回のコンパイルと同じであることを意味します。

ここには2つの重要な洞察があります。

  • 第一に、クエリQへのすべての入力が緑に色付けされている場合、 クエリQは前回と同じ値を必ず返すため、 再実行する必要はありません(そうでなければコンパイラは決定的ではありません)。
  • 第二に、クエリへの入力の一部が変化したとしても、そのクエリが 前回のコンパイルとなお同じ結果を生成する場合があります。 特に、クエリは入力の一部しか使用しないことがあります。
    • したがって、クエリを実行した後は、それが前回と同じ結果を 生成したかどうかを常に確認します。もしそうであれば、 そのクエリを緑としてマークできるため、依存するクエリの再実行を 回避できます。

try-mark-greenアルゴリズム

インクリメンタルコンパイルの中核には、“try-mark-green“と呼ばれる アルゴリズムがあります。これは、与えられたクエリQ(まだ実行されていては なりません)の色を判定する役割を持ちます。Qに赤の入力がある場合、 Qの色を判定するには、その出力を比較できるようにQを再実行する必要が あるかもしれません。しかし、Qのすべての入力が緑であれば、Qを再実行したり その値をまったく調べたりせずに、Qは緑でなければならないと結論できます。 コンパイラでは、これにより不要なときにディスクから結果をデシリアライズすることを 回避でき、実際には結果のシリアライズも時にはスキップできるようになります (下記の改良のセクションを参照)。

Try-mark-greenは次のように動作します。

  • まず、クエリQが前回のコンパイル中に実行されたかどうかを確認します。
    • 実行されていない場合は、通常どおりクエリを再実行し、その色を 赤に割り当てるだけです。
  • 実行されていた場合は、Qの「依存クエリ」をロードします。
  • 保存された結果がある場合は、クエリDAGからreads(Q)ベクターを ロードします。“reads“は、Qが実行中に実行したクエリの集合です。
    • reads(Q)内の各クエリRについて、try-mark-greenを使用して Rの色を再帰的に要求します。
      • 注: reads(Q)内の各ノードを、元のコンパイルで出現したのと同じ順序で 訪問することが重要です。詳しくは下記のクエリDAGに関するセクションを 参照してください。
      • reads(Q)内のノードのいずれかが最終的にに色付けされた場合、 Qはdirtyです。
        • Qを再実行し、その結果のハッシュを前回のコンパイルでの結果のハッシュと 比較します。
        • ハッシュが変化していなければ、Qをとしてマークして戻ることができます。
      • そうでない場合、reads(Q)内のノードはすべて****緑でなければなりません。 その場合、Qをに色付けして戻ることができます。

クエリDAG

クエリDAGのコードは compiler/rustc_middle/src/dep_graphに格納されています。DAGの構築は、 クエリ実行をインストルメントすることで行われます。

重要な点の1つは、クエリDAGが順序も追跡するということです。つまり、 各クエリQについて、Qが読み取るクエリを追跡するだけでなく、それらが 読み取られた順序も追跡します。これにより、try-mark-greenは それらのクエリを同じ順序でたどり直すことができます。これは重要です。 なぜなら、あるサブクエリが赤として返ってくると、Qが以前と同じ経路を 進み続けるかどうかをもはや確信できないからです。つまり、次のような クエリを想像してください。

fn main_query(tcx) {
    if tcx.subquery1() {
        tcx.subquery2()
    } else {
        tcx.subquery3()
    }
}

ここで、最初のコンパイルではmain_queryがまずsubquery1を実行し、 これがtrueを返すと想像してください。その場合、main_queryが次に実行する クエリはsubquery2であり、subquery3はまったく実行されません。

しかし、次のコンパイルでは、入力が変化してsubquery1falseを 返すようになったと想像してください。この場合、subquery2は決して 実行されません。ところが、try-mark-greenがreads(main_query)を順不同で 訪問した場合、subquery1より先にsubquery2を訪問し、その結果それを 実行してしまう可能性があります。 これは、コンパイラ内でICEやその他の問題につながることがあります。

基本アルゴリズムの改善

基本アルゴリズムの説明では、コンパイルの終了時に、実行されたすべての クエリの結果を保存すると述べました。実際には、これはかなり無駄になり得ます。 それらの結果の多くは再計算が非常に安価であり、それらをシリアライズおよび デシリアライズしても特に得にはなりません。実際には、実行したすべての サブクエリのハッシュを保存します。そして、選択された場合には、 結果も併せて保存します。

これが、インクリメンタルアルゴリズムが、ノードの値を必要としないことが多い ノードのの計算と、ノードの結果の計算を分離している理由です。 結果の計算は、次のような単純なアルゴリズムによって行われます。

  • Qの保存済み結果が利用可能かどうかを確認します。利用可能であれば、Qの色を計算します。 Qが緑であれば、保存済み結果をデシリアライズして返します。
  • そうでない場合は、Qを実行します。
    • その後、結果のハッシュを比較し、変化していなければQを緑として 色付けできます。

リソース

初期設計ドキュメントはこちらにあり、メモ化の詳細を 掘り下げ、このシステムについてのより高水準な概要と動機を提供しています。

脚注


  1. 私は長い間、これをSalsaアルゴリズムに改名したいと思っていましたが、定着することはありませんでした。-@nikomatsakis

インクリメンタルコンパイルの詳細

インクリメンタルコンパイルの仕組みは、本質的には、クエリシステム全体に対する驚くほど単純な拡張です。 これは、次の事実に依存しています。

  1. クエリは純粋関数である – 同じ入力が与えられれば、クエリは常に同じ結果を返す。
  2. クエリモデルは、個々の計算間の依存関係を明示する非巡回グラフとしてコンパイルを構造化する。

この章では、これらの性質をどのように利用してインクリメンタルにできるかを説明し、その後、バージョンの実装上の問題について説明します。

インクリメンタルなクエリ評価の基本アルゴリズム

クエリ評価モデル入門で説明したように、クエリの呼び出しは有向非巡回グラフを形成します。 前の章の例をもう一度示します。

  list_of_all_hir_items <----------------------------- type_check_crate()
                                                               |
                                                               |
  Hir(foo) <--- type_of(foo) <--- type_check_item(foo) <-------+
                                      |                        |
                    +-----------------+                        |
                    |                                          |
                    v                                          |
  Hir(bar) <--- type_of(bar) <--- type_check_item(bar) <-------+

あるクエリから別のクエリへのすべてのアクセスはクエリコンテキストを経由する必要があるため、これらのアクセスを記録でき、したがって実際にこの依存関係グラフをメモリ内に構築できます。 依存関係追跡が有効になっている場合、コンパイルが完了すると、どのクエリが呼び出されたか(グラフのノード)、そして各呼び出しについて、そのクエリの結果を計算するためにどの他のクエリまたは入力が使われたか(グラフのエッジ)が分かります。

ここで、プログラムのソースコードを変更して、bar の HIR が以前とは異なるようになったとします。 私たちの目標は、変更によって実際に影響を受けるクエリだけを再計算し、それ以外のすべてのクエリについてはキャッシュ済みの結果を再利用することです。 依存関係グラフがあれば、まさにそれができます。 あるクエリ呼び出しについて、グラフはその結果の計算にどのデータが使われたかを正確に教えてくれるので、変更されたものに到達するまでエッジをたどるだけで済みます。 変更されたものに何も遭遇しなければ、そのクエリはキャッシュ内にすでにある結果と同じ結果に評価されるはずだと分かります。

上の type_of(foo) 呼び出しを例に取ると、その入力へのエッジをたどることで、キャッシュ済みの結果がまだ有効かどうかを確認できます。 唯一のエッジは Hir(foo) につながっており、これは変更の影響を受けていない入力です。 そのため、type_of(foo) のキャッシュ済みの結果はまだ有効であると分かります。

type_check_item(foo) については、話が少し異なります。再びエッジをたどり、type_of(foo) は問題ないことがすでに分かっています。 次に、まだ確認していない type_of(bar) に到達するので、type_of(bar) のエッジをたどると、変更された Hir(bar) に遭遇します。 したがって、type_of(bar) の結果はキャッシュ内にあるものとは異なる結果を返す可能性があり、推移的に、type_check_item(foo) の結果も変わっている可能性があります。 そのため、type_check_item(foo) を再実行します。すると今度は type_of(bar) が再実行され、Hir(bar) の最新バージョンを読み取るため、最新の結果が返されます。 また、type_of(bar) の結果が変わっている可能性があるため、type_check_item(bar) も再実行します。

基本アルゴリズムの問題: 偽陽性

前の段落を注意深く読むと、入力の 1 つが変更されたため、type_of(bar) は変わった可能性があると述べていることに気付くでしょう。 また、入力が変わったにもかかわらず、まったく同じ結果を返す可能性もあります。 整数の符号を計算するだけの単純なクエリの例を考えてみましょう。

  IntValue(x) <---- sign_of(x) <--- some_other_query(x)

IntValue(x) が最初は 1000 で、その後 2000 に設定されたとします。 2 つの場合で IntValue(x) は異なっていますが、sign_of(x) はどちらの場合も結果 + を返します。

しかし、基本アルゴリズムに従うと、some_other_query(x) は変更された入力に推移的に依存しているため、(不必要に)再評価されなければなりません。 この場合、変更検出は「偽陽性」を生じます。なぜなら、some_other_query(x) がその変更された入力の影響を受けている可能性がある、と保守的に仮定しなければならないからです。

残念ながら、コンパイラ内の実際のクエリにはこのような例が数多くあり、入力への小さな変更が、出力バイナリの非常に大きな部分に影響する可能性がしばしばあります。 その結果、変更検出システムをより賢く、より正確にする必要がありました。

精度の向上: 赤緑アルゴリズム

「偽陽性」の問題は、変更検出とクエリの再評価を交互に行うことで解決できます。 あるキャッシュ済みの結果がまだ有効かどうかを調べる際に、グラフを入力までずっとたどる代わりに、再評価を余儀なくされた後で結果が実際に変わったかどうかを確認できます。

このアルゴリズムを赤緑アルゴリズムと呼びます。依存関係グラフ内のノードには、そのキャッシュ済みの結果がまだ有効であることを証明できた場合は緑色が割り当てられ、再評価後に結果が異なることが判明した場合は赤色が割り当てられるためです。

赤緑の変更追跡の中核は try-mark-green アルゴリズムに実装されています。これは、ご想像のとおり、与えられたノードを緑にマークしようとするものです。

```rust,ignore
fn try_mark_green(tcx, current_node) -> bool {

    // `current_node` への入力を取得する、つまり `node` からの直接の
    // エッジが指すノードを取得する。
    let dependencies = tcx.dep_graph.get_dependencies_of(current_node);

    // 次に、すべての入力に変更がないか確認する
    for dependency in dependencies {

        match tcx.dep_graph.get_node_color(dependency) {
            Green => {
                // この入力は以前にすでにチェック済みであり、
                // 変更されていない。そのため、次の入力の確認へ進める
            }
            Red => {
                // 変更された入力が見つかった。対応するクエリを
                // 再実行せずに `current_node` を緑としてマークすることは
                // できない。
                return false
            }
            Unknown => {
                // このノードを見るのは今回が初めてである。
                // try_mark_green() を再帰的に呼び出して、緑として
                // マークできるか試す。
                if try_mark_green(tcx, dependency) {
                    // 入力を緑としてマークすることに成功したので、
                    // 次へ進む。
                } else {
                    // 入力を緑としてマークすることは *できなかった*。
                    // これは、その値が変更されたかどうか分からないことを
                    // 意味する。それを調べるために、対応するクエリを今
                    // 再実行する!
                    tcx.run_query_for(dependency);

                    // ノードの色を再度取得して確認する。クエリを実行したことで、
                    // (キャッシュ内にある結果と異なる結果を返した場合は)
                    // 赤、または(同じ結果を返した場合は)緑のいずれかに
                    // 強制されている。
                    match tcx.dep_graph.get_node_color(dependency) {
                        Red => {
                            // 入力は赤であることが判明したため、
                            // `current_node` を緑としてマークすることは
                            // できない。
                            return false
                        }
                        Green => {
                            // クエリの再実行は功を奏した!結果は以前と同じなので、
                            // この特定の入力は `current_node` を無効化しない。
                        }
                        Unknown => {
                            // クエリの再実行後にノードに色が付いていないことは
                            // ありえない。
                            panic!("unreachable")
                        }
                    }
                }
            }
        }
    }

    // ループ全体を通過できた場合、それはすべての入力が緑であることが
    // 判明したことを意味する。すべての入力が変更されていないなら、
    // `current_node` に対応するクエリ結果も変更されているはずがない。
    tcx.dep_graph.mark_green(current_node);

    true
}

注: 実際の実装は compiler/rustc_middle/src/dep_graph/graph.rs にあります

赤緑マーキングを使用することで、変更検出における偽陽性がもたらす壊滅的な累積効果を避けることができます。 インクリメンタルモードでクエリが実行されるたびに、まずそれがすでに緑であるか確認します。 そうでない場合、そのクエリに対して try_mark_green() を実行します。 その後もまだ緑でない場合、実際にクエリプロバイダーを呼び出して結果を再計算します。 クエリの再計算では、さらに再帰的により多くのクエリが呼び出されることがあり、その結果、依存関係について再帰的に try_mark_green() アルゴリズムへ戻ってくる可能性があります。

現実世界: 永続化がすべてを複雑にする仕組み

上記のセクションでは、インクリメンタルコンパイルの基礎となるアルゴリズムを説明しました。しかし、コンパイラプロセスは完了後に終了し、結果キャッシュを持つクエリコンテキストもろとも消滅してしまうため、次回のコンパイルセッションで利用できるようにデータをディスクへ永続化する必要があります。 これには、まったく新しい一連の実装上の課題が伴います。

  • クエリ結果キャッシュはディスクに保存されるため、変更比較のためにすぐ利用できるわけではありません。
  • 後続のコンパイルセッションは、任意の変更が適用された新しいバージョンのコードで開始されます。 グローバルな逐次カウンターから生成されるあらゆる種類の ID やインデックス(例: NodeIdDefId など)はずれている可能性があり、その結果、ディスクに永続化された結果は、同じ数値 ID やインデックスが新しいコンパイルセッションではまったく新しいものを指している可能性があるため、もはや直ちには利用できなくなります。
  • ディスクへの永続化にはコストがかかるため、ごく小さな情報のすべてを実際にコンパイルセッション間でキャッシュすべきではありません。 高価な(逆)シリアライズ手順を通す必要がある複雑なものよりも、固定サイズの単純なデータが好まれます。

以下のセクションでは、コンパイラがこれらの問題をどのように解決しているかを説明します。

安定性の問題: コンパイルセッション間のギャップを埋める

前述のとおり、さまざまな ID(DefId など)は、コンパイル対象のソースコードの内容に依存する形でコンパイラによって生成されます。 ID の割り当ては通常、決定的です。 つまり、まったく同じコードを 2 回コンパイルした場合、同じものには同じ ID が割り当てられます。 しかし、たとえばファイルの途中に関数が追加されるなど、何かが変更された場合、どれかが以前と同じ ID を持つ保証はありません。

その結果、ディスク上のキャッシュ内のデータを、メモリ内で表現されるのと同じ方法で表現することはできません。 たとえば、TyKind::FnDef(DefId, &'tcx Substs<'tcx>) のような型情報の断片を(メモリ内で行っているように)そのまま保存し、そこに含まれる DefId が新しいコンパイルセッションで別の関数を指してしまった場合、問題が発生します。

この問題の解決策は、コンパイルセッション間でも有効なままの「安定した」形式の ID を見つけることです。 最も重要なケースである DefId については、これがいわゆる DefPath です。 各 DefId には対応する DefPath がありますが、数値 ID の代わりに、DefPath は識別対象の項目へのパス、たとえば std::collections::HashMap に基づいています。このような ID の利点は、無関係な変更の影響を受けないことです。 たとえば、std::collections に新しい関数を追加することはできますが、std::collections::HashMap は依然として std::collections::HashMap のままです。 DefId がそうでないのに対し、DefPath はソースコードへの変更をまたいで「安定」しています。

また、DefPath の 128 ビットハッシュ値にすぎない DefPathHash もあります。 両者は同じ情報を含んでおり、私たちは主に DefPathHash を使用します。これは Copy で自己完結しているため、扱いがより簡単だからです。

この安定識別子の原則は、ディスク上キャッシュ内のデータをソースコード変更に対して堅牢にするために使用されます。 DefId を保存する代わりに、DefPathHash を保存し、キャッシュから何かをデシリアライズするときに、DefPathHash現在の コンパイルセッション内の対応する DefId に対応付けます(これは単純なハッシュテーブル検索にすぎません)。 独自の DefId を持たない HIR コンポーネントを識別するために使われる HirId も、そのような安定 ID の一種です。 これは(概念的には)DefPathLocalId のペアであり、LocalId は その「所有者」(例: hir::Item)の内部でローカルに何か(例: hir::Expr)を識別します。所有者が移動されても、 その内部の LocalId は同じままです。

クエリ結果の変更を確認する: StableHashFingerprint

赤緑マーキングを行うために、クエリの結果が前回のコンパイルセッションでの結果と比べて 変わったかどうかを確認する必要があることがよくあります。 ただし、これには 2 つの性能上の問題があります。

  • 比較を行うためだけに、前回の結果をディスクから読み込むことは避けたいです。 新しい結果はすでに計算済みであり、それを使用することになります。 また、ディスクから結果を読み込むと、今後使われる可能性が低いデータで インターナーを「汚染」してしまいます。
  • すべての結果をオンディスクキャッシュに保存したいわけではありません。 たとえば、上流クレートですでに利用可能なものをディスクへ永続化するのは、 労力の無駄になります。

コンパイラは、いわゆる Fingerprint を使うことでこれらの問題を回避します。 新しいクエリ結果が計算されるたびに、クエリエンジンはその結果の 128 ビットハッシュ値を 計算します。 このハッシュ値を「クエリ結果の Fingerprint」と呼びます。 ハッシュ化は「安定した方法」で行われます(また、そうしなければなりません)。 これは、コンパイルセッション間で変わる可能性があるもの(例: DefId)をハッシュ化するときには、 代わりにその安定した等価物(例: 対応する DefPath)をハッシュ化するという意味です。 StableHash インフラストラクチャ全体はそのためにあります。 これにより、2 つの異なるコンパイルセッションで計算された Fingerprint であっても比較可能になります。

次のステップは、これらのフィンガープリントを依存グラフとともに保存することです。 フィンガープリントは単にコピーされるバイト列なので、これは低コストです。 依存グラフとともにフィンガープリント一式を読み込むのも低コストです。

これで、赤緑マーキングが結果の変更有無を確認する必要がある地点に到達したときには、 (すでに読み込まれている)前回のフィンガープリントを新しい結果のフィンガープリントと比較するだけで済みます。

このアプローチはかなりうまく機能しますが、欠点がないわけではありません。

  • ハッシュ衝突が起こる可能性がわずかにあります。 つまり、2 つの異なる結果が同じフィンガープリントを持つ可能性があり、その場合システムは誤って 結果が変更されていないとみなし、更新の見逃しにつながります。

    このリスクは、高品質なハッシュ関数と 128 ビット幅のハッシュ値を使うことで軽減しています。 これらの対策により、実用上のハッシュ衝突リスクは無視できます。

  • フィンガープリントの計算はかなり高コストです。 これは、インクリメンタルコンパイルが非インクリメンタルコンパイルより遅くなり得る 主な理由です。 優れた、したがって高コストなハッシュ関数を使わざるを得ず、さらにハッシュ化の際には 対象を安定した等価物へ対応付ける必要があります。

2 つの DepGraph の物語: 古いものと新しいもの

依存関係追跡の最初の説明では、実際に実装しようとするとすぐに頭を悩ませることになる いくつかの詳細が省かれています。 特に見落としやすいのは、実際には 2 つ の依存グラフを扱っているという点です。 つまり、前回のコンパイルセッション中に構築したものと、 現在のコンパイルセッション用に構築しているものです。

コンパイルセッションが開始されると、コンパイラは前回の依存グラフを 不変のデータとしてメモリに読み込みます。 そして、クエリが呼び出されると、 まずグラフ内の対応するノードを green としてマークしようとします。 これは実際には、前回 の dep-graph 内で、 現在 のセッションのクエリキーに対応するノードを green としてマークしようとしている、という意味です。 現在のクエリキーと前回の DepNode の間のこの対応付けは、どのように行うのでしょうか? 答えはここでも Fingerprint です。依存グラフ内のノードは、 クエリキーのフィンガープリントによって識別されます。 フィンガープリントはコンパイルセッションをまたいで安定しているため、 現在のセッションでフィンガープリントを計算することで、前回のセッションの依存グラフ内のノードを 見つけることができます。 与えられたフィンガープリントを持つノードが見つからない場合は、 そのクエリキーが前回のセッションにはまだ存在していなかったものを参照していることを意味します。

こうして前回の依存グラフ内の dep-node を見つけたら、その依存関係 (すなわち、前回のグラフ内の dep-node)を調べ、 try-mark-green アルゴリズムの残りを続行できます。 次に興味深いことが起こるのは、そのノードを green として正常にマークできたときです。 その時点で、古いグラフから新しいグラフへ、 ノードと、その依存先へのエッジをコピーします。 これは、新しい dep-graph が通常の依存関係追跡を通じて そのノードとエッジを獲得することができないため、必要になります。 追跡システムは、実際にクエリを実行している間にしかエッジを記録できません。しかし、 結果がすでにキャッシュされているにもかかわらずクエリを実行することこそ、まさに避けたいことです。

コンパイルセッションが終了すると、変更されていない部分はすべて 古い依存グラフから新しい依存グラフへコピーされており、一方で変更された部分は 追跡システムによって新しいグラフに追加されています。 この時点で、新しいグラフはクエリ結果キャッシュとともにディスクへシリアライズされ、 次回以降のコンパイルセッションで前回の dep-graph として機能できます。

何か忘れていませんか?: キャッシュの昇格

ここまで説明したシステムには、やや微妙な性質があります。dep-node のすべての入力が green であれば、対応するクエリ結果を計算したり読み込んだりすることなく、 その dep-node 自体を green としてマークできます。 この性質を推移的に適用すると、次の例のように、一部の中間結果が 実際にはディスクからまったく読み込まれない状況になることがよくあります。

   input(A) <-- intermediate_query(B) <-- leaf_query(C)

コンパイラは、何らかの出力成果物を生成するために leaf_query(C) の値を必要とするかもしれません。 leaf_query(C) を green としてマークできる場合、コンパイラはオンディスクキャッシュからその結果を読み込みます。 しかし、intermediate_query(B) の結果は読み込まれません。 その結果、コンパイラがメモリ上のすべてのクエリ結果をディスクに書き込むことで 新しい 結果キャッシュを永続化する際、intermediate_query(B) は メモリ上に存在しないため、新しい結果キャッシュから欠落することになります。

その後、別のコンパイルセッションで実際に intermediate_query(B) の結果が必要になった場合、 その直前までキャッシュ内に完全に有効な結果があったにもかかわらず、 再計算しなければならなくなります。

これを防ぐために、コンパイラは「キャッシュの昇格」と呼ばれることを行います。 新しい結果キャッシュを出力する前に、すべての green な dep-node を走査し、 それらのクエリ結果がメモリに読み込まれていることを確認します。 これにより、結果キャッシュが不必要に再び縮小することを防げます。

インクリメンタルコンパイルとコンパイラバックエンド

コンパイラバックエンド、つまり LLVM に関わる部分は、クエリシステムを使用していますが、 それ自体がクエリとして実装されているわけではありません。 その結果、依存関係追跡に自動的に参加することはありません。 しかし、追跡システムとの手動統合はかなり単純です。 コンパイラは、各コード生成ユニット (CGU) の初期 LLVM 版を生成するときにどのクエリが呼び出されるかを単に追跡し、 その結果として各 CGU に対する dep-node が作られます。 以降のコンパイルセッションでは、CGU の dep-node を green としてマークしようとします。 成功した場合、対応するディスク上のオブジェクトファイルとビットコードファイルがまだ有効であることが分かります。 成功しなかった場合は、CGU 全体を再コンパイルする必要があります。

これは通常のクエリに使用されるのと同じアプローチです。 主な違いは次のとおりです。

  • LLVM モジュールのフィンガープリントを簡単には計算できないこと (LLVM モジュールは不透明な C++ オブジェクトであるため)、

  • キャッシュされた値を扱うロジックが通常のクエリとはかなり異なること。 ここでは共通の結果キャッシュファイル内のシリアライズされた Rust 値ではなく、 ビットコードファイルとオブジェクトファイルを扱うためです。そして、

  • LLVM 周辺の操作は、計算時間とメモリ消費の観点で非常に高コストであるため、 何をいつ実行し、何をどのくらいの期間メモリ内に保持するかを 厳密に制御する必要があることです。

クエリシステムはおそらく、上記すべてに対処する汎用的な仕組みを備えるように拡張できるでしょうが、 これまでのところ、それによって削減できる手間よりも、実装の手間のほうが大きいように思われました。

クエリ修飾子

FIXME: rustc_middle::query::modifiers をクエリ修飾子ドキュメントの置き場所にし、 他の有用な修飾子ドキュメントがまだ正確であることを確認したうえで、すべてそこへ移行する。

クエリシステムでは、クエリに修飾子を適用できます。 これらの修飾子は、インクリメンタルコンパイルに関して、 システムがそのクエリをどのように扱うかの特定の側面に影響します。

  • eval_always - eval_always 属性を持つクエリは、 インクリメンタルコンパイル中に無条件で再実行されます。 すなわち、 システムはそのクエリの dep-node を green としてマークしようとすらしません。 この属性には 2 つの用途があります。

    • eval_always クエリは入力(ファイル、グローバル状態などから)を読み取ることができます。 また、ファイルへの書き込みやグローバル状態の変更のような副作用を生み出すこともできます。

    • 一部のクエリは、その結果がソースコード全体に依存するため、 再評価される可能性が非常に高くなります。 この場合、eval_always は最適化として使用できます。 というのも、システムはそもそも依存関係の記録をスキップできるためです。

  • no_force - このクエリの dep-node は、クエリのキー型が復元可能であっても、決して「force」しません。

  • no_hash - クエリに no_hash を適用すると、そのクエリの結果の フィンガープリントを計算しないようにシステムへ指示します。 これには 2 つの結果があります。

    • フィンガープリントを計算しないことで、かなりの時間を節約できます。 フィンガープリントの計算は、特に大きく複雑な値に対しては高コストだからです。

    • フィンガープリントがない場合、システムはクエリの結果が変更されたと 無条件に仮定しなければなりません。 その結果、no_hash クエリに依存するものは常に再実行されます。

    クエリに no_hash を使用することが理にかなう状況は 2 つあります。

    • クエリの結果が、その入力の 1 つが変わるたびに変化する可能性が非常に高い場合。 たとえば |a, b, c| -> (a * b * c) のような関数です。このような場合、 入力の 1 つが red であれば、クエリを再計算しても常に red ノードが得られるため、 その手間をかけずに、即座に red をデフォルトとすることができます。 反例としては、|a| -> (a == 42) のような関数があります。 ここでは、a のほとんどの変更に対して結果は変化しません。

    • クエリの結果が大きなモノリシックなコレクション(例: index_hir)であり、 そのコレクションから読み取る「射影クエリ」が存在する場合 (例: hir_owner)。このような場合、大きなコレクションは上記の条件を満たす可能性が高く (入力が変わればコレクション全体を再計算することを意味するため)、 射影クエリの結果はいずれにしてもハッシュ化されます。 コレクションクエリもハッシュ化すると、実質的に同じデータを 2 回ハッシュ化することになります。 つまり、コレクションをハッシュ化するときに 1 回、さらにすべての 射影クエリ結果をハッシュ化するときにもう 1 回です。 no_hash により、この冗長性を避けることができ、 射影クエリは「ファイアウォール」として機能し、その依存元を 無条件に red となる no_hash ノードから保護します。

  • cache_on_disk - クエリの戻り値はディスクにキャッシュされ、 対応する dep-node が green であれば以降のセッションで読み込むことができます。 separate_provide_extern 修飾子も存在する場合、値は「ローカル」キーについてのみ ディスクにキャッシュされます。外部クレートの値は、 クレートメタデータから読み込めるべきであるためです。

射影クエリパターン

eval_alwaysno_hash を、いわゆる「射影クエリ」パターンで一緒に使用できることは興味深い点です。 コンパイラの入力全体に依存する 1 つのクエリ(例: インデックス化された HIR)と、 このモノリシックな値から個々の値を射影する別のクエリ (例: 特定の DefId を持つ HIR アイテム)があることはよくあります。これらの射影クエリにより、 変更伝播の「ファイアウォール」を構築できます。なぜなら、モノリシックなクエリの結果が変わったとしても (それは非常に起こりやすいことですが)、小さな射影はそれでもほとんどの場合 green としてマークできるからです。

  +------------+
  |            |           +---------------+           +--------+
  |            | <---------| projection(x) | <---------| foo(a) |
  |            |           +---------------+           +--------+
  |            |
  | monolithic |           +---------------+           +--------+
  |   query    | <---------| projection(y) | <---------| bar(b) |
  |            |           +---------------+           +--------+
  |            |
  |            |           +---------------+           +--------+
  |            | <---------| projection(z) | <---------| baz(c) |
  |            |           +---------------+           +--------+
  +------------+

monolithic_query の結果が変わり、そのため projection(x) の結果も変わったと仮定しましょう。 つまり、両方の dep-node が red としてマークされることになります。 その結果、foo(a) は再実行する必要がありますが、bar(b)baz(c) は green としてマークできます。 しかし、foobarbazmonolithic_query に直接依存していたなら、それらはすべて再評価されなければならなかったでしょう。 このパターンは、eval_alwaysno_hash がなくても機能しますが、これら 2 つの 修飾子を使うことで不要なオーバーヘッドを回避できます。 モノリシックなクエリが コンパイラの入力に対するどんな小さな変更でも変わる可能性が高い場合は、それを eval_always としてマークするのが理にかなっています。これにより、その依存関係追跡コストを取り除けます。 また、モノリシックなクエリを no_hash としてマークすることは常に理にかなっています。 なぜなら、可能な限り物事を green に保つ処理はプロジェクションが担っているからです。

現在のシステムの欠点

まだ改善できることは多くあります。

ディスク上のデータ構造の増分性

現在のシステムは、ディスク上のキャッシュと依存関係グラフをインプレースで更新できません。 その代わり、各コンパイルセッションで各ファイル全体を書き直す必要があります。 これによるオーバーヘッドは、総コンパイル時間の数パーセントです。

不要なデータ依存関係

クエリ結果として使われるデータ構造は、依存関係グラフから エッジを取り除くように分解できる可能性があります。 特に「span」情報は非常に変わりやすいため、 それをクエリ結果に含めると、その結果が再利用できなくなる可能性が高まります。 詳細については https://github.com/rust-lang/rust/issues/47389 を参照してください。

依存関係のデバッグとテスト

依存グラフのテスト

依存グラフに対してテストを書く方法はいくつかあります。最も単純な仕組みは、#[rustc_if_this_changed]#[rustc_then_this_would_need] アノテーションです。これらは ui テストで、期待されるパスの集合が依存グラフ内に存在するかどうかをテストするために使われます。

例として、tests/ui/dep-graph/dep-graph-caller-callee.rs、または以下のテストを参照してください。

#[rustc_if_this_changed]
fn foo() { }

#[rustc_then_this_would_need(TypeckTables)] //~ ERROR OK
fn bar() { foo(); }

これは次のように読むべきです。

これ(foo)が変更された場合、この(つまり bar)の TypeckTables を変更する必要がある。

技術的には、このテストは、この行に関連付けられて stderr に文字列 “OK” を出力することが期待されています。

次の行を追加することもできます。

#[rustc_then_this_would_need(TypeckTables)] //~ ERROR no path
fn baz() { }

その意味は次のとおりです。

foo が変更された場合、baz の TypeckTables を変更する必要はない。 マクロはエラーを出力しなければならず、そのエラーメッセージには “no path” が含まれていなければならない。

//~ ERROR OK は、テスト対象の Rust コードの観点からはコメントですが、テスト自体の観点からは意味を持つことを思い出してください。

依存グラフのデバッグ

グラフのダンプ

コンパイラは、デバッグのために依存グラフをダンプすることもできます。そのためには、-Z dump-dep-graph フラグを渡します。グラフは現在のディレクトリの dep_graph.{txt,dot} にダンプされます。RUST_DEP_GRAPH 環境変数でファイル名を上書きできます。

ただし、多くの場合、完全な依存グラフは非常に圧倒的で、特に役立つものではありません。そのため、コンパイラではグラフをフィルタリングすることもできます。フィルタリングには 3 つの方法があります。

  1. 特定のノード集合(通常は単一ノード)から始まるすべてのエッジ。
  2. 特定のノード集合に到達するすべてのエッジ。
  3. 指定された開始ノードと終了ノードの間にあるすべてのエッジ。

フィルタリングするには、RUST_DEP_GRAPH_FILTER 環境変数を使用します。これは次のいずれかの形式である必要があります。

source_filter     // source_filter から始まるノード
-> target_filter  // target_filter に到達できるノード
source_filter -> target_filter // source_filter と target_filter の間にあるノード

source_filtertarget_filter は、& で区切られた文字列のリストです。ノードは、それらの文字列がすべてそのラベルに含まれている場合にフィルタに一致するとみなされます。したがって、たとえば次のようにします。

RUST_DEP_GRAPH_FILTER='-> TypeckTables'

これは、すべての TypeckTables ノードの先行ノードを選択します。ただし通常は、特定の fn に対する TypeckTables ノードが必要なので、次のように書くことがあります。

RUST_DEP_GRAPH_FILTER='-> TypeckTables & bar'

これは、名前に bar を含む関数の TypeckTables ノードの先行ノードのみを選択します。

おそらく、foo を変更したときに bar を再度型チェックする必要があることに気づいたものの、そうする必要はないはずだと考えているかもしれません。その場合は、次のようにすることがあります。

RUST_DEP_GRAPH_FILTER='Hir & foo -> TypeckTables & bar'

これにより、Hir(foo) から TypeckTables(bar) へつながるすべてのノードがダンプされ、そこから誤ったエッジの原因を(うまくいけば)確認できます。

不正なエッジの追跡

依存グラフをダンプしたあと、本来存在すべきでないパスを見つけることがありますが、それがどのようにしてできたのかはよく分からないかもしれません。コンパイラがデバッグアサーション付きでビルドされている場合、 それを追跡するのに役立ちます。単に RUST_FORBID_DEP_GRAPH_EDGE 環境変数にフィルタを設定してください。依存グラフ内で作成されるすべてのエッジがそのフィルタに対してテストされます。一致した場合は bug! が報告されるため、バックトレースを簡単に確認できます(RUST_BACKTRACE=1)。

これらのフィルタの構文は、前のセクションで説明したものと同じです。ただし、前のセクションとは異なり、このフィルタはすべてのエッジに適用され、グラフ内のより長いパスは扱わないことに注意してください。

例:

fooHir から bar の型チェックへのパスが存在することが分かり、それは存在すべきではないと考えているとします。前のセクションで説明したように依存グラフをダンプし、dep-graph.txt を開くと、次のような内容が表示されます。

Hir(foo) -> Collect(bar)
Collect(bar) -> TypeckTables(bar)

この最初のエッジは疑わしく見えます。そこで、RUST_FORBID_DEP_GRAPH_EDGEHir&foo -> Collect&bar に設定し、再実行してからバックトレースを確認します。これでバグ修正完了です!

Salsa の仕組み

この章は、Niko Matsakis による Salsa についてのこの 動画での説明に基づいています。 さらに詳しく知りたい場合は、同じく Niko Matsakis による Salsa In More Depth を見るとよいでしょう。

2022 年 11 月時点では、Salsa は rustc のクエリシステムに

(他のものとともに)触発されていますが、rustc で直接使われているわけではありません。 Rust のトレイトシステムの実装である chalk や、 Rust 向け Language Server Protocol の公式実装である rust-analyzer では 広く使われていますが、コンパイラに統合するための中期的または長期的な具体的な 計画はありません。

Salsa とは何か?

Salsa はインクリメンタルな再計算のためのライブラリです。これは、過去にすでに行われた 計算を再利用することで、将来の計算の効率を高められることを意味します。

Salsa の目的は次のとおりです。

  • その機能を自動的な形で提供し、古い計算の再利用を ライブラリによって自動的に行うこと。
  • それを「健全」または「正しい」方法で行い、その結果として 最初から行った場合と同じ結果を導くこと。

Salsa の実際のモデルははるかに豊かで、多くの種類の入力と多くの異なる出力を扱えます。 たとえば、Salsa を IDE と統合する場合、 入力はマニフェスト(Cargo.tomlrust-toolchain.toml)、ソースファイル全体 (foo.rs)、スニペットなどになり得ます。そのような統合の出力は、 バイナリ実行可能ファイルから、lint、型(たとえば、ユーザーが特定の変数を 選択してその型を見たい場合)、補完などまでさまざまです。

どのように機能するのか?

Salsa が最初に行う必要があるのは、計算されたものではなく入力として与えられる 「基底入力」を識別することです。

次に Salsa は、中間的な「派生」値も識別する必要があります。これは ライブラリが生成するものですが、各派生値について、その派生値を計算する 「純粋」関数が存在します。

たとえば、ast(x: Path) -> AST という関数があるかもしれません。生成される 抽象構文木(AST)は最終的な値ではなく、ライブラリが計算に使用する中間値です。

これは、ライブラリで計算しようとすると、Salsa がさまざまな派生値を 計算し、最終的に入力を読み取り、求められた計算の結果を生成することを意味します。

計算の過程で、Salsa はどの入力がアクセスされ、どの値が派生されたかを追跡します。 この情報は、入力が変更されたときに何が起こるか、つまり派生値がまだ有効かどうかを 判断するために使われます。

これは必ずしも、入力の下流にある各計算がチェックされることを意味しません。 それは高コストになり得ます。Salsa は、変更されていないものを見つけるまで、 各下流の計算をチェックするだけで済みます。その時点で、他の派生計算は 変更する必要がないため、チェックされません。

これは、ノードを持つグラフとして考えるとわかりやすいです。各派生値は 他の値に依存しており、それらは基底値または派生値のいずれかです。 基底値には依存関係がありません。

I <- A <- C ...
          |
J <- B <--+

入力 I が変化すると、派生値 A は変化する可能性があります。 派生値 BIA、または AI から派生したどの値にも 依存していないため、変更の対象ではありません。したがって、Salsa は B について過去に行った計算を、再計算することなく再利用できます。

計算は早期に終了することもあります。前と同じグラフのまま、 入力 I が何らかの形で変化した(そして入力 J は変化していない)とします。 しかし、A を再度計算したところ、A が前回の計算から変化していないことが 判明したとします。これは「早期終了」につながります。なぜなら、C の直接の入力である AB の両方が変化していないため、C が変更される必要があるかどうかを チェックする必要がないからです。

Salsa の主要な概念

クエリ

クエリとは、Salsa が計算の過程でアクセスできる何らかの値です。各 クエリは(0 個から多数までの)キーをいくつか持つことができ、すべてのクエリは 関数と同様に結果を持ちます。0-key クエリは「入力」クエリと呼ばれます。

データベース

データベースは基本的に計算全体のコンテキストであり、Salsa の内部状態、 各クエリのすべての中間値、そして計算に必要となるその他すべてのものを 保存するためのものです。データベースは、構築される前にライブラリが行う すべてのクエリを知っている必要がありますが、それらを同じ場所で指定する必要はありません。

データベースが形成されると、関数によく似たクエリでアクセスできます。 各クエリの結果はデータベースに保存されているため、クエリが N 回呼び出されると、 (入力が再計算を正当化するような形で変更されていない限り)クエリを再計算することなく、 N 個の clone された 結果を返します。

各入力クエリ(0-key)については、「set」メソッドが生成され、ユーザーは そのようなクエリの出力を変更し、以前にメモ化された値が無効化される可能性を 引き起こすことができます。

クエリグループ

クエリグループとは、1 つの単位としてまとめて定義されたクエリの集合です。 データベースはクエリグループを組み合わせることで形成されます。クエリグループは 「Salsa モジュール」に似ています。

クエリグループ内のクエリの集合は、トレイト内のメソッドの集合にすぎません。

クエリグループを作成するには、特定の属性 (#[salsa::query_group(...)])で注釈付けされたトレイトを作成する必要があります。

その属性には引数も指定しなければなりません。これは、後でデータベースが作成されるときに 使用される struct を Salsa が作成するために使われます。

入力クエリグループの例:

/// この属性はこのツリーを処理し、このツリーを出力として生成し、Salsa も使用する
/// 中間的なものを大量に生成します。これらのうちの 1 つは
/// "StorageStruct" であり、その名前は属性で指定しています。
///
/// このクエリグループは、いかなる派生入力にも依存しない **入力** クエリの集まりです。
#[salsa::query_group(InputsStorage)]
pub trait Inputs {
    /// この属性(`#[salsa::input]`)は、このクエリが基底
    /// 入力であることを示すため、`set_manifest` が自動生成されます
    #[salsa::input]
    fn manifest(&self) -> Manifest;

    #[salsa::input]
    fn source_text(&self, name: String) -> String;
}

派生 クエリグループを作成するには、次の例に示すように、 このクエリグループがどの他のクエリグループに依存するかを、 それらをスーパートレイトとして指定することで指定しなければなりません。

/// このクエリグループには、派生値に依存するクエリが含まれます。
/// クエリグループは、依存関係をスーパートレイトとして指定することで、別の
/// クエリグループのクエリにアクセスできます。クエリグループは、このパターンを使って
/// 必要なだけ積み重ねることができます。
#[salsa::query_group(ParserStorage)]
pub trait Parser: Inputs {
    /// このクエリ `ast` は入力クエリではなく、派生クエリです。これは
    /// 定義が必要であることを意味します。
    fn ast(&self, name: String) -> String;
}

派生クエリを作成する場合、そのクエリの実装は trait の外部で定義する必要があります。定義は、 その他のキーに加えて、定義が属するクエリグループである trait を impl Trait(または dyn Trait)としてデータベースパラメーターに取る必要があります。

/// これは `Parser` trait における `ast` クエリの定義になります。
/// そのため、クエリ `ast` が呼び出され、再計算が必要になった場合、Salsa は
/// この関数を呼び出し、データベースを `impl Parser` として渡します。
/// この関数は、すべてのクエリグループのすべてのクエリを認識する必要はありません
fn ast(db: &impl Parser, name: String) -> String {
    //! なお、ここでは `impl Parser` が使用されていますが、`dyn Parser` でも同様に動作します
    /* code */
    ///`impl Parser` を渡すことで、これは許可されます
    let source_text = db.input_file(name);
    /* 実際のパースを行う */
    return ast;
}

最終的に、すべてのクエリグループが定義された後、struct を宣言することでデータベースを 作成できます。

どのクエリグループをデータベースの一部にするかを指定するには、attribute#[salsa::database(...)])を追加する必要があります。その attribute の引数は、 クエリグループの storages を指定する identifiers のリストです。

///この属性は、どのクエリグループをデータベースに含めるかを指定します
#[salsa::database(InputsStorage, ParserStorage)]
#[derive(Default)] //任意!
struct MyDatabase {
    ///このフィールドも 1 つ必要です
    runtime : salsa::Runtime<MyDatabase>,
}
///そして、この trait を実装する必要があります
impl salsa::Database for MyDatabase {
    fn salsa_runtime(&self) -> &salsa::Runtime<MyDatabase> {
        &self.runtime
    }
}

使用例:

fn main() {
    let db = MyDatabase::default();
    db.set_manifest(...);
    db.set_source_text(...);
    loop {
        db.ast(...); //結果を再利用します
        db.set_source_text(...);
    }
}

rustc におけるメモリ管理

一般に、rustc はメモリの管理方法についてかなり慎重であろうとします。 コンパイラはコンパイル全体を通じて 非常に多くの データ構造を割り当てるため、 注意しないと、それに多くの時間と空間がかかってしまいます。

コンパイラがこれを管理する主な方法の 1 つは、arenainterning を使用することです。

アリーナと インターン化

コンパイル中には非常に多くのデータ構造が作成されるため、パフォーマンス上の 理由から、それらをグローバルなメモリプールから割り当てます。 それぞれは長命な アリーナ から一度だけ割り当てられます。 これは アリーナ割り当て と呼ばれます。 この仕組みにより、メモリの割り当て/解放が削減されます。 また、等価性のための型(型について詳しくはこちら)の比較も容易になります。 インターン化された各型 X について、X に対する PartialEq を実装しているため、 単にポインタを比較できます。 CtxtInterners 型には、インターン化された型のマップ群とアリーナ自体が含まれます。

例: ty::TyKind

コンパイラ内の型を表す ty::TyKind の例を取り上げます(詳しくはこちらを 読めます)。 型を構築したいとき、コンパイラは単純にバッファから割り当てることは しません。 代わりに、その型がすでに構築されているかどうかを確認します。すでに構築されて いれば、以前と同じポインタを取得するだけです。そうでなければ新しいポインタを作成します。 この方式では、2 つの型が同じかどうかを知りたい場合、必要なのはポインタを比較することだけで、 これは効率的です。 ty::TyKind は決してスタック上で構築すべきではなく、そうした場合は 使用できません。 常にこのアリーナからそれらを割り当て、常にそれらをインターン化することで、それらは 一意になります。

コンパイルの開始時にバッファを作成し、型を割り当てる必要があるたびに、このメモリバッファの一部を使用します。 空間が足りなくなった場合は、別のバッファを取得します。そのバッファのライフタイムは 'tcx です。私たちの型はそのライフタイムに結び付けられているため、コンパイルが終了すると、そのバッファに関連する すべてのメモリが解放され、'tcx 参照は無効になります。

型に加えて、割り当て可能なアリーナ割り当てのデータ構造は他にも多数あり、 それらはこのモジュール内にあります。以下にいくつか例を示します。

  • GenericArgsmk_args で割り当てられます – これは型のスライスをインターン化し、多くの場合 ジェネリック引数に代入される値を指定するために使用されます(例: HashMap<i32, u32> は スライス &'tcx [tcx.types.i32, tcx.types.u32] として表されます)。
  • TraitRef は通常値渡しされます – トレイト参照 は、トレイトへの参照と そのさまざまな型パラメータ(Self を含む)からなります。たとえば i32: Display のようなものです(ここでは、def-id は Display トレイトを参照し、args には i32 が含まれます)。なお、def-idAdtDefDefId セクションで定義され、詳しく説明されています。
  • Predicate は、トレイトシステムが証明しなければならないものを定義します(traits モジュールを参照)。

tcx とそのライフタイムの使い方

型付けコンテキスト (tcx) はコンパイラ内の中心的なデータ構造です。これは、 あらゆる種類のクエリを実行するために使用するコンテキストです。struct TyCtxt は、この共有 コンテキストへの参照を定義します。

tcx: TyCtxt<'tcx>
//          ----
//          |
//          アリーナのライフタイム

お分かりのように、TyCtxt 型はライフタイムパラメータを取ります。 'tcx のようなライフタイムを持つ参照を見た場合、それはアリーナ割り当てされたデータ (または、いずれにせよアリーナと同じだけ長く生きるデータ)を参照していることを意味します。

ライフタイムに関する注記

Rust コンパイラはかなり大規模なプログラムであり、多数の大きなデータ 構造(例: 抽象構文木 (AST)高水準中間 表現 (HIR)、および型システム)を含んでいます。そのため、不要なメモリ使用を 最小限に抑えるために、アリーナと参照に大きく依存しています。これは、 人々がコンパイラに組み込む方法(つまり ドライバー)にも現れており、より Rust らしい “pull” スタイル (Iterator トレイトを考えてください)ではなく、“push” スタイルの API(コールバック)が好まれます。

スレッドローカルストレージとインターン化は、重複を減らしつつ、 広範に存在する多数のライフタイムに起因する使い勝手上の問題の多くを防ぐために、 コンパイラ全体で多用されています。これらのスレッドローカルにアクセスするには rustc_middle::ty::tls モジュールが使用されますが、触れる必要はほとんどないはずです。

rustc におけるシリアライズ

rustc は、コンパイル中にさまざまなデータをシリアライズおよびデシリアライズする必要があります。 具体的には、次のとおりです。

  • 主にクエリ出力で構成される「クレートメタデータ」は、ライブラリクレートのコンパイル時に出力される rlib ファイルおよび rmeta ファイルへ、バイナリ形式でシリアライズされます。これらの rlib ファイルおよび rmeta ファイルは、そのライブラリに依存するクレートによってデシリアライズされます。
  • 特定のクエリ出力は、インクリメンタルコンパイル結果を永続化するために、バイナリ形式でシリアライズされます。
  • CrateInfo-Z no-link フラグが使用されると JSON へシリアライズされ、-Z link-only フラグが使用されると JSON からデシリアライズされます。

Encodable トレイトと Decodable トレイト

rustc_serialize クレートは、シリアライズ可能な型のために 2 つのトレイトを定義しています。

pub trait Encodable<S: Encoder> {
    fn encode(&self, s: &mut S) -> Result<(), S::Error>;
}

pub trait Decodable<D: Decoder>: Sized {
    fn decode(d: &mut D) -> Result<Self, D::Error>;
}

また、整数型、浮動小数点型、boolcharstr など、さまざまな一般的な標準ライブラリのプリミティブ型に対するこれらの実装も定義しています。

それらの型から構築される型については、EncodableDecodable は通常 derive によって実装されます。これらは、構造体または enum のフィールドへデシリアライズを委譲する実装を生成します。構造体の場合、それらの impl はおおよそ次のようになります。

#![feature(rustc_private)]
extern crate rustc_serialize;
use rustc_serialize::{Decodable, Decoder, Encodable, Encoder};

struct MyStruct {
    int: u32,
    float: f32,
}

impl<E: Encoder> Encodable<E> for MyStruct {
    fn encode(&self, s: &mut E) -> Result<(), E::Error> {
        s.emit_struct("MyStruct", 2, |s| {
            s.emit_struct_field("int", 0, |s| self.int.encode(s))?;
            s.emit_struct_field("float", 1, |s| self.float.encode(s))
        })
    }
}

impl<D: Decoder> Decodable<D> for MyStruct {
    fn decode(s: &mut D) -> Result<MyStruct, D::Error> {
        s.read_struct("MyStruct", 2, |d| {
            let int = d.read_struct_field("int", 0, Decodable::decode)?;
            let float = d.read_struct_field("float", 1, Decodable::decode)?;

            Ok(MyStruct { int, float })
        })
    }
}

アリーナに割り当てられた型のエンコードとデコード

rustc には多数のアリーナに割り当てられた型があります。 これらの型をデシリアライズするには、それらを割り当てる必要があるアリーナへのアクセスが必要です。 TyDecoder トレイトと TyEncoder トレイトは、TyCtxt へのアクセスを可能にする DecoderEncoder のサブトレイトです。

arena に割り当てられた型を含む型は、その Encodable および Decodable 実装の型パラメーターを、これらのトレイトで境界付けることができます。 例:

impl<'tcx, D: TyDecoder<'tcx>> Decodable<D> for MyStruct<'tcx> {
    /* ... */
}

TyEncodable および TyDecodablederive マクロは、そのような実装へ展開されます。

実際の arena に割り当てられた型のデコードはより困難です。なぜなら、一部の実装は孤児ルールのために書けないからです。これを回避するために、RefDecodable トレイトが rustc_middle で定義されています。これは任意の型に対して実装できます。TyDecodable マクロは参照をデコードするために RefDecodable を呼び出しますが、さまざまなジェネリックコードでは、型が特定のデコーダーで実際に Decodable である必要があります。

インターン化された型については、RefDecodable を手動で実装する代わりに、ty::Predicate のような newtype ラッパーを使用し、EncodableDecodable を手動で実装するほうが簡単な場合があります。

Derive マクロ

[rustc_macros] クレートは、DecodableEncodable の実装を助けるさまざまな derive を定義しています。

  • Encodable マクロと Decodable マクロは、すべての EncodersDecoders に適用される実装を生成します。これらは、rustc_middle に依存しないクレート、または TyEncoder を実装していない型によってシリアライズされる必要があるクレートで使用すべきです。
  • [MetadataEncodable] は、[rustc_metadata::rmeta::encoder::EncodeContext] によるデコードのみを許可する実装を生成します。
  • [BlobDecodable] と [LazyDecodable] は、MetadataEncodable に対応するデコード側として機能します。これらは rustc_metadata::rmeta 内のメタデータ blob デコーダーでデコードする実装を生成します。型に遅延メタデータハンドルがない場合は BlobDecodable を使用し、ある場合は LazyDecodable を使用します。
  • TyEncodableTyDecodable は、任意の TyEncoder または TyDecoder に適用される実装を生成します。これらは、クレートメタデータやインクリメンタルキャッシュでのみシリアライズされる型に使用すべきです。これは rustc_middle 内のシリアライズ可能な型の大半に該当します。 [BlobDecodable]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_macros/derive.BlobDecodable.html [LazyDecodable]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_macros/derive.LazyDecodable.html [MetadataEncodable]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_macros/derive.MetadataEncodable.html [rustc_macros]: https://github.com/rust-lang/rust/tree/HEAD/compiler/rustc_macros [rustc_metadata::rmeta]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_metadata/rmeta/index.html [rustc_metadata::rmeta::encoder::EncodeContext]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_metadata/rmeta/encoder/struct.EncodeContext.html rustc_middle: https://github.com/rust-lang/rust/tree/HEAD/compiler/rustc_middle

短縮表記

Ty は深く再帰的になる可能性があり、各 Ty を素朴にエンコードすると、クレートメタデータは非常に大きくなります。これに対処するため、各 TyEncoder は、型をシリアライズした出力内の位置のキャッシュを持ちます。エンコード中の型がキャッシュ内にある場合、通常どおり型をシリアライズする代わりに、書き込み中のファイル内のバイトオフセットがエンコードされます。同様の方式が ty::Predicate にも使用されます。

LazyValue<T>

クレートメタデータは TyCtxt<'tcx> が作成される前に最初に読み込まれるため、一部のデシリアライズはメタデータの初期読み込みから遅延させる必要があります。LazyValue<T> 型は、T がシリアライズされているクレートメタデータ内の(相対)オフセットをラップします。また、いくつかのバリアント、LazyArray<T>LazyTable<I, T> もあります。

LazyArray<[T]> 型と LazyTable<I, T> 型は、Lazy<Vec<T>>Lazy<HashMap<I, T>> に対していくつかの機能を提供します。

  • 先に Vec<T> に収集することなく、Iterator から直接 LazyArray<T> をエンコードできます。
  • LazyTable<I, T> へのインデックス指定では、読み取っているもの以外のエントリをデコードする必要はありません。

: LazyValue<T> は、初回デシリアライズ後にその値をキャッシュしません。代わりに、クエリシステム自体がこれらの結果をキャッシュする主な方法です。

特殊化

いくつかの型、とりわけ DefId は、異なる Encoder ごとに異なる実装を持つ必要があります。これは現在、アドホックな特殊化によって処理されています。例: DefId には Encodable<E>default 実装と、Encodable<CacheEncoder> に特化した実装があります。

並列コンパイル

2024年11月時点で、 並列フロントエンドは大きな変更の最中にあるため、 このページにはかなり古くなった情報が含まれています。

追跡 issue: https://github.com/rust-lang/rust/issues/113349

2024年11月時点で、Rust コンパイラの大部分は

現在では並列化されています。

  • codegen 部分はデフォルトで並行実行されます。 -C codegen-units=n オプションを使用して、並行タスクの数を制御できます。
  • 型チェック、借用チェック、mir 最適化など、HIR lowering から codegen までの部分は nightly 版で並列化されています。 現在、これらはデフォルトでは直列に実行され、並列化は -Z threads = n オプションを使用してユーザーが手動で有効にします。
  • 字句解析、HIR lowering、マクロ展開などの他の部分は、 まだ直列モードで実行されています。
以下のセクションは現時点では残されていますが、かなり古くなっています。

コード生成

単相化中、コンパイラは生成されるすべてのコードを codegen units と呼ばれるより小さなチャンクに分割します。 これらは並列に実行される LLVM の独立したインスタンスによって生成されます。 最後に、リンカーが実行され、 すべての codegen units を 1 つのバイナリに結合します。 この処理は rustc_codegen_ssa::base モジュールで行われます。

データ構造

並列コンパイラで使用される基盤となるスレッドセーフなデータ構造は、 rustc_data_structures::sync モジュールにあります。 これらのデータ構造は、 parallel-compiler が true かどうかによって異なる方法で実装されています。

データ構造並列非並列
Lock<T>(parking_lot::Mutex<T>)(std::cell::RefCell)
RwLock<T>(parking_lot::RwLock<T>)(std::cell::RefCell)
ReadGuardparking_lot::RwLockReadGuardstd::cell::Ref
MappedReadGuardparking_lot::MappedRwLockReadGuardstd::cell::Ref
WriteGuardparking_lot::RwLockWriteGuardstd::cell::RefMut
MappedWriteGuardparking_lot::MappedRwLockWriteGuardstd::cell::RefMut
LockGuardparking_lot::MutexGuardstd::cell::RefMut
  • これらのスレッドセーフなデータ構造はコンパイル中に散在しており、 ロック競合を引き起こす可能性があり、その結果、スレッド数が 4 を超えて増えると パフォーマンスが低下します。そのため、これらのデータ構造の使用を監査し、 共有状態の使用を減らすためのリファクタリング、または 不変条件、アトミック性、ロック順序の詳細を網羅する永続的なドキュメントの作成につなげています。

  • 一方で、コンパイル中の他のどのような不変条件が 並列コンパイルで成立しない可能性があるのかは、まだ明らかにする必要があります。

WorkerLocal

WorkerLocal は並列コンパイラ向けに実装された特別なデータ構造です。 これはスレッドプール内の各スレッドに対して worker-local な値を保持します。 worker local の値には、 それが構築されたスレッドプール上の Deref impl を通じてのみアクセスできます。 それ以外の場合は panic します。

WorkerLocal は並列環境で Arena アロケータを実装するために使用され、 これは並列クエリにおいて重要です。 その実装は rustc_data_structures::sync::worker_local モジュールにあります。 ただし、 非並列コンパイラでは、これは (OneThread<T>) として実装され、その T には Deref::deref を通じて直接アクセスできます。

並列イテレータ

rayon crate によって提供される並列イテレータは、 並列処理を実装するための簡単な方法です。 並列コンパイラの現在の実装では、 タスクを並列に実行するために 独自に fork した rayon を使用しています。

parallel-compiler が true の場合にループを並列に実行するための いくつかのイテレータ関数が実装されています。

関数(SendSync は省略)概要所有モジュール
par_iter<T: IntoParallelIterator>(t: T) -> T::Iter並列イテレータを生成するrustc_data_structure::sync
par_for_each_in<T: IntoParallelIterator>(t: T, for_each: impl Fn(T::Item))並列イテレータを生成し、各要素に対して for_each を実行するrustc_data_structure::sync
Map::par_body_owners(self, f: impl Fn(LocalDefId))crate 内のすべての hir owner に対して f を実行するrustc_middle::hir::map
Map::par_for_each_module(self, f: impl Fn(LocalDefId))crate 内のすべてのモジュールとサブモジュールに対して f を実行するrustc_middle::hir::map
ModuleItems::par_items(&self, f: impl Fn(ItemId))モジュール内のすべての item に対して f を実行するrustc_middle::hir
ModuleItems::par_trait_items(&self, f: impl Fn(TraitItemId))モジュール内のすべての trait item に対して f を実行するrustc_middle::hir
ModuleItems::par_impl_items(&self, f: impl Fn(ImplItemId))モジュール内のすべての impl item に対して f を実行するrustc_middle::hir
ModuleItems::par_foreign_items(&self, f: impl Fn(ForeignItemId))モジュール内のすべての foreign item に対して f を実行するrustc_middle::hir

コンパイラには、これらの関数を使用して並列化できる可能性のあるループが多数あります。

2022年8月時点で、並列イテレータ関数が使用されているシナリオは次のとおりです。
呼び出し元シナリオ呼び出し先
rustc_metadata::rmeta::encoder::prefetch_mirメタデータのエンコードで後から必要になるクエリをプリフェッチするpar_iter
rustc_monomorphize::collector::collect_crate_mono_items非ジェネリック項目から到達可能な単相化済み項目を収集するpar_for_each_in
rustc_interface::passes::analysismatch 文の妥当性をチェックするMap::par_body_owners
rustc_interface::passes::analysisMIR の borrow checkMap::par_body_owners
rustc_typeck::check::typeck_item_bodies型チェックMap::par_body_owners
rustc_interface::passes::hir_id_validator::check_cratehir の妥当性をチェックするMap::par_for_each_module
rustc_interface::passes::analysisループ本体、属性、naked 関数、不安定な ABI、const 本体の妥当性をチェックするMap::par_for_each_module
rustc_interface::passes::analysisMIR の liveness と intrinsic のチェックMap::par_for_each_module
rustc_interface::passes::analysis到達不能性のチェックMap::par_for_each_module
rustc_interface::passes::analysisプライバシーチェックMap::par_for_each_module
rustc_lint::late::check_crateモジュールごとの lint を実行するMap::par_for_each_module
rustc_typeck::check_cratewell-formedness のチェックMap::par_for_each_module

並列イテレーターを使用できる可能性のあるループは、まだ多数あります。

クエリシステム

クエリモデルには、あまり多くの労力をかけずに複数のクエリを並列に評価することを実際に可能にする性質がいくつかあります。

  • クエリプロバイダーがアクセスできるすべてのデータはクエリコンテキスト経由であるため、 クエリコンテキストがアクセスの同期を処理できます。
  • クエリ結果は不変であることが要求されるため、 異なるスレッドから同時に安全に使用できます。

クエリ foo が評価されると、foo のキャッシュテーブルがロックされます。

  • すでに結果がある場合は、それをクローンし、ロックを解放して 完了します。
  • キャッシュエントリがなく、同じ結果を計算している他のアクティブなクエリ呼び出しもない場合は、 そのキーを「進行中」としてマークし、ロックを解放して 評価を開始します。
  • 同じキーに対する別のクエリ呼び出しが進行中である場合は、 ロックを解放し、待機している結果を別の呼び出しが計算するまで そのスレッドをブロックするだけです。 並列コンパイラにおける循環エラー検出には、 シングルスレッドモードよりも複雑なロジックが必要です。 並列クエリ内のワーカースレッドが相互依存のために進捗しなくなると、 コンパイラは追加のスレッド (deadlock handler という名前) を使用して、 循環エラーを検出、除去、報告します。

並列クエリ機能には、まだ実装すべきことが残っており、その大部分は 前述の Data StructuresParallel Iterators に関連しています。 この未解決の機能追跡 issueを参照してください。

Rustdoc

2022年11月時点では、`rustdoc` のレンダリングを並列化できるようにするまでに

完了すべきステップがまだいくつかあります(並列 rustdoc に関する未解決の議論を参照してください)。

リソース

詳細を学ぶために使用できるリソースをいくつか示します。

Rustdoc の内部

このページでは、rustdoc のパスとモードについて説明します。 rustdoc の概要については、“Rustdoc の概要” の章を参照してください。

クレートから clean へ

core.rs には、中心となる項目が 2 つあります。rustdoc::core::DocContext struct と、rustdoc::core::run_global_ctxt 関数です。 後者は、rustdocrustc を呼び出して、rustdoc が引き継げる段階まで クレートをコンパイルする場所です。 前者は、クレートをクロールしてドキュメントを収集する際に使用される状態コンテナーです。

クレートのクロールの主な処理は、clean/mod.rs で、clean_ で始まる名前を持つ 複数の関数を通じて行われます。 各関数は hir または ty のデータ構造を受け取り、rustdoc が使用する clean 構造を出力します。 たとえば、ライフタイムを変換するこの関数です。

fn clean_lifetime<'tcx>(lifetime: &hir::Lifetime, cx: &mut DocContext<'tcx>) -> Lifetime {
    if let Some(
        rbv::ResolvedArg::EarlyBound(did)
        | rbv::ResolvedArg::LateBound(_, _, did)
        | rbv::ResolvedArg::Free(_, did),
    ) = cx.tcx.named_bound_var(lifetime.hir_id)
        && let Some(lt) = cx.args.get(&did).and_then(|arg| arg.as_lt())
    {
        return lt.clone();
    }
    Lifetime(lifetime.ident.name)
}

また、clean/mod.rs は、後でドキュメントページをレンダリングするために使用される 「clean 済み」の 抽象構文木 (AST) の型を定義します。 それぞれには通常、 rustc からの何らかの AST または 高水準中間表現 (HIR の型を受け取り、それを適切な「clean 済み」の型に変換する clean_* 関数が付随しています。 モジュールや関連項目のような「大きな」項目では、その clean 関数で 追加の処理が行われることもありますが、ほとんどの場合、これらの impl は単純な変換です。 このモジュールへの「エントリーポイント」は clean::utils::krate で、これは run_global_ctxt から呼び出されます。

clean::utils::krate の最初のステップは、 visit_ast::RustdocVisitor を呼び出し、モジュールツリーを中間の visit_ast::Module に処理することです。 これは、実際に rustc_middle::hir::Crate をクロールし、次のような名前解決のさまざまな側面を正規化するステップです。

  • #[doc(inline)]#[doc(no_inline)] の処理
  • インポート glob とサイクルを処理し、重複や無限の ディレクトリツリーが発生しないようにする
  • 非公開項目の公開 use エクスポートをインライン化する、またはモジュールページに “Reexport” 行を表示する
  • ベース項目が hidden である場合に、#[doc(hidden)] が付いた項目をインライン化するが、その
  • #[macro_export] されたマクロを、それが再エクスポートとして定義されているかどうかに関係なく、 クレートルートに表示する

このステップの後、clean::krateclean_doc_module を呼び出し、これが実際に HIR 項目を clean 済みの AST に変換します。 これは、クロスクレートのインライン化が行われるステップでもあり、 そのためには rustc_middle のデータ構造を clean 済みの AST に変換する必要があります。

clean/mod.rs で行われるもう 1 つの主要な処理は、doc コメントと #[doc=""] 属性を、Attributes struct の別フィールドに収集することです。この struct は、手書きのドキュメントを持つものすべてに存在します。 これにより、プロセスの後半でこのドキュメントを収集しやすくなります。

このプロセスの主な出力は、対象クレート内の公開ドキュメント化可能な項目を記述する Item のツリーを持つ clean::types::Crate です。

ガソリンスタンド以外は何でも Pass する(または: Hot Potato

次の主要なステップに進む前に、clean 済みの AST に対して いくつかの重要な「パス」が実行されます。 これらのパスのいくつかは lint やレポートですが、一部は項目を変更したり、新しい項目を生成したりします。

これらはすべて、[librustdoc/passes] ディレクトリに、1 パスにつき 1 ファイルとして実装されています。 デフォルトでは、これらのパスはすべてクレートに対して実行されますが、 private/hidden 項目の削除に関するものは、rustdoc--document-private-items を渡すことで回避できます。 前述の AST 変換の集合とは異なり、 パスは clean 済み のクレートに対して実行されることに注意してください。

2023 年 3 月時点のパスの一覧は次のとおりです。
- `calculate-doc-coverage` は、`--show-coverage`
  フラグで使用される情報を計算します。

- `check-doc-test-visibility` は、`doctest` の可視性に関連する `lint` を実行します。
  このパスは `strip-private` より前に実行されるため、
  `run-lints` とは別にしておく必要があります。

- `collect-intra-doc-links` は、[ドキュメント内リンク](https://doc.rust-lang.org/nightly/rustdoc/write-documentation/linking-to-items-by-name.html)を解決します。

- `collect-trait-impls` は、クレート内の各アイテムについて `trait` の `impl` を収集します。
  たとえば、ある `trait` を実装する `struct` を定義した場合、
  このパスは、その `struct` がその `trait` を実装していることを記録します。

- `propagate-doc-cfg` は、`#[doc(cfg(...))]` を子アイテムへ伝播させます。

- `run-lints` は、`passes/lint` で定義されている `rustdoc` の `lint` の一部を実行します。
  これは最後に実行されるパスです。

  - `bare_urls` は、リンク化されていないリンクを検出します。たとえば、
    `Go to https://example.com/.` のような Markdown 内のリンクです。これは、リンクをリンク化するために山括弧で囲むことを提案します:
    `Go to <https://example.com/>.`
    これは、<!-- date-check: may 2022 --> `rustdoc::bare_urls` `lint` の背後にあるコードです。

  - `check_code_block_syntax` は、Rust コードブロック内の構文を検証します
    (<code>```rust</code>)

  - `html_tags` は、doc コメント内の無効な `HTML`(閉じられていない `<span>` など)を検出します。

- `strip-hidden` と `strip-private` は、すべての `doc(hidden)` と private アイテムを
  出力から取り除きます。
  `strip-private` は `strip-priv-imports` を含意します。
  基本的に、その目的は公開ドキュメントに関係しないアイテムを取り除くことです。
  このパスは、`--document-hidden-items` が渡された場合はスキップされます。

- `strip-priv-imports` は、クレートからすべての private なインポート文(`use`、`extern
  crate`)を取り除きます。
  これが必要なのは、`rustdoc` が *public* な
  インポートを、アイテムのドキュメントをモジュールにインライン化するか、
  そのインポートを含む "Reexports" セクションを作成することで処理するためです。
  このパスは、これらのインポートがすべて実際にドキュメントに関連していることを保証します。
  技術的には `--document-private-items` が渡された場合にのみ実行されますが、`strip-private`
  でも同じことが達成されます。

- `strip-private` は、外部から見えないクレート内のすべての private アイテムを取り除きます。
  このパスは、`--document-private-items` が渡された場合はスキップされます。

`librustdoc/passes` には [`stripper`] モジュールもありますが、これは
`strip-*` パス用のユーティリティ関数の集合であり、それ自体はパスではありません。

[`librustdoc/passes`]: https://github.com/rust-lang/rust/tree/HEAD/src/librustdoc/passes
[`stripper`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc/passes/stripper/index.html

## clean から HTML へ

ここから、`rustdoc` における「第 2 フェーズ」が始まります。
このフェーズは主に
[`librustdoc/formats`] および [`librustdoc/html`] フォルダー内にあり、すべては
[`formats::renderer::run_format`] から始まります。
このコードは、`impl FormatRenderer` する型をセットアップする役割を担っており、
`HTML` の場合、それは [`Context`] です。

この構造体には、ドキュメントレンダリングを駆動するために `run_format` から呼び出されるメソッドが含まれます。これには次のものが含まれます。

* `init` は、検索インデックスおよび `src/` とともに `static.files` を生成します
* `item` は、アイテムの `HTML` ファイルそのものを生成します
* `after_krate` は、`all.html` のようなその他のグローバルリソースを生成します

`item` では、[Askama] テンプレートと手動の `write!()` 呼び出しの組み合わせによって、「ページレンダリング」が行われます。これは [`html/layout.rs`] から始まります。
テンプレートに変換されていない部分は、一連の `std::fmt::Display`
実装と、`&mut std::fmt::Formatter` を受け渡す関数の中に存在しています。

アイテムとドキュメントから実際に `HTML` を生成する部分は、[`html/render/print_item.rs`] で定義されている [`print_item`] から始まります。これは、レンダリングされる `Item` の種類に基づいて、複数ある `item_*` 関数のいずれかへ切り替えます。

探しているレンダリングコードの種類によって、おそらくそれは、
「`struct` ページにはどのセクションを出力すべきか」のような主要アイテムについては [`html/render/mod.rs`] に、
「他のアイテムの一部として where 句をどのように出力すべきか」のような小さなコンポーネント部分については [`html/format.rs`] に見つかるでしょう。

`rustdoc` は、手書きのドキュメントを併せて出力すべきアイテムに出会うたびに、Markdown パーサーとのインターフェイスを担う [`html/markdown.rs`] を呼び出します。
これは、Markdown 文字列をラップし、`HTML` テキストを出力するために `fmt::Display` を実装する一連の型として公開されています。
Markdown パーサーを実行する前に、脚注や表のような特定の機能を有効にし、Rust コードブロックへ(`html/highlight.rs` を介して)構文ハイライトを追加するよう、特別な注意が払われています。
また、[`find_codes`] という関数もあります。これは
`find_testable_codes` から呼び出され、テストランナーコードがクレート内のすべての `doctest` を見つけられるよう、Rust コードブロックを特にスキャンします。

[`find_codes`]: https://doc.rust-lang.org/nightly/nightly-rustc/src/rustdoc/html/markdown.rs.html#749-818
[`formats::renderer::run_format`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc/formats/renderer/fn.run_format.html
[`html/format.rs`]: https://github.com/rust-lang/rust/blob/HEAD/src/librustdoc/html/format.rs
[`html/layout.rs`]: https://github.com/rust-lang/rust/blob/HEAD/src/librustdoc/html/layout.rs
[`html/markdown.rs`]: https://github.com/rust-lang/rust/blob/HEAD/src/librustdoc/html/markdown.rs
[`html/render/mod.rs`]: https://github.com/rust-lang/rust/blob/HEAD/src/librustdoc/html/render/mod.rs
[`html/render/print_item.rs`]: https://github.com/rust-lang/rust/blob/HEAD/src/librustdoc/html/render/print_item.rs
[`librustdoc/formats`]: https://github.com/rust-lang/rust/tree/HEAD/src/librustdoc/formats
[`librustdoc/html`]: https://github.com/rust-lang/rust/tree/HEAD/src/librustdoc/html
[`print_item`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc/html/render/print_item/fn.print_item.html
[Askama]: https://docs.rs/askama/latest/askama/

### 最初から最後まで(または: [「あの最初の `Cell` たちから私たちまで、途切れない一本の糸が伸びている」][video])

[video]: https://www.youtube.com/watch?v=hOLAGYmUQV0

重要なのは、`rustdoc` は `HTML` 生成中であっても、型情報をコンパイラに直接問い合わせることができるという点です。
これは[以前はそうではありませんでした][didn't used to be the case]。また、`rustdoc` のアーキテクチャの多くは、それを行わないことを前提に設計されていました。しかし現在では
`TyCtxt` が `formats::renderer::run_format` に渡されており、これは
`HTML` と(<!-- date-check --> 2026年5月時点で unstable な)JSON 形式の両方の生成を実行するために使用されます。

この変更により、他の変更で "clean" [`AST`][ast] から、`TyCtxt` クエリから容易に導出できるデータを取り除けるようになりました。また、私たちは通常、
"clean" からフィールドを取り除く PR を受け入れます(これは soft-deprecated されています)。しかし、これは
`rustdoc` が置かれている他の 2 つの制約によって複雑になります。
* ドキュメントは、実際には型チェックに通らないクレートに対しても生成できます。
  これは、`libstd` がサポートされているすべてのオペレーティングシステムを網羅する単一のドキュメントパッケージを持つ場合のように、相互排他的なプラットフォーム構成を網羅するドキュメントを生成するために使用されます。
  つまり、`rustdoc` は `HIR` からドキュメントを生成できる必要があります。
* ドキュメントはクレートをまたいでインライン展開できます。
  クレートのメタデータには `HIR` が含まれていないため、
  `rustc_middle` のデータからインライン展開されたドキュメントを生成できなければなりません。

「clean」な [`AST`][ast] は、両方の入力形式に対する共通の出力形式として機能します。
clean には、`HIR` に直接対応しないデータもいくつかあります。たとえば、
auto trait に対する合成 `impl` や、`collect-trait-impls` パスによって生成される blanket `impl` などです。

追加のデータの一部は `html::render::context::{Context, SharedContext}` に格納されます。
これら 2 つの型は、将来的にマルチスレッドのドキュメント生成を行う場合に備えて `rustdoc` のデータを分離する手段として、また単に整理された状態を保つ手段として機能します。

* [`Context`] は、現在のページを生成するために使用されるデータを格納します。たとえば、そのパス、使用済みの `HTML` ID のリスト(重複する `id=""` を避けるため)、および `SharedContext` へのポインターなどです。
* [`SharedContext`] は、ページごとに変化しないデータを格納します。たとえば、`tcx` ポインターや、すべての型のリストなどです。

[`Context`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc/html/render/context/struct.Context.html
[didn't used to be the case]: https://github.com/rust-lang/rust/pull/80090
[`SharedContext`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc/html/render/context/struct.SharedContext.html

## その他の奥の手

ここまでで、Rust クレートから `HTML` ドキュメントを生成するプロセスについて説明しましたが、`rustdoc` が動作する主要なモードは他にもいくつかあります。
スタンドアロンの Markdown ファイルに対して実行することもできますし、Rust コードやスタンドアロンの Markdown ファイルで `doctest` を実行することもできます。
前者の場合、`html/markdown.rs` へ直接ショートカットします。必要に応じて、出力 `HTML` に目次を挿入するモードも含まれます。

後者の場合、`rustdoc` は `test.rs` で関連するドキュメントを取得するために同様の部分的なコンパイルを実行しますが、完全な clean およびレンダリングのプロセスを通る代わりに、はるかに単純なクレート走査を実行して、手書きのドキュメント*だけ*を取得します。
前述の `html/markdown.rs` 内の「`find_testable_code`」と組み合わせることで、テストランナーに渡す前に、実行するテストのコレクションを構築します。
`test.rs` で注目すべき場所の 1 つは `make_test` 関数で、ここで手書きの `doctest` が実行可能なものへと変換されます。

`make_test` に関する追加の読み物は
[こちら](https://quietmisdreavus.net/code/2018/02/23/how-the-doctests-get-made/)にあります。

## ローカルでのテスト

生成された `HTML` ドキュメントの一部の機能では、ページをまたいでローカルストレージを使用する必要がある場合がありますが、これは `HTTP` サーバーなしではうまく機能しません。
これらの機能をローカルでテストするには、次のようにローカル `HTTP` サーバーを実行できます。

```console
$ ./x doc library
# ドキュメントは `build/[YOUR ARCH]/doc` に生成されました。
$ python3 -m http.server -d build/[YOUR ARCH]/doc

これで、インターネット上でホストされている場合と同じようにドキュメントを閲覧できます。 たとえば、std の URL は rust/std/ になります。

関連項目

Rustdoc 検索

Rustdoc Search は search_index.rssearch.js という 2 つのプログラムです。前者は、ドキュメントバンドル内の クレートに含まれるアイテムと関数シグネチャの完全なリストを持つ厄介な JSON ファイルを生成し、後者はそれを読み込み、いくつかのメモリ内構造に変換して、 それらを線形に走査して検索します。

検索インデックス形式

search.js はこれを Raw と呼びます。読み込み後に、 より通常のオブジェクトツリーへ変換するためです。 容量削減のため、改行やスペースなしでも書き出されます。

[
    [ "crate_name", {
        // 名前
        "n": ["function_name", "Data"],
        // 型
        "t": "HF",
        // 親モジュール
        "q": [[0, "crate_name"]],
        // 親型
        "i": [2, 0],
        // 型辞書
        "p": [[1, "i32"], [1, "str"], [5, "Data", 0]],
        // 関数シグネチャ
        "f": "{{gb}{d}}`", // [[3, 1], [2]]
        // impl 曖昧性解消子
        "b": [],
        // 非推奨フラグ
        "c": "OjAAAAAAAAA=", // 空のビットマップ
        // 空の説明フラグ
        "e": "OjAAAAAAAAA=", // 空のビットマップ
        // エイリアス
        "a": [["get_name", 0]],
        // 説明シャード
        "D": "g", // 3
        // インライン化された再エクスポート
        "r": [],
    }]
]

src/librustdoc/html/static/js/rustdoc.d.ts は、TypeScript の type で実際のスキーマを定義しています。

KeyNameDescription
n名前アイテム名
tアイテム型1 文字のアイテム型コード
q親モジュールMap<index, path>
i親型インデックスのリスト
f関数シグネチャエンコード済み
bImpl 曖昧性解消子Map<index, string>
c非推奨フラグroaring bitmap
e説明が空roaring bitmap
p型辞書[[item type, path]]
aエイリアスMap<string, index>
D説明シャードエンコード済み

上のインデックスは、crate_name というクレートを定義しています。 そこには、function_name という自由関数と Data という構造体があり、 型シグネチャは Data, i32 -> str です。 さらに、function_name を同等に参照するエイリアス get_name があります。

検索インデックスは、rustdoc コンパイラ、 search.js フロントエンドのニーズを満たし、 さらにコンパクトで高速にデコードできる必要があります。 そのため、多くの妥協がなされています。

  • rustdoc コンパイラは一度に 1 つのクレートに対して実行されるため、 各クレートは本質的に個別の検索インデックスを持ちます。 各クレートを 1 行に置き、 最初のクォートされた文字列を見ることで、それらをマージします。
  • 検索インデックス内の名前は、 元の大文字小文字とアンダースコアを保持したまま与えられます。 検索インデックスが読み込まれると、 search.js は表示用に元の名前を保存しますが、 検索用には小文字に畳み込み、アンダースコアを取り除きます。 それらは normalized と呼ばれているのを目にするでしょう。
  • f 配列は、型を p 配列へのオフセットとして格納します。 これらの型は実際には別のクレート由来である可能性があるため、 同じインデックス内の複数のクレートが同じ型に言及している場合に それらを重複排除するため、search.js は数値を名前へ変換し、 その後で再び数値へ戻す必要があります。
  • これは JSON ファイルですが、人間が読みやすいようには設計されていません。 ブラウザにはすでに最適化された JSON デコーダが含まれているため、 これにより search.js のコードを節約でき、小さなクレートでは性能も向上します。 しかし、通常の JSON 形式のようにオブジェクトを使う代わりに、 同じ型のデータを互いに隣接させようとします。 そうすることで、DEFLATE が使用するスライディングウィンドウが冗長性を見つけられるためです。 search.js が独自に圧縮を行う箇所では、 それはディスク上やネットワーク転送時のサイズだけでなく、 ファイルが最終的に読み込まれたときのメモリを節約するように設計されています。

並列配列とインデックス付きマップ

抽象的には、Rustdoc Search のデータは列優先形式で格納されたテーブルです。 インデックス内のほとんどのデータは、同じ位置にあれば同じデータを参照する、 並列配列(「列」)の集合を表します。

たとえば、 上の検索インデックスは次のテーブルへ変換できます。

ntdqifbc
0crate_nameDDocumentationNULL0NULLNULL0
1function_nameHThis function gets the name of an integer with Datacrate_name2{{gb}{d}}NULL0
2DataFThe data structcrate_name0`NULL0

クレート行はほとんどの列で暗黙的に扱われます。なぜなら、その型は既知であり(クレートです)、 親を持つことができず(クレートはモジュールツリーのルートを形成します)、 名前はマップキーとして指定され、 impl 曖昧性解消子のような関数固有のデータも適用できないためです。 ただし、説明を持つことはでき、非推奨にすることもできます。 したがって、クレートは 0 という主キーを持ちます。

上のコードでは、非推奨のインデックスを保持する c や、 インデックスを文字列へマップする b は使用していません。 もし crate_name::function_name が両方を使用していたなら、次のようになるかもしれません。

        "b": [[0, "impl-Foo-for-Bar"]],
        "c": "OjAAAAEAAAAAAAIAEAAAABUAbgZYCQ==",

これはインデックス 1 に曖昧性解消子を付与し、それを非推奨としてマークします。

このレイアウトの利点は、これらの API がしばしば、 DEFLATE が活用できる暗黙の構造を持つ一方で、 rustdoc はそれを仮定できないことです。 たとえば、名前は通常 CamelCase や snake_case ですが、 説明はそうではありません。 また、boolean フラグのようなものにスパースデータを使いやすくもなります。

q は、最初に適用可能な ID から親モジュールパスへの Map です。 これは奇妙なトリックですが、疑似コードではより理解しやすくなります。

#![allow(unused)]
fn main() {
let mut parent_module = "";
for (i, entry) in search_index.iter().enumerate() {
    if q.contains(i) {
        parent_module = q.get(i);
    }
    // ... `entry` を使って他の処理を行う ...
}
}

これは、すべてのものが親モジュールを持つため有効です (たとえそれがクレート自体であるだけだとしても)。 また、rustdoc ジェネレータはシリアライズ前にパスでソートするため、組み立ても簡単です。 これにより rustdoc は検索インデックスを小さくできるだけでなく、 親パスを表す同じ文字列を、複数のメモリ内アイテム間で再利用できます。

スパース列の表現

VLQ Hex

この形式は、私の知る限り rustdoc 以外では使われていません。 これは次の文法に従います。

VLQHex = { VHItem | VHBackref }
VHItem = VHNumber | ( '{', {VHItem}, '}' )
VHNumber = { '@' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' }, ( '`' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k ' | 'l' | 'm' | 'n' | 'o' )
VHBackref = ( '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | ':' | ';' | '<' | '=' | '>' | '?' )

VHNumber は可変長の自己終端型16進数です (最後の16進桁が小文字で、それ以外がすべて大文字であるため終端します)。 符号ビットは zig-zag encoding を使用して表現されます。

このアルファベットが選ばれているのは、ASCII エンコーディングの下位4ビットを マスクすることで、文字を16進桁に変換できるためです。

rustdoc で行われるすべての「圧縮」と同様に、このエンコーディングの大きな特徴は、 ランタイムのメモリ上でさえ 圧縮形式のままでいられることです。 これが、HBackref がトップレベルでのみ使用される理由であり、 また、すべてに単に Flate を使わない理由です。search.js のデコーダーは backref が見つかるたびにデコード済みオブジェクト全体を再利用し、 デコード作業とメモリを節約します。

Roaring Bitmaps

非推奨かどうかや空の説明などのフラグ形式のデータは、 standard Roaring Bitmap serialization format with runs を使用して保存されます。 そのデータは書き込み時に base64 エンコードされます。

簡単に概説すると、roaring bitmap はチャンク化されたビット配列であり、 this paper で説明されています。 チャンクは、整数のリスト、ビットフィールド、または run のリストのいずれかです。 いずれの場合も、検索エンジンはそれを base64 デコードし、 チャンクインデックス自体を読み取る必要がありますが、 ペイロードデータはそのまま残ります。

rustdoc のすべての roaring bitmap は現在、各項目インデックスのフラグを保存します。 クレートは項目 0 で、それ以外はすべて 1 から始まります。

説明の保存方法

最も大きなデータ量を占めるもの、 そして Rustdoc Search が扱うもののうち実際には 検索に使われない主なものは、説明です。 SERP テーブルでは、これは一番右の列に表示されるものです。

項目の種類項目パス説明(この部分)
関数my_crate::my_functionこの関数は Data を使って整数の名前を取得します

誰かが rustdoc で初めて検索を実行すると、そのブラウザーは 3つのステップからなる「サンドイッチワークロード」を処理します。

  1. search-index.js ファイルと search.js ファイルをダウンロードする(ネットワークのボトルネック)。
  2. 実際の検索を実行する(CPU とメモリ帯域幅のボトルネック)。
  3. 説明データをダウンロードする(もう1つのネットワークのボトルネック)。

ここでダウンロードするデータ量を減らすと、ほぼ常にレイテンシが増加します。 これは、何をダウンロードするかの決定が他の作業の後に遅らされるため、かつ/または、 何か別のものを先にダウンロードしなければダウンロードできないものが生じるような データ依存関係が追加されるためです。この場合、検索が完了するまで 説明のダウンロードを開始できません。なぜなら、それによってどの 説明をダウンロードするかを決定できるようになるからです (結果をソートしてから 200 件に切り詰める必要があります)。

これを実現するため、Roaring Bitmaps と VLQ Hex の両方を土台として、 検索インデックスには2つの列が保存されます。

  • e は空の説明のインデックスです。これは各項目(クレート自体は項目 0、 残りは 1 から始まります)の roaring bitmap です。
  • D はシャードリストで、整数のフラットなリストとして VLQ hex に保存されます。 各整数は、そのシャード内の説明の数を示します。 デコーダーがインデックスをたどる際、説明が空かどうかを確認します。 空でなければ、それは「現在の」シャード内にあります。すべての項目を 処理し終えると、次のシャードへ進みます。

各シャードの中には、JSONP 形式の関数呼び出しでラップされた、 改行区切りの説明リストがあります。

if、および p

if はどちらも、親項目の配列である p へのインデックスです。

i は単なる1始まりの数値です (親項目を持たない項目に 0 が使われるため、0始まりではありません)。 これは q とは異なります。q は、すべての項目が持つ親のモジュールまたはクレート を表すのに対し、 i/q はメソッドのような型およびトレイト関連項目に使われるためです。

関数シグネチャである f は、VLQ hex ツリーを使用します。 数値は、p への1始まりの参照、 ジェネリックを表す負数、 または null を表すゼロのいずれかです。

(内部オブジェクト表現でも、 デコード後であっても、 ジェネリックを表すために負数を使用します)。

たとえば、{{gb}{d}} は JSON の [[3, 1], [2]] に相当します。 zigzag 符号化により、` は +0、a は -0(これは使用されません)、 b は +1、c は -1 です。

名前による検索

名前による検索は、検索インデックスをループ処理し、 各項目に対して以下の関数を実行することで動作します。

  • editDistance は一致を判定するために常に使用されます (引用符が指定されている場合を除き、その場合は代わりに単純な等価性が使用されます)。 これは、クエリ名をエントリー名に変換するために必要な 入れ替え、挿入、削除の回数を計算します。 たとえば、foo はそれ自身との距離がゼロですが、 ofo(1回の入れ替え)および foob(1回の挿入)からの距離は 1 です。 これはヒューリスティックな閾値と照合され、その後、 その閾値内にある場合は、ランキングのために距離が保存されます。
  • String.prototype.indexOf は一致を判定するために常に使用されます。 -1 以外を返した場合、editDistance がその閾値を超えていても、 結果が追加され、 ランキングのためにインデックスが保存されます。
  • checkPath は、クエリで親パスが指定されている場合にのみ使用されます。 たとえば、vec には親パスがありませんが、vec::vec にはあります。 checkPath 内では、editDistance と indexOf が使用され、 パスクエリにも独自のヒューリスティックな閾値があります。 閾値内にない場合、最初の2つが通っていても、 エントリーは拒否されます。 閾値内にある場合、ランキングのためにパス距離が保存されます。
  • checkType は、struct:vec における struct のような型フィルターがある場合にのみ使用されます。 失敗した場合、 エントリーは拒否されます。

4つの基準すべてを満たした場合 (技術的にはクエリの一部ではないクレートフィルターも加えて)、 結果は sortResults によってソートされます。

型による検索

型による検索は2つのフェーズに分けることができ、 2番目のフェーズには2つのサブフェーズがあります。

  • クエリ内の名前を数値に変換する。
  • 検索インデックス内の各エントリーをループ処理する:
    • ブルームフィルターを使用した高速な拒否。
    • 再帰的な型単一化アルゴリズムを使用した低速な拒否。 names->numbers フェーズでは、クエリに名前が1つだけ含まれている場合、 完全一致に失敗すると、editDistance 関数を使用して近い一致を探しますが、 クエリに複数の項目がある場合は、 一致しない項目は代わりにジェネリクスとして扱われます。 つまり、hahsmap は単独では hashmap に一致しますが、hahsmap, u32T, u32 が一致するものと同じものに一致することになります (ただし rustdoc はこの特定の問題を検出して警告します)。

次に、実際に各項目をループするとき、 ブルームフィルターはおそらく、クエリで言及されているすべての型を 持っていないエントリを拒否します。 たとえば、ブルームクエリでは i32 -> u32 というクエリが 型 i32, u32 -> bool を持つ関数に一致できますが、 単一化は後でそれを拒否します。

単一化フィルターは、次のことを保証します。

  • バッグセマンティクスが尊重されます。クエリが i32, i32 と指定している場合、 関数は i32 を1つだけではなく、2つ 言及している必要があります。
  • ネストのセマンティクスが尊重されます。クエリが vec<option> と指定している場合、 vec<option<i32>> は問題ありませんが、option<vec<i32>> は一致しません
  • 戻り値の型とパラメーターの区分が尊重されます。 i32 -> u32u32 -> i32 は完全に異なります。

ブルームフィルターはこれらを一切チェックせず、 さらに偽陽性が発生することもあります。 しかし高速で、使用するメモリが非常に少ないため、ブルームフィルターは役に立ちます。

再エクスポート

再エクスポートのインライン化 により、同じ項目を複数の名前で見つけられます。 検索では、同じ項目に複数のエントリを与え、 指定されたパスと異なる項目については正規パスを追跡することで、これをサポートしています。

たとえば、このサンプルインデックスには、2つのパスからエクスポートされた単一の構造体があります。

[
    [ "crate_name", {
        "doc": "Documentation",
        "n": ["Data", "Data"],
        "t": "FF",
        "d": ["The data struct", "The data struct"],
        "q": [[0, "crate_name"], [1, "crate_name::submodule"]],
        "i": [0, 0],
        "p": [],
        "f": "``",
        "b": [],
        "c": [],
        "a": [],
        "r": [[0, 1]],
    }]
]

この例で重要なのは r 配列です。 これは、q 配列内のパスエントリ 1 が 項目 0 の正規パスであることを示しています。 つまり、crate_name::Data の正規パスは crate_name::submodule::Data です。

これは重複したデータを持っているため、奇妙な設計に聞こえるかもしれません。 このようになっているのは、インライン化がクレートをまたいで発生する可能性があり、 それらのクレートは個別にコンパイルされ、すべてがドキュメントに存在するとは限らないためです。

[
  [ "crate_name", ... ],
  [ "crate_name_2", { "q": [[0, "crate_name::submodule"], [5, "core::option"]], ... }]
]

上の例では、正規パスの1つは実際には依存関係から来ており、 もう1つはインライン化された標準ライブラリ項目から来ています。 正規パスはインデックス内にすらありません! 正規パスが private である可能性もあります。 どちらの場合でも、それはユーザーには表示されず、重複排除にのみ使用されます。

関連型は、メソッドと同様に、異なる方法で保存されます。 これらの型は p(その「親」)内のエントリと結び付けられており、 それぞれに省略可能な3番目のタプル要素があります。

"p": [[5, "Data", 0, 1]]

これは次を意味します。

  • 5: 構造体であること
  • “Data”: その名前
  • 0: その表示パス、“crate_name”
  • 1: その正規パス、“crate_name::submodule”

どちらの場合でも、正規パスはまったく公開されていない可能性があり、 またはドキュメントに含まれていない別のクレートから来ている可能性があります。 そのため、ユーザーには表示されず、重複排除に使用されます。

検索エンジンのテスト

生成された UI は rustdoc-gui テストを使用してテストされますが、 検索エンジンをテストする主な方法は rustdoc-jsrustdoc-js-std テストです。これらは NodeJS で実行されます。

rustdoc-js テストには、同じ名前の .rs ファイルと .js ファイルがあります。 .rs ファイルは、検索を実行する仮想的なライブラリクレートを指定します (見つける必要があるものは必ず pub としてマークしてください)。 .js ファイルは、実際の検索を指定します。 rustdoc-js-std テストも同じですが、標準ライブラリを使用するため、 .rs ファイルは必要ありません。

.js ファイルはモジュールのようなものです(ただし、ローダーが exports を処理してくれます)。次の変数を使用します。

名前説明
FILTER_CRATEstring指定されたクレートの結果のみを含めます。GUI では、これは「crate 内の結果」ドロップダウンメニューです。
EXPECTED[ResultsTable]|ResultsTable実行するテストのリスト。仮想ユーザーが検索ボックスに入力し、タブで見る内容を指定します
PARSED[ParsedQuery]|ParsedQuery実際の検索を実行せずに実行するパーサーテストのリスト

FILTER_CRATE は省略できます(「すべてのクレート」を検索することと同等です)が、 EXPECTED または PARSED を指定する必要があります。

デフォルトでは、テストケースで指定された結果のいずれかが検索の実行後に 見つからない場合、または検索の実行後に見つかった結果が テスト内と同じ順序で表示されない場合、テストは失敗します。 ただし、実際の検索結果には、テストに含まれていない結果が含まれる場合があります。 これを上書きするには、次のマジックコメントのいずれかを指定します。 インデントせず、それぞれ単独の行に置いてください。

  • // exact-check: テストケースの一部ではない検索結果が表示された場合、 失敗します。
  • // ignore-order: 検索結果が任意の順序で表示されることを許可します。
  • // should-fail: ネガティブテストを書くために使用します。

標準ライブラリのテストでは通常、// exact-check を指定すべきではありません。 これは、libs チームが新しい項目を追加しても、無関係な テストが失敗しないようにしたいためです。一方、スタンドアロンのテストでは、より頻繁に使用されます。

ResultsTable 型と ParsedQuery 型は、 rustdoc.d.ts で指定されています。

たとえば、constructor という名前の関数を見つけられないバグを 修正する必要があったとします。これを行うには、2つのファイルを書きます。

#![allow(unused)]
fn main() {
// tests/rustdoc-js/constructor_search.rs
// テストケースはこの結果を見つける必要があります。
pub fn constructor(_input: &str) -> i32 { 1 }
}
// tests/rustdoc-js/constructor_search.js
// exact-check
// このテストは自身のクレートに対して実行されるため、
// 新しい項目は検索結果に表示されるべきではありません。
const EXPECTED = [
  // この最初のテストは名前ベースの検索を対象とします。
  {
    query: "constructor",
    others: [
      { path: "constructor_search", name: "constructor" },
    ],
    in_args: [],
    returned: [],
  },
  // このテストは2番目のタブを対象とします。
  {
    query: "str",
    others: [],
    in_args: [
      { path: "constructor_search", name: "constructor" },
    ],
    returned: [],
  },
  // このテストは3番目のタブを対象とします。
  {
    query: "i32",
    others: [],
    in_args: [],
    returned: [
      { path: "constructor_search", name: "constructor" },
    ],
  },
  // このテストは高度な型駆動検索を対象とします。
  {
    query: "str -> i32",
    others: [
      { path: "constructor_search", name: "constructor" },
    ],
    in_args: [],
    returned: [],
  },
]

//@ revisions ディレクティブが使用されている場合、JSファイルは REVISION という変数にアクセスできます。

const EXPECTED = [
  // この最初のテストは名前ベースの検索を対象とします。
  {
    query: "constructor",
    others: REVISION === "has_constructor" ?
      [
        { path: "constructor_search", name: "constructor" },
      ] :
      [],
    in_args: [],
    returned: [],
  },
];

rustdoc-html テストスイート

このページでは、rustdoc の HTML 出力をテストするために使用される rustdoc-html という名前のテストスイートについて説明します。 その他の rustdoc 固有のテストスイートについては、Rustdoc テストスイートを参照してください。

このテストスイートの各テストファイルは、通常の Rust コードコメント内に配置される、いわゆるディレクティブが散りばめられた Rust ソースファイル file.rs にすぎません。 これらには CompiletestHtmlDocCk の 2 種類があります。

前者について詳しく知るには、Compiletest ディレクティブを読んでください。 後者については、このまま読み進めてください。

内部的には、compiletest が補助チェッカースクリプト htmldocck.py を呼び出します。

HtmlDocCk ディレクティブ

HtmlDocCk へのディレクティブは、生成された HTML に制約を課すアサーションです。 //@ コメントの形式を取るという点で compiletest に与えられるものと似ていますが、 最終的には完全に別物であり、異なるプログラムによって処理されます。

HTML ドキュメントツリーの一部をクエリするために XPath が使用されます。

導入例:

//@ has file/type.Alias.html
//@ has - '//*[@class="rust item-decl"]//code' 'type Alias = Option<i32>;'
pub type Alias = Option<i32>;

ここでは、クレート file のために生成されたドキュメントに、 先頭にあるコードブロックがそのアイテムの期待されるレンダリングを含んでいる 公開型エイリアス Alias のページが含まれていることを確認しています。 //*[@class="rust item-decl"]//code は XPath 式です。

慣習的には、これらのディレクティブは、テスト対象のものの直上に配置します。 ただし技術的に言えば、HtmlDocCk はディレクティブだけを探すため、そうする必要はありません。

すべてのディレクティブは PATH 引数を取ります。 繰り返しを避けるため、前回の PATH 引数を再利用する目的で - を渡せます。 パスにはクレート名が含まれるため、結果として得られるパスを短くするために、クレートルートに #![crate_name = "foo"] 属性を追加するのが慣習です。

すべての引数はシェル形式の(一重または二重の)引用符付き文字列の形式を取ります。 ただし、COUNTPATH の特殊な - 形式は例外です。

すべてのディレクティブ(files を除く)は、名前の前に ! を付けることで否定できます。 否定ディレクティブを追加する前に、その注意点を読んでください。

シェルコマンドと同様に、 ディレクティブは最後の文字が \ である場合、複数行にまたがることができます。 この場合、次の行の先頭は // である必要があり、@ は付けません。

compiletest ディレクティブと同様に、ディレクティブ名と引数を区切るには空白だけでなくコロン : も使用できますが、 HtmlDocCk ディレクティブでは空白が推奨されます。

現在のリリースチャネル(例: stable または nightly)を指す CHANNEL を含む URL https://doc.rust-lang.org/CHANNEL を参照したい場合は、 XPath、PATTERN 引数、およびスナップショットファイルで特殊文字列 {{channel}} を使用してください。

使用可能なすべてのディレクティブを以下に示します。

has

用法 1: //@ has PATH

PATH で指定されたファイルが存在することを確認します。

用法 2: //@ has PATH XPATH PATTERN

PATH で指定された空白正規化済み1ファイル内で、XPATH によって選択された各要素 / 属性 / テキストのテキストが、 (同じく空白正規化された)文字列 PATTERN と一致することを確認します。

ヒント: 空白正規化を避けたい場合、または正規表現で一致させたい場合は、 代わりに matches を使用してください。

hasraw

用法: //@ hasraw PATH PATTERN

PATH で指定された空白正規化済み1ファイルの内容が、 (同じく空白正規化された)文字列 PATTERN と一致することを確認します。

ヒント: 空白正規化を避けたい場合、または正規表現で一致させたい場合は、 代わりに matchesraw を使用してください。

matches

用法: //@ matches PATH XPATH PATTERN

PATH で指定されたファイル内で、XPATH によって選択された各要素 / 属性 / テキストのテキストが、 Python 風2の正規表現 PATTERN と一致することを確認します。

matchesraw

用法: //@ matchesraw PATH PATTERN

PATH で指定されたファイルの内容が、 Python 風2の正規表現 PATTERN と一致することを確認します。

count

用法: //@ count PATH XPATH COUNT

PATH で指定されたファイル内に、XPATH に一致するものがちょうど COUNT 個あることを確認します。

snapshot

用法: //@ snapshot NAME PATH XPATH

PATH で指定されたファイル内で、XPATH によって選択された要素 / テキストが、 ファイル FILE_STEM.NAME.html 内に事前記録されたサブツリーまたはテキスト(「スナップショット」)と一致することを確認します。ここで FILE_STEM はテストファイルのファイルステムです。

現在のサブツリー/テキストを期待値として受け入れるには、compiletest--bless オプションを渡します。 これにより、前述のファイルが上書きされます(存在しない場合は作成されます)。 チャネルに依存する URL https://doc.rust-lang.org/CHANNEL は、自動的に特殊文字列 {{channel}} に正規化されます。

has-dir

用法: //@ has-dir PATH

PATH で指定されたディレクトリの存在を確認します。

files

用法: //@ files PATH ENTRIES

PATH で指定されたディレクトリに、ちょうど ENTRIES が含まれていることを確認します。 ENTRIES は引用符付き文字列内の Python 風の文字列リストです。

: //@ files "foo/bar" '["index.html", "sidebar-items.js"]'

Compiletest ディレクティブ(概要)

導入部で述べたように、compiletest ディレクティブにもアクセスできます。 最も重要なのは、補助クレートを登録し、 テスト対象の rustdoc バイナリにフラグを渡せることです。 それらについてまだ何も知らない場合は、その章を読むことを強く推奨します。

このテストスイートに特に関連する詳細をいくつか示します。

  • rustdoc にフラグを渡すには //@ compile-flags//@ doc-flags の両方を使用できますが、 意図を示すために後者を使用することを推奨します。 前者は rustc 向けです。
  • 補助クレートを持つテストファイルには //@ build-aux-docs を追加して、補助クレートを rustc でコンパイルするだけでなく、 rustdoc でそれらのドキュメントも生成するようにします。

注意点

要素またはテキスト片が存在しないことをテストするのはかなり壊れやすく、将来の変更に強いとは言えません。

生成される HTML ドキュメントツリーの形状は時々変わることがあります。 これには、たとえば CSS クラス名の変更が含まれます。

そのようなことが起こるたびに、肯定チェックは、(その XPath 式が十分に一般的 / 緩やかであれば)意図した要素 / 属性 / テキストに引き続き一致し、 したがって正しい対象をテストし続けるか、そうでなければ一致せず、その場合は失敗するため、 変更の作者にそれらを確認させることになります。

これを、XPath 式が「もはや」一致しなくなっても失敗しない否定チェック(例: //@ !has PATH XPATH PATTERN)と比較してください。 そのため、「形状」を変更した作者には通知されず、 結果として他の誰かが意図せず PATTERN を生成されたドキュメントに再導入しても、 元の否定チェックが失敗しない可能性があります。

注記: 否定チェックの使用は避けてください!

ヒント: どうしても避けられない場合は、「形状」を変更する人が気づいて否定チェックを更新できるように、 近接した場所で、必ず類似の肯定チェックと常に組み合わせてください!

制限事項

HtmlDocCk は Python 標準ライブラリの XPath 実装を使用します。 これにより、いくつかの制限があります。

  • 実装上の欠陥により、すべての XPATH 引数は // で始まる必要があります。
  • 多くの XPath 機能(関数、軸など)はサポートされていません。
  • 整形式の HTML のみ解析できます(rustdoc がタグの不一致を出力しないことが期待されます)。

さらに、compiletest のリビジョンはサポートされていません。


  1. 空白正規化とは、連続するすべての空白のまとまりが単一の空白に置き換えられることを意味します。 ↩2

  2. これらは Unicode 対応であり(フラグ UNICODE が設定されます)、大文字小文字を区別して、単一行モードで一致します。 ↩2

rustdoc-gui テストスイート

FIXME: このセクションはスタブです。内容を充実させるためにご協力ください!

このページでは、rustdoc の「GUI」(つまり、ブラウザーでレンダリングされる HTML/JS/CSS)をテストするために使用される、rustdoc-gui という名前のテストスイートについて説明します。 その他の rustdoc 固有のテストスイートについては、Rustdoc のテストスイートを参照してください。

これらは、browser-UI-test という NodeJS ベースのツールを使用します。このツールは puppeteer を使用してヘッドレスブラウザーでテストを実行し、レンダリングとインタラクティブ性をチェックします。この形式のテストの書き方については、tests/rustdoc-gui/README.md.goml 形式の説明を参照してください。

rustdoc-json テストスイート

このページでは、rustdoc の json output をテストする、rustdoc-json という名前のテストスイートについて特に説明します。 rustdoc のテストに使用される他のテストスイートについては、§Rustdoc テストスイートを参照してください。

テストは compiletest で実行され、通常の一連のディレクティブを利用できます。 ここで頻繁に使用されるディレクティブは次のとおりです。

各クレートの JSON 出力は、jsondoclintjsondocck という 2 つのプログラムでチェックされます。

jsondoclint

jsondoclint は、すべての Idindex(または paths)に存在することをチェックします。 これにより、ダングリングな Id が存在しないことを確認します。

jsondocck

jsondocck は、出力内の値が期待どおりであることをアサートするために、コメント内で指定されたディレクティブを処理します。 その点では htmldocck とよく似ています。

これはクエリ言語として JSONPath を使用します。JSONPath はパスを受け取り、そのパスがマッチするとされる値のリストを返します。

ディレクティブ

  • //@ has <path>: <path> が存在すること、つまり少なくとも 1 つの値にマッチすることをチェックします。
  • //@ !has <path>: <path> が存在しないこと、つまり 0 個の値にマッチすることをチェックします。
  • //@ has <path> <value>: <path> が存在し、マッチしたもののうち少なくとも 1 つが指定された <value> と等しいことをチェックします。
  • //@ !has <path> <value>: <path> が存在するが、マッチしたもののいずれも指定された <value> と等しくないことをチェックします。
  • //@ is <path> <value>: <path> がちょうど 1 つの値にマッチし、それが指定された <value> と等しいことをチェックします。
  • //@ is <path> <value> <value>...: <path> が指定されたすべての <value> にちょうどマッチすることをチェックします。 ここでは順序は関係ありません。
  • //@ !is <path> <value>: <path> がちょうど 1 つの値にマッチし、その値が指定された <value> と等しくないことをチェックします。
  • //@ count <path> <number>: <path><number> 個の値にマッチすることをチェックします。
  • //@ set <name> = <path>: <path> がちょうど 1 つの値にマッチすることをチェックし、その値を <name> という名前の変数に保存します。

これらは directive.rs で定義されています。

値には JSON 値または変数を指定できます。

  • JSON 値は JSON リテラルです。例: true"string"{"key": "value"}。 これらは 1 つの値として処理されるように、多くの場合 ' で引用する必要があります。 §引数の分割を参照してください。

  • 変数は、あるパス内の値を保存し、後続のクエリで使用するために利用できます。 変数は //@ set <name> = <path> ディレクティブで設定し、$<name> でアクセスします。

    #![allow(unused)]
    fn main() {
    //@ set foo = $some.path
    //@ is $.some.other.path $foo
    }

引数の分割

ディレクティブへの引数は、POSIX シェルのエスケープを実装する shlex クレートを使用して分割されます。 これは、ディレクティブへの <path> 引数と <value> 引数のどちらにも、 空白と引用符の両方が頻繁に含まれるためです。

<path>$.index[?(@.docs == "foo")].some.field、値に "bar" 1 を指定して @ is を使用するには、次のように書きます。

#![allow(unused)]
fn main() {
//@ is '$.is[?(@.docs == "foo")].some.field' '"bar"'
}

  1. 値は shlex による分割の"bar" である必要があります。なぜなら、 それは JSON 文字列値である必要があるためです。

std::offload

このモジュールは活発に開発されています。 アップストリームに取り込まれれば、Rust 開発者が GPU 上で Rust コードを実行できるようになるはずです。 私たちは、安全で便利で、デフォルトで十分に高速な rusty な GPU プログラミングインターフェイスの開発を目指しています。 これには、GPU へのデータ移動と GPU からのデータ移動を効率的に自動で行うことが含まれます。 また、(後で)より高度な制御を可能にする、 場合によっては unsafe な、より高度なインターフェイスも提供する予定です。

実装は LLVM の “offload” プロジェクトに基づいています。 これは、OpenMP が Fortran や C++ コードを GPU 上で実行するためにすでに使用しています。 プロジェクトが開発中である間は、 ユーザーはコンパイルプロセスを完了するために clang などの他のコンパイラを呼び出す必要があります。

高レベルなコンパイル設計:

単一ソース・2 パスのコンパイル方式を使用します。

まず、デバイス向けにオフロードされるべきすべての関数をコンパイルします (例: nvptx64、amdgcn-amd-amdhsa、将来的には intel)。 現在は扱いにくい #cfg(target_os="") アノテーションが必要ですが、将来的には offload intrinsic に基づいてそれらを認識する予定です。 この最初のコンパイルは現在 rustc の内部 Query システムを活用していないため、現時点では常にカーネルを再コンパイルします。 これは簡単に修正できるはずですが、現時点では機能とランタイム性能の改善を優先しています。 とはいえ、これを実装したい場合はぜひ連絡してください!

次に、ホスト(例: x86-64)向けにコードをコンパイルします。オフロード処理の大半はここで行われます。 ホスト側では、openmp offload ランタイムへの呼び出しを生成し、 型のレイアウト(autodiff TypeTrees の簡略版)をそれに通知します。 また、型システムを使用して、カーネル引数をデバイスへ移動するだけでよいのか(例: &[f32;1024])、 デバイスから移動する必要があるのか、またはその両方なのか(例: &mut [f64])を判断します。 その後、カーネルを起動し、 その後でランタイムにこの環境を終了することと、(必要な範囲で)データを戻すことを通知します。

ホスト向けの 2 回目のパスでは、前回のコンパイルからカーネルアーティファクトを読み込みます。 一般に rustc はビルドディレクトリのレイアウトを「推測」したりハードコードしたりしてはならず、 そのため 2 回目の呼び出しでは、カーネルアーティファクトへのパスを伝える必要があります。 このためのロジックは cargo に統合できますが、 必要なのは単純な cargo ラッパーだけでもあります。 これは、より広く採用されるまで、crates.io 経由で簡単に提供できます。

単一ソース・単一パスのコンパイル方式を考えたくなるかもしれません。 しかし、rustc フロントエンドの多く(例: AST)は、あらゆるデッドコード(例: 非アクティブな cfg の背後にあるコード)を削除します。 フロントエンドに 2 つのターゲット向けのコードを素朴に展開および lowering させると、同じシンボルの複数定義(およびその他の問題)が発生します。 rustc の middle と backend 全体に、任意のシンボルが今や 2 つの実装を含む可能性があることを認識させようとするのは大きな取り組みであり、 代替案が約 5 行の cargo ラッパーであるなら、コンパイラ全体をより複雑にするべき理由は疑問です。 それでも私たちはコンパイルパイプライン全体を制御しており、ホストコードとデバイスコードの両方を利用できるため、 2 つの方式の間にランタイム性能の違いはないはずです。

インストール

std::offload は、ユーザー向けの nightly ビルドで一部利用可能です。 ただし現時点では、すべての機能を使用するには、引き続き rustc をソースからビルドする必要があります。

ビルド手順

まず、Rust リポジトリをクローンして設定する必要があります。

git clone git@github.com:rust-lang/rust
cd rust
./configure --enable-llvm-link-shared --release-channel=nightly --enable-llvm-assertions --enable-llvm-offload --enable-llvm-enzyme --enable-clang --enable-lld --enable-option-checking --enable-ninja --disable-docs

その後、以下を使用して rustc をビルドできます。

./x build --stage 1 library

その後、rustc ツールチェーンのリンクにより、cargo 経由で使用できるようになります。

rustup toolchain link offload build/host/stage1
rustup toolchain install nightly # -Z unstable-options を有効化します

LLVM 自体のビルド手順

git clone git@github.com:llvm/llvm-project
cd llvm-project
mkdir build
cd build
cmake -G Ninja ../llvm -DLLVM_TARGETS_TO_BUILD="host;AMDGPU;NVPTX" -DLLVM_ENABLE_ASSERTIONS=ON -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_ENABLE_RUNTIMES="offload;openmp" -DLLVM_ENABLE_PLUGINS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=.
ninja
ninja install

これにより、動作する LLVM ビルドが得られます。

テスト

offload 固有のテストには、このテストスクリプトを実行してください。

./x test --stage 1 tests/codegen-llvm/gpu_offload

CI をローカルでテストするには、Docker によるテストで説明されているコマンドを使用できます。

cargo run --manifest-path src/ci/citool/Cargo.toml run-local dist-x86_64-linux

これにより、すべてのコンパイラ成果物が obj ディレクトリに保存されます。ただし、rustc 固有のコードを変更した場合、Docker イメージが状態をキャッシュするため、このディレクトリを削除する必要があるかもしれません。

この時点で、サブモジュールもチェックアウトされている必要があります。

使い方

この機能は開発中であり、まだ使用できる状態ではありません。 ここにある手順は、コントリビューター、または最新の進捗を追いたい人向けです。 現在、GPU 上で次の Rust カーネルを起動する作業を進めています。 一緒に試すには、これを src/lib.rs ファイルにコピーしてください。

#![feature(abi_gpu_kernel)]
#![feature(rustc_attrs)]
#![feature(core_intrinsics)]
#![no_std]

#[cfg(target_os = "linux")]
extern crate libc;
#[cfg(target_os = "linux")]
use libc::c_char;

#[cfg(target_os = "linux")]
use core::mem;

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

#[cfg(target_os = "linux")]
#[unsafe(no_mangle)]
#[inline(never)]
fn main() {
    let array_c: *mut [f64; 256] =
        unsafe { libc::calloc(256, (mem::size_of::<f64>()) as libc::size_t) as *mut [f64; 256] };
    let output = c"The first element is zero %f\n";
    let output2 = c"The first element is NOT zero %f\n";
    let output3 = c"The second element is %f\n";
    unsafe {
        let val: *const c_char = if (*array_c)[0] < 0.1 {
            output.as_ptr()
        } else {
            output2.as_ptr()
        };
        libc::printf(val, (*array_c)[0]);
    }

    unsafe {
        kernel(array_c);
    }
    core::hint::black_box(&array_c);
    unsafe {
        let val: *const c_char = if (*array_c)[0] < 0.1 {
            output.as_ptr()
        } else {
            output2.as_ptr()
        };
        libc::printf(val, (*array_c)[0]);
        libc::printf(output3.as_ptr(), (*array_c)[1]);
    }
}

#[inline(never)]
unsafe fn kernel(x: *mut [f64; 256]) {
    core::intrinsics::offload(kernel_1, [256, 1, 1], [32, 1, 1], (x,))
}

#[cfg(target_os = "linux")]
unsafe extern "C" {
    pub fn kernel_1(array_b: *mut [f64; 256]);
}

#[cfg(not(target_os = "linux"))]
#[unsafe(no_mangle)]
#[inline(never)]
#[rustc_offload_kernel]
pub extern "gpu-kernel" fn kernel_1(x: *mut [f64; 256]) {
    unsafe { (*x)[0] = 21.0 };
}

コンパイル手順

rustc と同じ LLVM でビルドされた clang コンパイラを使用することが重要です。 フルパスなしで clang を呼び出すだけでは、おそらくシステムの clang が使用され、それはおそらく互換性がありません。 そのため、以下の clang/lld 呼び出しを絶対パスに置き換えるか、PATH を適切に設定してください。

まず、デバイス(GPU)コードを生成します。

target-cpu(gfx90a)を、お使いの GPU に適したコードに置き換えてください。これらはしばしば「LLVM ターゲット名」と呼ばれます1

RUSTFLAGS="-Ctarget-cpu=gfx90a --emit=llvm-bc,llvm-ir -Zoffload=Device -Csave-temps -Zunstable-options" cargo +offload build -Zunstable-options -r -v --target amdgcn-amd-amdhsa -Zbuild-std=core

その後、次の手順の前に、現時点では target/release/deps/<lib_name>.bc を lib.bc にコピーする必要があるかもしれません。

次に、ホスト(CPU)コードを生成します。

RUSTFLAGS="--emit=llvm-bc,llvm-ir -Csave-temps -Zoffload=Host=/p/lustre1/drehwald1/prog/offload/r/target/amdgcn-amd-amdhsa/release/deps/device.bin -Zunstable-options" cargo +offload build -r

この呼び出しも多くの処理を行い、LLVM オフロード用に複数の中間ファイルを生成します。 現時点ではオフロード手順の大部分を rustc に統合済みですが、まだ 1 つのバイナリ呼び出しが残っています。

"clang-linker-wrapper" "--should-extract=gfx90a" "--device-compiler=amdgcn-amd-amdhsa=-g" "--device-compiler=amdgcn-amd-amdhsa=-save-temps=cwd" "--device-linker=amdgcn-amd-amdhsa=-lompdevice" "--host-triple=x86_64-unknown-linux-gnu" "--save-temps" "--linker-path=/ABSOlUTE_PATH_TO/rust/build/x86_64-unknown-linux-gnu/lld/bin/ld.lld" "--hash-style=gnu" "--eh-frame-hdr" "-m" "elf_x86_64" "-pie" "-dynamic-linker" "/lib64/ld-linux-x86-64.so.2" "-o" "bare" "/lib/../lib64/Scrt1.o" "/lib/../lib64/crti.o" "/ABSOLUTE_PATH_TO/crtbeginS.o" "-L/ABSOLUTE_PATH_TO/rust/build/x86_64-unknown-linux-gnu/llvm/bin/../lib/x86_64-unknown-linux-gnu" "-L/ABSOLUTE_PATH_TO/rust/build/x86_64-unknown-linux-gnu/llvm/lib/clang/21/lib/x86_64-unknown-linux-gnu" "-L/lib/../lib64" "-L/usr/lib64" "-L/lib" "-L/usr/lib" "target/<GPU_DIR>/release/host.o" "-lstdc++" "-lm" "-lomp" "-lomptarget" "-L/ABSOLUTE_PATH_TO/rust/build/x86_64-unknown-linux-gnu/llvm/lib" "-lgcc_s" "-lgcc" "-lpthread" "-lc" "-lgcc_s" "-lgcc" "/ABSOLUTE_PATH_TO/crtendS.o" "/lib/../lib64/crtn.o"

これらのファイルへのパスを自分のシステム上で探してみることもできます。 ただし、パスを修正するのではなく、ベアモードの OpenMP の例をコピーして、それを自分の clang でコンパイルすることで再生成することをお勧めします。 clang 呼び出しに -### を追加すると、個々の手順を確認できます。 複数の手順が表示されるので、clang-linker-wrapper の例を探してください。 次の呼び出しで c++ の例をコンパイルしたときに得られた一時ファイルではなく、必ず host.o ファイルへのパスを含めるようにしてください。

myclang++ -fuse-ld=lld -O3 -fopenmp  -fopenmp-offload-mandatory --offload-arch=gfx90a omp_bare.cpp -o main -###

最後の手順では、これでバイナリを実行できます。

./main
The first element is zero 0.000000
The first element is NOT zero 21.000000
The second element is  0.000000

メモリ転送に関する詳細情報を得るには、次のようにして情報出力を有効にできます。

LIBOMPTARGET_INFO=-1  ./main

  1. https://rocm.docs.amd.com/en/latest/reference/gpu-arch-specs.html または https://developer.nvidia.com/cuda/gpus。あるいは、rustc --print target-cpus を確認してください。

コントリビューション

コントリビューションは常に歓迎します。 このプロジェクトは実験的なものなので、ドキュメントとコードは未完成である可能性があります。 行き詰まった場合や、ドキュメントが不明確な場合は、Zulip(推奨)または Rust Community Discord で助けを求めてください。

通常、ユーザー向けにはコンパイルプロセスのできるだけ多くを自動化するようにしています。 しかし、コントリビューターとしては、rustc を繰り返し再コンパイルすることなく、変更をすばやく反復するために、LLVM-IR モジュール(.ll)を直接書き換えてコンパイルする方が簡単な場合があります。 そのため、LLVM に詳しい人向けに以下のシェルスクリプトを用意しています。 IR の変更に満足できるようになってから初めて、rustc を更新して新しい目的の出力を生成する作業に取りかかれます。

set -e
# エラー時に続行しないよう set -e を設定します。続行すると古いアーティファクトが使われる可能性が高いためです
# 入力:
# lib.ll(ホストコード)+ host.out(デバイス)

# rust コードから lib.ll と host.out を生成するには、最初の 3 つのコマンドを一度だけ実行する必要があります。

# RUSTFLAGS="-Ctarget-cpu=gfx90a --emit=llvm-bc,llvm-ir -Zoffload=Device -Csave-temps -Zunstable-options" cargo +offload build -Zunstable-options -v --target amdgcn-amd-amdhsa -Zbuild-std=core -r
#
# RUSTFLAGS="--emit=llvm-bc,llvm-ir -Csave-temps -Zoffload=Host=/absolute/path/to/project/target/amdgcn-amd-amdhsa/release/deps/host.out -Zunstable-options" cargo +offload build -r
#
# cp target/release/deps/<project_name>.ll lib.ll

opt lib.ll -o lib.bc

"clang-21" "-cc1" "-triple" "x86_64-unknown-linux-gnu" "-S" "-save-temps=cwd" "-disable-free" "-clear-ast-before-backend" "-main-file-name" "lib.rs" "-mrelocation-model" "pic" "-pic-level" "2" "-pic-is-pie" "-mframe-pointer=all" "-fmath-errno" "-ffp-contract=on" "-fno-rounding-math" "-mconstructor-aliases" "-funwind-tables=2" "-target-cpu" "x86-64" "-tune-cpu" "generic" "-resource-dir" "/<path>/rust/build/x86_64-unknown-linux-gnu/llvm/lib/clang/21" "-ferror-limit" "19" "-fopenmp" "-fopenmp-offload-mandatory" "-fgnuc-version=4.2.1" "-fskip-odr-check-in-gmf" "-fembed-offload-object=host.out" "-fopenmp-targets=amdgcn-amd-amdhsa" "-faddrsig" "-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o" "host.s" "-x" "ir" "lib.bc"

"clang-21" "-cc1as" "-triple" "x86_64-unknown-linux-gnu" "-filetype" "obj" "-main-file-name" "lib.rs" "-target-cpu" "x86-64" "-mrelocation-model" "pic" "-o" "host.o" "host.s"

"/<path>/rust/build/x86_64-unknown-linux-gnu/llvm/bin/clang-linker-wrapper" "--should-extract=gfx90a" "--device-compiler=amdgcn-amd-amdhsa=-g" "--device-compiler=amdgcn-amd-amdhsa=-save-temps=cwd" "--device-linker=amdgcn-amd-amdhsa=-lompdevice" "--host-triple=x86_64-unknown-linux-gnu" "--save-temps" "--linker-path=/<path>/rust/build/x86_64-unknown-linux-gnu/lld/bin/ld.lld" "--hash-style=gnu" "--eh-frame-hdr" "-m" "elf_x86_64" "-pie" "-dynamic-linker" "/lib64/ld-linux-x86-64.so.2" "-o" "a.out" "/lib/../lib64/Scrt1.o" "/lib/../lib64/crti.o" "/opt/rh/gcc-toolset-12/root/usr/lib/gcc/x86_64-redhat-linux/12/crtbeginS.o" "-L/<path>/rust/build/x86_64-unknown-linux-gnu/llvm/bin/../lib/x86_64-unknown-linux-gnu" "-L/<path>/rust/build/x86_64-unknown-linux-gnu/llvm/lib/clang/21/lib/x86_64-unknown-linux-gnu" "-L/opt/rh/gcc-toolset-12/root/usr/lib/gcc/x86_64-redhat-linux/12" "-L/opt/rh/gcc-toolset-12/root/usr/lib/gcc/x86_64-redhat-linux/12/../../../../lib64" "-L/lib/../lib64" "-L/usr/lib64" "-L/lib" "-L/usr/lib" "host.o" "-lstdc++" "-lm" "-lomp" "-lomptarget" "-L/<path>/rust/build/x86_64-unknown-linux-gnu/llvm/lib" "-lgcc_s" "-lgcc" "-lpthread" "-lc" "-lgcc_s" "-lgcc" "/opt/rh/gcc-toolset-12/root/usr/lib/gcc/x86_64-redhat-linux/12/crtendS.o" "/lib/../lib64/crtn.o"

LIBOMPTARGET_INFO=-1 OFFLOAD_TRACK_ALLOCATION_TRACES=true ./a.out

clang-linker-wrapper 呼び出し上の <path> プレースホルダーを更新してください。 おそらくライブラリパスも調整する必要があります。 詳細については、リンク先の使用方法セクションを参照してください: 使用方法

Autodiff の内部

Rust の std::autodiff モジュールでは、微分可能プログラミングを利用できます。

#![feature(autodiff)]
use std::autodiff::*;

// f(x) = x * x、f'(x) = 2.0 * x
// したがって bar は (x * x, 2.0 * x) を返す
#[autodiff_reverse(bar, Active, Active)]
fn foo(x: f32) -> f32 { x * x }

fn main() {
    assert_eq!(bar(3.0, 1.0), (9.0, 6.0));
    assert_eq!(bar(4.0, 1.0), (16.0, 8.0));
}

std::autodiff モジュールの詳細なドキュメントは std::autodiff で入手できます。

微分可能プログラミングは、数値計算、固体力学計算化学流体力学、バックプロパゲーションによるニューラルネットワークのトレーニング、ODE ソルバー微分可能レンダリング量子コンピューティング、気候シミュレーションなど、さまざまな分野で使用されています。

std::autodiff は現在、自動微分のための LLVM ベースのツールである Enzyme を基にしています。コンパイラーベースの自動微分に依存する主な理由は 3 つあります。

  • 使いやすさ: 現在の自動微分クレートは、通常の Rust プログラムをサポートしていません。カスタム DSL を強制するか、ライブラリが提供する型(たとえばスライスや配列の代わり)を使用する必要があるか、スカラー関数に限定されています。コンパイラーベースの自動微分では、配列、スライス、ユーザー定義の構造体や列挙型、制御フローなどを含む通常の Rust コードをユーザーが記述できます。
  • パフォーマンス: 既存の Rust 自動微分アプローチのほとんどには、操作ごとに一定のオーバーヘッドがあります。これは、大きなテンソルに対する高コストな操作が少ない ML アプリケーションでは簡単に償却できます。しかし、HPC や科学技術計算の分野のアプリケーションでは、多くの場合許容できません。(最適化された)LLVM IR に対して動作することで、コンパイラーベースの自動微分は、そのようなケースで大幅に優れたパフォーマンスを実現できます。
  • 機能: このような低レベルで動作し、他の LLVM ベース言語と実装を共有することで、Enzyme プロジェクトですでに行われている大量の作業を活用できます。たとえば、MPI ルーチンを呼び出す Rust コードや、CuBLAS のようなライブラリを含む GPU コードをサポートできます。

インストール

近い将来、std::autodiff は rustup 経由でユーザーが利用できるようになるはずです。 ただし、rustc/enzyme/autodiff のコントリビューターとしては、引き続きソースから rustc をビルドする必要があります。 当面の間、次のいずれかを使用している場合は、最新の nightly ツールチェーンで std::autodiff を有効にするための最新ビルドをダウンロードできます。 Linuxx86_64-unknown-linux-gnu または aarch64-unknown-linux-gnu Windowsx86_64-llvm-mingw または aarch64-llvm-mingw

以前は Apple(aarch64-apple)向けのビルドもダウンロードできましたが、現時点では使用できません。

他のプラットフォームが必要な場合は、autodiff を含む rustc をソースからビルドできます。 希望するターゲット向けの自動ビルドを有効にすることに協力したい場合は、issue を開いてください。

インストールガイド

Linux または Windows で std::autodiff を使用したいだけで、プロジェクトへ PR を送る予定がない場合は、既存の nightly インストールをそのまま使用し、不足しているコンポーネントをダウンロードすることをおすすめします。次を実行してください。

rustup +nightly component add enzyme

Apple サポートは、下流での破損のため一時的に元に戻されました。再び有効化できるまで、ソースからビルドしてください。

Nix ユーザー向けインストールガイド。

このセットアップは、nix と autodiff のユーザーによって推奨されたものです。 これは Overlay を使用します。 そのリポジトリを使用して問題ないかどうかは、ご自身で確認してください。 その場合、std::autodiff をサポートする rustc を取得するために、次の nix 設定を使用できます。

{
  enzymeLib = pkgs.fetchzip {
    url = "https://ci-artifacts.rust-lang.org/rustc-builds/ec818fda361ca216eb186f5cf45131bd9c776bb4/enzyme-nightly-x86_64-unknown-linux-gnu.tar.xz";
    sha256 = "sha256-Rnrop44vzS+qmYNaRoMNNMFyAc3YsMnwdNGYMXpZ5VY=";
  };

  rustToolchain = pkgs.symlinkJoin {
    name = "rust-with-enzyme";
    paths = [pkgs.rust-bin.nightly.latest.default];
    nativeBuildInputs = [pkgs.makeWrapper];
    postBuild = ''
      libdir=$out/lib/rustlib/x86_64-unknown-linux-gnu/lib
      cp ${enzymeLib}/enzyme-preview/lib/rustlib/x86_64-unknown-linux-gnu/lib/libEnzyme-22.so $libdir/
      wrapProgram $out/bin/rustc --add-flags "--sysroot $out"
    '';
  };
}

ビルド手順

まず、Rust リポジトリをクローンして設定する必要があります。 好みに応じて、--enable-clang または --enable-lld も有効にしたい場合があります。

git clone git@github.com:rust-lang/rust
cd rust
./configure --release-channel=nightly --enable-llvm-enzyme --enable-llvm-link-shared --enable-llvm-assertions --enable-ninja --enable-option-checking --disable-docs --set llvm.download-ci-llvm=false

その後、次を使用して rustc をビルドできます。

./x build --stage 1 library

その後、rustc toolchain link により cargo 経由で使用できるようになります。

rustup toolchain link enzyme build/host/stage1
rustup toolchain install nightly # -Z unstable-options を有効にする

次に、テストケースを実行できます。

./x test --stage 1 tests/codegen-llvm/autodiff
./x test --stage 1 tests/pretty/autodiff
./x test --stage 1 tests/ui/autodiff
./x test --stage 1 tests/run-make/autodiff
./x test --stage 1 tests/ui/feature-gates/feature-gate-autodiff.rs

Autodiff はまだ実験的なため、自分のプロジェクトで使用したい場合は、Cargo.toml に lto="fat" を追加し、 cargo または cargo +nightly の代わりに RUSTFLAGS="-Zautodiff=Enable" cargo +enzyme を使用する必要があります。

Compiler Explorer と dist ビルド

私たちの compiler explorer インスタンスも、同様の方法で新しい rustc に更新できます。 まず、docker インスタンスを準備します。

docker run -it ubuntu:22.04
export CC=clang CXX=clang++
apt update
apt install wget vim python3 git curl libssl-dev pkg-config lld ninja-build cmake clang build-essential

次に、少し変更した方法で rustc をビルドします。

git clone https://github.com/rust-lang/rust
cd rust
./configure --release-channel=nightly --enable-llvm-enzyme --enable-llvm-link-shared --enable-llvm-assertions --enable-ninja --enable-option-checking --disable-docs --set llvm.download-ci-llvm=false
./x dist

次に、tarball をホストにコピーします。 dockerid は docker ps -a の下にある最新のエントリです。

docker cp <dockerid>:/rust/build/dist/rust-nightly-x86_64-unknown-linux-gnu.tar.gz rust-nightly-x86_64-unknown-linux-gnu.tar.gz

その後、EnzymeAD/rust リポジトリに新しい(プレリリース)タグを作成し、EnzymeAD/enzyme-explorer リポジトリに対してタグを更新する PR を作成できます。 PR で tgymnich にメンションし、彼の更新スクリプトを実行してもらうことを忘れないでください。 注: EnzymeAD/rust をアーカイブし、ここの手順を更新する必要があります。 explorer はまもなく、公式の rust サーバーから rustc ツールチェーンを取得できるようになるはずです。

Enzyme 自体のビルド手順

上記の Rust ビルド手順に従うと、Rust コンパイラとともに LLVMEnzyme、LLDEnzyme、ClangEnzyme がビルドされます。 単にそれらのいずれかを使いたいだけで、cmake の経験がない場合は、この方法をおすすめします。 ただし、Rust なしで Enzyme だけをビルドしたい場合は、これらの手順が役立つかもしれません。

git clone git@github.com:llvm/llvm-project
cd llvm-project
mkdir build
cd build
cmake -G Ninja ../llvm -DLLVM_TARGETS_TO_BUILD="host" -DLLVM_ENABLE_ASSERTIONS=ON -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_ENABLE_RUNTIMES="openmp" -DLLVM_ENABLE_PLUGINS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=.
ninja
ninja install

これにより動作する LLVM ビルドが得られるため、続いて Enzyme のビルドを進められます。 llvm-project フォルダーを離れ、次のコマンドを実行してください。

git clone git@github.com:EnzymeAD/Enzyme
cd Enzyme/enzyme
mkdir build
cd build
cmake .. -G Ninja -DLLVM_DIR=<YourLocalPath>/llvm-project/build/lib/cmake/llvm/ -DLLVM_EXTERNAL_LIT=<YourLocalPath>/llvm-project/llvm/utils/lit/lit.py -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=YES -DBUILD_SHARED_LIBS=ON
ninja

これにより Enzyme がビルドされ、Enzyme/enzyme/build/lib/<LLD/Clang/LLVM/lib>Enzyme.so に見つけることができます。 (末尾は OS によって異なる場合があります)。

バックエンドのクラッシュを報告する

コンパイル失敗後に大量の llvm-ir コードが表示された場合、Enzyme バックエンドがあなたのコードのコンパイルに失敗した可能性が高いです。これらのケースはデバッグが難しいため、あなたの協力は非常にありがたいものです。また、現時点では通常、リリースビルドの方がはるかに動作する可能性が高いことも覚えておいてください。

ここでの最終目標は、Enzyme リポジトリでバグレポートを作成するために、Enzyme の compiler explorer であなたのバグを再現することです。

これを支援するために、rustflags に渡せる autodiff フラグがあります。これは、いくつかの __enzyme_fwddiff または __enzyme_autodiff 呼び出しとともに、llvm-ir モジュール全体を出力します。Linux での想定されるワークフローは次のようになります:

llvm-ir 生成を制御する

llvm-ir を生成する前に、関連する Rust コードをデバッグ用に見える状態にするために役立つ 2 つのテクニックを覚えておいてください。

  • std::hint::black_box: Rust の変数または式を std::hint::black_box() でラップし、Rust と LLVM によって最適化で取り除かれるのを防ぎます。これは、llvm-ir 内の特定の値を調査したり手動で操作したりする必要がある場合に便利です。
  • extern "rust" または extern "c": 特定の関数宣言が llvm-ir にどのように低下されるかを確認したい場合は、それを extern "rust" または extern "c" として宣言できます。例として、生成されたモジュール内にある既存の __enzyme_autodiff または類似の宣言を探すこともできます。

1) llvm-ir 再現用ファイルを生成する

RUSTFLAGS="-Z autodiff=Enable,PrintModBefore" cargo +enzyme build --release &> out.ll 

これにより、モジュールの上と下にあるいくつかの警告と情報メッセージもキャプチャされます。out.ll を開き、; moduleid = <somehash> より上のすべての行を削除してください。次にファイルの末尾を見て、llvm-ir の一部ではないもの、つまりエラーや警告をすべて削除してください。llvm-ir の最後の行は、!<somenumber> = で始まるはずです。つまり、!40831 = !{i32 0, i32 1037508, i32 1037538, i32 1037559} または !43760 = !dilocation(line: 297, column: 5, scope: !43746) のようになります。

実際の数値はあなたのコードによって異なります。

2) llvm-ir 再現用ファイルを確認する

前の手順が成功したことを確認するために、LLVM の opt ツールを使用します。opt バイナリへのパスを探してください。これは <some_dir>/rust/build/<x86/arm/...-target-triple>/ci-llvm/bin/opt のようなパスです。LLVM をソースからビルドする場合は、ci-llvmbuild に置き換える必要がある可能性が高いです。また、/rust/build/target-triple/enzyme/build/enzyme/llvmenzyme-21 のような llvmenzyme-21.<so/dll/dylib> パスも探してください。LLVM は LLVM バックエンドを頻繁に更新するため、バージョン番号はより高い可能性があります(20、21、…)。両方が揃ったら、次のコマンドを実行してください。

<path/to/opt> out.ll -load-pass-plugin=/path/to/build/<target-triple>/stage1/lib/libEnzyme-21.so -passes="enzyme" -enzyme-strict-aliasing=0  -S

このコマンドは、将来のバージョンやあなたのシステムでは失敗する可能性があります。その場合は、libEnzyme-21.so を LLVMEnzyme-21.so に置き換えてください。ビルド方法については Enzyme のドキュメントを参照してください。LLVM バージョンのビルド方法も調整する必要があるかもしれません。

前の手順が成功した場合、cargo で Rust コードをコンパイルしたときに見たものと同じエラーが表示されます。

同じエラーを得られない場合は、Rust リポジトリで issue を開いてください。成功した場合は、おめでとうございます!ファイルはまだ巨大なので、自動的に最小化してみましょう。

3) llvm-ir 再現用ファイルを最小化する

まず llvm-extract バイナリを探してください。これは opt バイナリと同じフォルダーにあります。次に実行します。

<path/to/llvm-extract> -S --func=<name> --recursive --rfunc="enzyme_autodiff*" --rfunc="enzyme_fwddiff*" --rfunc=<fnc_called_by_enzyme> out.ll -o mwe.ll

このコマンドは、最小の動作例である mwe.ll を作成します。

最後の --func フラグで渡す名前を調整してください。微分する関数に #[no_mangle] 属性を適用すれば、Rust の名前に置き換えられます。そうでない場合は、マングルされた関数名を調べる必要があります。そのためには、out.ll を開き、__enzyme_fwddiff または __enzyme_autodiff を検索してください。その関数呼び出し内の最初の文字列が、あなたの関数の名前です。例:

define double @enzyme_opt_helper_0(ptr %0, i64 %1, double %2) {
  %4 = call double (...) @__enzyme_fwddiff(ptr @_zn2ad3_f217h3b3b1800bd39fde3e, metadata !"enzyme_const", ptr %0, metadata !"enzyme_const", i64 %1, metadata !"enzyme_dup", double %2, double %2)
  ret double %4
}

ここでは、_zn2ad3_f217h3b3b1800bd39fde3e が正しい名前です。先頭の @ をコピーしないようにしてください。opt コマンドをもう一度実行して 2) をやり直しますが、今回は入力ファイルとして out.ll ではなく mwe.ll を渡します。この最小化された例でもクラッシュを再現するか確認してください。

4) (任意)llvm-ir 再現用ファイルをさらに最小化する。

前の手順の後、約 5k loc の mwe.ll ファイルができているはずです。これを 50 まで減らしてみましょう。optllvm-extract の隣にある llvm-reduce バイナリを探してください。エラーメッセージの最初の行をコピーします。例は次のようになります。

opt: /home/manuel/prog/rust/src/llvm-project/llvm/lib/ir/instructions.cpp:686: void llvm::callinst::init(llvm::functiontype*, llvm::value*, llvm::arrayref<llvm::value*>, llvm::arrayref<llvm::operandbundledeft<llvm::value*> >, const llvm::twine&): assertion `(args.size() == fty->getnumparams() || (fty->isvararg() && args.size() > fty->getnumparams())) && "calling a function with bad signature!"' failed.

単に segfault しか出ない場合は、意味のあるエラーメッセージがなく、自動的にできることはあまりないため、5) に進んでください。
そうでない場合は、次を含む script.sh ファイルを作成します。

#!/bin/bash
<path/to/your/opt> $1 -load-pass-plugin=/path/to/llvmenzyme-19.so -passes="enzyme" \
    |& grep "/some/path.cpp:686: void llvm::callinst::init"

grep に渡すエラーメッセージを少し試してみてください。エラーが一意であることを確実にするのに十分な長さである必要があります。ただし、() を含む長いエラーでは、それらを正しくエスケープする必要があり、面倒になることがあります。実行します。

<path/to/llvm-reduce> --test=script.sh mwe.ll 

input isn't interesting! verify interesting-ness test が表示された場合は、script.sh 内のエラーメッセージが間違っています。grep が実際のエラーに一致することを確認する必要があります。すべてうまくいけば、多数の反復が表示され、最後に新しい reduced.ll ファイルが生成されます。opt で、まだ同じエラーが発生することを確認してください。

高度なデバッグ: 手動での llvm-ir 調査

最小化された再現用ファイル(mwe.ll または reduced.ll)ができたら、さらに深く掘り下げることができます。

  • 手動編集: llvm-ir を手動で書き換えてみてください。間接呼び出しに関するものなど、特定の問題では、__enzyme_virtualreverse のような Enzyme 固有の intrinsic を調査するとよいかもしれません。これらの使い方を理解するには、Enzyme のドキュメントやソースコードを参照する必要がある場合があります。
  • Enzyme のテストケース: Enzyme リポジトリ 内で、あなたの問題に関連する機能や intrinsic の正しい使い方を示している可能性のある関連テストケースを探してください。

5) バグを報告する。

その後、mwe.ll(または reduced.ll)の例をコピーして、私たちの compiler explorer に貼り付けられるはずです。

  • 言語として llvm ir を選択し、コンパイラとして opt 20 を選択します。
  • コンパイラの右側にあるフィールドがまだ設定されていない場合は、-passes="enzyme" に置き換えます。
  • うまくいけば、もはや見慣れたエラーが再び表示されるはずです。
  • 共有ボタンを使用して、それらへのリンクをコピーしてください。
  • https://github.com/enzymead/enzyme/issues で issue を作成し、mwe.ll と(もしあれば)reduced.ll、および compiler explorer へのリンクを共有してください。rust コードやそれへのリンクも自由に追加してください。

調査結果の文書化

"attempting to call an indirect active function whose runtime value is inactive" のような enzyme エラーは、これまで混乱を招いてきました。このような問題を調査した場合、完全な解決策が見つからなくても、調査結果の文書化を検討してください。その知見が enzyme 一般に関するものであり、rust での使用に固有でない場合は、メインの enzyme documentation に貢献することが、多くの場合最初の最善策です。関連する enzyme の GitHub issue で調査結果に言及したり、適切であればこれらのドキュメントへの更新を提案したりすることもできます。これにより、他の人がゼロから始めるのを防ぐのに役立ちます。

明確な再現例とドキュメントがあれば、enzyme 開発者があなたのバグを修正できる可能性が高まります。それが実現すると、rust コンパイラ内の enzyme サブモジュールが更新され、rust コードを微分できるようになるはずです。rust-ad の改善にご協力いただきありがとうございます。

rust コードを最小化する

最小の llvm-ir 再現例があることに加えて、依存関係のない最小の rust 再現例があると有用です。これにより、修正後にそれを CI のテストケースとして追加でき、将来のリグレッションを回避できます。

rust 再現例の最小化に役立つ解決策はいくつかあります。おそらく最も単純な自動化されたアプローチはこれです: cargo-minimize

それ以外にも、treereducehalfemptypicireny など、さまざまな代替手段があります。場合によっては creduce も使えます。

サポートされている RUSTFLAGS

デバッグやプロファイリング時の支援として、実験的な -Z autodiff rustc フラグ(RUSTFLAGS 経由で cargo に渡すことができます)のサポートを追加しました。これにより、rustc を再コンパイルすることなく Enzyme の挙動を変更できます。現在、autodiff では以下の値をサポートしています。

デバッグフラグ

PrintTA // TypeAnalysis 情報を出力する
PrintTAFn // 特定の関数の TypeAnalysis 情報を出力する
PrintAA // ActivityAnalysis 情報を出力する
Print // 生成および最適化中の微分済み関数を出力する
PrintPerf // AD 関連のパフォーマンス警告を出力する
PrintModBefore // AD を実行する直前に LLVM-IR モジュール全体を出力する
PrintModAfter // AD を実行した後、最適化の前に LLVM-IR モジュール全体を出力する
PrintModFinal // 最適化と AD を実行した後に LLVM-IR モジュール全体を出力する
LooseTypes // Type Info が不足している場合に中止する代わりに、不正確な導関数のリスクを許容する

LooseTypes は、Can not deduce type of <X> という Enzyme エラーを解消し、一部のコードを実行できるようにするために役立つことがよくあります。ただし、このフラグは不正確な勾配を引き起こす可能性が十分にあることに注意してください。さらに悪いことに、その勾配は特定の入力値では正しくても、別の入力値では正しくない可能性があります。そのため、このようなバグについては issue を作成し、バグが修正されるのを待つ間だけ、このフラグを一時的に使用してください。

ベンチマークフラグ

パフォーマンス実験とベンチマークのために、以下もサポートしています。

NoPostopt // AD の後に LLVM-IR モジュールを最適化しない
RuntimeActivity // Enzyme のランタイムアクティビティ機能を有効にする
Inline // LLVM のデフォルトを超えて、可能な限りインライン化を最大化するよう Enzyme に指示する

複数の autodiff 値は、カンマを区切り文字として使用して組み合わせることができます。

RUSTFLAGS="-Z autodiff=Enable,LooseTypes,PrintPerf" cargo +enzyme build

-Zautodiff=Enable を使用すると、autodiff を使用できるようになり、通常の rustc コンパイルパイプラインが更新されます。

  1. 選択したコンパイルパイプラインを実行します。リリースビルドを選択した場合、ベクトル化とループ展開を無効にします。
  2. 関数を微分します。
  3. モジュール全体に対して、選択したコンパイルパイプラインを再度実行します。この時点では、ベクトル化やループ展開は無効にしません。

自動微分のための TypeTrees

TypeTrees とは?

Enzyme のためのメモリレイアウト記述子です。型がメモリ内でどのように構造化されているかを Enzyme に正確に伝えることで、導関数を効率的に計算できるようにします。

構造

#![allow(unused)]
fn main() {
TypeTree(Vec<Type>)

Type {
    offset: isize,  // バイトオフセット(-1 = どこでも)
    size: usize,    // バイト単位のサイズ
    kind: Kind,     // Float、Integer、Pointer など
    child: TypeTree // ネストされた構造
}
}

例: fn compute(x: &f32, data: &[f32]) -> f32

入力 0: x: &f32

#![allow(unused)]
fn main() {
TypeTree(vec![Type {
    offset: -1, size: 8, kind: Pointer,
    child: TypeTree(vec![Type {
        offset: 0, size: 4, kind: Float,  // 単一の値: オフセット 0 を使用
        child: TypeTree::new()
    }])
}])
}

入力 1: data: &[f32]

#![allow(unused)]
fn main() {
TypeTree(vec![Type {
    offset: -1, size: 8, kind: Pointer,
    child: TypeTree(vec![Type {
        offset: -1, size: 4, kind: Float,  // -1 = すべての要素
        child: TypeTree::new()
    }])
}])
}

出力: f32

#![allow(unused)]
fn main() {
TypeTree(vec![Type {
    offset: 0, size: 4, kind: Float,  // 単一のスカラー: オフセット 0 を使用
    child: TypeTree::new()
}])
}

なぜ必要か?

  • Enzyme は LLVM IR から複雑な型レイアウトを推論できない
  • 低速なメモリパターン解析を防ぐ
  • ネストされた構造に対する正しい導関数計算を可能にする
  • どのバイトが微分可能で、どのバイトがメタデータかを Enzyme に伝える

Enzyme がこの情報を使って行うこと:

TypeTrees なし:

; Enzyme は汎用的な LLVM IR を見る:
define float @distance(ptr %p1, ptr %p2) {
; これらのポインターが何を指しているかを推測する必要がある
; すべてのメモリ操作を低速に解析する
; 最適化の機会を逃す可能性がある
}

TypeTrees あり:

define "enzyme_type"="{[-1]:Float@float}" float @distance(
    ptr "enzyme_type"="{[-1]:Pointer, [-1,0]:Float@float}" %p1, 
    ptr "enzyme_type"="{[-1]:Pointer, [-1,0]:Float@float}" %p2
) {
; Enzyme は正確な型レイアウトを知っている
; 効率的な導関数コードを直接生成できる
}

TypeTrees - オフセットと -1 の解説

Type の構造

#![allow(unused)]
fn main() {
Type {
    offset: isize, // この型が始まる場所
    size: usize,   // この型の大きさ
    kind: Kind,    // データの種類(Float、Int、Pointer)
    child: TypeTree // 内部に含まれるもの(ポインター/コンテナー用)
}
}

オフセット値

通常のオフセット(0、4、8 など)

構造体内の特定のバイト位置

#![allow(unused)]
fn main() {
struct Point {
    x: f32, // オフセット 0、サイズ 4
    y: f32, // オフセット 4、サイズ 4
    id: i32, // オフセット 8、サイズ 4
}
}

&Point の TypeTree(内部表現):

#![allow(unused)]
fn main() {
TypeTree(vec![
    Type { offset: 0, size: 4, kind: Float },   // バイト 0 の x
    Type { offset: 4, size: 4, kind: Float },   // バイト 4 の y
    Type { offset: 8, size: 4, kind: Integer }  // バイト 8 の id
])
}

LLVM を生成

"enzyme_type"="{[-1]:Pointer, [-1,0]:Float@float, [-1,4]:Float@float, [-1,8]:Integer, [-1,9]:Integer, [-1,10]:Integer, [-1,11]:Integer}"

オフセット -1(特殊: 「どこでも」)

「このパターンがすべての要素に対して繰り返される」ことを意味します

例 1: 直接配列 [f32; 100](ポインター間接参照なし)

#![allow(unused)]
fn main() {
TypeTree(vec![Type {
    offset: -1, // すべての位置
    size: 4,    // 各 f32 は 4 バイト
    kind: Float, // すべての要素は float
}])
}

LLVM を生成: "enzyme_type"="{[-1]:Float@float}"

例 1b: 配列参照 &[f32; 100](ポインター間接参照あり)

#![allow(unused)]
fn main() {
TypeTree(vec![Type {
    offset: -1, size: 8, kind: Pointer,
    child: TypeTree(vec![Type {
        offset: -1, // すべての配列要素
        size: 4,    // 各 f32 は 4 バイト
        kind: Float, // すべての要素は float
    }])
}])
}

LLVM を生成: "enzyme_type"="{[-1]:Pointer, [-1,-1]:Float@float}"

オフセット 0,4,8,12...396 を持つ 100 個の個別の Type を列挙する代わりです

例 2: スライス &[i32]

#![allow(unused)]
fn main() {
// スライスデータへのポインター
TypeTree(vec![Type {
    offset: -1, size: 8, kind: Pointer,
    child: TypeTree(vec![Type {
        offset: -1, // すべてのスライス要素
        size: 4,    // 各 i32 は 4 バイト
        kind: Integer
    }])
}])
}

LLVM を生成: "enzyme_type"="{[-1]:Pointer, [-1,-1]:Integer}"

例 3: 混合構造

#![allow(unused)]
fn main() {
struct Container {
    header: i64,        // オフセット 0
    data: [f32; 1000],  // オフセット 8、ただし要素には -1 を使用
}
}
#![allow(unused)]
fn main() {
TypeTree(vec![
    Type { offset: 0, size: 8, kind: Integer }, // header
    Type { offset: 8, size: 4000, kind: Pointer,
        child: TypeTree(vec![Type {
            offset: -1, size: 4, kind: Float // すべての配列要素
        }])
    }
])
}

重要な違い: 単一の値と配列

単一の値では、精度のためにオフセット 0 を使用します:

  • &f32 はオフセット 0 にちょうど 1 つの f32 値を持つ
  • -1(「どこでも」)を使用するよりも正確
  • 生成結果: {[-1]:Pointer, [-1,0]:Float@float}

配列では、効率のためにオフセット -1 を使用します:

  • &[f32; 100] は同じパターンが 100 回繰り返される
  • -1 を使用することで、100 個の個別のオフセットの列挙を避けられる
  • 生成結果: {[-1]:Pointer, [-1,-1]:Float@float}

ソースコード表現

このパートでは、ユーザーから受け取った生のソースコードを取り込み、 コンパイラが容易に扱えるさまざまな形式へ変換するプロセスについて説明します。 これらは_中間表現(IR)_と呼ばれます。

このプロセスは、コンパイラがユーザーの要求を理解することから始まります。 つまり、与えられたコマンドライン引数を解析し、何をコンパイルすべきかを判断します。 その後、コンパイラはユーザー入力を一連の IR へと変換します。それらは ユーザーが書いたものとは徐々に似ていないものになっていきます。

構文と AST

ソースコードを直接扱うのは、非常に不便でエラーを起こしやすい作業です。 そのため、他の処理を行う前に、生のソースコードを 抽象構文木(AST)に変換します。 実際には、これには多くの作業が伴います。 その中には、字句解析、構文解析マクロ展開名前解決、条件付き コンパイル、feature gate チェック、および AST検証が含まれます。 この章では、これらすべての手順を見ていきます。

特に、これらのタスクの間に常に明確な順序があるわけではありません。 たとえば、マクロ展開は、マクロとインポートの名前を解決するために名前解決に依存します。 また、構文解析にはマクロ展開が必要であり、そのマクロ展開ではさらにマクロの出力を構文解析する必要がある場合があります。

字句解析と構文解析

コンパイラが最初に行うことは、プログラム(UTF-8 Unicode テキスト)を受け取り、 文字列よりもコンパイラが扱いやすいデータ形式に変換することです。 これは、字句解析と構文解析という 2 つの段階で行われます。

  1. 字句解析 は文字列を受け取り、それをトークンのストリームに変換します。たとえば、 foo.bar + buz は、foo.bar+buz というトークンに変換されます。 これは rustc_lexer で実装されています。
  1. 構文解析 はトークンのストリームを受け取り、コンパイラが扱いやすい構造化された形式に変換します。 これは通常、抽象構文木 (AST) と呼ばれます。

AST

AST は、Span を使用して特定の AST ノードを元のソーステキストにリンクしながら、 メモリ内で Rust プログラムの構造を反映します。AST は rustc_ast で定義されており、トークンとトークンストリームの定義、 AST を変更するためのデータ構造/トレイト、およびコンパイラの他の AST 関連部分 (レキサーやマクロ展開など)の共有定義も含まれています。

AST 内のすべてのノードには、構造体などのトップレベル項目だけでなく、 個々の文や式も含めて、それぞれ独自の NodeId があります。NodeId は、 クレート内の AST ノードを一意に識別する識別子番号です。

しかし、これらはクレート内で絶対的なものなので、AST 内の単一のノードを追加または削除すると、 それ以降のすべての NodeId が変わってしまいます。これにより、 できるだけ変更されるものを少なくしたいインクリメンタルコンパイルにおいて、 NodeId はほとんど役に立たなくなります。

NodeId は、マクロ展開や名前解決など、AST を直接操作する rustc のすべての部分で使用されます (これらについては、今後の数章でさらに説明します)。

構文解析

パーサーは rustc_parse で定義されており、 レキサーへの高レベルインターフェースと、マクロ展開後に実行されるいくつかの検証ルーチンも含まれています。 特に、rustc_parse::parser にはパーサーの実装が含まれています。

パーサーへの主なエントリポイントは、さまざまな parse_* 関数や rustc_parse 内のその他の関数を介したものです。これらを使うと、 SourceFile(たとえば単一ファイル内のソース)をトークンストリームに変換し、 そのトークンストリームからパーサーを作成し、その後パーサーを実行して Crate(ルート AST ノード)を取得するといったことができます。

実行されるコピーの量を最小限に抑えるため、 LexerParser はどちらも、親の ParseSess に束縛されるライフタイムを持ちます。 これには、構文解析中に必要なすべての情報と、SourceMap 自体が含まれています。

構文解析中に、マクロ定義やマクロ呼び出しに遭遇する場合があることに注意してください。 これらは展開されるように脇に置いておきます(マクロ展開を参照)。 展開自体により、マクロの出力を構文解析する必要が生じることがあり、 それによって展開すべきさらなるマクロが明らかになることもあり、これが繰り返されます。

字句解析の詳細

字句解析のコードは、2 つのクレートに分かれています。

  • rustc_lexer クレートは、&str をトークンを構成するチャンクに分割する役割を担います。 レキサーを生成された有限状態機械として実装することは一般的ですが、 rustc_lexer のレキサーは手書きです。

  • Lexer は、rustc_lexerrustc 固有のデータ構造と統合します。 具体的には、rustc_lexer が返すトークンに Span 情報を追加し、 識別子をインターンします。

マクロ展開

Rustには非常に強力なマクロシステムがあります。 前の章では、 パーサーが(一時的なプレースホルダーを使って)展開対象のマクロを取り分ける方法を見ました。 この章では、それらのマクロを反復的に展開し、 未展開のマクロがない(またはコンパイルエラーになる)クレートの完全な 抽象構文木 (AST)を得るまでのプロセスについて説明します。

まず、マクロの出力を展開しASTへ統合するアルゴリズムについて説明します。 次に、ハイジーンデータがどのように収集されるかを見ていきます。 最後に、さまざまな種類のマクロの展開に関する詳細を見ていきます。

以下で説明するアルゴリズムやデータ構造の多くはrustc_expandにあり、 基礎的なデータ構造はrustc_expand::baseにあります。

また、cfgcfg_attrは他のマクロとは特別に扱われ、 rustc_expand::configで処理されます。

展開とAST統合

まず、展開はクレートレベルで行われます。 クレートの生のソースコードが与えられると、 コンパイラーは、すべてのマクロが展開され、すべての モジュールがインライン化された巨大なASTなどを生成します。このプロセスの主なエントリポイントは MacroExpander::fully_expand_fragmentメソッドです。 いくつかの例外を除き、 このメソッドはクレート全体に対して使用します(エッジケースの展開問題に関するより詳しい議論については、 下記の「先行展開」を参照してください)。

大まかに言うと、fully_expand_fragmentは反復で動作します。 未解決のマクロ呼び出し(つまり、まだ定義が見つかっていないマクロ)の キューを保持します。 キューからマクロを1つ選び、それを解決し、展開し、元に統合し直すことを繰り返し試みます。 反復で進捗がない場合、これはコンパイルエラーを表します。 以下がアルゴリズムです:

  1. 未解決マクロのqueueを初期化します。
  2. queueが空になるまで繰り返します(または進捗がなくなるまで。これはエラーです):
    1. 部分的に構築されたクレート内のインポートを、可能な限り解決します。
    2. 部分的に構築されたクレートから、マクロのInvocationfn風、属性、derive)をできるだけ多く収集し、それらをキューに追加します。
    3. 先頭の要素をデキューし、それを解決しようとします。
    4. 解決された場合:
      1. TokenStreamまたは ASTを消費し、(マクロの種類に応じて) TokenStreamまたはAstFragmentを生成する、マクロのエキスパンダー関数を実行します。 (TokenStreamTokenTreeのコレクションであり、 それぞれはトークン(区切り記号、識別子、リテラル)または 区切り付きグループ(()/[]/{}の内側にあるもの)です)。
        • この時点で、マクロ自体についてはすべて把握しており、 set_expn_dataを呼び出して、そのプロパティをグローバル データ内に埋めることができます。それはExpnIdに関連付けられた ハイジーンデータです(下記の ハイジーンを参照)。
      2. そのAST断片を、現在存在しているものの 部分的に構築されたASTに統合します。 ここは本質的に、「トークンのような塊」が サイドテーブルを伴う、適切で確定したASTになる場所です。 これは次のように行われます:
        • マクロがトークンを生成する場合(例: 手続き型マクロ)、それを ASTへパースしますが、これによりパースエラーが発生することがあります。
        • 展開中に、SyntaxContext(階層2)を作成します(下記の ハイジーンを参照)。
        • 次の2つのパスは、マクロから新たに展開されたすべてのAST断片に対して 順番に実行されます:
          • NodeIdInvocationCollectorによって割り当てられます。 これにより、この新しいAST部分から新しいマクロ呼び出しも収集され、 キューに追加されます。
          • DefCollector「Defパス」を作成し、 対応するDefIdを割り当て、縮約グラフも 構築します(リゾルバーの観点からモジュールに名前を配置します)。
      3. 1つのマクロを展開し、その出力を統合した後、 fully_expand_fragmentの次の反復に進みます。
    5. 解決されない場合:
      1. マクロをキューに戻します。
      2. 次の反復へ進みます…

エラーリカバリー

ある反復で進捗がない場合、コンパイルエラーに到達したことになります (例: 未定義のマクロ)。診断を生成する意図で、失敗(つまり 未解決のマクロやインポート)からのリカバリーを試みます。 失敗からのリカバリーは、未解決のマクロを ExprKind::Errへ展開することで行われ、最初のエラーを越えてコンパイルを続行できるようにし、 rustcが元の失敗だけでなく、より多くのエラーを報告できるようにします。

名前解決

ここでは名前解決が関与していることに注意してください。つまり、上記のアルゴリズムではインポートと マクロ名を解決する必要があります。 これは rustc_resolve::macros で行われます。ここではマクロパスを解決し、それらの 解決結果を検証し、さまざまなエラー(例: 「not found」「found, but it’s unstable」「expected x, found y」)を報告します。 ただし、この時点ではまだ他の名前の解決は試みません。 これは後で行われます。章: 名前解決 で説明します。

先行展開

先行展開 とは、マクロ呼び出し自体を展開する前に、 そのマクロ呼び出しの引数を展開することを意味します。 これは、リテラルを期待するいくつかの特殊な 組み込みマクロに対してのみ実装されています。これらのマクロの一部では、 引数を先に展開することで、ユーザー体験がより滑らかになります。 例として、次を考えてみましょう。

macro bar($i: ident) { $i }
macro foo($i: ident) { $i }

foo!(bar!(baz));

遅延展開では、まず foo! を展開します。 先行展開では、まず bar! を展開します。

先行展開は、Rust で一般に利用可能な機能ではありません。 先行展開をより一般的に実装することは難しいため、ユーザー体験のために、 いくつかの特殊な組み込みマクロに対して実装しています。 組み込みマクロは rustc_builtin_macros に実装されており、標準ライブラリのインポートの注入や テストハーネスの生成など、いくつかの他の 初期コード生成機能もそこにあります。 AST フラグメントを構築するための追加のヘルパーが rustc_expand::build にいくつかあります。 先行展開は一般に、遅延(通常)展開が行う処理のサブセットを実行します。 これは、通常のようにクレート全体に対してではなく、クレートの一部のみに対して fully_expand_fragment を呼び出すことで行われます。

その他のデータ構造

展開と統合に関与する、その他の注目すべきデータ構造をいくつか挙げます。

  • ResolverExpand - クレートの依存関係を分断するために使われる trait。 これにより、rustc_resolve や それ以外のほぼすべてが rustc_ast に依存しているにもかかわらず、resolver サービスを rustc_ast で使用できます。
  • ExtCtxt/ExpansionData - さまざまな中間的な展開インフラストラクチャデータを保持します。
  • Annotatable - 属性の対象になれる AST の一部です。型やパターンを除けば AstFragment とほぼ同じものです。型やパターンは マクロによって生成できますが、属性で注釈付けすることはできません。
  • MacResult - 「多相的な」AST フラグメントです。これは、その AstFragmentKind に応じて 別の AstFragment に変換できるものです(つまり、アイテム、 式、パターンなど)。

衛生性と階層

C/C++ のプリプロセッサマクロを使ったことがあれば、 厄介でデバッグが難しい落とし穴がいくつかあることをご存じでしょう。 たとえば、次の C コードを考えてみてください。

#define DEFINE_FOO struct Bar {int x;}; struct Foo {Bar bar;};

// そして、別のどこかで
struct Bar {
    ...
};

DEFINE_FOO

多くの人はこのような C の書き方を避けます。そして、それには十分な理由があります。これはコンパイルできません。 マクロによって定義された struct Bar が、コード内で定義された struct Bar と名前衝突します。 次の例も考えてみてください。

#define DO_FOO(x) {\
    int y = 0;\
    foo(x, y);\
    }

// そして別の場所で
int y = 22;
DO_FOO(y);

問題がわかりますか? 私たちは foo(22, 0) という呼び出しを生成したかったのですが、実際には マクロが独自の y を定義したため、foo(0, 0) になってしまいました。

これらはいずれも マクロ衛生性 の問題の例です。 衛生性 は、マクロ内 で定義された名前をどのように扱うかに関係します。 特に、衛生的なマクロシステムは、マクロ内で導入された名前に起因するエラーを防ぎます。 Rust のマクロは衛生的であり、上記のような種類のバグを書けないようになっています。

大まかに言うと、Rust コンパイラ内の衛生性は、 名前が導入され、使用されるコンテキストを追跡することで実現されています。 その後、そのコンテキストに基づいて名前を曖昧さなく区別できます。 マクロシステムの将来の反復では、 そのコンテキストを使用するためのより大きな制御をマクロ作者に提供する予定です。 たとえば、 マクロ作者は、マクロが呼び出されたコンテキストに新しい名前を導入したい場合があります。 あるいは、マクロ作者は マクロ内でのみ使用する変数を定義している場合があります(つまり、その変数はマクロの外から見えてはなりません)。

コンテキストは AST ノードに付加されます。 マクロによって生成されたすべての AST ノードにはコンテキストが付加されています。 さらに、いくつかの脱糖された構文のように、コンテキストが 付加されている他のノードがある場合もあります(マクロ展開されていないノードは、 後述するように、単に「root」コンテキストを持つものと見なされます)。 コンパイラ全体を通じて、コード位置を参照するために rustc_span::Span を使用します。 後で見るように、この構造体にも衛生性情報が付加されています。

マクロ呼び出しと定義はネストできるため、 ノードの構文コンテキストは階層でなければなりません。 たとえば、あるマクロを展開し、生成された出力の中に 別のマクロ呼び出しや定義がある場合、構文 コンテキストはそのネストを反映する必要があります。

しかし実際には、目的に応じて追跡したいコンテキストには いくつかの種類があることがわかっています。 したがって、クレートの衛生性情報を構成する展開階層は、 1 つではなく 3 つ あります。

これらすべての階層には、展開の連鎖内の個々の 要素を識別するための、何らかの「マクロ ID」が必要です。 この ID が ExpnId です。 すべてのマクロは整数 ID を受け取り、新しいマクロ 呼び出しを発見するにつれて 0 から連続して割り当てられます。 すべての階層は ExpnId::root から始まり、これはそれ自身を親に持ちます。 rustc_span::hygiene クレートには、衛生性に関連するすべてのアルゴリズム (Resolver::resolve_crate_root 内のいくつかのハックを除く)と、 グローバルデータに保持される衛生性および展開に関連する構造体が含まれています。

実際の階層は HygieneData に格納されます。 これは、任意の Ident からコンテキストなしでアクセスできる、衛生性と展開情報を含む グローバルデータです。

展開順序の階層

最初の階層は、展開の順序、つまり、あるマクロ呼び出しが 別のマクロの出力内にある場合を追跡します。

ここで、階層内の子は「最も内側」のトークンになります。 ExpnData 構造体自体には、グローバルデータを通じて利用できる マクロ定義とマクロ呼び出しの両方のプロパティの一部が含まれます。 ExpnData::parent は、この階層における子から親へのリンクを追跡します。

例:

macro_rules! foo { () => { println!(); } }

fn main() { foo!(); }

このコードでは、最終的に生成される AST ノードは root -> id(foo) -> id(println) という階層を持ちます。

マクロ定義の階層

2 つ目の階層は、マクロ定義の順序、つまり、あるマクロを展開しているときに 別のマクロ定義がその出力内に現れる場合を追跡します。 これは少し扱いが難しく、他の 2 つの階層よりも複雑です。

SyntaxContext は、この階層内のチェーン全体を ID によって表します。 SyntaxContextData には、指定された SyntaxContext に関連付けられたデータが含まれます。主に、そのチェーンをさまざまな方法でフィルタリングした結果のキャッシュです。 SyntaxContextData::parent はここでの子から親への リンクであり、SyntaxContextData::outer_expns はチェーン内の個々の要素です。 「連結演算子」は、コンパイラコード内の SyntaxContext::apply_mark です。

上で述べた Span は、実際には コード位置と SyntaxContext のコンパクトな表現にすぎません。 同様に、Ident はインターン化された Symbol + Span(つまり、インターン化された文字列 + 衛生性データ)にすぎません。

組み込みマクロについては、次のコンテキストを使用します: SyntaxContext::empty().apply_mark(expn_id)。また、そのようなマクロは 階層のルートで定義されているとみなされます。 proc macro についても同じことを行います。これは、まだクロスクレート衛生性を実装していないためです。

トークンがマクロによって生成される前にコンテキスト X を持っていた場合、 マクロによって生成された後はコンテキスト X -> macro_id を持ちます。 いくつかの例を示します:

例 0:

macro m() { ident }

m!();

ここで、最初はコンテキスト SyntaxContext::root を持っていた ident は、 m によって生成された後、コンテキスト ROOT -> id(m) を持ちます。

例 1:

macro m() { macro n() { ident } }

m!();
n!();

この例では、ident は最初にコンテキスト ROOT を持ち、その後、最初の展開後に ROOT -> id(m)、 さらに ROOT -> id(m) -> id(n) になります。

例 2:

これらのチェーンは最後の要素だけで完全に決定されるわけではないことに注意してください。 言い換えると、ExpnIdSyntaxContext と同型ではありません。

macro m($i: ident) { macro n() { ($i, bar) } }

m!(foo);

すべての展開後、foo はコンテキスト ROOT -> id(n) を持ち、bar はコンテキスト ROOT -> id(m) -> id(n) を持ちます。

現在、マクロ定義を追跡するためのこの階層は、いわゆる “context transplantation hack” の対象になっています。現代的な(つまり実験的な) マクロは、レガシーな “Macros By Example” (MBE) システムよりも強い衛生性を持っており、その結果、両者の間で奇妙な相互作用が発生する可能性があります。 このハックは、現時点で物事が「そのまま動作する」ようにすることを意図しています。

呼び出し箇所の階層

3 つ目で最後の階層は、マクロ呼び出しの位置を追跡します。

この階層では、ExpnData::call_sitechild -> parent リンクです。

例を示します:

macro bar($i: ident) { $i }
macro foo($i: ident) { $i }

foo!(bar!(baz));

最終的な出力内の baz AST ノードについて、展開順序の階層は ROOT -> id(foo) -> id(bar) -> baz であり、一方で呼び出し箇所の階層は ROOT -> baz です。

マクロバックトレース

マクロバックトレースは、rustc_span 内で、rustc_span::hygiene の衛生性機構を使って 実装されています。

マクロ出力の生成

上では、マクロの出力がクレートの AST にどのように統合されるかを見ました。 また、クレートの衛生性データがどのように生成されるかも見ました。 しかし、実際にはどのようにマクロの出力を生成するのでしょうか。 それはマクロの種類によって異なります。

Rust には 2 種類のマクロがあります:

  1. macro_rules! マクロ(別名 “Macros By Example” (MBE))、および、
  2. 手続き型マクロ(proc macros)。custom derives を含みます。

パース段階では、通常の Rust パーサーは マクロとその呼び出しの内容を取り分けておきます。 後で、これらのコード部分を使用してマクロが展開されます。 ここにある重要なデータ構造/インターフェイス:

  • SyntaxExtension - 低水準化されたマクロ表現で、TokenStream または AST を別の TokenStream または AST へ変換する展開関数と、安定性やマクロ内で許可される 不安定機能のリストなどの追加データを含みます。
  • SyntaxExtensionKind - 展開関数は複数の異なるシグネチャを持つことがあります (1 つのトークンストリーム、2 つのトークンストリーム、AST の一部などを受け取ります)。 これはそれらを列挙する enum です。
  • BangProcMacro/TTMacroExpander/AttrProcMacro/MultiItemModifier - 展開関数のシグネチャを表す trait です。

例によるマクロ

MBE には、Rust パーサーとは異なる独自のパーサーがあります。 マクロが展開されるとき、マクロを解析して展開するために MBE パーサーを呼び出すことがあります。 さらに MBE パーサーは、マクロ呼び出しの内容を解析している間に メタ変数(例: $my_expr)を束縛する必要がある場合、Rust パーサーを呼び出すことがあります。 マクロ展開のコードは compiler/rustc_expand/src/mbe/ にあります。

macro_rules! printer {
    (print $mvar:ident) => {
        println!("{}", $mvar);
    };
    (print twice $mvar:ident) => {
        println!("{}", $mvar);
        println!("{}", $mvar);
    };
}

ここで $mvarメタ変数 と呼ばれます。 通常の変数とは異なり、メタ変数は 実行時 に値へ束縛されるのではなく、コンパイル時トークン のツリーへ束縛されます。 トークン は文法の単一の「単位」であり、 識別子(例: foo)や句読点(例: =>)などです。EOF のような 特殊なトークンもあり、これはそれ自体でそれ以上トークンがないことを示します。 対応する括弧のような文字(()[]、および {})から 生じるトークンツリーがあります。これらは開きと閉じ、およびその間のすべてのトークンを含みます (Rust では括弧のような文字が対応している必要があります)。 マクロ展開がソースファイルの生のバイト列ではなく トークンストリームに対して動作することで、多くの複雑さが抽象化されます。 マクロ展開器(およびコンパイラの他の多くの部分)は、 コード内の構文構造の正確な行と列ではなく、 コード内でどの構造が使用されているかを考慮します。 トークンを使用することで、どこ かを気にせずに かに注目できます。 トークンの詳細については、本書の Parsing の章を参照してください。

printer!(print foo); // `foo` は変数です

マクロ呼び出しを構文木 println!("{}", foo) へ展開し、その後その構文木を Display::fmt の呼び出しへ展開するプロセスは、マクロ展開 の一般的な例の 1 つです。

MBE パーサー

マクロパーサーによって行われる MBE 展開には 2 つの部分があります:

  1. 定義の解析、および、
  2. 呼び出しの解析。

MBE パーサーは、Earley parsing algorithm と精神的に似たアルゴリズムを使用するため、 非決定性有限オートマトン(NFA)に基づく正規表現パーサーのようなものと考えています。 マクロパーサーは compiler/rustc_expand/src/mbe/macro_parser.rs で定義されています。

マクロパーサーのインターフェイスは次のとおりです(これは少し簡略化されています):

fn parse_tt(
    &mut self,
    parser: &mut Cow<'_, Parser<'_>>,
    matcher: &[MatcherLoc]
) -> ParseResult

マクロパーサーでは次の項目を使用します:

  • parser 変数は、トークンストリームと解析セッションを含む通常の Rust パーサーの状態への参照です。 トークンストリームは、MBE パーサーに解析を依頼しようとしているものです。 生のトークンストリームを消費し、 メタ変数から対応するトークンツリーへの束縛を出力します。 解析セッションは、パーサーエラーを報告するために使用できます。
  • matcher 変数は、トークンストリームと照合したい MatcherLoc のシーケンスです。 これらは、照合の前にマクロ定義内の元のトークンツリーから変換されます。

正規表現パーサーの類推では、トークンストリームが入力であり、それを matcher によって定義されたパターンと 照合しています。 ここでの例を使うと、 トークンストリームは例の呼び出し print foo の内部を含むトークンのストリームであり、 matcher はトークン(ツリー)のシーケンス print $mvar:ident である可能性があります。

パーサーの出力は ParseResult で、3 つのケースのうちどれが発生したかを示します:

  • 成功: トークンストリームが指定された matcher と一致し、 メタ変数から対応するトークンツリーへの束縛が生成されています。
  • 失敗: トークンストリームが matcher と一致せず、 “No rule expected token …” のようなエラーメッセージになります。
  • エラー: パーサー内で 何らかの致命的なエラーが発生しています。 たとえば、複数のパターン一致がある場合にこれが発生します。これは、 マクロが曖昧であることを示すためです。

完全なインターフェイスは こちら で定義されています。

マクロパーサーは、通常の正規表現パーサーとほぼまったく同じことを行いますが、 1 つ例外があります。identblockexpr などの 異なる種類のメタ変数を解析するために、マクロパーサーは通常の Rust パーサーをコールバックする必要があります。

マクロ定義を解析するコードは compiler/rustc_expand/src/mbe/macro_rules.rs にあります。 マクロパーサーの実装の詳細については、 compiler/rustc_expand/src/mbe/macro_parser.rs のコメントを参照してください。

この例では、呼び出しから得られたトークンストリーム print foo を、 マクロ定義内のルールから先に抽出した matcher print $mvar:ident および print twice $mvar:ident と 照合しようとします。 マクロパーサーが現在の matcher 内で 非終端記号(例: $mvar:ident)に一致する必要がある場所に到達すると、通常の Rust パーサーをコールバックして その非終端記号の内容を取得します。 この場合、Rust パーサーは ident トークンを探し、それを見つけて(foo)マクロパーサーへ返します。 その後、マクロパーサーは解析を続行します。

さまざまなルールの matcher のうち、正確に 1 つだけが呼び出しに一致する必要があることに注意してください。 複数の一致がある場合、解析は曖昧です。一方、まったく一致がない場合は、構文エラーです。 ちょうど1つのルールが一致すると仮定すると、マクロ展開は次にそのルールの右辺を転写し、 左辺との照合時に捕捉した任意の一致の値を置換します。

手続き的マクロ

手続き的マクロも解析中に展開されます。 ただし、コンパイラ内にパーサーを持つのではなく、proc macro はカスタムの サードパーティ製クレートとして実装されます。 コンパイラは proc macro クレートと、 それらの中で特別に注釈された関数(つまり proc macro そのもの)をコンパイルし、 トークンのストリームを渡します。 proc macro はその後、トークンストリームを変換し、 新しいトークンストリームを出力できます。これは AST に合成されます。

proc macro で使用されるトークンストリーム型は_安定_しているため、rustc はそれを内部では使用しません。 コンパイラの(不安定な)トークンストリームは rustc_ast::tokenstream::TokenStream で定義されています。 これは rustc_expand::proc_macrorustc_expand::proc_macro_server で、 安定版の proc_macro::TokenStream へ、またその逆へ変換されます。 Rust ABI は現在不安定であるため、この変換には C ABI を使用します。

カスタム導出

カスタム導出は特殊な種類の proc macro です。

Macros By Example と Macros 2.0

MBE システムを改善するための、古く、ほとんど文書化されていない取り組みがあります。 これにより、より多くの hygiene 関連機能、よりよいスコープと可視性の ルールなどを与えます。内部的には、これは現在の MBE と同じ仕組みを使用しますが、 いくつかの追加の構文糖衣を備えており、名前空間内に存在することが許可されています。

名前解決

前の章では、すべてのマクロが展開された状態で 抽象構文木 (AST) がどのように構築されるかを見ました。 また、それを行うには、インポートとマクロ名を解決するために ある程度の名前解決が必要になることも見ました。 この章では、実際にそれがどのように行われるのか、そしてそれ以上の内容を示します。

実際には、マクロ展開中に完全な名前解決を行うわけではありません。その時点では インポートとマクロだけを解決します。 これは、そもそも何を展開するのかを知るために必要です。 その後、AST 全体が得られた後で、クレート内のすべての名前を解決するために 完全な名前解決を行います。 これは rustc_resolve::late で行われます。 マクロ展開中とは異なり、この後期展開では、新しい名前を追加できないため、 名前の解決は一度だけ試みれば十分です。 名前の解決に失敗した場合、それはコンパイラーエラーになります。

名前解決は複雑です。さまざまな名前空間(例: マクロ、値、型、ライフタイム)があり、名前は異なる(ネストした) スコープで有効になる場合があります。 また、名前の種類が異なれば解決の失敗の仕方も異なり、 スコープが異なれば失敗の起こり方も異なります。 たとえば、モジュールスコープでは、 失敗とは、そのモジュール内に未展開のマクロも未解決の glob インポートも 存在しないことを意味します。 一方、関数本体のスコープでは、失敗するには、対象の名前が 現在のブロック、すべての外側のスコープ、およびグローバルスコープに 存在しないことが必要です。

基本

プログラム内では、変数、型、関数などを名前で参照します。 これらの名前は常に一意であるとは限りません。 たとえば、次の有効な Rust プログラムを見てください。

#![allow(unused)]
fn main() {
type x = u32;
let x: x = 1;
let y: x = 2;
}

3 行目の x が型(u32)なのか値(1)なのかを、どうすれば判断できるのでしょうか? これらの衝突は名前解決中に解決されます。 この具体的なケースでは、 名前解決により、型名と変数名は別々の名前空間に存在するため、 共存できるものとして定義されます。

Rust における名前解決は 2 段階のプロセスです。 第 1 段階は macro 展開中に実行され、 モジュールのツリーを構築し、インポートを解決します。 マクロ展開と名前解決は、ResolverAstLoweringExt トレイトを介して 相互に通信します。

第 2 段階への入力は、入力ファイルをパースし、 macros を展開することで生成された構文木です。 この段階では、ソース内のすべての名前から、 その名前が導入された関連箇所へのリンクが生成されます。 また、タイプミスの候補、インポートすべきトレイト、 未使用項目に関する lint など、有用なエラーメッセージも生成します。

第 2 段階(Resolver::resolve_crate)が正常に実行されると、 コンパイルの残りの部分が、現在存在する名前について問い合わせるために使用できる 一種のインデックスが作成されます (hir::lowering::Resolver インターフェイスを通じて)。

名前解決は rustc_resolve クレート内にあり、その大部分は lib.rs に、いくつかのヘルパーやシンボル型固有のロジックは他のモジュールにあります。

名前空間

異なる種類のシンボルは異なる名前空間に存在します。たとえば、型は 変数と衝突しません。 通常これは起こりません。なぜなら、変数は 小文字で始まり、型は大文字で始まるからです。しかし、これは単なる 慣習です。 次は、コンパイル可能な(警告付きの)合法な Rust コードです。

#![allow(unused)]
fn main() {
type x = u32;
let x: x = 1;
let y: x = 2; // ほら、ここでも x はまだ型です。
}

これに対処し、またこれらの名前空間ごとに少し異なるスコープ規則に対応するため、 リゾルバーはそれらを分離したままにし、それぞれに対して別々の構造を構築します。

言い換えると、コードが名前空間について述べるとき、それはモジュール階層を 意味しているのではなく、型、値、マクロの対比を意味しています。

スコープと rib

名前はソースコード内の特定の領域でのみ可視です。 これにより階層構造が形成されますが、 必ずしも単純なものとは限りません。あるスコープが 別のスコープの一部であるからといって、外側のスコープで可視な名前が 内側のスコープでも可視であるとは限らず、同じものを参照するとも限りません。

これに対処するために、コンパイラーは Rib という概念を導入します。 これはスコープの抽象化です。 可視な名前の集合が変わる可能性があるたびに、 新しい Rib がスタックにプッシュされます。 これが起こり得る場所には、たとえば次のものが含まれます。

  • 明らかな場所、つまりブロックを囲む波括弧、関数の境界、 モジュール。
  • let 束縛の導入。これは同じ名前を持つ別の束縛をシャドーイングできます。
  • マクロ展開の境界。これはマクロ衛生に対処するためです。

名前を検索するとき、ribs のスタックは最も内側から 外側へと走査されます。 これにより、その名前の最も近い意味(他の何かによって シャドーイングされていないもの)を見つけやすくなります。 外側の Rib への遷移は、 どの名前が使用可能かにも影響する場合があります。ネストした関数(クロージャではありません)がある場合、 内側の関数は外側の関数のパラメーターやローカル束縛にアクセスできません。 通常のスコープ規則では可視であるはずだとしてもです。 例を示します。

#![allow(unused)]
fn main() {
fn do_something<T: Default>(val: T) { // <- 型と値の両方で新しい rib (1)
    // `val` はアクセス可能で、ヘルパー関数も同様
    // `T` はアクセス可能
   let helper = || { // ブロック上の新しい rib (2)
        // ここでは `val` はアクセス可能
    }; // (2) の終了、`helper` 上の新しい rib (3)
    // `val` はアクセス可能、`helper` 変数が `helper` 関数をシャドーイングする
    fn helper() { // <- 型と値の両方で新しい rib (4)
        // ここでは `val` はアクセスできない、(4) はローカルに対して透過的ではない
        // ここでは `T` はアクセスできない
    } // (4) の終了
    let val = T::default(); // 新しい rib (5)
    // ここでの `val` は変数であり、パラメーターではない
} // (5)、(3)、(1) の終了
}

異なる名前空間の規則は少し異なるため、各名前空間は 他の名前空間と並行して構築される、独立した専用の Rib スタックを持ちます。 さらに、ローカルラベル(例: ループやブロックの名前)用の Rib スタックもありますが、 これはそれ自体では完全な名前空間ではありません。

全体的な戦略

クレート全体の名前解決を実行するために、構文木を トップダウンに走査し、遭遇したすべての名前を解決します。 これはほとんどの種類の名前に対して機能します。 なぜなら、名前が使用される時点では、それはすでに Rib 階層に導入されているからです。

これにはいくつか例外があります。 項目は少し扱いが難しいです。なぜなら、遭遇する前であっても 使用できるため、各ブロックはまず項目をスキャンして その Rib を埋める必要があるからです。

さらに問題となるものとして、再帰的な不動点解決を必要とするインポートや、 コードの残りを処理する前に解決して展開する必要があるマクロがあります。

したがって、解決は複数の段階で実行されます。

投機的なクレート読み込み

有用なエラーを提供するため、rustc はパスが見つからない場合に、それらをスコープにインポートすることを提案します。 これはどのように行われるのでしょうか? すべてのクレートのすべてのモジュールを調べ、候補となる一致を探します。 これには、まだ読み込まれていないクレートさえ含まれます!

まだ読み込まれていないインポート候補を含めるためにクレートを積極的に読み込むことは、投機的なクレート読み込み と呼ばれます。これは、その過程で発生したエラーを報告すべきではないためです。読み込みを決定したのはユーザーではなく rustc_resolve です。 これを行う関数は lookup_import_candidates で、rustc_resolve::diagnostics にあります。

投機的な読み込みとユーザーによって開始された読み込みを区別するために、rustc_resolverecord_used パラメーターを受け渡します。この値は、読み込みが投機的な場合は false です。

TODO: #16

これは、コードを学ぶ最初の段階の結果です。 間違いなく不完全で、詳細も十分ではありません。 また、場所によっては不正確な可能性もあります。 それでも、おそらくそこで何が起こっているかについての有用な最初の道しるべにはなります。

  • それは具体的に何にリンクし、それはコンパイルの後続ステージでどのように公開され、消費されるのか?
  • 誰がそれを呼び出し、実際にどのように使用されるのか。
  • これはパスであり、その結果だけが使用されるのか、それともインクリメンタルに計算できるのか?
  • 全体的な戦略の説明がやや曖昧。
  • Rib という名前はどこから来たのか?
  • これは独自のテストを持っているのか、それとも何らかの e2e テストの一部としてのみテストされているのか?

属性

属性には、不活性(または組み込み)と能動的非組み込み)の 2 種類があります。

組み込み/不活性属性

これらの属性は、コンパイラ自体の compiler/rustc_feature/src/builtin_attrs.rs で定義されています。

例としては、#[allow]#[macro_use] があります。

これらの属性には、いくつかの重要な特徴があります。

  • これらは常にスコープ内にあり、通常のパスベースの名前解決には参加しません。
  • これらは名前を変更できません。たとえば、use allow as foo はコンパイルされますが、#[foo] と書くと エラーが発生します。
  • これらは「不活性」です。つまり、マクロ展開コードによってそのまま残されます。 その結果、何らかの動作は、コンパイラがそれらの存在を明示的に確認することによって生じます。 たとえば、lint 関連のコードは、属性自体の展開から動作が生じるのではなく、#[allow]#[warn]#[deny]、および #[forbid] を明示的に確認します。

「非組み込み」/「能動的」属性

これらの属性は、クレート(標準ライブラリ、または proc-macro クレート)によって定義されます。

重要: #[derive] など、多くの非組み込み属性は、依然として Rust のコア言語の一部と見なされます。しかし、標準ライブラリに対応する定義があるため、これらは 「組み込み属性」とは呼ばれません

非組み込み属性の定義には、2 つの形式があります。

  1. proc-macro 属性。proc-macro クレート内で #[proc_macro_attribute] が付与された関数によって定義されます。
  2. AST ベースの属性。標準ライブラリで定義されます。これらの属性には、library/core/src/macros/mod.rs のような場所で定義された特別な「スタブ」 マクロがあります。

これらの定義は、マクロが通常のパスベースの名前解決に参加できるようにするために存在します。つまり、他の項目定義と同様に、 インポート、再エクスポート、名前変更ができます。しかし、定義の本体は空です。代わりに、 そのマクロには #[rustc_builtin_macro] 属性が付与されており、これは rustc_builtin_macros 内の対応する関数を実行するようコンパイラに指示します。

すべての非組み込み属性には、次の特徴があります。

  • 他のすべての定義(例: 構造体)と同様に、インポートによってスコープに取り込まれなければなりません。 多くの標準ライブラリ属性はプレリュードに含まれています。これが、#[derive] と書いても インポートなしで動作する理由です。
  • これらはマクロ展開に参加します。マクロの実装は、属性の対象を変更せずに残すことも、 対象を変更することも、新しい AST ノードを生成することも、対象を完全に削除することもあります。

#[test] 属性

多くの Rust プログラマーは、#[test] と呼ばれる組み込み属性を利用しています。必要なのは、次のように関数にマークを付け、いくつかのアサーションを含めることだけです。

#[test]
fn my_test() {
    assert!(2+2 == 4);
}

このプログラムを rustc --test または cargo test を使用してコンパイルすると、このテスト関数や他の任意のテスト関数を実行できる実行可能ファイルが生成されます。このテスト方法により、テストを自然な形でコードのそばに配置できます。テストをプライベートモジュール内に置くことさえできます。

mod my_priv_mod {
    fn my_priv_func() -> bool {}

    #[test]
    fn test_priv_func() {
        assert!(my_priv_func());
    }
}

したがって、プライベート項目は、何らかの外部テスト装置に公開する方法を気にすることなく、簡単にテストできます。これは Rust におけるテストのエルゴノミクスにとって重要です。しかし意味論的には、これはかなり奇妙です。見えないはずのこれらのテストを、何らかの main 関数はどのように呼び出すのでしょうか? rustc --test は正確には何をしているのでしょうか?

#[test] は、コンパイラの rustc_ast 内部で構文変換として実装されています。本質的には、クレートを 3 つのステップで書き換える高度な macro です。

ステップ 1: 再エクスポート

前述のとおり、テストはプライベートモジュール内に存在できるため、既存のコードを壊さずに、それらを main 関数に公開する方法が必要です。そのために、rustc_ast__test_reexports というローカルモジュールを作成し、テストを再帰的に再エクスポートします。この展開により、上記の例は次のように変換されます。

mod my_priv_mod {
    fn my_priv_func() -> bool {}

    pub fn test_priv_func() {
        assert!(my_priv_func());
    }

    pub mod __test_reexports {
        pub use super::test_priv_func;
    }
}

これで、テストには my_priv_mod::__test_reexports::test_priv_func としてアクセスできます。より深いモジュール構造では、__test_reexports はテストを含むモジュールを再エクスポートするため、a::b::my_test にあるテストは a::__test_reexports::b::__test_reexports::my_test になります。このプロセスはかなり安全に見えますが、既存の __test_reexports モジュールが存在する場合はどうなるのでしょうか?答えは、何も起こりません。

説明するには、Rust の 抽象構文木識別子 をどのように表現するかを理解する必要があります。すべての関数、変数、モジュールなどの名前は文字列として格納されるのではなく、不透明な Symbol として格納されます。これは本質的に、各識別子に対する ID 番号です。コンパイラは、必要に応じて(構文エラーを出力するときなど)Symbol の人間が読める名前を復元できるように、別個のハッシュテーブルを保持しています。コンパイラが __test_reexports モジュールを生成するとき、その識別子に対して新しい Symbol を生成するため、コンパイラ生成の __test_reexports は手書きのものと名前を共有することはあっても、Symbol を共有することはありません。この手法はコード生成時の名前衝突を防ぎ、Rust の macro 衛生性の基盤となっています。

ステップ 2: ハーネスの生成

これでテストにクレートのルートからアクセスできるようになったので、rustc_ast を使用してそれらに対して何かを行う必要があります。これは次のようなモジュールを生成します。

#[main]
pub fn main() {
    extern crate test;
    test::test_main_static(&[&path::to::test1, /*...*/]);
}

ここで、path::to::test1test::TestDescAndFn 型の定数です。

この変換は単純ですが、テストが実際にどのように実行されるかについて多くの洞察を与えてくれます。テストは配列に集約され、test_main_static というテストランナーに渡されます。TestDescAndFn が正確には何であるかについては後で戻りますが、現時点で重要なのは、Rust core の一部であり、テスト用のすべてのランタイムを実装する test というクレートが存在するということです。test のインターフェイスは不安定であるため、それとやり取りする唯一の安定した方法は #[test] マクロを介することです。

ステップ 3: テストオブジェクトの生成

以前に Rust でテストを書いたことがあるなら、テスト関数で利用できるいくつかの任意属性に馴染みがあるかもしれません。たとえば、テストがパニックを引き起こすことを期待する場合、テストに #[should_panic] を注釈できます。これは次のようになります。

#[test]
#[should_panic]
fn foo() {
    panic!("intentional");
}

これは、テストが単なる単純な関数以上のものであり、設定情報も持っていることを意味します。test はこの設定データを TestDesc という struct にエンコードします。クレート内の各テスト関数について、rustc_ast はその属性を解析し、TestDesc インスタンスを生成します。その後、TestDesc とテスト関数を、予測どおりの名前である TestDescAndFn struct に結合し、test_main_static はそれを操作します。 あるテストについて、生成される TestDescAndFn インスタンスは次のようになります。

self::test::TestDescAndFn{
  desc: self::test::TestDesc{
    name: self::test::StaticTestName("foo"),
    ignore: false,
    should_panic: self::test::ShouldPanic::Yes,
    allow_fail: false,
  },
  testfn: self::test::StaticTestFn(||
    self::test::assert_test_result(::crate::__test_reexports::foo())),
}

これらのテストオブジェクトの配列を構築したら、ステップ 2 で生成されたハーネスを介してテストランナーに渡されます。

生成されたコードの調査

nightlyrustc には、macro 展開後のモジュールソースを出力するために使用できる unpretty という不安定なフラグがあります。

$ rustc my_mod.rs -Z unpretty=hir

Rust におけるパニック

ステップ 1: panic! マクロの呼び出し。

実際には、パニックマクロは 2 つあります。1 つは core で定義され、もう 1 つは std で定義されています。 これは、core 内のコードがパニックできるという事実によるものです。 corestd より前にビルドされますが、 パニックが core に由来するか std に由来するかにかかわらず、実行時には同じ仕組みを使うようにしたいのです。

core における panic! の定義

corepanic! マクロは、最終的に次の呼び出しを行います(library/core/src/panicking.rs 内):

#![allow(unused)]
fn main() {
// NOTE この関数は FFI 境界を越えません。これは Rust から Rust への呼び出しです
extern "Rust" {
    #[lang = "panic_impl"]
    fn panic_impl(pi: &PanicInfo<'_>) -> !;
}

let pi = PanicInfo::new(
    &fmt,
    Location::caller(),
    /* can_unwind */ true,
    /* force_no_backtrace */ false,
);
unsafe { panic_impl(&pi) }
}

これを実際に解決する処理は、複数の間接層を経由します。

  1. compiler/rustc_hir/src/weak_lang_items.rs では、panic_impl は シンボル rust_begin_unwind を持つ「weak lang item」として宣言されています。 これは rustc_hir_analysis/src/collect.rs で、実際のシンボル名を rust_begin_unwind に設定するために使われます。

    panic_implextern "Rust" ブロック内で宣言されていることに注意してください。 つまり、core は rust_begin_unwind という名前の外部シンボルを呼び出そうとします (リンク時に解決されます)。

  2. library/std/src/panicking.rs には、次の定義があります。

#![allow(unused)]
fn main() {
/// core クレートからのパニックのエントリポイント(`panic_impl` lang item)。
#[cfg(not(any(test, doctest)))]
#[panic_handler]
pub fn panic_handler(info: &core::panic::PanicInfo<'_>) -> ! {
    ...
}
}

特別な panic_handler 属性は、compiler/rustc_passes/src/lang_items.rs 経由で解決されます。 extract_ast 関数は、panic_handler 属性を panic_impl lang item に変換します。

これで、std 内に一致する panic_handler lang item が存在することになります。 この関数は、core 内の extern { fn panic_impl } 定義と同じプロセスを経て、 最終的に rust_begin_unwind というシンボル名になります。 リンク時に、core 内のシンボル参照は std の定義(Rust ソース内で panic_handler と呼ばれる関数)に解決されます。

したがって、実行時には制御フローが core から std へ渡されます。 これにより、core からのパニックは、 他のパニックが使うものと同じインフラストラクチャ(パニックフック、アンワインドなど)を通ることができます。

std における panic! の実装

ここから、実際のパニック関連ロジックが始まります。 library/std/src/panicking.rs では、 制御は panic_with_hook に渡されます。 このメソッドは、グローバルパニックフックの呼び出しと、二重パニックのチェックを担当します。 最後に、 パニックランタイムによって提供される __rust_start_panic を呼び出します。

__rust_start_panic への呼び出しは非常に奇妙です。*mut &mut dyn PanicPayload が渡され、 usize に変換されます。 この型を分解して見てみましょう。

  1. PanicPayload は内部トレイトです。 これは PanicPayload (ユーザーが提供したペイロード型のラッパー)に対して実装されており、 fn take_box(&mut self) -> *mut (dyn Any + Send) というメソッドを持ちます。 このメソッドは、ユーザーが提供したペイロード(T: Any + Send)を受け取り、 それをボックス化し、そのボックスを raw ポインターに変換します。

  2. __rust_start_panic を呼び出すとき、手元には &mut dyn PanicPayload があります。 しかし、これはファットポインター(usize の 2 倍のサイズ)です。 これを FFI 境界を越えてパニックランタイムに渡すために、この可変参照への可変参照 (&mut &mut dyn PanicPayload)を取り、それを raw ポインター (*mut &mut dyn PanicPayload)に変換します。 外側の raw ポインターは、Sized 型(可変参照)を指しているため、シンポインターです。 したがって、このシンポインターを usize に変換できます。 これは FFI 境界を越えて渡すのに適しています。

最後に、この usize を指定して __rust_start_panic を呼び出します。 これで、パニックランタイムに入ったことになります。

ステップ 2: パニックランタイム

Rust は 2 つのパニックランタイム、panic_abortpanic_unwind を提供します。 ユーザーはビルド時に Cargo.toml を通じてどちらかを選択します。

panic_abort は非常に単純です。その __rust_start_panic の実装は、予想どおり単に中止します。

panic_unwind は、より興味深いケースです。

その __rust_start_panic の実装では、usize を受け取り、それを *mut &mut dyn PanicPayload に戻し、逆参照して、&mut dyn PanicPayload に対して take_box を呼び出します。 この時点で、ペイロードそのものへの raw ポインター (*mut (dyn Send + Any))があります。つまり、これは panic! を呼び出したユーザーが 提供した実際の値への raw ポインターです。

この時点で、プラットフォーム非依存のコードは終わります。 ここから、プラットフォーム固有のアンワインドロジック(例: unwind)を呼び出します。 このコードは、スタックをアンワインドし、各フレームに関連付けられた「landing pad」 (現在はデストラクタの実行)を実行し、catch_unwind フレームへ制御を移すことを担当します。

すべてのパニックは、プロセスを中止するか、何らかの catch_unwind 呼び出しによって捕捉されることに注意してください。 特に、std の runtime service では、 ユーザーが提供した main 関数の呼び出しは catch_unwind でラップされています。

AST 検証

AST 検証 は、ツリー内の各アイテムを訪問して簡単なチェックを行う独立した AST パスです。このパスは、複雑な解析、型チェック、名前解決を行いません。

検証を実行する前に、コンパイラはまずマクロを展開します。その後、このパスは各 AST アイテムが正しい状態にあることをチェックする検証を実行します。そしてこのパスが完了すると、コンパイラはクレート解決パスを実行します。

検証

検証は AstValidator 型で定義されており、この型自体は rustc_ast_passes クレートにあります。この型は、特定の言語規則が破られたときにエラーを出力する、さまざまな単純なチェックを実装しています。

さらに、AstValidator は AST アイテム(関数、トレイト、列挙型など)を訪問する方法を定義する Visitor トレイトを実装しています。

各アイテムに対して、visitor は固有のチェックを実行します。たとえば、関数宣言を訪問するとき、AstValidator はその関数について次のことをチェックします。

  • パラメーター数が u16::MAX を超えていないこと。
  • c 可変長引数が宣言内で最後に来ること。
  • ドキュメンテーションコメントが関数パラメーターに適用されていないこと。
  • その他の検証。

機能ゲートのチェック

機能ゲートを追加、削除、名前変更、または安定化するための手順については、 機能ゲートを参照してください。

機能ゲートは、nightly 限定の #![feature(...)] による明示的な有効化なしに、不安定な言語機能やライブラリ機能が使用されることを防ぎます。 この章では、機能ゲートの実装、つまりゲートがどこで定義され、どのように有効化され、使用がどのように検証されるかを文書化します。

機能の定義

すべての機能ゲート定義は rustc_feature クレートに配置されています。

rustc_feature::Features 型は、クレートに対する有効な機能セットを表します。 enabledincompleteinternal のようなヘルパーは、コンパイル中に状態をチェックするために使用されます。

機能の収集

AST 検証または展開の前に、rustc はクレートレベルの #![feature(...)] 属性を収集し、有効な Features セットを構築します。

  • 収集は rustc_expand/src/config.rsfeatures で行われます。
  • #![feature] エントリは、unstableacceptedremoved テーブルに照らして分類されます。
    • 削除済み機能は即座にエラーになります。
    • 受理済み機能は記録されますが、nightly は必要ありません。 stable/beta では、 rustc_ast_passes/src/feature_gate.rsmaybe_stage_features が非 nightly の 診断を発行し、安定した機能を列挙します。ここが「すでに安定化済み」 というメッセージの出所です。
    • 不安定な機能は有効として記録されます。
    • 不明な機能はライブラリ機能として扱われ、後で検証されます。
  • -Z allow-features=... を指定すると、allowlist に含まれていない不安定な機能または不明な機能は拒否されます。
  • RUSTC_BOOTSTRAPUnstableFeatures::from_environment に渡されます。 この変数は、コンパイラを「nightly」として扱うかどうかを制御し、ブートストラップ中に機能ゲートを迂回できるようにするか、または明示的に無効化します(-1)。

パーサーでのゲート

一部の構文は、解析中に検出され、ゲートされます。 パーサーは後続のチェックのために span を記録し、診断の一貫性を保ち、解析後まで遅延させます。

チェックパス

中心となるロジックは rustc_ast_passes/src/feature_gate.rs にあり、主に check_crate とその AST ビジターにあります。

check_crate

check_crate は高レベルの検証を行います。

  • maybe_stage_features: stable/beta で #![feature] を拒否します。
  • check_incompatible_features: 互換性のない機能の組み合わせ (rustc_feature::INCOMPATIBLE_FEATURES で宣言)が同時に使用されないようにします。
  • check_new_solver_banned_features: 次期トレイトソルバーのコンパイラモードと互換性のない機能を禁止します。
  • check_features_requiring_new_solver: 古いソルバーと互換性のない機能に対して、新しいトレイトソルバーを要求します。
  • パーサーでゲートされた span: 解析中に記録された GatedSpans を処理します (GatedSpans のチェックを参照)。

GatedSpans のチェック

check_cratesess.psess.gated_spans を反復処理します。

  • gate_all! マクロは、機能が有効でない場合、ゲートされた各 span について診断を発行します。
  • 一部のゲートには追加のロジックがあります(例: yieldcoroutines または gen_blocks によって許可される場合があります)。
  • レガシーゲート(例: box_patternstry_blocks)は、ハードエラーではなく将来互換性警告を発行する別の経路を使用する場合があります。

AST ビジター

PostExpansionVisitor は、展開後の方が検証しやすい構文をチェックするために、展開済み AST を走査します。

  • このビジターはヘルパーマクロ(gate!gate_alt!gate_multi!)を使用して次をチェックします。
    1. 機能が有効か。
    2. span.allows_unstable がそれを許可しているか(内部コンパイラマクロ用)。
  • 例には、trait_aliasdecl_macroextern types、およびさまざまな impl Trait 形式が含まれます。

属性と cfg

構文に加えて、rustc は属性と cfg オプションもゲートします。

組み込み属性

  • rustc_ast_passes::check_attribute は、属性を BUILTIN_ATTRIBUTE_MAP と照合して検査します。
  • 属性が AttributeGate::Gated で、機能が有効になっていない場合、 feature_err が発行されます。

cfg オプション

診断

診断ヘルパーは rustc_session/src/parse.rs に配置されています。

  • feature_errfeature_warn は標準化された診断を発行し、可能であれば追跡 issue 番号を添付します。
  • rustc_span/src/lib.rsSpan::allows_unstable は、span が #[allow_internal_unstable] でマークされたマクロに由来するかどうかをチェックします。 これにより、ユーザーコードにはゲートを適用しつつ、内部マクロが stable チャネル上で不安定な機能を使用できるようになります。

言語アイテム

コンパイラには、いくつかのプラグ可能な操作があります。つまり、言語にハードコードされているのではなく、ライブラリで実装され、コンパイラにそれが存在することを伝える特別なマーカーを持つ機能です。そのマーカーは属性 #[lang = "..."] であり、... にはさまざまな値、すなわちさまざまな「言語アイテム」があります。

このような言語アイテムの多くは、addtrait core::ops::Add)や future_traittrait core::future::Future)のように、妥当な実装方法が 1 つしかありません。一方で、特定の目的を達成するためにオーバーライドできるものもあります。たとえば、バイナリのエントリポイントを制御できます。

言語アイテムによって提供される機能には、次のものがあります。

  • トレイトによるオーバーロード可能な演算子: ==<、参照外し(*)、+ などの演算子に対応するトレイトはすべて言語アイテムでマークされています。これら特定の 4 つは、それぞれ eqordderefadd です。
  • パニックとスタックアンワインディング: eh_personalitypanicpanic_bounds_checks 言語アイテム。
  • コンパイラが使用する型の性質を示すために使われる std::marker のトレイト: 言語アイテム sendsynccopy
  • core::marker にある分散性インジケーターに使われる特殊なマーカー型: 言語アイテム phantom_data

言語アイテムはコンパイラによって遅延ロードされます。たとえば、Box をまったく使わないのであれば、exchange_mallocbox_free の関数を定義する必要はありません。あるアイテムが必要になったものの、現在のクレートやそれが依存するいずれのクレートにも見つからない場合、rustc はエラーを出力します。

ほとんどの言語アイテムは core ライブラリによって定義されていますが、#![no_std] で実行可能ファイルをビルドしようとしている場合は、通常 std によって提供されるいくつかの言語アイテムを定義する必要があります。

言語アイテムの取得

tcx.lang_items() を呼び出すことで言語アイテムを取得できます。

以下は、trait Sized {} 言語アイテムを取得する小さな例です。

#![allow(unused)]
fn main() {
// `#![no_core]` の場合、このトレイトは利用できないことに注意してください。
if let Some(sized_trait_def_id) = tcx.lang_items().sized_trait() {
    // `sized_trait_def_id` で何かを行う
}
}

sized_trait()DefId そのものではなく、Option を返すことに注意してください。 これは、言語アイテムが標準ライブラリで定義されているため、誰かが #![no_core](または一部の言語アイテムでは #![no_std])でコンパイルすると、その言語アイテムが存在しない可能性があるからです。 次のいずれかを行えます。

  • 続行するためにその言語アイテムが必要な場合は、ハードエラーを出す(これはユーザーコードで発生する可能性があるため、パニックしないでください)。
  • DefId を使って行おうとしていた処理を単に省くことで、限定された機能で処理を続行する。

すべての言語アイテムの一覧

言語アイテムは次の場所で見つけることができます。

  • コンパイラドキュメント内の網羅的なリファレンス: rustc_hir::LangItem
  • ripgrep を使用した、ソース位置付きの自動生成リスト: rg '#\[.*lang =' library/

言語アイテムは明示的に不安定であり、新しいリリースで変更される可能性があることに注意してください。

HIR

HIR –「High-Level Intermediate Representation(高レベル中間表現)」– は、rustc の大部分で使用される主要な IR です。 これは、パース、マクロ展開、名前解決の後に生成される抽象構文木(AST)の、コンパイラにとって扱いやすい表現です(HIR がどのように作成されるかについては Lowering を参照してください)。 HIR の多くの部分は Rust の表層構文に非常によく似ていますが、Rust の式形式の一部が脱糖されて取り除かれている点は例外です。 たとえば、for ループは loop に変換され、HIR には現れません。 これにより、HIR は通常の AST よりも解析しやすくなっています。

この章では、HIR の主要な概念について説明します。

rustc に -Z unpretty=hir-tree フラグを渡すことで、コードの HIR 表現を確認できます。

cargo rustc -- -Z unpretty=hir-tree

また、-Z unpretty=hir オプションを使用して、元のソースコードの式により近い HIR を生成することもできます。

cargo rustc -- -Z unpretty=hir

アウトオブバンドストレージと Crate

HIR におけるトップレベルのデータ構造は Crate であり、現在コンパイル中のクレートの内容を格納します(HIR は現在のクレートに対してのみ構築します)。 AST ではクレートのデータ構造は基本的にルートモジュールだけを含みますが、HIR の Crate 構造体には、クレートの内容をより簡単にアクセスできるよう整理するための多数のマップやその他のものが含まれます。

たとえば、HIR における個々のアイテム(モジュール、関数、トレイト、impl など)の内容は、親から直接アクセスできるわけではありません。 したがって、たとえば関数 bar() を含むモジュールアイテム foo があるとします。

#![allow(unused)]
fn main() {
mod foo {
    fn bar() { }
}
}

この場合、HIR ではモジュール foo の表現(Mod 構造体)は、bar()ItemId I だけを持ちます。 関数 bar() の詳細を取得するには、items マップで I を検索します。

この表現から得られる良い結果の 1 つは、これらのマップ内のキーと値のペアを反復処理することで、クレート内のすべてのアイテムを反復処理できることです(HIR 全体をくまなく探索する必要はありません)。 トレイトアイテムや impl アイテムのようなもの、および「本体」(後述)についても同様のマップがあります。

このように表現を構成するもう 1 つの理由は、インクリメンタルコンパイルとの統合をより良くするためです。 この方法では、(たとえば mod foo について)&rustc_hir::Item へのアクセスを得たとしても、関数 bar() の内容に直ちにアクセスできるわけではありません。 代わりに、bar()id だけにアクセスでき、その id を指定して bar() の内容を検索する何らかの関数を呼び出す必要があります。これにより、コンパイラはあなたが bar() のデータにアクセスしたことを観測し、その依存関係を記録する機会を得ます。

HIR における識別子

HIR では、共存しながら異なる目的を果たすさまざまな識別子を使用します。

  • DefId は、その名前が示すように、特定のクレート内の特定の定義、またはトップレベルアイテムを識別します。 これは 2 つの部分から構成されます。定義の由来となるクレートを識別する CrateNum と、クレート内の定義を識別する DefIndex です。 HirId とは異なり、すべての式に DefId があるわけではないため、コンパイル間でより安定しています。

  • LocalDefId は基本的に、現在のクレートに由来することがわかっている DefId です。 これにより、CrateNum 部分を省略でき、ローカル定義を期待する関数にローカル定義だけが渡されることを型システムで保証できます。

  • HirId は、現在のクレートの HIR 内のノードを一意に識別します。 これは 2 つの部分から構成されます。 owner と、owner 内で一意な local_id です。 この組み合わせにより、インクリメンタルコンパイルに役立つ、より安定した値になります。 DefId とは異なり、HirId は式のような細粒度のエンティティを参照できますが、現在のクレート内にローカルなままです。

  • BodyId は、現在のクレート内の HIR Body を識別します。 現在のところ、これは HirId の単なるラッパーです。 HIR の本体に関する詳細については、HIR の章を参照してください。

これらの識別子は TyCtxt を通じて相互に変換できます。

HIR の操作

HIR を扱うときは、ほとんどの場合 TyCtxt を介して行います。 これには、さまざまな種類の ID 間の変換や、HIR ノードに関連付けられたデータの検索を行うためのメソッドが多数含まれています。これらは hir::map モジュールで定義されており、主に hir_ というプレフィックスが付いています。

たとえば、LocalDefId を持っていて、それを HirId に変換したい場合は、tcx.local_def_id_to_hir_id(def_id) を使用できます。 HIR ノードを持つのはローカルアイテムだけであるため、DefId ではなく LocalDefId が必要です。

同様に、tcx.hir_node(n) を使用して、HirId に対応するノードを検索できます。 これは Option<Node<'hir>> を返します。ここで Node はマップ内で定義されている enum です。 これに対してマッチすることで、HirId がどの種類のノードを参照していたのかを調べることができ、データ自体へのポインタも取得できます。多くの場合、n がどの種類のノードであるかはわかっています。たとえば、n が何らかの HIR 式でなければならないことがわかっている場合は、tcx.hir_expect_expr(n) を実行できます。これは &hir::Expr を抽出して返し、n が実際には式でない場合はパニックします。

最後に、[tcx.parent_hir_node(n)][parent_hir_node] のような呼び出しを介して、ノードの親を見つけることができます。 [parent_hir_node]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.TyCtxt.html#method.parent_hir_node

HIRボディ

rustc_hir::Body は、関数/クロージャの本体や 定数の定義など、何らかの実行可能コードを表します。 ボディは 所有者 に関連付けられており、これは通常、何らかのアイテム (例: fn()const)ですが、クロージャ式である場合もあります (例: |x, y| x + y)。TyCtxt を使用して、指定したdef-idに 関連付けられたボディ(hir_maybe_body_owned_by)を検索したり、ボディの 所有者(hir_body_owner_def_id)を検索したりできます。

AST lowering

AST lowering ステップは AST を HIR に変換します。 これは、型解析や同様の構文に依存しない解析に関係しない多くの構造が削除されることを意味します。 このような構造の例には、以下が含まれますが、これらに限定されません。

  • 括弧
    • 置き換えなしに削除されます。ツリー構造によって順序が明示されるためです
  • for ループ
    • match + loop + match に変換されます
  • 全称的な impl Trait
    • ジェネリック引数に変換されます (ただし、ユーザーがそれらを書いていないことを把握するためのフラグがいくつか付きます)
  • 存在的な impl Trait
    • 仮想的な existential type 宣言に変換されます

AST lowering の実装は rustc_ast_lowering クレートにあります。 エントリーポイントは lower_to_hir で、これは展開後の AST とリゾルバーのデータを TyCtxt から取得し、クレート全体の hir::Crate を構築します。

Lowering は HIR owner を中心に構成されています。 lower_to_hir はまずクレートにインデックスを付け、その後 ItemLowerer::lower_node が各クレート、アイテム、関連アイテム、外部アイテムを lowering します。

lowering ロジックの大部分は LoweringContext 上にあります。 実装は rustc_ast_lowering クレート内の item.rsexpr.rspat.rspath.rs など複数のファイルに分割されていますが、それらはすべて同じ LoweringContext の状態と ID lowering の仕組みを共有しています。

各 owner は、それぞれ独自の with_hir_id_owner スコープ内で lowering されます。 これが、以下の HirId 不変条件が重要である理由です。lower_node_id は AST の NodeId を現在の owner にマップし、一方で next_id は desugaring 中に導入される HIR 専用の新しいノードを作成します。

Lowering では、compiler/rustc_passes/src/hir_id_validator.rs にある健全性チェックを発火させないために、いくつかの不変条件を守る必要があります。

  1. 作成された HirId は使用されなければなりません。 したがって、lower_node_id を使用する場合は、得られた NodeId または HirId必ず使用しなければなりません(どちらでも問題ありません。HIR 内の任意の NodeId は、対応する HirId が存在するかチェックされるためです)。
  2. HirId の lowering は、その HirId所有するアイテムのスコープ内で行われなければなりません。 これは、現在 lowering されているものとは別のアイテムの一部を作成している場合、with_hir_id_owner を使用する必要があることを意味します。 これは、たとえば存在的な impl Trait の lowering 中に発生します。
  3. HIR 構造に配置される NodeId は、その HirId が未使用であっても lowering されなければなりません。 let _ = self.lower_node_id(node_id); を呼び出すことは完全に正当です。
  4. AST に存在しなかった新しいノードを作成している場合は、それらに対して新しい ID を必ず作成しなければなりません。 これは next_id メソッドを呼び出すことで行われます。 このメソッドは新しい NodeId を生成すると同時に、それを自動的に lowering するため、HirId も取得できます。

新しい DefId を作成している場合、各 DefId には対応する NodeId が必要なので、lowering 中に新しいものを生成しなくて済むように、これらの NodeIdAST に追加することを推奨します。 これには、何かの DefId をその NodeId 経由で見つける方法を作れるという利点があります。 lowering がこの DefId を複数の場所で必要とする場合、それらすべての場所で新しい NodeId を生成することはできません。そうすると、そのたびに新しい DefId も得られてしまうためです。 AST 由来の NodeId があれば、これは問題になりません。

NodeId があることで、lowering がその場で DefId を生成する必要がなくなり、代わりに DefCollectorDefId を生成できるようにもなります。 DefId の生成を 1 か所に集中させることで、リファクタリングや推論が容易になります。

HIR デバッグ

-Z unpretty=hir フラグを使用すると、HIR の人間が読める表現を生成できます。 Cargo プロジェクトでは、cargo rustc -- -Z unpretty=hir でこれを実行できます。 この出力は、AST lowering 中にコードがどのように脱糖され、変換されたかを一目で確認する必要がある場合に役立ちます。

HIR 内のデータの完全な Debug ダンプを得るには、-Z unpretty=hir-tree フラグを使用します。 これは、コンパイラの観点から HIR の完全な構造を確認する必要がある場合に役立つことがあります。

NodeIdDefId をソースコードと対応付けようとしている場合は、 -Z unpretty=expanded,identified フラグが役立つことがあります。

TODO: 他に何かあるか? #1159

曖昧/非曖昧な型と const

AST/HIR における型と const 引数は、曖昧 (ambig) または非曖昧 (unambig) の 2 種類の位置に置かれる可能性があります。曖昧な位置とは、型または const のどちらとして解析しても妥当な位置であり、非曖昧な位置とは、片方の種類としてのみ解析することが妥当な位置です。

#![allow(unused)]
fn main() {
fn func<T, const N: usize>(arg: T) {
    //                          ^ 非曖昧な型位置
    let a: _ = arg; 
    //     ^ 非曖昧な型位置

    func::<T, N>(arg);
    //     ^  ^
    //     ^^^^ 曖昧な位置 

    let _: [u8; 10];
    //      ^^  ^^ 非曖昧な const 位置
    //      ^^ 非曖昧な型位置
}

}

曖昧な位置にあるほとんどの型/const は、解析中に型または const のどちらかとして曖昧性を解消できます。これに対する唯一の例外は、パスと推論されたジェネリック引数です。

パス

#![allow(unused)]
fn main() {
struct Foo<const N: usize>;

fn foo<const N: usize>(_: Foo<N>) {}
}

解析時には、中括弧で囲まれていないすべてのジェネリック引数を として解析します(つまり、それらは [ast::GenericArg::Ty] になります)。上記の例では、これは Foo へのジェネリック引数を、[ast::Ty::Path(N)] をラップする ast::GenericArg::Ty として解析することを意味します。

その後、名前解決中に:

  • ジェネリック引数位置で、ジェネリック引数を持たない単一セグメントのパスに遭遇した場合、まず型名前空間で解決を試み、それが失敗した場合に値名前空間での解決を試みます。
  • それ以外の種類のパスはすべて、型名前空間でのみ解決を試みます。

これが実装されている場所については、[LateResolutionVisitor::visit_generic_arg] を参照してください。

最後に AST lowering 中に、型引数を lower しようとするとき、まずそれが Ty::Path であるかどうか、そしてそれが値名前空間内の何かに解決されたかどうかを確認します。そうであれば、anon const を作成し、型引数ではなく const 引数へ lower します。

これが実装されている場所については、[LoweringContext::lower_generic_arg] を参照してください。

パスの曖昧性は HIR には伝播されないことに注意してください。HIR ty lowering 中に Ty または Const のどちらかに変換される hir::GenericArg::Path は存在しません(ただし、そのようなことを行うことは可能です)。

推論された引数 (_)

#![allow(unused)]
fn main() {
struct Foo<const N: usize>;

fn foo() {
    let _unused: Foo<_>;
}
}

lowering 後も曖昧なまま残る唯一のジェネリック引数は、パスセグメント内の推論されたジェネリック引数 (_) です。上記の例では、解析時点では Foo への _ 引数が、推論された型引数なのか、推論された const 引数なのかは明確ではありません。

曖昧な AST 位置では、推論された引数は [ast::Ty::Infer] をラップする [ast::GenericArg::Ty] として解析されます。その後、AST lowering 中に ast::GenericArg::Ty を lower するとき、それが推論された型であるかどうかを確認し、そうであれば [hir::GenericArg::Infer] へ lower します。

非曖昧な AST 位置では、推論された引数は ast::Ty::Infer または [ast::AnonConst] のどちらかとして解析されます。AnonConst のケースはかなり奇妙です。実際にはこれを HIR 内の anon const へ lower するわけではありませんが、「anon const」の「本体」を表すために [ast::ExprKind::Underscore] を使用しています。

AST に ast::GenericArg::Infer を持たせるようにリファクタリングし、この AnonConst の多義的な意味と、曖昧な位置における ast::Ty::Infer の再利用をなくせるかどうか検討する価値があるかもしれません。

非曖昧な AST 位置では、AST lowering 中に、推論された引数を、それが型位置か const 位置かに応じて、それぞれ [hir::TyKind::Infer][ty_infer] または [hir::ConstArgKind::Infer][const_infer] へ lower します。 曖昧な AST 位置では、AST lowering 中に、推論された引数を [hir::GenericArg::Infer][generic_arg_infer] へ lower します。これが実装されている場所については、[LoweringContext::lower_generic_arg] を参照してください。

この単純な実装では、HIR の構造を見ると、HIR 内で推論された型/const が見つかる可能性があると思われる場所が最大 5 つ存在することになります:

  1. 非曖昧な型位置における TyKind::Infer
  2. 非曖昧な const 引数位置における ConstArgKind::Infer
  3. 曖昧な位置における [GenericArg::Type(TyKind::Infer)][generic_arg_ty]
  4. 曖昧な位置における [GenericArg::Const(ConstArgKind::Infer)][generic_arg_const]
  5. 曖昧な位置における GenericArg::Infer

場所 3 と 4 は、実際に遭遇することは決してないことに注意してください。これは、ジェネリック引数位置では常に GenericArg::Infer へ lower するためです。

これにはいくつかの失敗モードがあります:

  • GenericArg::Infer をチェックするものの hir::TyKind/ConstArgKind::Infer のチェックを忘れる visitor を書いてしまい、偶然にも曖昧な位置にある infer だけを処理してしまう可能性があります。
  • TyKind/ConstArgKind::Infer をチェックするものの GenericArg::Infer のチェックを忘れる visitor を書いてしまい、偶然にも非曖昧な位置にある infer だけを処理してしまう可能性があります。
  • GenericArg::Type/Const(TyKind/ConstArgKind::Infer)GenericArg::Infer をチェックする visitor を書いてしまい、曖昧な位置では推論された型/const を GenericArg::Type/Const として表現することは決してないと気づかない可能性があります。
  • ConstArgKind::Infer ではなく TyKind::Infer だけ をチェックする visitor を書いてしまい、推論された const 引数も存在することを忘れる可能性があります(またはその逆)。

推論された型/const に関心があるときに HIR visitor を書く際のエラーを減らすため、比較的複雑なシステムを用意しています:

  1. 型または const が非曖昧な位置にある場合と曖昧な位置にある場合とで、コンパイラ内に異なる型、Ty<AmbigArg>Ty<()> があります。[AmbigArg][ambig_arg] は非居住型であり、曖昧な位置にいる場合に TyKindConstArgKindInfer バリアントを選択的に「無効化」するために使用します。

  2. HIR visitor の [visit_ty][visit_ty] メソッドと [visit_const_arg][visit_const_arg] メソッドは、型/const の曖昧な位置バージョンのみを受け取ります。非曖昧な型/const は、visit 処理中に暗黙的に曖昧な型/const へ変換され、Infer バリアントは専用の [visit_infer][visit_infer] メソッドによって処理されます。

これには多くの利点があります:

  • GenericArg::Type/Const が推論された型/const 引数を表現できないことが明確です
  • visit_tyvisit_const_arg の実装者が推論された型/const に遭遇することは決してないため、一見正しく動作するがエッジケースを誤って処理する visitor を書くことは不可能になります
  • visit_infer メソッドは HIR 内の推論された型/const の すべて のケースを処理するため、visitor は推論された型/const を専用の 1 か所で簡単に処理でき、ケースを忘れることがありません [ty_infer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.TyKind.html#variant.Infer [const_infer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.ConstArgKind.html#variant.Infer [generic_arg_ty]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.GenericArg.html#variant.Type [generic_arg_const]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.GenericArg.html#variant.Const [generic_arg_infer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.GenericArg.html#variant.Infer [ambig_arg]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.AmbigArg.html [visit_ty]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/intravisit/trait.Visitor.html#method.visit_ty [visit_const_arg]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/intravisit/trait.Visitor.html#method.visit_const_arg [visit_infer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/intravisit/trait.Visitor.html#method.visit_infer [LateResolutionVisitor::visit_generic_arg]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_resolve/late/struct.LateResolutionVisitor.html#method.visit_generic_arg [LoweringContext::lower_generic_arg]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast_lowering/struct.LoweringContext.html#method.lower_generic_arg [ast::GenericArg::Ty]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast/ast/enum.GenericArg.html#variant.Type [ast::Ty::Infer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast/ast/enum.TyKind.html#variant.Infer [ast::AnonConst]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast/ast/struct.AnonConst.html [hir::GenericArg::Infer]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.GenericArg.html#variant.Infer [ast::ExprKind::Underscore]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast/ast/enum.ExprKind.html#variant.Underscore [ast::Ty::Path(N)]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast/ast/enum.TyKind.html#variant.Path

THIR

THIR(“Typed High-Level Intermediate Representation”)は、以前は “High-Level Abstract IR” の略として HAIR と呼ばれていた、rustc で使われるもう一つの IR であり、 型検査の後に生成されます。これは( 2024年1月時点で) MIR 構築網羅性検査、および unsafe 性検査に使われています。

名前から推測できるかもしれませんが、THIR は HIR を低水準化したバージョンであり、 すべての型が埋められています。これは型検査が完了した後に可能になります。 しかし、THIR には HIR と区別される、他にも興味深い特徴があります。

  • MIR と同様に、THIR はボディ、つまり「実行可能なコード」のみを表します。これには 関数ボディだけでなく、たとえば const 初期化子も含まれます。 具体的には、すべてのボディ所有者について THIR が作成されます。 したがって、THIR には structtrait のようなアイテムを表すものはありません。

  • THIR の各ボディは一時的にのみ保存され、不要になり次第すぐに破棄されます。 これは、コンパイルプロセスの最後まで保存される場合(HIR で行われていること)とは対照的です。

  • THIR では、すべてのノードの型を利用可能にすることに加えて、HIR と比べて追加の 脱糖も行われています。 たとえば、自動的な参照とデリファレンスは 明示化され、メソッド呼び出しとオーバーロードされた演算子は通常の関数呼び出しに変換されます。 破棄スコープも明示化されます。

  • 文、式、match アーム、ブロック、パラメータは別々に保存されます。 たとえば、 stmts 配列内の文は、exprs 配列内の式をそのインデックス(ExprId として表されます)で参照します。

THIR は rustc_mir_build::thir にあります。 thir::Expr を構築するには、 THIR が割り当てられるメモリアリーナを渡して、thir_body 関数を使うことができます。 このアリーナを破棄すると THIR も破棄されます。 これはピークメモリを抑えるのに役立ちます。 クレートのすべてのボディの THIR 表現を同時にメモリ上に保持するのは、非常に重くなります。

rustc-Zunpretty=thir-tree フラグを渡すことで、THIR のデバッグ表現を取得できます。

実例として、次の例を使ってみましょう。

fn main() {
    let x = 1 + 2;
}

これが THIR でどのように表現されるかを以下に示します( 2022年8月時点)。

```rust,no_run
Thir {
    // match アームなし
    arms: [],
    exprs: [
        // 式 0、値 1 のリテラル
        Expr {
            ty: i32,
            temp_lifetime: Some(
                Node(1),
            ),
            span: oneplustwo.rs:2:13: 2:14 (#0),
            kind: Literal {
                lit: Spanned {
                    node: Int(
                        1,
                        Unsuffixed,
                    ),
                    span: oneplustwo.rs:2:13: 2:14 (#0),
                },
                neg: false,
            },
        },
        // 式 1、リテラル 1 を囲むスコープ
        Expr {
            ty: i32,
            temp_lifetime: Some(
                Node(1),
            ),
            span: oneplustwo.rs:2:13: 2:14 (#0),
            kind: Scope {
                // 上記の式 0 への参照
                region_scope: Node(3),
                lint_level: Explicit(
                    HirId {
                        owner: DefId(0:3 ~ oneplustwo[6932]::main),
                        local_id: 3,
                    },
                ),
                value: e0,
            },
        },
        // 式 2、リテラル 2
        Expr {
            ty: i32,
            temp_lifetime: Some(
                Node(1),
            ),
            span: oneplustwo.rs:2:17: 2:18 (#0),
            kind: Literal {
                lit: Spanned {
                    node: Int(
                        2,
                        Unsuffixed,
                    ),
                    span: oneplustwo.rs:2:17: 2:18 (#0),
                },
                neg: false,
            },
        },
        // 式 3、リテラル 2 を囲むスコープ
        Expr {
            ty: i32,
            temp_lifetime: Some(
                Node(1),
            ),
            span: oneplustwo.rs:2:17: 2:18 (#0),
            kind: Scope {
                region_scope: Node(4),
                lint_level: Explicit(
                    HirId {
                        owner: DefId(0:3 ~ oneplustwo[6932]::main),
                        local_id: 4,
                    },
                ),
                // 上記の式 2 への参照
                value: e2,
            },
        },
        // 式 4、1 + 2 を表す
        Expr {
            ty: i32,
            temp_lifetime: Some(
                Node(1),
            ),
            span: oneplustwo.rs:2:13: 2:18 (#0),
            kind: Binary {
                op: Add,
                // 上記のリテラルを囲むスコープへの参照
                lhs: e1,
                rhs: e3,
            },
        },
        // 式 5、式 4 を囲むスコープ
        Expr {
            ty: i32,
            temp_lifetime: Some(
                Node(1),
            ),
            span: oneplustwo.rs:2:13: 2:18 (#0),
            kind: Scope {
                region_scope: Node(5),
                lint_level: Explicit(
                    HirId {
                        owner: DefId(0:3 ~ oneplustwo[6932]::main),
                        local_id: 5,
                    },
                ),
                value: e4,
            },
        },
        // 式 6、文を囲むブロック
        Expr {
            ty: (),
            temp_lifetime: Some(
                Node(9),
            ),
            span: oneplustwo.rs:1:11: 3:2 (#0),
            kind: Block {
                body: Block {
                    targeted_by_break: false,
                    region_scope: Node(8),
                    opt_destruction_scope: None,
                    span: oneplustwo.rs:1:11: 3:2 (#0),
                    // 下記の文 0 への参照
                    stmts: [
                        s0,
                    ],
                    expr: None,
                    safety_mode: Safe,
                },
            },
        },
        // 式 7、式 6 内のブロックを囲むスコープ
        Expr {
            ty: (),
            temp_lifetime: Some(
                Node(9),
            ),
            span: oneplustwo.rs:1:11: 3:2 (#0),
            kind: Scope {
                region_scope: Node(9),
                lint_level: Explicit(
                    HirId {
                        owner: DefId(0:3 ~ oneplustwo[6932]::main),
                        local_id: 9,
                    },
                ),
                value: e6,
            },
        },
        // 式 7 を囲む破棄スコープ
        Expr {
            ty: (),
            temp_lifetime: Some(
                Node(9),
            ),
            span: oneplustwo.rs:1:11: 3:2 (#0),
            kind: Scope {
                region_scope: Destruction(9),
                lint_level: Inherited,
                value: e7,
            },
        },
    ],
    stmts: [
        // let 文
        Stmt {
            kind: Let {
                remainder_scope: Remainder { block: 8, first_statement_index: 0},
                init_scope: Node(1),
                pattern: Pat {
                    ty: i32,
                    span: oneplustwo.rs:2:9: 2:10 (#0),
                    kind: Binding {
                        mutability: Not,
                        name: "x",
                        mode: ByValue,
                        var: LocalVarId(
                            HirId {
                                owner: DefId(0:3 ~ oneplustwo[6932]::main),
                                local_id: 7,
                            },
                        ),
                        ty: i32,
                        subpattern: None,
                        is_primary: true,
                    },
                },
                initializer: Some(
                    e5,
                ),
                else_block: None,
                lint_level: Explicit(
                    HirId {
                        owner: DefId(0:3 ~ oneplustwo[6932]::main),
                        local_id: 6,
                    },
                ),
            },
            opt_destruction_scope: Some(
                Destruction(1),
            ),
        },
    ],
}

MIR(Mid-level IR)

MIR は Rust の Mid-level Intermediate Representation です。これは HIR から構築されます。MIR は RFC 1211 で導入されました。 これは Rust を根本的に単純化した形式であり、特定のフロー依存の安全性チェック (特に借用チェッカー!)に使われるほか、最適化やコード生成にも使われます。

MIR の非常に高レベルな導入、および MIR が依存しているコンパイラの概念 (制御フローグラフやデシュガリングなど)について知りたい場合は、 MIR を紹介した rust-lang のブログ記事を読むとよいでしょう。

MIR の概要

MIR は [compiler/rustc_middle/src/mir/][mir] モジュールで定義されていますが、 それを操作するコードの多くは [compiler/rustc_mir_build][mirmanip_build]、 [compiler/rustc_mir_transform][mirmanip_transform]、および [compiler/rustc_mir_dataflow][mirmanip_dataflow] にあります。

MIR の主な特徴には、次のようなものがあります。

  • 制御フローグラフに基づいている。
  • ネストした式を持たない。
  • MIR 内のすべての型は完全に明示的である。

MIR の主要な語彙

このセクションでは、MIR の主要な概念を紹介します。要約すると次のとおりです。

  • 基本ブロック: 制御フローグラフの単位で、次のもので構成されます。
    • 文: 後続が 1 つのアクション
    • ターミネータ: 複数の後続を持つ可能性があるアクション。常に ブロックの末尾にある
    • 基本ブロック という用語に馴染みがない場合は、背景の章を参照してください)
  • ローカル: スタック上に(少なくとも概念的には)割り当てられるメモリ位置。 関数引数、ローカル変数、一時値などがあります。これらはインデックスで識別され、 _1 のように先頭にアンダースコアを付けて表記されます。また、戻り値を格納するために 割り当てられる特別な「ローカル」(_0)もあります。
  • プレース: _1_1.f のように、メモリ内の位置を識別する式。
  • Rvalue: 値を生成する式。「R」は、これらが代入の「右辺」 であることを表しています。
    • オペランド: rvalue への引数で、22 のような定数、または _1 のようなプレースのいずれかです。

単純なプログラムを MIR に変換し、整形表示された出力を読むことで、MIR がどのように 構築されるかを感覚的に理解できます。実際、playground では MIR ボタンが用意されており、 プログラムの MIR を表示できるため、これを簡単に試せます。このプログラムを play に入力する (またはこのリンクをクリックする)し、上部の「MIR」ボタンをクリックしてみてください。

fn main() {
    let mut vec = Vec::new();
    vec.push(1);
    vec.push(2);
}

次のようなものが表示されるはずです。

// 警告: この出力形式は人間の利用者のみを対象としています
// また、予告なく変更される可能性があります。自由に試してみてください。
fn main() -> () {
    ...
}

これは main 関数の MIR 形式です。 上記リンクで表示される MIR は最適化されています。 最適化では、StorageLive のような一部の文が削除されます。 これは、その値がコード内で一度もアクセスされないことをコンパイラが検出するために起こります。 最適化されていない MIR を表示するには、rustc [filename].rs -Z mir-opt-level=0 --emit mir を使用できます。 これには nightly ツールチェーンが必要です。

変数宣言。 少し詳しく見ていくと、多数の変数宣言から始まっていることがわかります。 それらは次のような形をしています。

let mut _0: ();                      // 戻り値のプレース
let mut _1: std::vec::Vec<i32>;      // src/main.rs:2:9: 2:16 のスコープ 0 内
let mut _2: ();
let mut _3: &mut std::vec::Vec<i32>;
let mut _4: ();
let mut _5: &mut std::vec::Vec<i32>;

MIR の変数には名前がなく、_0_1 のようなインデックスを持っていることがわかります。 また、ユーザーの変数(例: _1)と一時値(例: _2_3)が混在しています。 ユーザー定義変数は、それらに debuginfo が関連付けられているため区別できます(下記参照)。

ユーザー変数の debuginfo。 変数宣言の下には、_1 がユーザー変数を表していることを示す 唯一の手がかりがあります。

scope 1 {
    debug vec => _1;                 // src/main.rs:2:9: 2:16 のスコープ 1 内
}

debug <Name> => <Place>; アノテーションは、名前付きのユーザー変数と、 デバッガーがその変数のデータを見つけられる場所(つまりプレース)を記述します。 ここでの対応関係は単純ですが、最適化によってプレースが複雑になったり、 複数のユーザー変数が同じプレースを共有するようになったりすることがあります。 さらに、クロージャのキャプチャも同じ仕組みで記述されるため、最適化がなくても 複雑になります。例: debug x => (*((*_1).0: &T));

「scope」ブロック(例: scope 1 { .. })は、ソースプログラムの字句構造 (どの名前がいつスコープ内にあったか)を記述します。そのため、たとえばデバッガーで コードをステップ実行している場合、// in scope 0 と注釈付けされたプログラムのどの部分にも vec は存在しないことになります。

基本ブロック。 さらに読み進めると、最初の 基本ブロック が見えます (当然、実際に表示すると少し異なる場合があります。また、ここでは一部のコメントを無視しています)。

bb0: {
    StorageLive(_1);
    _1 = const <std::vec::Vec<T>>::new() -> bb2;
}

基本ブロックは、一連の と最後の ターミネータ によって定義されます。 この場合、文は 1 つです。

StorageLive(_1);

この文は、変数 _1 が「live」であることを示します。つまり、後で使われる可能性があるということです。 これは、変数 _1 の使用が終わったことを示す StorageDead(_1) 文に遭遇するまで継続します。 これらの「ストレージ文」は、LLVM によってスタック領域を割り当てるために使われます。

ブロック bb0ターミネータ は、Vec::new への呼び出しです。

_1 = const <std::vec::Vec<T>>::new() -> bb2;

ターミネータは、複数の後続を持つことができるため、文とは異なります。つまり、制御が異なる場所へ 流れる可能性があるということです。Vec::new への呼び出しのような関数呼び出しは、 巻き戻しの可能性があるため常にターミネータです。ただし、Vec::new の場合は実際には巻き戻しが 不可能であることがわかるため、後続ブロックとして bb2 だけを列挙しています。

先に進んで bb2 を見ると、次のようになっています。

bb2: {
    StorageLive(_3);
    _3 = &mut _1;
    _2 = const <std::vec::Vec<T>>::push(move _3, const 1i32) -> [return: bb3, unwind: bb4];
}

ここには 2 つの文があります。_3 一時値を導入する別の StorageLive と、 次の代入です。

_3 = &mut _1;

一般に、代入は次の形式をとります。

<Place> = <Rvalue>

プレースとは、_3_3.f*_3 のような式で、メモリ内の位置を表します。 Rvalue は値を作成する式です。この場合、rvalue は可変借用式であり、 &mut <Place> のような形をしています。そのため、rvalue の文法はおおよそ次のように定義できます。

<Rvalue>  = & (mut)? <Place>
          | <Operand> + <Operand>
          | <Operand> - <Operand>
          | ...

<Operand> = Constant
          | copy Place
          | move Place

この文法からわかるように、右辺値をネストすることはできません。右辺値は場所と定数のみを参照できます。さらに、場所を使う場合には、その場所をコピーしているのか(そのためには場所の型が T であり、T: Copy である必要があります)、それともムーブしているのか(これは任意の型の場所に対して機能します)を示します。したがって、たとえば Rust に x = a + b + c という式があった場合、それは 2 つの文と一時変数にコンパイルされます。

TMP1 = a + b
x = TMP1 + c

試して確認してみてください。ただし、オーバーフローチェックを省くために release モードにした方がよいかもしれません。)

MIR のデータ型

MIR のデータ型は、[compiler/rustc_middle/src/mir/][mir] モジュールで定義されています。前のセクションで述べた主要な概念はそれぞれ、かなり直接的な形で Rust の型に対応します。

主要な MIR データ型は [Body] です。これは単一の関数のデータを含みます(「昇格された定数」のための Mir のサブインスタンスも含みますが、それらについては以下で読むことができます)。

  • 基本ブロック: 基本ブロックはフィールド [Body::basic_blocks][basicblocks] に格納されます。これは [BasicBlockData] 構造体のベクターです。基本ブロックを直接参照する人はいません。代わりに、[BasicBlock] 値を受け渡しします。これは、このベクターへのインデックスを [newtype 化した][newtype’d]ものです。
  • は型 [Statement] で表されます。
  • 終端子は [Terminator] で表されます。
  • ローカルは [newtype 化された][newtype’d]インデックス型 [Local] で表されます。 ローカル変数のデータは [Body::local_decls][localdecls] ベクターにあります。また、戻り値を表す特殊な「ローカル」を識別するための特別な定数 [RETURN_PLACE] もあります。
  • 場所は構造体 [Place] によって識別されます。いくつかのフィールドがあります。
    • _1 のようなローカル変数
    • 射影。これは基底となる場所から「取り出される」フィールドやその他のものです。これらは [newtype 化された][newtype’d]型 [ProjectionElem] で表されます。したがって、たとえば場所 _1.f は射影であり、f が「射影要素」、_1 が基底パスです。 *_1 も射影であり、* は [ProjectionElem::Deref] 要素によって表されます。
  • 右辺値は enum [Rvalue] で表されます。
  • オペランドは enum [Operand] で表されます。

定数の表現

コードが MIR 段階に到達すると、定数は一般に 2 つの形式を取り得ます。 MIR 定数([mir::Constant])と 型システム定数([ty::Const])です。 MIR 定数はオペランドとして使われます。x + CONST では、CONST は MIR 定数です。 同様に、x + 2 では、2 は MIR 定数です。型システム定数は型システムで使われ、特に配列の長さに使われますが、const ジェネリクスにも使われます。

一般に、どちらの種類の定数も「未評価」または「すでに評価済み」であり得ます。 未評価の定数は、この結果を計算するために評価する必要があるものの DefId を単に格納します。 評価済みの定数(「値」)はすでに計算されています。その表現は型システム定数と MIR 定数で異なります。MIR 定数は mir::ConstValue に評価され、型システム定数は ty::ValTree に評価されます。

型システム定数には、const ジェネリクスをサポートするためにさらにいくつかのバリアントがあります。ローカルな const ジェネリックパラメーターを参照でき、推論の対象にもなります。 さらに、mir::Constant::Ty バリアントにより、任意の型システム定数を MIR 定数として使うことができます。これは、const ジェネリックパラメーターがオペランドとして使われるたびに発生します。

MIR 定数値

一般に、MIR 定数値(mir::ConstValue)は、ユーザーが書いた何らかの定数を評価することで計算されたものです。この const 評価 は、結果を個々のバイトという非常に低レベルな表現で生成します。この値はメモリ内に格納されるため、これを「間接」定数(mir::ConstValue::Indirect)と呼びます。

しかし、すべてをメモリ内に格納するのは非常に非効率です。そのため、mir::ConstValue には、特定の単純で一般的な値をより効率的に表現できる他のバリアントがあります。特に、Rust でリテラルとして直接書けるもの(整数、浮動小数点数、char、bool に加えて、"string literals"b"byte string literals" も含む)には、メモリ内表現の完全なオーバーヘッドを避ける最適化されたバリアントがあります。

ValTree

評価済みの型システム定数は「valtree」です。ty::ValTree データ構造により、次のものを表現できます。

  • 配列、
  • 多くの構造体、
  • タプル、
  • enum、および
  • ほとんどのプリミティブ。

この表現における最も重要な規則は、すべての値が一意に表現されなければならないということです。 言い換えると、特定の値は特定の 1 つの方法でのみ表現可能でなければなりません。たとえば、2 つの整数からなる配列を ValTree として表現する方法は 1 つしかありません。 Branch([Leaf(first_int), Leaf(second_int)]) です。 理論上は [u32; 2]u64 にエンコードできるため、単なる Leaf(bits_of_two_u32) にできるとしても、それは ValTree の正当な構築ではありません (また、それを行うのは非常に複雑なので、誰かがそうしようとする可能性は低いでしょう)。

これらの規則は、一部の値が表現できないことも意味します。型レベル定数には union は存在できません。アクティブなバリアントが不明であるため、どのように表現すべきか明確ではないからです。同様に、生ポインターを表現する方法もありません。アドレスはコンパイル時には不明であり、そのためそれらについていかなる仮定もできないからです。一方で参照は表現できます。参照の等価性はその値に対する等価性として定義されているため、アドレスは無視し、背後にある値だけを見ます。参照のポインター値がコンパイル時に観測可能でないようにしなければなりません。そのため、&4242 とまったく同じようにエンコードします。 valtree から MIR 定数値への任意の変換では、実際の間接参照を再導入しなければなりません。コード生成時には、複数の使用箇所の間でアドレスが重複排除される場合もされない場合もあり、それは完全に任意の最適化上の選択に依存します。

結果として、ValTree のすべてのデコードは、まず型に対してマッチし、それに応じて判断することで行わなければなりません。値それ自体は、それに属する型がなければ有用な情報を何も与えません。

昇格された定数

const-eval WG の昇格に関するドキュメントを参照してください。 [mir]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/index.html [mirmanip_build]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir_build/index.html [mirmanip_transform]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir_transform/index.html [mirmanip_dataflow]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir_dataflow/index.html [Body]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Body.html [newtype’d]: ../appendix/glossary.html#newtype [basicblocks]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Body.html#structfield.basic_blocks [BasicBlock]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.BasicBlock.html [BasicBlockData]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.BasicBlockData.html [Statement]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Statement.html [Terminator]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/terminator/struct.Terminator.html [Local]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Local.html [localdecls]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Body.html#structfield.local_decls [RETURN_PLACE]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/constant.RETURN_PLACE.html [Place]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Place.html [ProjectionElem]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.ProjectionElem.html [ProjectionElem::Deref]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.ProjectionElem.html#variant.Deref [Rvalue]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.Rvalue.html [Operand]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.Operand.html [mir::Constant]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.Const.html [ty::Const]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.Const.html

MIR の構築

HIR から MIR への lowering は、次の(おそらく不完全な) 項目の一覧に対して行われます。

  • 関数とクロージャの本体
  • static 項目と const 項目の初期化子
  • enum 判別子の初期化子
  • あらゆる種類のグルーと shim
    • タプル構造体の初期化関数
    • Drop コード(Drop::drop 関数は直接呼び出されません)
    • 明示的な Drop 実装を持たない型の Drop 実装

lowering は mir_built クエリを呼び出すことでトリガーされます。 MIR ビルダーは実際には HIR を使用せず、 代わりに THIR を操作し、 THIR 式を再帰的に処理します。

lowering は、シグネチャで指定されたすべての引数に対してローカル変数を作成します。 次に、指定されたすべての束縛に対してローカル変数を作成します(例: (a, b): (i32, String))。 これは 3 つの束縛を生成し、1 つは引数用、2 つは束縛用です。 次に、 引数からフィールドを読み取り、 その値を束縛変数へ書き込むフィールドアクセスを生成します。

この初期化が完了すると、lowering は本体(Block 式)の MIR を生成する関数への再帰呼び出しをトリガーし、 その結果を RETURN_PLACE に書き込みます。

すべてを unpack! する

MIR を生成する関数は、2 つのパターンのいずれかに分類される傾向があります。 まず、その関数が文だけを生成する場合、その関数は それらの文を追加すべき基本ブロックを引数として受け取ります。 その後、通常どおり結果を返すことができます。

fn generate_some_mir(&mut self, block: BasicBlock) -> ResultType {
   ...
}

しかし、新しい基本ブロックも生成する可能性のある関数もあります。 たとえば、if foo { 22 } else { 44 } のような式を lowering するには、 小さな「ひし形のグラフ」を生成する必要があります。 この場合、関数はコードが開始する基本ブロックを受け取り、 コード生成が終了する(可能性のある)新しい基本ブロックを返します。 これを表すために BlockAnd 型が使用されます。

fn generate_more_mir(&mut self, block: BasicBlock) -> BlockAnd<ResultType> {
    ...
}

これらの関数を呼び出すときは、実質的に「カーソル」であるローカル変数 block を持つのが一般的です。 これは、新しい MIR を追加している位置を表します。 generate_more_mir を呼び出すときは、このカーソルを更新したいはずです。 これは手動でも行えますが、面倒です。

let mut block;
let v = match self.generate_more_mir(..) {
    BlockAnd { block: new_block, value: v } => {
        block = new_block;
        v
    }
};

このため、let v = unpack!(block = self.generate_more_mir(...)) と書けるマクロを提供しています。 これは新しいブロックを抽出し、 unpack! で指定した変数 block を上書きするだけです。

式を目的の MIR へ lowering する

式について望む表現には、本質的に 4 種類あります。

  • Place は既存のメモリ位置(ローカル、static、promoted)の(またはその一部の)参照です
  • RvaluePlace に代入できるものです
  • Operand は、たとえば + 演算や関数呼び出しへの引数です
  • 値のコピーを含む一時変数です

次の画像は、これらの表現間の相互作用の概要を示しています。

より詳細な図を表示するにはここをクリックしてください

まず、関数本体を Rvalue へ lowering することで、 RETURN_PLACE への代入を作成できるようにします。この Rvalue の lowering は、今度はその引数(もしあれば)を Operand へ lowering することをトリガーします。 Operand の lowering は、const オペランドを生成するか、 Place からムーブまたはコピーするため、Place の lowering をトリガーします。 Place へ lowering される式は、lowering される式に演算が含まれている場合、 一時変数の作成をトリガーすることがあります。 ここで蛇が自分の尾を噛むことになり、 そのローカルへ書き込まれる式に対して Rvalue の lowering をトリガーする必要があります。

演算子の lowering

組み込み型に対する演算子は、関数呼び出しへは lowering されません(そうすると、 トレイト impl が再びその演算自体を含むだけなので、 無限再帰呼び出しになってしまいます)。 代わりに、二項演算子、単項演算子、インデックス演算用の Rvalue があります。 これらの Rvalue は後で LLVM のプリミティブ演算または LLVM intrinsic に codegen されます。

それ以外のすべての型に対する演算子は、 その演算子に対応するトレイトの impl への関数呼び出しへ lowering されます。

lowering の種類に関係なく、演算子への引数は Operand へ lowering されます。 つまり、すべての引数は定数であるか、 ローカルまたは static のどこかにすでに存在する値を参照します。

メソッド呼び出しの lowering

メソッド呼び出しは、関数呼び出しと同じ TerminatorKind へ lowering されます。 MIR では、メソッド呼び出しと関数呼び出しの違いはもはやありません。

条件

if 条件と、フィールドを持たないバリアントを持つ enummatch 文は、 TerminatorKind::SwitchInt へ lowering されます。 取り得る各値(したがって if 条件では 01)には、 コードが継続する対応する BasicBlock があります。 分岐対象となる引数は、(ここでも)if 条件の値を表す Operand です。

パターンマッチング

フィールドを持つバリアントを持つ enummatch 文も TerminatorKind::SwitchInt へ lowering されますが、Operand は 値の判別子を見つけられる Place を参照します。 これは多くの場合、判別子を新しい一時変数へ読み込むことを伴います。

集約値の構築

あらゆる種類の集約値(例: 構造体やタプル)は Rvalue::Aggregate を介して構築されます。 すべてのフィールドは Operator へ lowering されます。 これは本質的に、 集約値の各フィールドにつき 1 つの代入文に加え、 enum の場合には判別子への代入を行うのと同等です。

MIR ビジター

MIR ビジターは、MIR を走査し、何かを探したり変更を加えたりするための便利なツールです。ビジタートレイトは the rustc_middle::mir::visit module で定義されています。これらは単一のマクロによって生成される 2 つのトレイトです。Visitor&Mir に対して動作し、共有参照を返す)と MutVisitor&mut Mir に対して動作し、可変参照を返す)です。

ビジターを実装するには、ビジターを表す型を作成する必要があります。通常、この型には MIR の処理中に必要となる何らかの状態を「保持」させます。

struct MyVisitor<...> {
    tcx: TyCtxt<'tcx>,
    ...
}

そして、その型に対して Visitor または MutVisitor トレイトを実装します。

impl<'tcx> MutVisitor<'tcx> for MyVisitor {
    fn visit_foo(&mut self, ...) {
        ...
        self.super_foo(...);
    }
}

上に示したように、impl の中では任意の visit_foo メソッド(たとえば visit_terminator)をオーバーライドして、foo が見つかるたびに実行されるコードを書くことができます。foo の内容を再帰的に走査したい場合は、super_foo メソッドを呼び出します。(注意: super_foo をオーバーライドすることはありません。)

ビジターの非常に単純な例は LocalFinder にあります。このビジターは visit_local メソッドを実装することで、並べ替えの候補になり得るローカル変数を特定します。

走査

ビジターに加えて、the rustc_middle::mir::traversal module には、MIR CFG を さまざまな標準的な順序(たとえば先行順、逆後行順など)で走査するための便利な関数が含まれています。

MIR クエリとパス

MIR を取得したい場合:

  • 関数については、optimized_mir クエリ(通常は codegen によって使用されます)または mir_for_ctfe クエリ(通常はコンパイル時関数評価、すなわち CTFE によって使用されます)を使用できます。
  • promoted については、promoted_mir クエリを使用できます。

これらは、最終的な最適化済み MIR を返します。外部の def-id については、単に他のクレートのメタデータから MIR を読み取ります。しかしローカルの def-id については、そのクエリは 上流クエリのパイプラインを要求することで、最適化済み MIR を構築します1。 各クエリには一連のパスが含まれます。 このセクションでは、それらのクエリとパスがどのように動作するか、またそれらをどのように拡張できるかを説明します。

与えられた def-id D に対して最適化済み MIR を生成するために、optimized_mir(D) は、 それぞれがクエリごとにグループ化された、複数のパス群を通過します。 各パス群は、lint、解析、変換、または最適化を行うパスで構成されます。 各クエリは、型チェックやその他の目的のために MIR ダイアレクトへアクセスできる、 有用な中間地点を表します。

  • mir_built(D) – 構築直後の初期 MIR を返します。
  • mir_const(D) – MIR が const qualification の準備を完了するように、 いくつかの単純な変換パスを適用します。
  • mir_promoted(D) - promoted 可能な一時値を別個の MIR 本体へ抽出し、さらに MIR が 借用検査の準備を完了するようにします。
  • mir_drops_elaborated_and_const_checked(D) - 借用検査を実行し、主要な 変換パス(drop elaboration など)を実行して、MIR が最適化の準備を完了するようにします。
  • optimized_mir(D) – 有効化されているすべての最適化を実行し、最終状態に到達します。

パスの実装と登録

MirPass は MIR を処理するコード片であり、通常はその過程で何らかの形で MIR を変換します。 ただし、lint(たとえば CheckPackedRefCheckConstItemMutationFunctionItemReferences。これらは MirLint を実装します)や 最適化(たとえば SimplifyCfgRemoveUnneededDrops)のような、他の処理を行う場合もあります。ほとんどの MIR パスは rustc_mir_transform クレートで定義されていますが、MirPass トレイト自体は rustc_middle クレートにあり、基本的には主要なメソッドが 1 つ、 run_pass だけで構成されています。これは単に &mut Body(および tcx)を受け取ります。 したがって、MIR はインプレースで変更されます(これは効率を保つのに役立ちます)。

MIR パスの基本的な例は RemoveStorageMarkers です。これは MIR を走査し、codegen 中に出力されない場合はすべての storage mark を削除します。 そのソースからわかるように、MIR パスはまず ダミー型、つまりフィールドを持たない構造体を定義することで定義されます。

#![allow(unused)]
fn main() {
pub struct RemoveStorageMarkers;
}

これに対して MirPass トレイトを実装します。その後、このパスを mir_builtoptimized_mir などのクエリにある適切なパスのリストへ挿入できます。 (これが最適化である場合は、optimized_mir のリストに入れるべきです。)

単純な MIR パスのもう 1 つの例は CleanupPostBorrowck です。これは MIR を走査し、コード生成に関連しないすべての文を削除します。 そのソースからわかるように、これはまずダミー型、つまりフィールドを持たない 構造体を定義することで定義されます。

#![allow(unused)]
fn main() {
pub struct CleanupPostBorrowck;
}

これに対して MirPass トレイトを実装します。

#![allow(unused)]
fn main() {
impl<'tcx> MirPass<'tcx> for CleanupPostBorrowck {
    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
        ...
    }
}
}

このパスは mir_drops_elaborated_and_const_checked クエリ内で登録します。 (これが最適化である場合は、optimized_mir のリストに入れるべきです。)

パスを書いている場合、おそらく MIR visitor を使いたくなるでしょう。MIR visitor は、何かを検索したり小さな 編集を加えたりするために、MIR のすべての部分を走査する便利な方法です。

スティール

中間クエリ mir_const()mir_promoted() は、 tcx.alloc_steal_mir() を使って割り当てられた &'tcx Steal<Body<'tcx>> を返します。 これは、その結果が後続のクエリによって stolen される可能性があることを示します。これは MIR のクローンを避けるための最適化です。stolen された結果を使用しようとすると、 コンパイラで panic が発生します。したがって、MIR 処理パイプラインにおける 依存関係を考慮せずに、これらの中間クエリから誤って読み取らないようにすることが重要です。

このスティール機構があるため、処理パイプラインの特定のフェーズにある MIR が stolen される前に、それを読み取りたい可能性があるすべての者が すでに読み取りを完了していることを保証するよう、注意が必要です。

具体的には、mir_promoted(D) の結果にアクセスしたい クエリ foo(D) がある場合、foo(D) はまず mir_const(D) クエリを呼び出す必要があります。 これにより、その結果を直接必要としていなくても、 それを強制的に実行できます。

この機構は少し危ういものです。より洗練された 代替案についての議論が rust-lang/rust#41710 にあります。

概要

以下は、MIR 処理パイプラインにおけるスティール依存関係の概要です2

flowchart BT
  mir_for_ctfe* --borrow--> id40
  id5 --steal--> id40

  mir_borrowck* --borrow--> id3
  id41 --steal part 1--> id3
  id40 --steal part 0--> id3

  mir_const_qualif* -- borrow --> id2
  id3 -- steal --> id2

  id2 -- steal --> id1

  id1([mir_built])
  id2([mir_const])
  id3([mir_promoted])
  id40([mir_drops_elaborated_and_const_checked])
  id41([promoted_mir])
  id5([optimized_mir])

  style id1 fill:#bbf
  style id2 fill:#bbf
  style id3 fill:#bbf
  style id40 fill:#bbf
  style id41 fill:#bbf
  style id5 fill:#bbf

濃い色のスタジアム形クエリ(例: mir_built)は パイプライン内の主要なクエリであり、一方で薄い色の長方形クエリ(例: mir_const_qualif*3)は &'tcx Steal<Body<'tcx>> から結果を読み取る必要がある後続のクエリです。 スティール機構では、依存ツリー内で同じまたはより高い位置にあるスタジアム形クエリが 実行される前に、長方形クエリを実行しなければなりません。

例として、MIR の const qualification を考えてみましょう。これは、mir_const クエリによって生成された結果を読み取ろうとします。しかし、その結果はパイプラインのどこかの時点で mir_promoted クエリによって奪取されます。mir_promoted が一度でも問い合わせられる前であれば、mir_const_qualif クエリの呼び出しは成功します。なぜなら、mir_constSteal 結果を(初めて問い合わせられた場合は)生成し、(複数回問い合わせられた場合は)キャッシュし、その結果はまだ奪取されていないからです。mir_promoted が問い合わせられた後は、その結果は奪取されているため、結果を読み取るために mir_const_qualif クエリを呼び出すとパニックが発生します。

したがって、この奪取メカニズムでは、mir_promoted は実際に奪取する前に必ず mir_const_qualif* クエリが呼び出されることを保証すべきです。これにより、読み取りがすでに行われていることが保証されます(クエリはメモ化されますので、同じクエリを 2 回実行すると、2 回目は単にキャッシュから読み込まれます)。


  1. クエリの一般的な概念については、クエリの章を参照してください。

  2. mir_promoted クエリはタプル (&'tcx Steal<Body<'tcx>>, &'tcx Steal<IndexVec<Promoted, Body<'tcx>>>) を返し、promoted_mir は part 1(&'tcx Steal<IndexVec<Promoted, Body<'tcx>>>)を steal し、mir_drops_elaborated_and_const_checked は part 0(&'tcx Steal<Body<'tcx>>)を steal します。そして、それらのスティールは互いに無関係であり、 つまり個別に実行できます。

  3. クエリにおける * 接尾辞は、同じ接頭辞を持つ一連のクエリを表すことに注意してください。 たとえば、mir_borrowck*mir_borrowckmir_borrowck_const_arg、および mir_borrowck_opt_const_arg を表します。

インラインアセンブリ

概要

rustc におけるインラインアセンブリは、主に asm! マクロ呼び出しを受け取り、それを コンパイラのすべてのレイヤーを通して LLVM コード生成までつなぎ込むことを中心にしています。 さまざまな段階を通じて、 InlineAsm は一般に 3 つのコンポーネントで構成されます。

  • テンプレート文字列。これは InlineAsmTemplatePiece の配列として格納されます。 各要素はリテラル、またはオペランドのプレースホルダーのいずれかを表します (フォーマット文字列と同様です)。

    #![allow(unused)]
    fn main() {
    pub enum InlineAsmTemplatePiece {
        String(String),
        Placeholder { operand_idx: usize, modifier: Option<char>, span: Span },
    }
    }
  • asm! へのオペランドのリスト(in, [late]out, in[late]out, sym, const)。 これらは lowering の各段階で異なる形で表現されますが、 共通のパターンに従います。

    • inoutinout はすべて、関連付けられたレジスタクラス(reg) または明示的なレジスタ("eax")を持ちます。
    • inout には 2 つの形式があります。 1 つは読み取りと書き込みの両方が行われる単一の式を持つ形式で、 もう 1 つは入力部分と出力部分に別々の 2 つの式を持つ形式です。
    • outinout には late フラグ(lateout / inlateout)があり、この出力に対して レジスタアロケータが入力レジスタを再利用してよいことを示します。
    • outinout の分割バリアントでは、出力に _ を指定できます。 これは出力が破棄されることを意味します。 これはアセンブリコード用のスクラッチレジスタを割り当てるために使用されます。
    • const は無名定数を参照し、 一般にはインライン const のように動作します。
    • sym はパス式のみを受け付けるため、少し特殊です。 そのパスは static または fn を指していなければなりません。
  • asm! マクロの末尾で設定されるオプション。 rustc にとって特に関心があるものは、 asm!() ではなく ! を返すようにする NORETURN と、 フォーマット文字列の解析を無効にする RAW だけです。 残りのオプションは、ほとんど処理されずに LLVM にそのまま渡されます。

    #![allow(unused)]
    fn main() {
    bitflags::bitflags! {
        pub struct InlineAsmOptions: u16 {
            const PURE = 1 << 0;
            const NOMEM = 1 << 1;
            const READONLY = 1 << 2;
            const PRESERVES_FLAGS = 1 << 3;
            const NORETURN = 1 << 4;
            const NOSTACK = 1 << 5;
            const ATT_SYNTAX = 1 << 6;
            const RAW = 1 << 7;
            const MAY_UNWIND = 1 << 8;
        }
    }
    }

AST

InlineAsm は、AST では ast::InlineAsm の式として表現されます。

asm! マクロは rustc_builtin_macros で実装されており、InlineAsm AST ノードを出力します。 テンプレート文字列は fmt_macros を使用して解析され、 位置指定オペランドと名前付きオペランドは明示的なオペランドインデックスに解決されます。 ターゲット情報はマクロ呼び出しでは利用できないため、 レジスタとレジスタクラスの検証は AST lowering まで延期されます。

HIR

InlineAsm は、HIR では hir::InlineAsm の式として表現されます。

AST lowering では、InlineAsmRegOrRegClassSymbol から実際のレジスタまたは レジスタクラスに変換されます。 テンプレート文字列のプレースホルダーに修飾子が指定されている場合、それらは そのオペランド型に対して許可されている集合に照らして検証されます。 最後に、入力と出力の明示的なレジスタについて、 競合(同じレジスタが異なるオペランドに使用されていること)がないかチェックされます。

型検査

各レジスタクラスには、それとともに使用できる型の許可リストがあります。 すべてのオペランドの型が決定された後、 intrinsicck パスはこれらの型が許可リストに含まれていることをチェックします。 また、分割された inout オペランドの型に互換性があること、および const オペランドが整数または浮動小数点数であることもチェックします。 渡された型に基づいて、オペランドにテンプレート修飾子を 使用すべき場合には、必要に応じて提案が出力されます。

THIR

InlineAsm は、THIR では InlineAsmExpr の式として表現されます。

HIR と比較した唯一の重要な変更は、Sym が、exprfnLiteral ZST である SymFn、 または staticDefId を指す SymStatic のいずれかに lowering されていることです。

MIR

InlineAsm は、MIR では TerminatorKind::InlineAsm バリアントTerminator として表現されます。

THIR lowering の一部として、InOut および SplitInOut オペランドは、 個別の in_valueout_place を持つ分割形式に lowering されます。

意味的には、InlineAsm ターミネータは Call ターミネータに似ていますが、 Call が単一の戻り場所出力しか持たないのに対して、複数の出力場所を持つ点が異なります。

コード生成

オペランドは、LLVM コード生成に渡される前にもう一度 lowering されます。 これは rustc_codegen_ssaInlineAsmOperandRef によって表現されます。

オペランドは、次のように LLVM オペランドおよび制約コードに lowering されます。

  • out および inout オペランドの出力部分は、LLVM の要求どおり最初に追加されます。 late 出力オペランドには制約コードの先頭に = が追加され、 非 late 出力オペランドには制約コードの先頭に =& が追加されます。
  • in オペランドは通常どおり追加されます。
  • inout オペランドは対応する出力オペランドに結び付けられます。
  • sym オペランドは、"s" 制約を使用して、 関数ポインタまたはポインタとして渡されます。
  • const オペランドは文字列にフォーマットされ、テンプレート文字列に直接挿入されます。

テンプレート文字列は LLVM 形式に変換されます。

  • $ 文字は $$ としてエスケープされます。
  • const オペランドは文字列に変換され、直接挿入されます。
  • プレースホルダーは ${X:M} としてフォーマットされます。 ここで、X はオペランドインデックス、M は修飾子文字です。 修飾子は Rust 形式から LLVM 形式に変換されます。

さまざまなオプションは、clobber 制約または LLVM 属性に変換されます。 詳細については RFC を参照してください。

LLVM は特定の制約コードに対して受け付ける型について、かなり厳しい場合があることに注意してください。 そのため、サポートされている型との間で変換を挿入する必要があることがあります。 各レジスタクラスでサポートされる型の詳細については、 LLVM のターゲット固有の ISelLowering.cpp ファイルを参照してください。

新しいアーキテクチャのサポート追加

アーキテクチャにインラインアセンブリのサポートを追加することは、ほとんどの場合、そのアーキテクチャのレジスタと レジスタクラスを定義することです。 レジスタクラスのすべての定義は、 compiler/rustc_target/asm/ にあります。

さらに、これらのレジスタクラスを LLVM 制約コードへ lowering する処理を compiler/rustc_codegen_llvm/asm.rs に実装する必要があります。 新しいアーキテクチャを追加する場合は、必ず LLVM のソースコードと相互参照してください。

  • LLVM には、特定の制約コードで使用できる型に制限があります。 lib/Target/${ARCH}/${ARCH}ISelLowering.cppgetRegForInlineAsmConstraint 関数を参照してください。
  • LLVM は、内部使用のために特定のレジスターを予約しており、 そのためインラインアセンブリブロックの前後でそれらが適切に保存/復元されません。 これらのレジスターは、lib/Target/${ARCH}/${ARCH}RegisterInfo.cppgetReservedRegs 関数に一覧されています。 フレーム/ベースポインターのような「条件付きで」予約されるレジスターは、 Rust の目的では常に予約済みとして扱わなければなりません。これは、 関数がフレーム/ベースポインターを必要とするかどうかを事前に知ることができないためです。

テスト

インラインアセンブリ用の各種テストが利用可能です。

  • tests/assembly-llvm/asm
  • tests/ui/asm
  • tests/codegen-llvm/asm-*

インラインアセンブリでサポートされるすべてのアーキテクチャには、 tests/assembly-llvm/asm に、レジスタークラスと型のすべての組み合わせをテストする網羅的なテストがなければなりません。

コマンドライン引数

コマンドラインフラグは rustc book に文書化されています。すべての stable フラグはそこで文書化されているべきです。不安定なフラグは unstable book に文書化されているべきです。

新しいコマンドライン引数を追加する手順の詳細については、 forge guide for new options を参照してください。

ガイドライン

  • フラグは互いに直交しているべきです。たとえば、複数のアクション foobar に JSON を出力するバリアントがある場合、--foo-json--bar-json を追加するよりも、 追加の --json フラグを用意するほうが適切です。
  • no- プレフィックスを持つフラグは避けてください。代わりに、-C embed-bitcode=no のように parse_bool 関数を使用してください。
  • フラグが複数回渡された場合の挙動を検討してください。状況によっては、 値を(順番どおりに!)蓄積するべきです。他の状況では、 後続のフラグが前のフラグを上書きするべきです(たとえば、 lint レベルのフラグ)。また、一部のフラグ(-o など)は、 複数のフラグが何を意味するのかが曖昧すぎる場合には、エラーを発生させるべきです。
  • オプションには常に長く説明的な名前を付けてください。少なくとも、 コンパイラスクリプトをより理解しやすくするためにも有用です。
  • --verbose フラグは、rustc の出力に詳細情報を追加するためのものです。 たとえば、--version フラグと一緒に使用すると、 コンパイラコードのハッシュに関する情報が得られます。
  • 実験的なフラグとオプションは、-Z unstable-options フラグの背後で保護しなければなりません。

rustc_driverrustc_interface

rustc_driver

rustc_driver は本質的に rustcmain 関数です。 これは、rustc_interface crate で定義されたインターフェイスを使用して、 コンパイラのさまざまなフェーズを正しい順序で実行するための接着剤として機能します。可能な場合は、rustc_interface ではなく rustc_driver を使用することが推奨されます。

rustc_driver の主なエントリーポイントは rustc_driver::run_compiler です。 このビルダーは、rustc と同じコマンドライン引数に加えて、Callbacks の実装と、その他の任意のオプションをいくつか受け取ります。 Callbacks は、カスタムのコンパイラ設定を可能にする trait であり、 コンパイルのさまざまなフェーズの後にカスタムコードを実行できるようにもします。

rustc_interface

rustc_interface crate は、コンパイルプロセスを手動で駆動するための低レベル API を外部ユーザーに提供します。 これにより、サードパーティは、crate を解析するため、または rustc_driver が十分に柔軟でない場合(つまり、rustdoc がコードをコンパイルして出力を提供する場合)にコンパイラをアドホックにエミュレートするために、rustc の内部をライブラリとして効果的に使用できます。

rustc_interface の主なエントリーポイント(rustc_interface::run_compiler)は、コンパイラ用の設定変数と、 まだ解決されていない Compiler を受け取る closure を取ります。 run_compiler は設定から Compiler を作成し、それを closure に渡します。 closure の内部では、Compiler を使用してさまざまな関数を呼び出し、crate をコンパイルして結果を取得できます。 rustc_interface の使い方の最小限の例はこちらで確認できます。

rustc_interface を使用する際に必要となるさまざまな関数の使い方の例は、rustc_driver の実装を見ることで確認できます。 具体的には rustc_driver_impl::run_compiler です (rustc_interface::run_compiler と混同しないでください)。

警告: その性質上、コンパイラの内部 API は常に 不安定です。とはいえ、不要に壊さないようには努めています。

外部の rustc_driver

rustc_private

概要

rustc_private 機能により、外部クレートがコンパイラー内部を使用できるようになります。

公式ツールチェーンで rustc_private を使用する

rustup 経由で配布されている公式 Rust ツールチェーンで rustc_private 機能を使用する場合は、追加で 2 つのコンポーネントをインストールする必要があります。

  1. rustc-dev: コンパイラーライブラリを提供します
  2. llvm-tools: リンクに必要な LLVM ライブラリを提供します

インストール手順

rustup を使用して両方のコンポーネントをインストールします。

rustup component add rustc-dev llvm-tools

よくあるエラー

llvm-tools コンポーネントがない場合、次のようなリンクエラーが発生します。

error: linking with `cc` failed: exit status: 1
  |
  = note: rust-lld: error: unable to find library -lLLVM-{version}

カスタムツールチェーンで rustc-private を使用する

カスタムビルドしたツールチェーンや rustup を使用していない環境では、通常、追加の設定が必要です。

要件

  • LLVM ライブラリがシステムのライブラリ検索パスで利用可能である必要があります
  • LLVM のバージョンは Rust ツールチェーンのビルドに使用されたものと一致している必要があります

トラブルシューティング手順

  1. LLVM がインストールされ、アクセス可能であることを確認します
  2. ライブラリパスが設定されていることを確認します。
    export LD_LIBRARY_PATH=/path/to/llvm/lib:$LD_LIBRARY_PATH
    
  3. LLVM のバージョンが Rust ツールチェーンと互換性があることを確認します

ツリー外プロジェクト向けに rust-analyzer を設定する

rustc_private クレートを使用するツリー外プロジェクトを開発している場合、これらのクレートを認識するように rust-analyzer を設定できます。

設定手順

  1. エディター設定で rust-analyzer.rustc.source"discover" に設定します。

    VS Code の場合は、rust_analyzer_settings.json に次を追加します。

    {
        "rust-analyzer.rustc.source": "discover"
    }
    
  2. rustc_private を使用するすべてのクレートの Cargo.toml に次を追加します。

    [package.metadata.rust-analyzer]
    rustc_private = true
    

この設定により、rust-analyzer はツリー外プロジェクト内の rustc_private クレートを適切に認識し、IDE サポートを提供できます。

rustc_private の nightly ドキュメントを取得する

最新の nightly

最新の nightly では、rustc-docs コンポーネントをインストールし、ブラウザーで直接開くことができます。

rustup component add rustc-docs
rustup doc --rustc-docs

注: rustc-docs コンポーネントは最近の nightly ツールチェーンでのみ利用可能であり、すべての nightly 日付に存在するとは限りません。これは PR #75560(2020 年 8 月)で初めて導入されました。

古い nightly

古い nightly のコンパイラー内部に依存している場合、その特定の nightly の内部ドキュメントを参照したいことがあります。 これを行う唯一の方法は、ローカルでドキュメントを生成することです。 たとえば、nightly-2025-11-08 のドキュメントを取得するには、次のようにします。

その nightly の Git コミットハッシュを取得します。

rustup toolchain install nightly-2025-11-08
rustc +nightly-2025-11-08 --version --verbose

出力には、正確なソースリビジョンを識別する commit-hash 行が含まれます。 そのコミットで rust-lang/rust をチェックアウトし、その後 コンパイラードキュメント の手順に従ってください。

追加リソース

  • GitHub Issue #137421 では、rustc_private のリンカーエラーは llvm-tools がインストールされていないために発生することが多いと説明されています

例: rustc_driver による型チェック

rustc_driver を使用すると、コンパイルのさまざまな段階で Rust コードを操作できます。

式の型を取得する

式の型を取得するには、after_analysis コールバックを使用して TyCtxt を取得します。

// Tested with nightly-2025-03-28

#![feature(rustc_private)]

extern crate rustc_ast;
extern crate rustc_ast_pretty;
extern crate rustc_data_structures;
extern crate rustc_driver;
extern crate rustc_error_codes;
extern crate rustc_errors;
extern crate rustc_hash;
extern crate rustc_hir;
extern crate rustc_interface;
extern crate rustc_middle;
extern crate rustc_session;
extern crate rustc_span;

use std::io;
use std::path::Path;
use std::sync::Arc;

use rustc_ast_pretty::pprust::item_to_string;
use rustc_driver::{Compilation, run_compiler};
use rustc_interface::interface::{Compiler, Config};
use rustc_middle::ty::TyCtxt;

struct MyFileLoader;

impl rustc_span::source_map::FileLoader for MyFileLoader {
    fn file_exists(&self, path: &Path) -> bool {
        path == Path::new("main.rs")
    }

    fn read_file(&self, path: &Path) -> io::Result<String> {
        if path == Path::new("main.rs") {
            Ok(r#"
fn main() {
    let message = "Hello, World!";
    println!("{message}");
}
"#
            .to_string())
        } else {
            Err(io::Error::other("oops"))
        }
    }

    fn read_binary_file(&self, _path: &Path) -> io::Result<Arc<[u8]>> {
        Err(io::Error::other("oops"))
    }
}

struct MyCallbacks;

impl rustc_driver::Callbacks for MyCallbacks {
    fn config(&mut self, config: &mut Config) {
        config.file_loader = Some(Box::new(MyFileLoader));
    }

    fn after_crate_root_parsing(
        &mut self,
        _compiler: &Compiler,
        krate: &mut rustc_ast::Crate,
    ) -> Compilation {
        for item in &krate.items {
            println!("{}", item_to_string(&item));
        }

        Compilation::Continue
    }

    fn after_analysis(&mut self, _compiler: &Compiler, tcx: TyCtxt<'_>) -> Compilation {
        // Iterate over the top-level items in the crate, looking for the main function.
        for id in tcx.hir_free_items() {
            let item = &tcx.hir_item(id);
            // Use pattern-matching to find a specific node inside the main function.
            if let rustc_hir::ItemKind::Fn { body, .. } = item.kind {
                let expr = &tcx.hir_body(body).value;
                if let rustc_hir::ExprKind::Block(block, _) = expr.kind {
                    if let rustc_hir::StmtKind::Let(let_stmt) = block.stmts[0].kind {
                        if let Some(expr) = let_stmt.init {
                            let hir_id = expr.hir_id; // hir_id identifies the string "Hello, world!"
                            let def_id = item.hir_id().owner.def_id; // def_id identifies the main function
                            let ty = tcx.typeck(def_id).node_type(hir_id);
                            println!("{expr:#?}: {ty:?}");
                        }
                    }
                }
            }
        }

        Compilation::Stop
    }
}

fn main() {
    run_compiler(
        &[
            // The first argument, which in practice contains the name of the binary being executed
            // (i.e. "rustc") is ignored by rustc.
            "ignored".to_string(),
            "main.rs".to_string(),
        ],
        &mut MyCallbacks,
    );
}

例: rustc_interface を通じて診断を取得する

rustc_interface を使用すると、本来 stderr に出力される診断を インターセプトできます。

診断を取得する

コンパイラから診断を取得するには、 診断をバッファーに出力するように rustc_interface::Config を構成し、 各アイテムに対して TyCtxtEnsureOk::typeck を実行します。

// Tested with nightly-2025-03-28

#![feature(rustc_private)]

extern crate rustc_data_structures;
extern crate rustc_driver;
extern crate rustc_error_codes;
extern crate rustc_errors;
extern crate rustc_hash;
extern crate rustc_hir;
extern crate rustc_interface;
extern crate rustc_session;
extern crate rustc_span;

use std::sync::{Arc, Mutex};

use rustc_errors::emitter::Emitter;
use rustc_errors::registry::Registry;
use rustc_errors::translation::Translate;
use rustc_errors::{DiagInner, FluentBundle};
use rustc_session::config;
use rustc_span::source_map::SourceMap;

struct DebugEmitter {
    source_map: Arc<SourceMap>,
    diagnostics: Arc<Mutex<Vec<DiagInner>>>,
}

impl Translate for DebugEmitter {
    fn fluent_bundle(&self) -> Option<&FluentBundle> {
        None
    }

    fn fallback_fluent_bundle(&self) -> &FluentBundle {
        panic!("this emitter should not translate message")
    }
}

impl Emitter for DebugEmitter {
    fn emit_diagnostic(&mut self, diag: DiagInner, _: &Registry) {
        self.diagnostics.lock().unwrap().push(diag);
    }

    fn source_map(&self) -> Option<&SourceMap> {
        Some(&self.source_map)
    }
}

fn main() {
    let buffer: Arc<Mutex<Vec<DiagInner>>> = Arc::default();
    let diagnostics = buffer.clone();
    let config = rustc_interface::Config {
        opts: config::Options::default(),
        // This program contains a type error.
        input: config::Input::Str {
            name: rustc_span::FileName::Custom("main.rs".into()),
            input: "
fn main() {
    let x: &str = 1;
}
"
                .into(),
        },
        crate_cfg: Vec::new(),
        crate_check_cfg: Vec::new(),
        output_dir: None,
        output_file: None,
        file_loader: None,
        lint_caps: rustc_hash::FxHashMap::default(),
        psess_created: Some(Box::new(|parse_sess| {
            parse_sess.dcx().set_emitter(Box::new(DebugEmitter {
                source_map: parse_sess.clone_source_map(),
                diagnostics,
            }));
        })),
        register_lints: None,
        override_queries: None,
        make_codegen_backend: None,
        expanded_args: Vec::new(),
        ice_file: None,
        track_state: None,
        using_internal_features: &rustc_driver::USING_INTERNAL_FEATURES,
    };
    rustc_interface::run_compiler(config, |compiler| {
        let krate = rustc_interface::passes::parse(&compiler.sess);
        rustc_interface::create_and_enter_global_ctxt(&compiler, krate, |tcx| {
            // Iterate all the items defined and perform type checking.
            tcx.par_hir_body_owners(|item_def_id| {
                tcx.ensure_ok().typeck(item_def_id);
            });
        });
        // If the compiler has encountered errors when this closure returns, it will abort (!) the program.
        // We avoid this by resetting the error count before returning
        compiler.sess.dcx().reset_err_count();
    });
    // Read buffered diagnostics.
    buffer.lock().unwrap().iter().for_each(|diagnostic| {
        println!("{diagnostic:#?}");
    });
}

エラーとlint

rustc が優れたエラーメッセージを持つように、多くの労力が注がれてきました。 この章では、コンパイラからコンパイルエラーとlintを発行する方法について説明します。

診断の構造

診断エラーの主な部分は次のとおりです。

error[E0000]: main error message
  --> file.rs:LL:CC
   |
LL | <code>
   | -^^^^- secondary label
   |  |
   |  primary label
   |
   = note: note without a `Span`, created with `.note`
note: sub-diagnostic message for `.span_note`
  --> file.rs:LL:CC
   |
LL | more code
   |      ^^^^
  • レベル(errorwarning など)。メッセージの重大度を示します。 (診断レベルを参照)
  • コード(たとえば「mismatched types」の場合は E0308)。 これは、エラーコードインデックスにある問題の拡張説明を通じて、ユーザーが現在のエラーについてさらに情報を得るのに役立ちます。 すべての診断にコードがあるわけではありません。 たとえば、lintによって作成された診断にはコードがありません。
  • メッセージ。 問題の主な説明です。 単独でも意味が通じるように、一般的で、それ自体で完結しているべきです。
  • 診断ウィンドウ。 これにはいくつかのものが含まれます。
    • プライマリspanの開始位置のパス、行番号、列。
    • 影響を受けるユーザーのコードとその周辺。
    • ユーザーのコードの下に表示されるプライマリspanとセカンダリspan。 これらのspanには、必要に応じて1つ以上のラベルを含めることができます。
      • プライマリspanには、問題を説明するのに十分なテキストがあるべきです。 それだけが表示される場合(たとえばIDE内)でも意味が通じるようにするためです。 これは「空間認識的」(コードを指し示す)なので、通常はエラーメッセージよりも簡潔にできます。
      • 複数のspanラベルが重なった場合に出力が煩雑になることが予想されるなら、出力を適切に調整するのがよい考えです。 たとえば、if/else arms have incompatible types エラーは、アームがすべて同じ行にあるかどうか、いずれかのアームが空かどうか、またそれらのどのケースにも該当しないかに応じて、異なるspanを使用します。
  • サブ診断。 どのエラーも、エラーの主要部分に似た複数のサブ診断を持つことができます。 これらは、説明の順序がコードの順序と対応しない場合に使用されます。 説明の順序が「順不同」でよい場合は、通常そのほうが冗長でないため、メイン診断でセカンダリラベルを活用することが推奨されます。

テキストは事実を淡々と述べるものにし、複数の文が_必要_でない限り、大文字化やピリオドは避けるべきです。

error: the fobrulator needs to be krontrificated

メッセージやラベルにコードまたは識別子を表示する必要がある場合は、バッククォートで囲むべきです。

error: the identifier `foo.bar` is invalid

エラーコードと説明

ほとんどのエラーには、関連付けられたエラーコードがあります。 エラーコードは、エラーを発生させる方法の例と、そのエラーについての詳細な情報を含む長文の説明にリンクされています。 これらは --explain フラグ、またはエラーインデックスから確認できます。

一般的なルールとして、説明がエラー自体よりも多くの情報を提供するなら、そのエラーにコード(関連付けられた説明付き)を与えてください。 多くの場合、発行されるエラー自体にすべての情報を入れるほうが適切です。 しかし、そうするとエラーが冗長になってしまう場合や、発生条件が多すぎてすべてのケースに役立つ情報をエラーに含められない場合があります。そのような場合は、説明を追加するのがよい考えです。1 いつものように、確信が持てない場合はレビュー担当者に聞いてください!

関連付けられたエラーコードを持つ新しいエラーを追加することに決めた場合は、そのプロセスに関するガイドと重要な詳細について、このセクションを読んでください。

lintと固定診断

一部のメッセージは、ユーザーがレベルを制御できるlintsを介して発行されます。 ほとんどの診断はハードコードされており、ユーザーはそのレベルを制御できません。

通常、診断を「固定」にすべきかlintにすべきかは明白ですが、いくつか曖昧な領域もあります。

いくつか例を示します。

  • 借用チェッカーのエラー: これらは固定エラーです。 ユーザーは、借用チェッカーを黙らせるためにこれらの診断のレベルを調整することはできません。
  • デッドコード: これはlintです。 ユーザーはおそらく自分のクレート内にデッドコードを望まないでしょうが、これをハードエラーにすると、リファクタリングや開発が非常に苦痛になります。
  • これらを固定エラーにするとあまりにも多くの破壊的影響を引き起こすと判断されたため、代わりに警告が発行されます。 そして最終的には固定(ハード)エラーに変更されます。

ハードコードされた警告(span_warn のようなメソッドを使用するもの)は、通常のコードでは避け、代わりにlintを使用することを推奨します。 CLIフラグに関する警告など、一部の場合にはハードコードされた警告を使用する必要があります。

固定エラーの代わりにエラーレベルのlintをいつ使用するかの指針については、以下の deny lintレベルを参照してください。

診断出力スタイルガイド

  • 平易で簡潔な英語で書いてください。 メッセージが、しばらく掃除されていない可能性のある小さな画面に表示されたとき、 一晩パーティーをしてベッドから出てきたばかりの普通のプログラマーに理解できないなら、 それは複雑すぎます。
  • ErrorWarningNote、および Help メッセージは小文字で始め、 句読点で終わらないようにします。
  • エラーメッセージは簡潔にするべきです。 ユーザーはこれらのエラーメッセージを何度も目にし、 より詳細な説明は --explain フラグで確認できます。 とはいえ、理解しにくいほど簡潔にしすぎないでください。
  • “illegal” という語は無効です。 代わりに “invalid” またはより具体的な語を使ってください。
  • エラーは、それが発生したコードの範囲を記録するべきです(これを簡単に行うには rustc_errors::DiagCtxtspan_* メソッド、または diagnostic struct の #[primary_span] を使用します)。 また、その範囲が大きすぎない場合は、エラーの原因となった他の範囲も note してください。
  • 範囲付きのメッセージを出力するときは、問題を示すのに十分な範囲のうち、 可能な限り最小の範囲に縮小するようにしてください
  • 同じエラーに対して複数のエラーメッセージを出力しないようにしてください。 これには重複の検出が必要になる場合があります。
  • コンパイラーが特定のエラーメッセージに必要な情報を十分に持っていない場合は、 ライブラリコードに新しい属性を追加し、より多くの情報を追加できるようにするため、 コンパイラーチームに相談してください。 たとえば、#[rustc_on_unimplemented] を参照してください。 これらのアノテーションが利用可能な場合は使用してください!
  • Rust の学習曲線はかなり急であり、 コンパイラーメッセージは重要な学習ツールであることを念頭に置いてください。
  • コンパイラーについて話すときは、Rustrustc ではなく、the compiler と呼んでください。
  • 項目のリストを書くときは、Oxford comma を使用してください。

Lint の命名

RFC 0344 によると、lint 名は以下のガイドラインに従って一貫しているべきです。

基本ルールは次のとおりです。lint 名は、“allow lint-name” または “allow lint-name items” として読んだときに意味が通るべきです。 たとえば、“allow deprecated items” と “allow dead_code” は意味が通りますが、“allow unsafe_block” は文法的に正しくありません(複数形であるべきです)。

  • Lint 名は、チェック対象の悪いものを表すべきです。たとえば deprecated のようにして、 #[allow(deprecated)] (items) が正しく読めるようにします。 したがって、ctypes は適切な名前ではなく、improper_ctypes が適切です。

  • 任意の項目に適用される lint(安定性 lint など)は、 何をチェックするかだけを述べるべきです。deprecated_items ではなく deprecated を使用してください。 これにより lint 名を短く保てます。 (繰り返しますが、“allow lint-name items” と考えてください。)

  • lint が特定の文法上の分類に適用される場合は、その分類に言及し、 複数形を使用してください。unused_variable ではなく unused_variables を使用します。 これにより、#[allow(unused_variables)] が正しく読めるようになります。

  • コードの不要、未使用、または無用な側面を検出する lint には、 unused という用語を使用するべきです。例: unused_importsunused_typecasts

  • 関数名に対して行うのと同じ方法で snake case を使用してください。

Diagnostic レベル

さまざまな diagnostic レベルのガイドライン:

  • error: プログラムが無効である、またはプログラマーが特定の warning をエラーにすると決めたために、 コンパイラーがプログラムをコンパイルできなくなる問題を検出したときに出力されます。

  • warning: コンパイラーがプログラムについて何かおかしな点を検出したときに出力されます。 warning fatigue を避けるため、warning を追加するときは注意するべきであり、 コードに実際には問題がない false-positive を避けるべきです。 warning を発行するのが適切な場合の例をいくつか示します。

    • deprecated な項目を置き換える、または Result を使用するなど、ユーザーが対応を取るべき状況で、 それ以外の点ではコンパイルを妨げない場合。
    • コードの意味論に影響を与えずに削除できる不要な構文。 たとえば、未使用コードや不要な unsafe
    • 正しくない、危険、または紛らわしい可能性が非常に高いものの、 言語としては技術的に許可しており、エラーにする準備や確信がまだ十分ではないコード。 例として、unused_comparisons(範囲外の比較)や bindings_with_variant_name(ユーザーはおそらくパターン内で束縛を作成するつもりではなかった)があります。
    • Future-incompatible lint。過去に何かが偶然または誤って受け入れられていたが、 拒否するとエコシステムに過剰な破壊的変更を引き起こす場合です。
    • スタイル上の選択。 たとえば、camel case または snake case、あるいは 2018 edition における dyn trait warning です。 これらを追加する基準は高く、例外的な状況でのみ使用するべきです。 その他のスタイル上の選択は、 デフォルトで allow の lint にするか、Clippy や rustfmt のような他のツールの一部にするべきです。
  • help: error または warning の後に出力され、問題の解決方法についてユーザーに追加情報を提供します。 これらのメッセージには、ツールによる自動的なソース修正を導くために、 suggestion 文字列と rustc_errors::Applicability 信頼度レベルが含まれることがよくあります。 詳細については、Suggestions セクションを参照してください。

    error または warning の部分では、問題の修正方法を提案するべきではありません。 “help” sub-diagnostic のみが提案するべきです。

  • note: より多くのコンテキストを提供し、 warning または error の原因となった追加の状況やコードの箇所を特定するために出力されます。 たとえば、borrow checker は以前の競合する borrow を note します。

    helpnote: help は、問題を修正するためにユーザーが加えられる可能性のある変更を示すために使用するべきです。 note は、それ以外のすべてに使用するべきです。 たとえば、他のコンテキスト、情報や事実、読むべきオンラインリソースなどです。

lint レベルと混同しないでください。lint レベルのガイドラインは次のとおりです。

  • forbid: Lint のデフォルトを forbid にするべきではありません。

  • deny: error diagnostic レベルと同等です。 いくつかの例:

    • warning レベルから昇格した future-incompatible または edition ベースの lint。
    • 正しくないという信頼度が非常に高いが、 それでも通過を許可するための逃げ道を残したいもの。
  • warn: warning diagnostic レベルと同等です。 ガイドラインについては上記の warning を参照してください。

  • allow: デフォルトを allow にするべき lint の種類の例:

    • lint の false positive 率が高すぎる。
    • lint が独断的すぎる。
    • lint が実験的である。
    • lint が、通常は強制されないものを強制するために使用される。 たとえば、unsafe_code lint は unsafe code の使用を防ぐために使用できます。

lint レベルに関する詳細情報は、rustc bookreference にあります。

役立つヒントとオプション

エラーの発生源を見つける

特定のエラーがどこで発行されているかを見つける主な方法は3つあります:

  • エラーメッセージ/ラベルの一部、またはエラーコードを grep します。 これは通常うまく機能し、単純明快ですが、比較的深いコールスタックの背後で、 エラーを発行するコードがエラーを構築するコードから離れている場合があります。 それでも、状況を把握するための良い方法です。

  • nightly 専用フラグ -Z treat-err-as-bug=1 を指定して rustc を呼び出すと、 最初に発行されたエラーが内部コンパイラエラーとして扱われ、エラーが発行された地点の スタックトレースを取得できます。 それより後のエラーでトリガーしたい場合は、1 を別の値に変更してください。

    この方法には制限があります:

    • コンパイル済みの rustc でインライン化されるため、一部の呼び出しはスタックトレースから省かれます。
    • エラーの_構築_が、それが_発行_される場所から大きく離れています。 これは grep の方法で直面した問題と似ています。 場合によっては、複数のエラーを順番に発行するためにバッファリングします。
  • -Z track-diagnostics を指定して rustc を呼び出すと、エラーとともにエラーの作成場所が出力されます。

通常の開発プラクティスが適用されます。つまり、物事がどの順序で起きているかを把握するために、 debug!() 文を慎重に使用したり、デバッガーを使用してブレークポイントをトリガーしたりします。

Span

Span は、コンパイル対象のコード内の位置を表すために rustc で使用される主要なデータ構造です。 Span は HIR と MIR のほとんどの構成要素に付加され、より情報量の多いエラーレポートを可能にします。

SpanSourceMap で検索して、span_to_snippetSourceMap 上の その他の同様のメソッドでエラーを表示する際に有用な「スニペット」を取得できます。

エラーメッセージ

rustc_errors クレートは、エラーの報告に使用されるユーティリティの大部分を定義しています。

診断は、Diagnostic トレイトを実装する型として実装できます。 これは、診断を発行するロジックとメインのコードパスの分離を強制するため、 新しい診断では推奨されます。 あまり複雑でない診断については、Diagnostic トレイトを derive できます。 Diagnostic structs を参照してください。 トレイト実装内では、以下で説明する API を通常どおり使用できます。

DiagCtxt には、エラーを作成して発行するメソッドがあります。 これらのメソッドは通常、span_errstruct_span_errspan_warn などの名前を持ちます。 多数のメソッドがあり、警告、エラー、致命的エラー、提案など、さまざまな種類の「エラー」を発行します。

一般に、このようなメソッドには2つのクラスがあります。エラーを直接発行するものと、 何を発行するかをより細かく制御できるものです。 たとえば、span_err は、指定された Span で指定されたエラーメッセージを発行しますが、 struct_span_err は代わりに Diag を返します。

これらのメソッドのほとんどは文字列を受け取りますが、新しい診断では、翻訳可能な診断用の型付き識別子を 使用することが推奨されます(Translation を参照)。

Diag では、emit メソッドを呼び出してエラーを発行する前に、 関連する注記や提案をエラーに追加できます。 (Diag を発行するか cancel するかのいずれも行わなかった場合、ICE が発生します。) 何ができるかの詳細については、docs を参照してください。

// `Diag` を取得します。これはまだエラーを発行しません。
let mut err = sess.dcx.struct_span_err(sp, fluent::example::example_error);

// 場合によっては、マクロ生成コードに関する奇妙なエラーの出力を避けるため、
// `sp` がマクロによって生成されたものかどうかを確認する必要があります。

if let Ok(snippet) = sess.source_map().span_to_snippet(sp) {
    // スニペットを使用して修正案を生成します
    err.span_suggestion(suggestion_sp, fluent::example::try_qux_suggestion, format!("qux {}", snippet));
} else {
    // スニペットを生成できなかった場合は、具体的な「提案」の代わりに
    // 「help」メッセージを発行します。実際には、ここに到達する可能性は低いです。
    err.span_help(suggestion_sp, fluent::example::qux_suggestion);
}

// エラーを発行します
err.emit();
example-example-error = oh no! this is an error!
  .try-qux-suggestion = try using a qux here
  .qux-suggestion = you could use a qux here instead

提案

ユーザーに、そのコードが正確に_なぜ_間違っているのかを伝えるだけでなく、 多くの場合、それをどのように修正すればよいかを伝えることも可能です。 この目的のために、Diag は構造化された提案 API を提供しており、端末上でコードの提案を見やすく整形したり、 --error-format json フラグが渡された場合には rustfix のようなツールが利用できる JSON として出力したりします。

すべての提案が機械的に適用されるべきとは限らず、提案されたコードに対する信頼度には、 高いもの(Applicability::MachineApplicable)から低いもの(Applicability::MaybeIncorrect)まであります。 レベルを選択するときは保守的にしてください。 提案を行うには、Diagspan_suggestion メソッドを使用します。 最後の引数は、その提案が機械的に適用可能かどうかをツールに示すヒントを提供します。

提案は、現在の内容を置き換える対応するコードとともに、1つ以上の span を指します。

提案に付随するメッセージは、次のコンテキストで理解可能でなければなりません:

  • 独立したサブ診断として表示される(これがデフォルトの出力です)
  • 影響を受ける span を指すラベルとして表示される(冗長性に関するいくつかのヒューリスティックが満たされた場合に自動的に行われます)
  • 内容のない help サブ診断として表示される(提案がテキストから明らかであるものの、ツールがそれらを適用できるようにしたい場合に使用されます)
  • 表示されない(_非常に_明らかな場合に使用されますが、それでもツールがそれらを適用できるようにしたい場合です)

たとえば、qux の提案を機械的に適用可能にするには、次のようにします:

let mut err = sess.dcx.struct_span_err(sp, fluent::example::message);

if let Ok(snippet) = sess.source_map().span_to_snippet(sp) {
    err.span_suggestion(
        suggestion_sp,
        fluent::example::try_qux_suggestion,
        format!("qux {}", snippet),
        Applicability::MachineApplicable,
    );
} else {
    err.span_help(suggestion_sp, fluent::example::qux_suggestion);
}

err.emit();

これは次のようなエラーを出力することがあります

$ rustc mycode.rs
error[E0999]: oh no! this is an error!
 --> mycode.rs:3:5
  |
3 |     sad()
  |     ^ help: try using a qux here: `qux sad()`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0999`.

提案が複数行にまたがる場合や、提案が複数ある場合など、場合によっては 提案は独立して表示されます:

error[E0999]: oh no! this is an error!
 --> mycode.rs:3:5
  |
3 |     sad()
  |     ^
help: try using a qux here:
  |
3 |     qux sad()
  |     ^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0999`.

Applicability に指定可能な値は次のとおりです:

  • MachineApplicable: 機械的に適用できます。
  • HasPlaceholders: 提案にプレースホルダーテキストが含まれているため、 機械的に適用できません。 例: try adding a type: `let x: <type>`
  • MaybeIncorrect: 提案が適切な場合もそうでない場合もあるため、 機械的に適用できません。
  • Unspecified: 上記のどのケースに該当するかわからないため、 機械的に適用できません。

提案スタイルガイド

  • 提案は疑問文にするべきではありません。 特に、「〜の意味でしたか」のような表現は避けるべきです。 特定の提案がなぜ行われているのかが明確でない場合があります。 そのような場合は、その提案が何であるかを率直に示すほうが適切です。

    Foo の意味でしたか」と 「似た名前の構造体があります: Foo」を比較してください。

  • メッセージには、「次の」「示すように」などのフレーズを含めるべきではありません。 何について述べているかは span で伝えてください。

  • メッセージには、「xyz を行うには、使用してください」や「xyz を行うには、abc を使用してください」のような追加の指示を含めてもかまいません。

  • メッセージには関数、変数、型の名前を含めてもかまいませんが、式全体は避けてください。

リント

コンパイラのリント基盤は rustc_middle::lint モジュールで定義されています。

リントはいつ実行されるか?

リントが役割を果たすために必要な情報に応じて、異なるリントは異なるタイミングで実行されます。 一部のリントはパスにグループ化され、そのパス内のリントは 1 つのビジターを通じてまとめて処理されます。 パスには次のようなものがあります:

  • 展開前パス: マクロ展開の前の AST ノードに対して動作します。 一般的には避けるべきです。

    • 例: keyword_idents は、将来のエディションでキーワードになる 識別子をチェックしますが、マクロ内で使用される識別子に影響を受けやすいです。
  • 早期リントパス: マクロ展開と名前解決の後、 AST lowering の直前の AST ノードに対して動作します。 これらのリントは、純粋に構文的なリント用です。

    • 例: unused_parens リントは、if 条件のように不要な状況で かっこで囲まれた式をチェックします。
  • 後期リントパス: 解析の終盤(借用チェックなどの後)の HIR ノードに対して動作します。これらのリントでは完全な型情報を利用できます。 ほとんどのリントは後期リントです。

    • 例: invalid_value リント(明らかに無効な未初期化値をチェックするリント)は、 型が未初期化のままにできるかどうかを判定するために型情報を必要とするため、 後期リントです。
  • MIR パス: MIR ノードに対して動作します。 これは他のパスとまったく同じではありません。 MIR ノードに対して動作するリントには、実行のための独自のメソッドがあります。

    • 例: arithmetic_overflow リントは、オーバーフローする可能性のある 定数値を検出したときに生成されます。

ほとんどのリントはパスシステムを通じてうまく機能し、かなり単純なインターフェイスと 簡単な統合方法(たいていは特定の check 関数を実装するだけ)を備えています。 しかし、一部のリントは コンパイラ内の特定のコードパス上に置くほうが書きやすい場合があります。 たとえば、unused_mut リントは、借用チェッカー内のいくつかの 情報と状態を必要とするため、借用チェッカー内で実装されています。

これらのインラインリントの一部は、リントシステムの準備が整う前に発火します。 それらのリントはバッファリングされ、リントシステムの準備が整う コンパイラの後続フェーズまで保持されます。 コンパイラの初期段階でのリントを参照してください。

リント定義用語

リントは LintStore を介して管理され、さまざまな方法で登録されます。 次の用語は、一般に登録方法に基づいた リントのさまざまなクラスを指します。

  • 組み込み リントはコンパイラソース内で定義されます。
  • ドライバー登録 リントは、外部ドライバーによってコンパイラドライバーが作成されるときに 登録されます。 これは、たとえば Clippy が使用する仕組みです。
  • ツール リントは、clippy::rustdoc:: のようなパス接頭辞を持つリントです。
  • 内部 リントは、rustc ソースツリー自体でのみ実行される rustc:: スコープのツールリントであり、通常の組み込みリントと同様にコンパイラソースで定義されます。

リント登録の詳細は、LintStore の章を参照してください。

リントを宣言する

組み込みのコンパイラリントは rustc_lint クレートで定義されています。 他のクレートで実装する必要があるリントは rustc_lint_defs で定義されます。 可能であれば、リントは rustc_lint に配置することを優先すべきです。 利点の 1 つは、依存関係のルートに近いため、作業がはるかに高速になる可能性があることです。

各リントは、LintPass trait を実装する struct によって実装されます (リントを実行するのに最適なタイミングに応じて、より具体的なリントパスのトレイトである EarlyLintPass または LateLintPass のいずれかを実装することもできます)。 このトレイト実装により、リンターが AST を走査するときに、特定の構文構造をチェックできます。 その後、コンパイルエラーと非常によく似た方法でリントを出力できます。

また、declare_lint! マクロを使用して、特定のリントのメタデータも宣言します。 このマクロには、名前、デフォルトレベル、短い説明、およびさらにいくつかの詳細が含まれます。

リントとリントパスはコンパイラに登録する必要があることに注意してください。

たとえば、次のリントは while true { ... } の使用をチェックし、代わりに loop { ... } を使用することを提案します。

// `WHILE_TRUE` という名前のリントを宣言する
declare_lint! {
    WHILE_TRUE,

    // デフォルトで warn
    Warn,

    // この文字列はリントの説明
    "`while true { }` の代わりに `loop { }` の使用を提案する"
}

// これは、関連付けられたリントのリストを提供する struct とリントパスを宣言します。
// コンパイラは現在、関連付けられたリントを直接使用していません(たとえば、
// パスを実行しないようにしたり、パスが適切なリントのセットを出力するかを
// チェックしたりするためには使用していません)。ただし、ここで正確にしておくのはよいことです。
// なぜなら、リントパス上の get_lints メソッド(このマクロが生成するもの)経由で
// リントを登録するようになる可能性があるためです。
declare_lint_pass!(WhileTrue => [WHILE_TRUE]);

// `WhileTrue` リント用のヘルパー関数。
// 任意の数の括弧をたどり、最初の非括弧式を返す。
fn pierce_parens(mut expr: &ast::Expr) -> &ast::Expr {
    while let ast::ExprKind::Paren(sub) = &expr.kind {
        expr = sub;
    }
    expr
}

// `EarlyLintPass` には多くのメソッドがあります。このリントでは必要なのがそれだけなので、
// `check_expr` の定義のみをオーバーライドしていますが、独自のリントでは
// 他のメソッドをオーバーライドできます。メソッドの完全な一覧については
// rustc のドキュメントを参照してください。
impl EarlyLintPass for WhileTrue {
    fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &ast::Expr) {
        if let ast::ExprKind::While(cond, ..) = &e.kind
            && let ast::ExprKind::Lit(ref lit) = pierce_parens(cond).kind
            && let ast::LitKind::Bool(true) = lit.kind
            && !lit.span.from_expansion()
        {
            let condition_span = cx.sess.source_map().guess_head_span(e.span);
            cx.struct_span_lint(WHILE_TRUE, condition_span, |lint| {
                lint.build(fluent::example::use_loop)
                    .span_suggestion_short(
                        condition_span,
                        fluent::example::suggestion,
                        "loop".to_owned(),
                        Applicability::MachineApplicable,
                    )
                    .emit();
            })
        }
    }
}
example-use-loop = 無限ループを `loop {"{"} ... {"}"}` で表す
  .suggestion = `loop` を使用する

エディションでゲートされるリント

新しいエディションでリントの動作を変更したい場合があります。 これを行うには、 declare_lint! の呼び出しに遷移を追加するだけです。

declare_lint! {
    pub ANONYMOUS_PARAMETERS,
    Allow,
    "匿名パラメーターを検出する",
    Edition::Edition2018 => Warn,
}

これにより、ANONYMOUS_PARAMETERS リントは 2015 エディションではデフォルトで allow になりますが、 2018 エディションではデフォルトで warn になります。

詳細については、エディション固有のリントを参照してください。

機能でゲートされるリント

ある機能に属するリントは、その機能がクレートで有効になっている場合にのみ使用可能であるべきです。 これをサポートするために、リント宣言には次のように機能ゲートを含めることができます。

declare_lint! {
    pub SOME_LINT_NAME,
    Warn,
    "新しく有用だが、機能でゲートされるリント",
    @feature_gate = sym::feature_name;
}

将来非互換リント

コンパイラ内での future-incompatible という用語の使用は、rustc がコンパイラのユーザーに公開しているものよりも少し広い意味を持ちます。

rustc 内部では、将来非互換リントは、ユーザーが書いたコードが将来コンパイルできなくなる可能性があることをユーザーに知らせるためのものです。 一般に、将来非互換コードは次の 2 つの理由で存在します。

  • ユーザーが、コンパイラが誤って受け入れた不健全なコードを書いた場合。 健全性の穴を修正することは Rust の後方互換性保証の範囲内ですが (ユーザーのコードは壊れます)、このリントは、コードがどのエディションを使用しているかに関係なく、 今後の rustc のあるバージョンでこれが起こることをユーザーに警告するためにあります。これが、rustc が 「future incompatible」としてユーザーに排他的に公開している意味です。
  • ユーザーが、今後のエディションでコンパイルできなくなるまたは意味が変わるコードを書いた場合。 これらはしばしば「エディションリント」と呼ばれ、ユーザーがクレートのエディションを更新した場合に 壊れるコードをリントするために使用される、さまざまな「エディション互換性」リントグループ (例: rust_2021_compatibility)で一般的に見られます。 詳細については、移行リントを参照してください。

将来非互換リントは、追加の「フィールド」@future_incompatible を使用して宣言する必要があります。

declare_lint! {
    pub ANONYMOUS_PARAMETERS,
    Allow,
    "匿名パラメーターを検出する",
    @future_incompatible = FutureIncompatibleInfo {
        reason: fcw!(EditionError 2018 "slug-of-edition-guide-page")
    };
}

将来非互換の変更が発生する理由を説明する reason フィールドに注意してください。 これにより、ユーザーが受け取る診断メッセージが変更されるだけでなく、リントがどのリントグループに追加されるかも決定されます。 上記の例では、このリントは「エディションリント」です (その「reason」が EditionError であるため)。これは、匿名パラメーターの使用が Rust 2018 以降では コンパイルできなくなることをユーザーに示します。

LintStore::register_lints 内では、future_incompatible フィールドを持つリントは (その reason がエディションに結び付いている場合)エディションベースのリントグループ、または future_incompatibility リントグループのいずれかに配置されます。

declare_lint! マクロでサポートされていないオプションの組み合わせが必要な場合は、 いつでも declare_lint! マクロを変更してそれをサポートできます。

リントの名前変更または削除

リントの名前が不適切である、またはもはや不要であると判断された場合、 そのリントは名前変更または削除として登録する必要があります。これにより、ユーザーが古いリント名を使用しようとすると警告が発生します。 名前変更/削除を宣言するには、rustc_lint::register_builtins 関数のコードに store.register_renamed または store.register_removed を含む行を追加します。

store.register_renamed("single_use_lifetime", "single_use_lifetimes");

リントグループ

リントはグループ単位で有効にできます。 これらのグループは、rustc_lint::lib 内の register_builtins 関数で宣言されます。 add_lint_group! マクロは新しいグループを宣言するために使用されます。

例:

add_lint_group!(sess,
    "nonstandard_style",
    NON_CAMEL_CASE_TYPES,
    NON_SNAKE_CASE,
    NON_UPPER_CASE_GLOBALS);

これは、列挙されたリントを有効にする nonstandard_style グループを定義します。 ユーザーは、ソースコード内の #![warn(nonstandard_style)] 属性を使うか、 コマンドラインで -W nonstandard-style を渡すことで、これらのリントを有効にできます。

一部のリントグループは LintStore::register_lints で自動的に作成されます。 たとえば、 理由が FutureIncompatibilityReason::FutureReleaseError である FutureIncompatibleInfo とともに宣言された任意のリント (declare_lint!@future_incompatible が使われた場合のデフォルト)は、 future_incompatible リントグループに追加されます。 エディションにも独自のリントグループ (例: rust_2021_compatibility)があり、指定されたエディションで破壊的変更となる 将来互換性のないコードを通知する任意のリントに対して自動的に生成されます。

コンパイラの早い段階でのリント

場合によっては、リントシステムが初期化される前 (例: パース中やマクロ展開中)に実行されるリントを定義する必要があります。 これは、警告、エラー、または何も出力しないかを判断するには、 リントレベルが計算済みである必要があるため問題になります。

この問題を解決するために、リントシステムが処理されるまでリントをバッファリングします。 SessionParseSess はどちらも、 後で使うためにリントをバッファリングできる buffer_lint メソッドを持っています。 リントシステムは、後でバッファリングされたリントの処理を自動的に引き受けます。

したがって、コンパイルの早い段階で実行されるリントを定義するには、 通常どおりリントを定義しつつ、buffer_lint でそのリントを呼び出します。

コンパイラのさらに早い段階でのリント

パーサー(rustc_ast)は、他のどの rustc* クレートにも依存できないという点で興味深い存在です。 特に、コンパイラのリントインフラストラクチャがすべて定義されている rustc_middle::lintrustc_lint に依存できません。 これは厄介です!

これを解決するために、rustc_ast は独自のバッファリングされたリント型を定義しており、ParseSess::buffer_lint はそれを使用します。 マクロ展開後、これらのバッファリングされたリントは、 コンパイラの残りの部分で使用される Session::buffered_lints に投入されます。

JSON 診断出力

コンパイラは、診断を JSON オブジェクトとして出力するための --error-format json フラグを受け付けます(cargo fix などのツールのため)。 これは次のようになります。

$ rustc json_error_demo.rs --error-format json
{"message":"cannot add `&str` to `{integer}`","code":{"code":"E0277","explanation":"\nYou tried to use a type which doesn't implement some trait in a place which\nexpected that trait. Erroneous code example:\n\n```compile_fail,E0277\n// here we declare the Foo trait with a bar method\ntrait Foo {\n    fn bar(&self);\n}\n\n// we now declare a function which takes an object implementing the Foo trait\nfn some_func<T: Foo>(foo: T) {\n    foo.bar();\n}\n\nfn main() {\n    // we now call the method with the i32 type, which doesn't implement\n    // the Foo trait\n    some_func(5i32); // error: the trait bound `i32 : Foo` is not satisfied\n}\n```\n\nIn order to fix this error, verify that the type you're using does implement\nthe trait. Example:\n\n```\ntrait Foo {\n    fn bar(&self);\n}\n\nfn some_func<T: Foo>(foo: T) {\n    foo.bar(); // we can now use this method since i32 implements the\n               // Foo trait\n}\n\n// we implement the trait on the i32 type\nimpl Foo for i32 {\n    fn bar(&self) {}\n}\n\nfn main() {\n    some_func(5i32); // ok!\n}\n```\n\nOr in a generic context, an erroneous code example would look like:\n\n```compile_fail,E0277\nfn some_func<T>(foo: T) {\n    println!(\"{:?}\", foo); // error: the trait `core::fmt::Debug` is not\n                           //        implemented for the type `T`\n}\n\nfn main() {\n    // We now call the method with the i32 type,\n    // which *does* implement the Debug trait.\n    some_func(5i32);\n}\n```\n\nNote that the error here is in the definition of the generic function: Although\nwe only call it with a parameter that does implement `Debug`, the compiler\nstill rejects the function: It must work with all possible input types. In\norder to make this example compile, we need to restrict the generic type we're\naccepting:\n\n```\nuse std::fmt;\n\n// Restrict the input type to types that implement Debug.\nfn some_func<T: fmt::Debug>(foo: T) {\n    println!(\"{:?}\", foo);\n}\n\nfn main() {\n    // Calling the method is still fine, as i32 implements Debug.\n    some_func(5i32);\n\n    // This would fail to compile now:\n    // struct WithoutDebug;\n    // some_func(WithoutDebug);\n}\n```\n\nRust only looks at the signature of the called function, as such it must\nalready specify all requirements that will be used for every type parameter.\n"},"level":"error","spans":[{"file_name":"json_error_demo.rs","byte_start":50,"byte_end":51,"line_start":4,"line_end":4,"column_start":7,"column_end":8,"is_primary":true,"text":[{"text":"    a + b","highlight_start":7,"highlight_end":8}],"label":"no implementation for `{integer} + &str`","suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"the trait `std::ops::Add<&str>` is not implemented for `{integer}`","code":null,"level":"help","spans":[],"children":[],"rendered":null}],"rendered":"error[E0277]: cannot add `&str` to `{integer}`\n --> json_error_demo.rs:4:7\n  |\n4 |     a + b\n  |       ^ no implementation for `{integer} + &str`\n  |\n  = help: the trait `std::ops::Add<&str>` is not implemented for `{integer}`\n\n"}
{"message":"aborting due to previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"error: aborting due to previous error\n\n"}
{"message":"For more information about this error, try `rustc --explain E0277`.","code":null,"level":"","spans":[],"children":[],"rendered":"For more information about this error, try `rustc --explain E0277`.\n"}

出力は一連の行であり、その各行は JSON オブジェクトですが、残念ながら、一連の行全体をまとめたものは 有効な JSON ではないため、そのようなものを必要とするツールや小技(たとえば python3 -m json.tool へのパイプ)を妨げます。 (これは LSP のパフォーマンス上の目的で意図的だったのではないかとも推測されます。 各行/オブジェクトをフラッシュされ次第送信できるようにするためでしょうか?)

また、“rendered” フィールドにも注意してください。これは “human” 出力を 文字列として含みます。これは、UI テストが構造化された JSON を利用しつつ、 すべてを 2 回コンパイルしなくても “human” 出力(まあ、色は_なしで_)を 確認できるように導入されました。

“human” 可読形式と JSON 形式エミッターは、どちらも rustc_errors の下にあります。どちらも rustc_ast クレートから rustc_errors クレートへ移動されました。

JSON エミッターは、JSON シリアライズ用に 独自の Diagnostic 構造体 (およびサブ構造体)を定義しています。 これを errors::Diag と混同しないでください!

#[rustc_on_unimplemented]

この属性により、トレイト定義は、実装が 期待されたものの見つからなかった場合にエラーメッセージを変更できます。 属性内の文字列リテラルはフォーマット文字列であり、名前付きパラメーターでフォーマットできます。 許可されるパラメーターについては、後述の「フォーマット」セクションを参照してください。

#[rustc_on_unimplemented(message = "an iterator over \
    elements of type `{A}` cannot be built from a \
    collection of type `{Self}`")]
trait MyIterator<A> {
    fn next(&mut self) -> A;
}

fn iterate_chars<I: MyIterator<char>>(i: I) {
    // ...
}

fn main() {
    iterate_chars(&[1, 2, 3][..]);
}

ユーザーがこれをコンパイルすると、次のように表示されます。

error[E0277]: an iterator over elements of type `char` cannot be built from a collection of type `&[{integer}]`
  --> src/main.rs:13:19
   |
13 |     iterate_chars(&[1, 2, 3][..]);
   |     ------------- ^^^^^^^^^^^^^^ the trait `MyIterator<char>` is not implemented for `&[{integer}]`
   |     |
   |     required by a bound introduced by this call
   |
note: required by a bound in `iterate_chars`

以下の内容を変更できます:

  • メインエラーメッセージ (message)
  • ラベル (label)
  • note(複数可) (note)

たとえば、次の属性は

#[rustc_on_unimplemented(message = "message", label = "label", note = "note")]
trait MyIterator<A> {
    fn next(&mut self) -> A;
}

次の出力を生成します。

error[E0277]: message
  --> <file>:10:19
   |
10 |     iterate_chars(&[1, 2, 3][..]);
   |     ------------- ^^^^^^^^^^^^^^ label
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `MyIterator<char>` is not implemented for `&[{integer}]`
   = note: note
note: required by a bound in `iterate_chars`

ここまで説明した機能は、 #[diagnostic::on_unimplemented] でも利用できます。 可能であれば、代わりにそちらを使用するべきです。

フィルタリング

より対象を絞ったエラーメッセージを可能にするために、これらのフィールドの適用を on でフィルタリングできます。

以下の boolean フラグでフィルタリングできます:

  • crate_local: トレイト境界が満たされなくなる原因のコードが、 ユーザーのクレートの一部かどうか。 これは、依存関係の変更を必要とするコード変更を提案しないようにするために使用されます。
  • direct: これが派生したオブリゲーションではなく、ユーザー指定のオブリゲーションかどうか。
  • from_desugaring: たとえば ?try ブロックのような、 何らかのデシュガリングの中にいるかどうか。 このフラグもマッチ対象にできます。以下を参照してください。

name = "value" を使用して、以下の名前と値にマッチできます:

  • cause: ObligationCauseCode enum の 1 つのバリアントに対してマッチします。 サポートされているのは "MainFunctionType" のみです。
  • from_desugaring: DesugaringKind enum の特定のバリアントに対してマッチします。 デシュガリングは、そのバリアント名で識別されます。たとえば ? デシュガリングなら "QuestionMark"try ブロックなら "TryBlock" です。
  • Self と、トレイトの任意のジェネリック引数。たとえば Self = "alloc::string::String"Rhs="i32" です。

コンパイラーはいくつかのマッチ対象の値を提供できます。たとえば:

  • 型引数を解決した場合としない場合の両方でプリティプリントされた self_ty。
  • self_ty が型の判明している整数型である場合は "{integral}"
  • 該当する場合は "[]""[{ty}]""[{ty}; _]""[{ty}; $N]"
  • それらのスライスおよび配列への参照。
  • self が関数である場合は "fn""unsafe fn"、または "#[target_feature] fn"
  • 型が数値だが、まだ推論できていない場合は "{integer}""{float}"
  • self が ADT であることにマッチするための "{struct}""{enum}""{union}"
  • "[{integral}; _]" のような、上記の組み合わせ。

たとえば、Iterator トレイトは次のようにフィルタリングできます。

#[rustc_on_unimplemented(
    on(Self = "&str", note = "call `.chars()` or `.as_bytes()` on `{Self}`"),
    message = "`{Self}` is not an iterator",
    label = "`{Self}` is not an iterator",
    note = "maybe try calling `.iter()` or a similar method"
)]
pub trait Iterator {}

これにより、以下の出力が生成されます。

error[E0277]: `Foo` is not an iterator
 --> src/main.rs:4:16
  |
4 |     for foo in Foo {}
  |                ^^^ `Foo` is not an iterator
  |
  = note: maybe try calling `.iter()` or a similar method
  = help: the trait `std::iter::Iterator` is not implemented for `Foo`
  = note: required by `std::iter::IntoIterator::into_iter`

error[E0277]: `&str` is not an iterator
 --> src/main.rs:5:16
  |
5 |     for foo in "" {}
  |                ^^ `&str` is not an iterator
  |
  = note: call `.chars()` or `.bytes() on `&str`
  = help: the trait `std::iter::Iterator` is not implemented for `&str`
  = note: required by `std::iter::IntoIterator::into_iter`

on フィルターは、cfg 属性と同様に allanynot 述語を受け付けます。

#[rustc_on_unimplemented(on(
    all(Self = "&str", T = "alloc::string::String"),
    note = "you can coerce a `{T}` into a `{Self}` by writing `&*variable`"
))]
pub trait From<T>: Sized {
    /* ... */
}

フォーマット

文字列リテラルは、中括弧で囲まれたパラメーターを受け付けるフォーマット文字列ですが、 位置指定パラメーター、列挙されたパラメーター、フォーマット指定子は受け付けられません。 以下のパラメーター名が有効です:

  • Self と、トレイトのすべてのジェネリックパラメーター。
  • This: 属性が付いているトレイトの名前。ジェネリクスは含みません。
  • Trait: 「糖衣構文化された」トレイトの名前。 TraitRefPrintSugared を参照してください。
  • ItemContext: 現在いる hir::Node の種類。"an async block""a function""an async function" などです。

たとえば次のようなものです:

#![feature(rustc_attrs)]

#[rustc_on_unimplemented(message = "Self = `{Self}`, \
    T = `{T}`, this = `{This}`, trait = `{Trait}`, \
    context = `{ItemContext}`")]
pub trait From<T>: Sized {
    fn from(x: T) -> Self;
}

fn main() {
    let x: i8 = From::from(42_i32);
}

メッセージを次のようにフォーマットします

"Self = `i8`, T = `i32`, this = `From`, trait = `From<i32>`, context = `a function`"

  1. この経験則は、@estebank によってこちらで提案されました。

Diagnostic とサブ診断の構造体

rustc には、診断の作成に使用できる 2 つの診断トレイトがあります: DiagnosticSubdiagnostic です。

単純な診断では、 導出された impl を使用できます。たとえば #[derive(Diagnostic)] です。これらは、追加のサブ診断を追加するかどうかを決定するために多くのロジックを必要としない、単純な診断にのみ適しています。

診断がより複雑または動的な振る舞いを必要とする場合、たとえば条件付きでサブ診断を追加する、レンダリングロジックをカスタマイズする、実行時にメッセージを選択するなどの場合は、対応するトレイト(Diagnostic または Subdiagnostic)を手動で実装する必要があります。 このアプローチはより高い柔軟性を提供し、単純で静的な構造を超える診断に推奨されます。

Diagnostic は異なる言語に翻訳できます。

#[derive(Diagnostic)]

以下に示す “field already declared” 診断の[定義][defn]を考えてみましょう:

#[derive(Diagnostic)]
#[diag("field `{$field_name}` is already declared", code = E0124)]
pub struct FieldAlreadyDeclared {
    pub field_name: Ident,
    #[primary_span]
    #[label("field already declared")]
    pub span: Span,
    #[label("`{$field_name}` first declared here")]
    pub prev_span: Span,
}

Diagnostic は構造体と列挙型に対してのみ導出できます。 構造体では型に置かれる属性は、列挙型では各バリアントに置かれます(またはその逆です)。 各 Diagnostic には、構造体または各列挙型バリアントに適用される 属性 #[diag(...)] が 1 つ必要です。

エラーにエラーコード(例: “E0624”)がある場合は、code サブ属性を使用して指定できます。 code の指定は必須ではありませんが、Diag を使用する診断を Diagnostic を使用するように移植している場合は、元からコードがあったならそれを維持すべきです。

#[diag(..)] は、最初の位置引数としてメッセージを指定しなければなりません。 メッセージは英語で書かれますが、ユーザーが要求したロケールに翻訳される場合があります。 翻訳可能なエラーメッセージがどのように書かれ、どのように生成されるかの詳細については、翻訳ドキュメントを参照してください。

アノテーションのない Diagnostic のすべてのフィールドは、上記の例の field_name のように、Fluent メッセージ内で変数として利用できます。 これが望ましくない場合は、フィールドに #[skip_arg] を付けることができます。

型が Span であるフィールドに #[primary_span] 属性を使用すると、その診断のプライマリ span を示し、そこに診断のメインメッセージが付きます。

診断は単なるプライマリメッセージ以上のものです。多くの場合、ラベル、note、help メッセージ、suggestion が含まれ、それらはすべて Diagnostic に指定することもできます。

#[label]#[help]#[warning]#[note] はすべて、型が Span であるフィールドに適用できます。 これらの属性のいずれかを適用すると、その Span を持つ対応するサブ診断が作成されます。 これらの属性は、診断メッセージを引数として取ります。

Diagnostic derive で使用される場合、他の型には特殊な振る舞いがあります:

  • Option<T> に適用された属性はいずれも、その option が Some(..) の場合にのみ サブ診断を出力します。
  • Vec<T> に適用された属性はいずれも、ベクターの各要素について繰り返されます。

#[help]#[warning]#[note] は構造体自体に適用することもできます。その場合、 サブ診断が Span を持たないことを除いて、フィールドに適用した場合とまったく同じように動作します。 これらの属性は、同じ効果を得るために型 () のフィールドにも適用できます。これを Option 型と組み合わせることで、 任意の #[note]/#[help]/#[warning] サブ診断を表現できます。

suggestion は、4 つのフィールド属性のいずれかを使用して出力できます:

  • #[suggestion("message", code = "...", applicability = "...")]
  • #[suggestion_hidden("message", code = "...", applicability = "...")]
  • #[suggestion_short("message", code = "...", applicability = "...")]
  • #[suggestion_verbose("message", code = "...", applicability = "...")]

suggestion は、Span フィールドまたは (Span, MachineApplicability) フィールドのいずれかに適用しなければなりません。 他のフィールド属性と同様に、ユーザーに表示されるメッセージを指定する必要があります。 code は、置換として提案されるべきコードを指定するもので、フォーマット文字列です(例: {field_name} は、構造体の field_name フィールドの値に置き換えられます)。 applicability は、属性内で適用可能性を指定するために使用できますが、 フィールドの型に Applicability が含まれている場合は使用できません。

最終的に、Diagnostic derive は、次のような Diagnostic の実装を生成します:

impl<'a, G: EmissionGuarantee> Diagnostic<'a> for FieldAlreadyDeclared {
    fn into_diag(self, dcx: &'a DiagCtxt, level: Level) -> Diag<'a, G> {
        let mut diag = Diag::new(dcx, level, "field `{$field_name}` is already declared");
        diag.set_span(self.span);
        diag.span_label(
            self.span,
            "field already declared"
        );
        diag.span_label(
            self.prev_span,
            "`{$field_name}` first declared here"
        );
        diag
    }
}

これで診断を定義できましたが、どのように[使用][use]すればよいのでしょうか? 非常に簡単で、構造体のインスタンスを作成し、それを emit_err(または emit_warning)に渡すだけです:

tcx.dcx().emit_err(FieldAlreadyDeclared {
    field_name: f.ident,
    span: f.span,
    prev_span,
});

#[derive(Diagnostic)] のリファレンス

#[derive(Diagnostic)] は次の属性をサポートしています:

  • #[diag("message", code = "...")]
    • struct または enum variant に適用されます。
    • 必須
    • 診断に関連付けるテキストとエラーコードを定義します。
    • メッセージ(必須
    • code = "..."任意
      • エラーコードを指定します。
  • #[note("message")]任意
    • struct、または型が SpanOption<()>() の struct フィールドに適用されます。
    • note サブ診断を追加します。
    • 値は note のメッセージです。
    • Span フィールドに適用された場合、span 付き note を作成します。
  • #[help("message")]任意
    • struct、または型が SpanOption<()>() の struct フィールドに適用されます。
    • help サブ診断を追加します。
    • 値は help メッセージです。
    • Span フィールドに適用された場合、span 付き help を作成します。
  • #[label("message")]任意
    • Span フィールドに適用されます。
    • label サブ診断を追加します。
    • 値は label のメッセージです。
  • #[warning("message")]任意
    • struct、または型が SpanOption<()>() の struct フィールドに適用されます。
    • warning サブ診断を追加します。
    • 値は warning のメッセージです。
  • #[suggestion{,_hidden,_short,_verbose}("message", code = "...", applicability = "...")]任意
    • (Span, MachineApplicability) または Span フィールドに適用されます。
    • suggestion サブ診断を追加します。
    • メッセージ(必須
      • 値はユーザーに表示される suggestion メッセージです。
      • 翻訳ドキュメントを参照してください。
    • code = "..."/code("...", ...)必須
      • 置換として提案されるコードを示す 1 つまたは複数のフォーマット文字列。 複数の値は、複数の置換候補を意味します。
    • applicability = "..."任意
      • machine-applicablemaybe-incorrecthas-placeholdersunspecified のいずれかでなければならない文字列。
  • #[subdiagnostic]
    • Subdiagnostic#[derive(Subdiagnostic)] から)を実装する型に適用されます。
    • サブ診断 struct によって表されるサブ診断を追加します。
  • #[primary_span]任意
    • _Subdiagnostic 上の Span フィールドに適用されます。
    • 診断の primary span を示します。
  • #[skip_arg]任意
    • 任意のフィールドに適用されます。
    • フィールドが診断引数として提供されるのを防ぎます。

#[derive(Subdiagnostic)]

コンパイラでは、適用可能な場合に特定のサブ診断をエラーへ条件付きで追加する関数を書くことが一般的です。 多くの場合、これらのサブ診断は、診断全体を表せない場合であっても、診断 struct を使用して表現できます。 このような状況では、Subdiagnostic derive を使用して、部分的な診断(例: note、label、help、または suggestion)を struct として表すことができます。

以下に示す「expected return type」label の[定義][subdiag_defn]を考えてみます。

#![allow(unused)]
fn main() {
#[derive(Subdiagnostic)]
pub enum ExpectedReturnTypeLabel<'tcx> {
    #[label("expected `()` because of default return type")]
    Unit {
        #[primary_span]
        span: Span,
    },
    #[label("expected `{$expected}` because of return type")]
    Other {
        #[primary_span]
        span: Span,
        expected: Ty<'tcx>,
    },
}
}

Diagnostic と同様に、Subdiagnostic は struct または enum に対して derive できます。 struct の型に配置された属性は、enum では各 variant に配置されます(またはその逆)。 各 Subdiagnostic には、struct または各 variant に、次のいずれか 1 つの属性を適用する必要があります。

  • label を定義するための #[label(..)]
  • note を定義するための #[note(..)]
  • help を定義するための #[help(..)]
  • warning を定義するための #[warning(..)]
  • suggestion を定義するための #[suggestion{,_hidden,_short,_verbose}(..)]

上記はすべて、最初の位置引数として診断メッセージを提供する必要があります。 翻訳可能なエラーメッセージがどのように生成されるかについて詳しくは、翻訳ドキュメントを参照してください。

フィールド(型が Span)で #[primary_span] 属性を使用すると、 サブ診断の primary span を表します。 primary span が必要なのは label または suggestion のみで、これらは span なしにはできません。

アノテーションを持たない型/variant のすべてのフィールドは、 Fluent メッセージ内で変数として利用できます。 これが望ましくない場合、フィールドには #[skip_arg] を付与できます。

Diagnostic と同様に、SubdiagnosticOption<T> および Vec<T> フィールドをサポートします。

suggestion は、型/variant に次の 4 つの属性のいずれかを使用して出力できます。

  • #[suggestion("...", code = "...", applicability = "...")]
  • #[suggestion_hidden("...", code = "...", applicability = "...")]
  • #[suggestion_short("...", code = "...", applicability = "...")]
  • #[suggestion_verbose("...", code = "...", applicability = "...")]

suggestion には、フィールドに #[primary_span] が設定されている必要があり、次のサブ属性を持つことができます。

  • 最初の位置引数は、ユーザーに表示されるメッセージを指定します。
  • code は置換として提案されるべきコードを指定し、 Fluent 識別子ではなく、フォーマット文字列(例: {field_name} は struct の field_name フィールドの値に置き換えられます)です。
  • applicability は、属性内で applicability を指定するために使用できますが、 フィールドの型に Applicability が含まれる場合は使用できません。

applicability は、#[applicability] 属性を使用して(型が Applicability の)フィールドとして指定することもできます。

最終的に、Subdiagnostic derive は、次のような Subdiagnostic の実装を生成します。

#![allow(unused)]
fn main() {
impl<'tcx> Subdiagnostic for ExpectedReturnTypeLabel<'tcx> {
    fn add_to_diag(self, diag: &mut rustc_errors::Diagnostic) {
        use rustc_errors::{Applicability, IntoDiagArg};
        match self {
            ExpectedReturnTypeLabel::Unit { span } => {
                diag.span_label(span, "expected `()` because of default return type")
            }
            ExpectedReturnTypeLabel::Other { span, expected } => {
                diag.set_arg("expected", expected);
                diag.span_label(span, "expected `{$expected}` because of return type")
            }
        }
    }
}
}

定義後、サブ診断は、診断上の subdiagnostic 関数([example][subdiag_use_1] および [example][subdiag_use_2])へ渡すか、 診断 struct の #[subdiagnostic] アノテーション付きフィールドに割り当てることで使用できます。

引数の共有と分離

サブ診断は、情報をレンダリングする前に、独自の引数(つまり、その構造内の特定のフィールド)を Diag 構造体に追加します。 Diag 構造体はメイン診断からの引数も格納するため、サブ診断はメイン診断からの引数も使用できます。

ただし、#[derive(Subdiagnostic)] を実装することによってサブ診断がメイン診断に追加される場合、 rust-lang/rust#142724 で導入された次のルールが、 引数(つまり、Fluent メッセージで使用される変数)の処理に適用されます。

**サブ診断間の引数の分離**:
サブ診断によって設定された引数は、そのサブ診断のレンダリング中にのみ利用できます。
サブ診断がレンダリングされた後、そのサブ診断が導入したすべての引数はメイン診断から復元されます。
これにより、複数のサブ診断が互いの引数スコープを汚染しないことが保証されます。
たとえば、`Vec<Subdiag>` を使用する場合、同じ引数を反復的に何度も追加します。

**サブ診断とメイン診断間の同一引数のオーバーライド**:
サブ診断が、メイン診断にすでに存在する引数と同じ名前の引数を設定した場合、
両者がまったく同じ値でない限り、実行時にエラーが報告されます。
これには 2 つの利点があります:
- メイン診断の引数がサブ診断の属性に現れることを許可する柔軟性を維持します。
たとえば、サブ診断に属性 `#[suggestion("...", code = "{new_vis}")]` があり、`new_vis` はメイン診断構造体内のフィールドである場合です。
- メイン診断または他のサブ診断で必要とされる引数が、意図せず上書きまたは削除されることを防ぎます。

これらのルールにより、サブ診断によって注入される引数が、そのサブ診断自身のレンダリングに厳密にスコープされることが保証されます。
名前の衝突が存在する場合でも、メイン診断の引数はサブ診断のロジックによって影響を受けません。
さらに、サブ診断は必要に応じて、同じ名前のメイン診断の引数にアクセスできます。

### `#[derive(Subdiagnostic)]` のリファレンス
`#[derive(Subdiagnostic)]` は次の属性をサポートします:

- `#[label("message")]`、`#[help("message")]`、`#[warning("message")]`、または `#[note("message")]`
  - _構造体または列挙型バリアントに適用されます。
    構造体/列挙型バリアント属性とは相互に排他的です。_
  - _必須_
  - ラベル、ヘルプ、または注記を表す型を定義します。
  - メッセージ(_必須_)
    - ユーザーに表示される診断メッセージです。
    - [翻訳ドキュメント](./translation.md)を参照してください。
- `#[suggestion{,_hidden,_short,_verbose}("message", code = "...", applicability = "...")]`
  - _構造体または列挙型バリアントに適用されます。
    構造体/列挙型バリアント属性とは相互に排他的です。_
  - _必須_
  - 提案を表す型を定義します。
  - メッセージ(_必須_)
    - ユーザーに表示される診断メッセージです。
    - [翻訳ドキュメント](./translation.md)を参照してください。
  - `code = "..."`/`code("...", ...)`(_必須_)
    - 置換として提案されるコードを示す 1 つまたは複数のフォーマット文字列です。
      複数の値は、複数の可能な置換を意味します。
  - `applicability = "..."`(_任意_)
    - _フィールド上の `#[applicability]` とは相互に排他的です。_
    - 値は提案の適用可能性です。
    - 次のいずれかでなければならない文字列です:
      - `machine-applicable`
      - `maybe-incorrect`
      - `has-placeholders`
      - `unspecified`
- `#[multipart_suggestion{,_hidden,_short,_verbose}("message", applicability = "...")]`
  - _構造体または列挙型バリアントに適用されます。
    構造体/列挙型バリアント属性とは相互に排他的です。_
  - _必須_
  - 複数部分からなる提案を表す型を定義します。
  - メッセージ(_必須_): `#[suggestion]` を参照してください
  - `applicability = "..."`(_任意_): `#[suggestion]` を参照してください
- `#[primary_span]`(ラベルと提案では _必須_、それ以外では _任意_、複数部分からなる提案には適用不可)
  - _`Span` フィールドに適用されます。_
  - サブ診断のプライマリスパンを示します。
- `#[suggestion_part(code = "...")]`(_必須_、複数部分からなる提案にのみ適用可能)
  - _`Span` フィールドに適用されます。_
  - 複数部分からなる提案の 1 つの部分となるスパンを示します。
  - `code = "..."`(_必須_)
    - 値は、置換として提案されるコードを示すフォーマット文字列です。
- `#[applicability]`(_任意_、(単純および複数部分からなる)提案にのみ適用可能)
  - _`Applicability` フィールドに適用されます。_
  - 提案の適用可能性を示します。
- `#[skip_arg]`(_任意_)
  - _任意のフィールドに適用されます。_
  - フィールドが診断引数として提供されることを防ぎます。

[defn]: https://github.com/rust-lang/rust/blob/6201eabde85db854c1ebb57624be5ec699246b50/compiler/rustc_hir_analysis/src/errors.rs#L68-L77
[use]: https://github.com/rust-lang/rust/blob/f1112099eba41abadb6f921df7edba70affe92c5/compiler/rustc_hir_analysis/src/collect.rs#L823-L827

[subdiag_defn]: https://github.com/rust-lang/rust/blob/f1112099eba41abadb6f921df7edba70affe92c5/compiler/rustc_hir_analysis/src/errors.rs#L221-L234
[subdiag_use_1]: https://github.com/rust-lang/rust/blob/f1112099eba41abadb6f921df7edba70affe92c5/compiler/rustc_hir_analysis/src/check/fn_ctxt/suggestions.rs#L670-L674
[subdiag_use_2]: https://github.com/rust-lang/rust/blob/f1112099eba41abadb6f921df7edba70affe92c5/compiler/rustc_hir_analysis/src/check/fn_ctxt/suggestions.rs#L704-L707

翻訳

rustc の現在の診断翻訳インフラストラクチャ( 2024 年 10 月 時点)は、残念ながらコンパイラのコントリビューターにいくらかの摩擦を生じさせています。また、現在の インフラストラクチャは、コンパイラのコントリビューターと翻訳チームの双方のニーズにより適切に対応する再設計を ほぼ待っている状態です。 現在、進行中の 再設計提案は存在しない( 2024 年 10 月 時点)ことに注意してください!

ステータス更新については、トラッキング issue https://github.com/rust-lang/rust/issues/132181 を参照してください。

翻訳インフラは、まだ提案されていない再設計とそれに伴う手直しを待っているため、現在の翻訳インフラの使用を 必須にはしていません。 そのインフラを 使いたい 場合、または そうすることでコードがより明確になる場合は使用してください。しかし、より柔軟性が必要な場合は 翻訳インフラを迂回してください。

rustc の診断インフラストラクチャは、Fluent を使用した翻訳可能な診断をサポートしています。

翻訳可能な診断を書く

翻訳可能な診断を書く方法は 2 つあります。

  1. 単純な診断では、診断(またはサブ診断)の derive を使用します。 (「単純な」診断とは、サブ診断を発行するかどうかを決める際に多くのロジックを必要とせず、 したがって診断構造体として表現できるものです)。 診断構造体とサブ診断構造体のドキュメントを参照してください。
  2. Diag API(Diagnostic または Subdiagnostic の実装内)で型付き識別子を使用します。

翻訳可能な診断を追加または変更するとき、 翻訳について心配する必要はありません。 元の英語メッセージを更新するだけで十分です。

Fluent

Fluent は「非対称ローカライゼーション」という考え方を中心に構築されています。これは、 翻訳の表現力をソース言語(rustc の場合は英語)の文法から 切り離すことを目的としています。 翻訳以前は、rustc の診断は、 ユーザーに表示されるメッセージを構築するために補間に大きく依存していました。 補間された文字列は翻訳が困難です。自然に聞こえる 翻訳を書くには、英語の文字列よりも多い、少ない、または単に異なる補間が 必要になる場合があり、そのいずれもサポートするにはコンパイラのソースコードの変更が必要になるためです。

診断メッセージは Fluent リソースで定義されます。 特定のロケール(例: en-US)に対する Fluent リソースの結合された集合は、Fluent バンドルとして知られています。

typeck_address_of_temporary_taken = cannot take address of a temporary

上記の例では、typeck_address_of_temporary_taken は Fluent メッセージの識別子であり、英語の診断メッセージに対応しています。 別の言語のメッセージに対応する、他の Fluent リソースを書くことができます。 したがって、各診断には少なくとも 1 つの Fluent メッセージがあります。

typeck_address_of_temporary_taken = cannot take address of a temporary
    .label = temporary value

慣例として、サブ診断の診断メッセージは Fluent メッセージ上の「属性」として指定されます(追加の関連メッセージであり、 .<attribute-name> 構文で示されます)。 上記の例では、labeltypeck_address_of_temporary_taken の属性であり、この診断に追加される ラベルのメッセージに対応しています。

診断メッセージでは、型名や変数名など、 ユーザーに表示されるメッセージに追加のコンテキストを補間することがよくあります。 Fluent メッセージへの追加のコンテキストは、診断への「引数」として提供されます。

typeck_struct_expr_non_exhaustive =
    cannot create non-exhaustive {$what} using struct expression

上記の例では、Fluent メッセージは what という名前の引数を参照しており、 その引数が存在することが期待されています(診断に引数を提供する方法については後で詳しく説明します)。

Fluent とその構文の他の使用例については、Fluent のドキュメントを参照できます。

メッセージ命名のガイドライン

通常、fluent はメッセージ名内の単語を区切るために - を使用します。 しかし、 _ も fluent で受け入れられます。 Rust 側の識別子でも _ が使用されるため、 _ は Rust のユースケースにより適しています。そのため rustc 内では、単語を区切るための - は 許可されておらず、代わりに _ が推奨されます。 唯一の例外は、-passes_see_issue のようなメッセージ名における先頭の - です。

翻訳可能なメッセージを書くためのガイドライン

メッセージをさまざまな言語に翻訳可能にするには、どの言語でも必要となる すべての情報を、診断への 引数として提供しなければなりません(英語メッセージで必要な情報だけではありません)。

コンパイラチームが、さまざまな言語に翻訳するために必要なすべての 情報を備えた診断を書く経験を積むにつれて、このページは より多くのガイダンスで更新されます。 現時点では、Fluent のドキュメントに、 メッセージを異なるロケールに翻訳する優れた例と、それを行うために コードから提供する必要がある情報が掲載されています。

コンパイル時検証と型付き識別子

rustc の #[derive(Diagnostic)] マクロは、Fluent メッセージのコンパイル時検証を実行します。 Fluent リソースのコンパイル時検証は、コンパイラのビルド中に Fluent リソースからの解析エラーをすべて出力し、無効な Fluent リソースがコンパイラ内でパニックを引き起こすことを防ぎます。 コンパイル時検証は、複数の Fluent メッセージが同じ識別子を持つ場合にもエラーを出力します。

内部

翻訳をサポートするために、rustc の診断内部のさまざまな部分が変更されています。

メッセージ

rustc の従来の診断 API(例: struct_span_errnote)はすべて、 DiagMessage に変換できる任意のメッセージを受け取ります。

rustc_error_messages::DiagMessage は、従来の翻訳不可能な 診断メッセージと翻訳可能なメッセージを表現できます。 翻訳不可能なメッセージは単なる String です。 翻訳可能なメッセージは、Fluent メッセージの 識別子を持つ単なる &'static str です(場合によっては、属性を持つ追加の &'static str も含みます)。

DiagMessage と直接やり取りする必要はありません。 DiagMessage 定数は、 Fluent リソース内の各診断メッセージに対して作成される(以下でより詳しく説明します)か、または DiagMessage は 診断 derive のマクロ生成コード内で作成されます。

DiagMessage は、文字列に変換できる任意の型に対して Into を実装し、それらを翻訳不可能な診断に変換します。これにより、既存の診断呼び出しはすべて動作し続けます。

引数

メッセージ内容に補間される Fluent メッセージの追加コンテキストは、 翻訳可能な診断に提供する必要があります。

診断には、この追加のコンテキストを診断に提供するために使用できる set_arg 関数があります。

引数には、名前(前の例の “what” など)と値の両方があります。 引数の値は DiagArgValue 型を使用して表現されます。これは単なる文字列または数値です。 rustc の型は、文字列または数値への 変換を伴う IntoDiagArg を実装でき、Ty<'tcx> のような一般的な型にはすでに そのような実装があります。

set_arg の呼び出しは診断 derive によって透過的に処理されますが、 診断ビルダー API を使用する場合は手動で追加する必要があります。

Lint

このページでは、lint の登録に関する仕組みの一部と、コンパイラ内で lint をどのように実行するかについて説明します。

LintStore は中心となるインフラストラクチャであり、すべてはこれを中心に動きます。 LintStoreSession の一部として保持され、Session が作成された直後に lint の一覧で埋められます。

Lint と lint パス

コンパイラ内の lint 仕組みには、lint と lint パスという 2 つの部分があります。 残念ながら、既存のドキュメントの多くでは、これらの両方を単に「lint」と呼んでいます。

まず、lint 宣言そのものがあります。 ここで名前、デフォルトの lint レベル、その他のメタデータが定義されます。 これらは通常、declare_lint! マクロによって定義され、 最終的には型 &rustc_lint_defs::Lint の static になります (ただし、これは将来的に変わる可能性があります。 すべてのマクロと同様に、このマクロも新しいフィールドを追加するにはやや扱いにくいためです)。

マクロを使用しない直接の宣言に対しては lint を行います。

lint 宣言は「状態」を持ちません。単なるグローバルな識別子であり、 lint の説明にすぎません。 これらが(lint 名によって)二重に登録されていないことを実行時にアサートします。

lint パスは、あらゆる lint の中核です。 特に、lint と lint パスの間には 1 対 1 の関係はありません。 ある lint にはそれを発行する lint パスがまったくない場合もあれば、 多数ある場合も、1 つだけの場合もあります。コンパイラは、あるパスが特定の lint と 何らかの形で関連付けられているかどうかを追跡しません。また、多くの場合、 lint は他の処理(たとえば型チェックなど)の一部として発行されます。

登録

高レベルの概要

rustc_interface::run_compiler では、 LintStore が作成され、 すべての lint が登録されます。

lint には 3 つの「ソース」があります。

  • 内部 lint: rustc コードベースでのみ使用される lint
  • 組み込み lint: コンパイラに組み込まれており、外部ソースから提供されない lint
  • rustc_interface::Configregister_lints: 構築時にコンパイラへ渡される lint

lint は LintStore::register_lint 関数を介して登録されます。 これはどの lint についても一度だけ行われるべきであり、そうでない場合は ICE が発生します。

登録が完了すると、lint ストアを Arc に配置することで「凍結」します。

lint パスは、カテゴリのいずれか (展開前、early、late、late module)に個別に登録されます。 パスはクロージャとして登録されます。 つまり impl Fn() -> Box<dyn X> であり、ここで dyn X は early または late の lint パストレイトオブジェクトです。 lint パスを実行するときは、このクロージャを実行し、その後 lint パスメソッドを呼び出します。 lint パスメソッドは &mut self を受け取るため、内部で状態を追跡できます。

内部 lint

これらは、コンパイラや clippy のようなドライバーだけが使用する lint です。 rustc_lint::internal にあります。

このような lint の例として、lint パスが手書きではなく declare_lint_pass! マクロを使って実装されていることを確認するチェックがあります。 これは LINT_PASS_IMPL_WITHOUT_MACRO lint によって実現されています。

これらの lint の登録は rustc_lint::register_internals 関数で行われます。 この関数は、rustc_lint::new_lint_store 内で新しい lint ストアを構築するときに呼び出されます。

組み込み lint

これらは主に 2 つの場所、 rustc_lint_defs::builtinrustc_lint::builtin で説明されています。 多くの場合、前者は lint 自体の定義を提供し、 後者は lint パスの定義(および実装)を提供しますが、 常にそうであるとは限りません。

組み込み lint の登録は rustc_lint::register_builtins 関数で行われます。 内部 lint の場合と同様に、 これは rustc_lint::new_lint_store の内部で行われます。

ドライバー lint

これらは rustc_interface::Configregister_lints フィールドを介してドライバーから提供される lint であり、このフィールドはコールバックです。 ドライバーは、それがすでに設定されていることを検出した場合、 追加するコールバック内で現在設定されている関数を呼び出すべきです。 ドライバーがこれにアクセスする最善の方法は、 Callbacks::config 関数をオーバーライドすることです。これにより、Config 構造体へ直接アクセスできます。

コンパイラの lint パスは 1 つのパスにまとめられる

コンパイラ内では、パフォーマンス上の理由から、通常は何十もの lint パスを登録しません。代わりに、各種類につき 1 つの lint パス (たとえば BuiltinCombinedModuleLateLintPass)を持ち、それが内部で 個々の lint パスをすべて呼び出します。これは、そうすることで、 各(多くの場合は空の)トレイトメソッドについて動的ディスパッチではなく 静的ディスパッチの利点を得られるためです。

理想的には、これはコードの理解を複雑にするため、行う必要がない方が望ましいです。 しかし、現在の型消去された lint ストアのアプローチでは、 パフォーマンス上の理由からそうすることが有益です。

エラーコード

通常、各エラーメッセージには E0123 のような一意のコードを割り当てるようにしています。 これらのコードは、各 crate にある diagnostics.rs ファイル内でコンパイラに定義されており、 基本的にはマクロで構成されています。 すべてのエラーコードには関連する説明があります。新しいエラーコードにはそれを含める必要があります。 すべての_歴史的な_(もはや出力されない)エラーコードに説明があるわけではないことに注意してください。

エラーの説明

説明は Markdown で書かれており(構文の詳細については CommonMark Spec を参照)、そのすべてが rustc_error_codes crate でリンクされています。 長いエラーコードをどのようにフォーマットし、記述するかの詳細については RFC 1567 を読んでください。

2026 年 3 月時点で、この大部分が古くなった RFC を、新しいより柔軟な標準に置き換える取り組み[^new-explanations]があります。

エラーの説明は、エラーメッセージを補足し、そのエラーが_なぜ_発生するのかについて詳細を提供するべきです。 ユーザーが即席の修正をコピー&ペーストできるだけでは役に立ちません。 説明は、ユーザーが自分のコードをコンパイラが受け入れられない理由を理解する助けになるべきです。 Rust は役に立つエラーメッセージを誇りとしており、長文形式の説明も例外ではありません。 ただし、エラーの説明が 全面的に見直される1までは、具体的にどのように書くべきかについてはやや未確定です。 いつものように、レビュー担当者に尋ねるか、Rust Zulip で周囲に尋ねてください。

新しいコードを割り当てる

エラーコードは compiler/rustc_error_codes に格納されています。

新しいエラーを作成するには、まず次に利用可能なコードを見つける必要があります。 それを見つけるには、rustc_error_codes/src/lib.rs を開き、 error_codes! マクロ宣言の末尾までスクロールします。

ここで、使用中の最も大きいエラーコードが E0805 であることがわかった場合、おそらく E0806 が必要になるでしょう。 確実にするために、rg E0806 を実行して確認してください。参照は見つからないはずです。

エラーに対する拡張説明を書く必要があり、 それは rustc_error_codes/src/error_codes/E0806.md に入ります。 エラーを登録するには、次のようにコードを(適切な数値順で) error_codes! マクロに追加します。

#![allow(unused)]
fn main() {
macro_rules! error_codes {
...
0806,
}
}

実際にエラーを発行するには、struct_span_code_err! マクロを使用できます。

#![allow(unused)]
fn main() {
struct_span_code_err!(self.dcx(), // ここに `DiagCtxt` への何らかのパス
                 span, // ソース内の任意の span
                 E0806, // 新しいエラーコード
                 fluent::example::an_error_message)
    .emit() // 実際にエラーを発行する
}

注記やその他のスニペットを追加したい場合は、.emit() を呼び出す前にメソッドを呼び出すことができます。

#![allow(unused)]
fn main() {
struct_span_code_err!(...)
    .span_label(another_span, fluent::example::example_label)
    .span_note(another_span, fluent::example::separate_note)
    .emit()
}

エラーコードを追加する PR の例については、#76143 を参照してください。

エラーコードの doctest を実行する

rustc_error_codes/src/error_codes に追加された例をテストするには、次を使用して エラーインデックスジェネレーターを実行します。

./x test ./src/tools/error_index_generator

  1. RFC のドラフトはこちらを参照してください。

診断項目

lint を書いていると、特定の型、トレイト、関数をチェックすることがよくあります。これにより、それらをどのようにチェックするかという疑問が生じます。型は完全な型パスでチェックできます。しかし、これにはパスのハードコーディングが必要であり、一部のエッジケースでは誤分類につながる可能性があります。これに対処するために、rustc は Symbol を介して型を識別するために使用される診断項目を導入しました。

診断項目を見つける

診断項目は、rustc_diagnostic_item 属性を使って rustc/std/core/alloc 内の項目に追加されます。特定の型に対応する項目は、ドキュメントでソースコードを開き、この属性を探すことで見つけられます。テスト中のコンパイルエラーを避けるために、cfg_attr 属性とともに追加されていることが多い点に注意してください。定義は多くの場合、次のようになります。

// これはこの型の診断項目です   vvvvvvv
#[cfg_attr(not(test), rustc_diagnostic_item = "Penguin")]
struct Penguin;

診断項目は通常、トレイト、 型、 および独立した関数にのみ追加されます。 関連型またはメソッドをチェックすることが目的の場合は、 その項目の診断項目を使用し、 診断項目の使用を参照してください。

診断項目を追加する

新しい診断項目は、次の 2 つの手順で追加できます。

  1. Rust リポジトリ内で対象の項目を見つけます。次に、rustc_diagnostic_item 属性を介して、診断項目を文字列として追加します。これにより、テストの実行中にコンパイルエラーが発生することがあります。これらのエラーは、not(test) 条件とともに cfg_attr 属性を使用することで回避できます(予防策として、すべての rustc_diagnostic_item 属性に追加しても問題ありません)。最終的には、次のようになります。

    // これが新しい診断項目になります        vvv
    #[cfg_attr(not(test), rustc_diagnostic_item = "Cat")]
    struct Cat;
    

    診断項目の命名規則については、 命名規則を参照してください。

  2. コード内の診断項目は、 rustc_span::symbol::sym 内のシンボルを介してアクセスされます。 新しく作成した診断項目を追加するには、 モジュールファイルを開き、 リスト内の正しい位置に名前(この場合は Cat)を追加するだけです。

これで、変更内容のプルリクエストを作成できます。:tada:

注: Clippy のような他のプロジェクトで診断項目を使用する場合、 リポジトリが同期されるまでに少し時間がかかることがあります。

命名規則

診断項目にはまだ命名規則がありません。 以下は今後使用されるべきいくつかのガイドラインですが、 既存の名前とは異なる場合があります。

  • 型、トレイト、列挙型には UpperCamelCase を使用して名前を付けます (例: Iterator および HashMap
  • Writer のように複数回使用される型名については、 より正確な名前を選ぶとよいでしょう。 たとえばモジュール名を追加するなどです (例: IoWriter
  • 関連項目には独自の診断項目を付けるべきではなく、 代わりに、それらが由来する型の診断項目を介して間接的にアクセスするべきです。
  • std::mem::swap() のような自由関数には、 重要な(公開)モジュールを接頭辞として付けた snake_case を使用して名前を付けるべきです (例: mem_swap および cmp_max
  • モジュールには通常、診断項目を付けるべきではありません。 診断項目はパスの使用を避けるために追加されたものであり、 したがってモジュールに使用すると、ほとんどの場合逆効果になる可能性が高いです。

診断項目の使用

rustc では、診断項目は rustc_span::symbol::sym モジュール内の Symbol を介して検索されます。これらは、TyCtxt::get_diagnostic_item() を使用して DefId にマッピングしたり、TyCtxt::is_diagnostic_item() を使用して DefId と一致するかをチェックしたりできます。診断項目から DefId にマッピングする場合、そのメソッドは Option<DefId> を返します。これは、シンボルが診断項目でない場合や、たとえば #[no_std] でコンパイルしているときのように型が登録されていない場合に、None になる可能性があります。 以下のすべての例は DefId とその使用法に基づいています。

例: 型をチェックする

#![allow(unused)]
fn main() {
use rustc_span::symbol::sym;

/// この例では、指定された型(`ty`)が `HashMap` 型を持つかどうかを
/// `TyCtxt::is_diagnostic_item()` を使用してチェックします
fn example_1(cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
    match ty.kind() {
        ty::Adt(adt, _) => cx.tcx.is_diagnostic_item(sym::HashMap, adt.did()),
        _ => false,
    }
}
}

例: トレイト実装をチェックする

#![allow(unused)]
fn main() {
/// この例では、メソッドから取得した指定の [`DefId`] が、診断項目によって
/// 定義されたトレイト実装の一部であるかどうかをチェックします。
fn is_diag_trait_item(
    cx: &LateContext<'_>,
    def_id: DefId,
    diag_item: Symbol
) -> bool {
    if let Some(trait_did) = cx.tcx.trait_of_item(def_id) {
        return cx.tcx.is_diagnostic_item(diag_item, trait_did);
    }
    false
}
}

関連型

診断項目の関連型には、まずトレイトの DefId を取得し、その後で TyCtxt::associated_items() を呼び出すことで間接的にアクセスできます。これは AssocItems オブジェクトを返し、さらにチェックを行うために使用できます。この使用例については、clippy_utils::ty::get_iterator_item_ty() を確認してください。

Clippy での使用

Clippy は可能な場合に診断項目を使用しようとし、いくつかのラッパー関数とユーティリティ関数を開発しています。Clippy で診断項目を使用する際は、そのドキュメントも参照してください。(lint を書くための共通ツール を参照してください。)

関連する issue

これらは、このトピックについて本当に深く掘り下げたい人にとってのみ、おそらく興味深いものです :)

  • rust#60966: 診断項目を導入した Rust の PR
  • 診断項目へ移行するための Clippy の追跡 issue

ErrorGuaranteed

これまでのセクションでは、コンパイラのユーザーが目にするエラーメッセージについて扱ってきました。しかし、エラーを発行することには、コンパイラのソースコード内でもう 1 つ重要な副作用があります。それは、ErrorGuaranteed を生成することです。

ErrorGuaranteed は、rustc_errors クレートの外部では構築できないゼロサイズ型です。これはエラーがユーザーに報告されるたびに生成されるため、コンパイラコードが ErrorGuaranteed 型の値に遭遇した場合、そのコンパイルは_静的に失敗することが保証されます_。これは、エラーのコードパスが失敗につながることを静的に確認できるため、健全性を損なうバグを避けるのに役立ちます。

ErrorGuaranteed の使用については、いくつか重要な考慮事項があります。

  • これはエラーの_種類_に関する情報を伝えません。たとえば、そのエラーは(間接的に)遅延バグや他のコンパイラエラーに起因している可能性があります。 したがって、エラーを発行するかどうか、またはどの種類のエラーを発行するかを決定する際に、ErrorGuaranteed に依存すべきではありません。
  • ErrorGuaranteed は、コンパイルが将来エラーを_発行する_ことを示すために使用すべきではありません。これは、エラーが_すでに_発行されていること、つまり emit() 関数がすでに呼び出されていることを示すために使用すべきです。たとえば、コンパイラの将来の部分でエラーになることを検出したとしても、まず自分たちでエラーまたは遅延バグを発行しない限り、ErrorGuaranteed を使用することは_できません_。

ありがたいことに、ほとんどの場合、ErrorGuaranteed を誤用することは静的に不可能であるはずです。

解析

この部では、コードのさまざまなプロパティをチェックし、後続のステージに情報を提供するためにコンパイラーが使用する多くの解析について説明します。一般に、これは人々が「Rust の型システム」について話すときに意味するものです。これには、型の表現、推論、検査、トレイトシステム、借用チェッカーが含まれます。これらの解析は、1 つの大きなパスや連続したパスの集合として実行されるわけではありません。むしろ、コンパイルプロセスのさまざまな部分に分散しており、異なる中間表現を使用します。たとえば、型検査は HIR 上で行われる一方、借用検査は MIR 上で行われます。それでも、説明の都合上、このガイドのこの部では、これらすべての解析について説明します。

ジェネリックパラメータ定義

この章では、rustc が導入されたジェネリックパラメータをどのように追跡するかについて説明します。たとえば、ある struct Foo<T> が与えられたとき、rustc は Foo が型パラメータ T を定義していること(そして他のジェネリックパラメータは定義していないこと)をどのように追跡するのでしょうか。

ここでは、for<'a> 構文によって導入されるジェネリックパラメータ(たとえば where 句や fn 型の中のもの)をどのように追跡するかは扱いません。これは Binder に関する章 で別途扱われています。

ty::Generics

item によって導入されるジェネリックパラメータは、ty::Generics 構造体によって追跡されます。item によっては、親 item で定義されたジェネリクスの使用を許可することがあります。これは、ty::Generics 構造体が、ジェネリックパラメータの継承元となる親 item を指定するための optional なフィールドを持つことで実現されています。たとえば、次のコードがあるとします。

trait Trait<T> {
    fn foo<U>(&self);
}

foo に使用される ty::Generics には [U] が含まれ、親として Some(Trait) を持ちます。Trait は、親が None である [Self, T] を含む ty::Generics を持ちます。

GenericParamDef 構造体は、ty::Generics のリスト内の個々のジェネリックパラメータを表現するために使用されます。GenericParamDef 構造体には、その名前、defid、パラメータの種類(つまり型、const、ライフタイム)など、ジェネリックパラメータに関する情報が含まれています。

GenericParamDef には、そのパラメータの位置(最も外側の親から数えた位置)を表す u32 のインデックスも含まれています。これは、ジェネリックパラメータの使用を表現するために使われる値です(これについては 型の表現に関する章 で詳しく説明します)。

興味深いことに、現在の ty::Generics には、item に定義された すべての ジェネリックパラメータが含まれているわけではありません。関数の場合、そこに含まれるのは early bound パラメータだけです。

EarlyBinder とパラメーターのインスタンス化

ジェネリックパラメーター T を導入するアイテムがある場合、foo の外側から foo の内側の型(つまり戻り値の型や引数の型)を参照するときは常に、foo 上で定義されたジェネリックパラメーターを扱うよう注意しなければなりません。例を示します。

fn foo<T, U>(a: T, _b: U) -> T { a }

fn main() {
    let c = foo::<i32, u128>(1, 2);
}

main を型検査するとき、単純に foo の戻り値の型を見て変数 c に型 T を割り当てることはできません。関数 main はジェネリックパラメーターを一切定義しておらず、このコンテキストでは T はまったく意味を持ちません。より一般的に言えば、あるアイテムがジェネリックパラメーターを導入(束縛)する場合、そのアイテムの外側からアイテム内の型にアクセスするときには、ジェネリックパラメーターを外側のアイテムからの値でインスタンス化しなければなりません。

rustc では、これを EarlyBinder 型で追跡します。foo の戻り値の型は EarlyBinder<Ty> として表現され、Ty にアクセスする唯一の方法は、Ty が使用している可能性のあるジェネリックパラメーターに引数を提供することです。これは EarlyBinder::instantiate メソッドによって実装されており、このメソッドはバインダーを解消し、すべてのジェネリックパラメーターを提供された引数で置き換えた内側の値を返します。

先ほどの例に戻ると、main を型検査するとき、foo の戻り値の型は EarlyBinder(T/#0) として表現されます。その後、この関数をジェネリック引数 i32, u128 で呼び出しているため、戻り値の型に対して EarlyBinder::instantiate を呼び出し、args として [i32, u128] を渡します。その結果、インスタンス化された戻り値の型は i32 となり、これをローカル変数 c の型として使用できます。

さらにいくつか例を示します。

fn foo<T>() -> Vec<(u32, T)> { Vec::new() }
fn bar() {
    // インスタンス化する前の `foo` の戻り値の型は次のようになります。
    // `EarlyBinder(Adt(Vec, &[Tup(&[u32, T/#=0])]))`
    // 次に、バインダーを `[u64]` でインスタンス化し、結果として次の型になります。
    // `Adt(Vec, &[Tup(&[u32, u64])])`
    let a = foo::<u64>();
}
struct Foo<A, B> {
    x: Vec<A>,
    ..
}

fn bar(foo: Foo<u32, f32>) { 
    // インスタンス化する前の `foo` の `x` フィールドの型は次のようになります。
    // `EarlyBinder(Vec<A/#0>)`
    // 次に、`Foo` 構造体へのジェネリック引数がそれらであるため、
    // バインダーを `[u32, f32]` でインスタンス化します。その結果、次の型になります。
    // `Vec<u32>`
    let y = foo.x;
}

コンパイラーでは、このための instantiate 呼び出しは FieldDef::tysrc)で行われます。bar の型検査中のどこかで、foo.x の型を取得するために最終的に FieldDef::ty(x, &[u32, f32]) を呼び出すことになります。

インデックスに関する注記: Param のインデックスが EarlyBinder が束縛するものと一致しない場合、それはバグです。たとえば、インデックスが範囲外である場合や、ライフタイムのインデックスが型パラメーターに対応している場合です。この種のエラーは、コンパイラーのより早い段階の名前解決中に検出されます。そこでは、内側のアイテムから名前で参照可能であるべきでないアイテムによって導入されたジェネリックパラメーターへの参照を許可しません。


前述のように、アイテムの_外側_にいるときは、内側の値にアクセスする前に EarlyBinder をジェネリック引数でインスタンス化することが重要です。しかし、概念的にすでにバインダーの内側にいる場合の設定は少し異なります。

例を示します。

#![allow(unused)]
fn main() {
impl<T> Trait for Vec<T> {
    fn foo(&self, b: Self) {}
}
}

b パラメーターの型を表す Ty を構築するとき、現在内側にいる impl 上の Self の型を取得する必要があります。これは implDefIdtype_of クエリを呼び出すことで取得できます。ただし、impl ブロックはジェネリックパラメーターを束縛しており、impl の外側にいる場合にはそれらを解消しなければならない可能性があるため、この呼び出しは EarlyBinder<Ty> を返します。

EarlyBinder 型は、「すでにその内側にいる」場合にバインダーを解消するための instantiate_identity 関数を提供します。これは実質的に、EarlyBinder::instantiate(GenericArgs::identity_for_item(..)) と書くことの、より高性能なバージョンです。概念的には、これはルート universe 内のプレースホルダーでインスタンス化することによってバインダーを解消します(これが何を意味するかについては、次の数章で説明します)。ただし実際には、変更を一切行わずに内側の値を返すだけです。

Binder と高ランクリージョン

ジェネリックパラメータを、アイテム上ではなく、型または where 句の一部として定義することがあります。 例として、型 for<'a> fn(&'a u32) と where 句 for<'a> T: Trait<'a> はどちらも、'a という名前のジェネリックライフタイムを導入します。 現在、for<T>for<const N: usize> の安定版構文はありませんが、 nightly では feature(non_lifetime_binders) を使うことで、for<T>/for<const N: usize> を使用した where 句(ただし型ではない)を書くことができます。

for は新しい名前をスコープに導入するため、「バインダー」と呼ばれます。 rustc では、これらのパラメータがどこで導入されるか、またそのパラメータが何であるか(つまり、数はいくつか、パラメータが型/const/リージョンのどれか)を追跡するために Binder 型を使用します。for<'a> fn(&'a u32) のような型は、rustc では次のように表現されます。

Binder(
    fn(&RegionKind::Bound(DebruijnIndex(0), BoundVar(0)) u32) -> (),
    &[BoundVariableKind::Region(...)],
)

これらのパラメータの使用は、RegionKind::Bound(または TyKind::Bound/ConstKind::Bound バリアント)によって表現されます。 これらの束縛リージョン/型/const は、主に 2 つのデータから構成されます。

  • どのバインダーを参照しているかを指定するための DebruijnIndex
  • Binder が導入するパラメータのうち、どれを参照しているかを指定する BoundVar

また、診断上の理由から BoundTyKind/BoundRegionKind を介して追加情報を保存することもありますが、 これは型の等価性、またはより一般的には Ty のセマンティクスにとって重要ではありません。 (上記の例からは省略)

デバッグ出力(およびお互いに話すときの非公式な表現)では、 これらの束縛変数を ^DebruijnIndex_BoundVar の形式で書く傾向があります。 上記の例は、代わりに Binder(fn(&'^0_0), &[BoundVariableKind::Region]) と書かれます。 DebruijnIndex0 の場合は、それを省いて ^0 と書くこともあります。

もう 1 つの具体例として、今回は where 句と型の中で for<'a> が混在している例です。

where
    for<'a> Foo<for<'b> fn(&'a &'b T)>: Trait,

これは次のように表現されます。

Binder(
    Foo<Binder(
        fn(&'^1_0 &'^0 T/#0),
        [BoundVariableKind::Region(...)]
    )>: Trait,
    [BoundVariableKind::Region(...)]
)

'^1_0'a パラメータを参照している点に注目してください。 最内側のバインダーから 1 レベル上のバインダーを参照するために DebruijnIndex1 を使用し、束縛された最初のパラメータである 'a を参照するために var に 0 を使用しています。 また、'^0 を使用して 'b パラメータを参照しています。DebruijnIndex0(最内側のバインダーを参照)なので省き、'b である最初の束縛パラメータを参照する boundvar の 0 だけを残します。

以前は、各 Binder によって導入される束縛変数の集合を常に明示的に追跡していたわけではありませんでした。 そのため、いくつものバグ(つまり ICE #81193#79949#83017)が発生していました。 これらを明示的に追跡することで、高ランクの where 句/型を構築するときに、エスケープしている束縛変数や別のバインダー由来の変数が存在しないことをアサートできます。 バインダー内の不正な型の例を以下に示します。

Binder(
    fn(&'^1_0 &'^1 T/#0),
    &[BoundVariableKind::Region(...)],
)

これは、リージョン '^1_0 が最外側のバインダーよりも高いレベルのバインダーを参照している、つまりエスケープしている束縛変数であるため、あらゆる種類の問題を引き起こします。 '^1 リージョン('^0_1 とも書けます)も、それが参照するバインダーが 2 番目のパラメータを導入していないため、不正な形式です。 現在の rustc は、これら両方の理由により、このバインダーを構築するときに ICE します。 以前は、単にこれが動作することを許可してしまい、その後コードベースの他の部分で問題に遭遇していました。

Binder のインスタンス化

EarlyBinder とよく似て、Binder の内側にアクセスするときは、まず束縛された変数を何らかの別の値で置き換えることで、それを解放しなければなりません。 これは EarlyBinder の場合とほぼ同じ理由によるもので、Binder によって導入されたパラメータを参照する型は、その binder の外側では意味をなしません。 次のエラーになる例を見てください。

fn foo<'a>(a: &'a u32) -> &'a u32 {
    a
}
fn bar<T>(a: fn(&u32) -> T) -> T {
    a(&10)
}

fn main() {
    let higher_ranked_fn_ptr = foo as for<'a> fn(&'a u32) -> &'a u32;
    // `T=for<'a> &'a u32` と推論しようとするが、これは満たせない
    let references_bound_vars = bar(higher_ranked_fn_ptr);
}

この例では、型 for<'a> fn(&'^0 u32) -> &'^0 u32 の引数を bar に渡しています。 T が型 &'^0 u32 に推論されることは許可したくありません。これはかなり意味をなさないためです(たまたま ICE しなかった場合、おそらく unsound でもあります)。 main'a について知らないため、borrow checker はライフタイム 'a を持つ borrow を扱うことができません。

EarlyBinder とは異なり、通常 Binder をユーザーからの何らかの具体的な引数集合でインスタンス化することはありません。つまり、for<'a1, 'a2> fn(&'a1 u32, &'a2 u32) に対する引数として ['b, 'static] を使うようなことは通常しません。代わりに、通常は binder を推論変数または placeholder でインスタンス化します。

推論変数によるインスタンス化

binder の可能なインスタンス化を推論しようとしているとき、たとえば higher-ranked 関数ポインタを呼び出す場合や、higher-ranked where-clause を使って何らかの bound を証明しようとする場合に、binder を推論変数でインスタンス化します。たとえば、上の例の higher_ranked_fn_ptr が与えられ、それを &10_u32 で呼び出す場合は、次のようになります。

  • binder を推論変数でインスタンス化し、fn(&'?0 u32) -> &'?0 u32) というシグネチャを得る
  • 渡された引数 &10_u32(&’static u32)の型を、シグネチャ内の型 &'?0 u32 と等置し、'?0 = 'static と推論する
  • 渡された引数の型を fn ptr シグネチャ内の引数の型と正常に unify できたため、渡された引数は正しかった

推論変数によるインスタンス化の別の例として、何らかの for<'a> T: Trait<'a> where-clause が与えられ、T: Trait<'static> が成り立つことを証明しようとしている場合は、次のようになります。

  • binder を推論変数でインスタンス化し、T: Trait<'?0> という where clause を得る
  • T: Trait<'static> という goal を、インスタンス化された where clause と等置し、'?0 = 'static と推論する
  • T: Trait<'static>T: Trait<'?0> と正常に unify できたため、goal は成り立つ

binder を推論変数でインスタンス化するには、InferCtxtinstantiate_binder_with_fresh_vars メソッドを使用します。 binder の特定の 1 つのインスタンス化だけを考えればよい場合は、推論変数で binder をインスタンス化するべきです。一方で、binder のすべての可能なインスタンス化について推論したい場合は、代わりに placeholder を使用するべきです。

placeholder によるインスタンス化

placeholder は Ty/ConstKind::Param/ReEarlyParam と非常によく似ており、それ自体とだけ等しい何らかの未知の型を表します。 Ty/ConstRegion はすべて、UniverseBoundVar から構成される Placeholder variant を持ちます。

Universe は placeholder がどの binder に由来するかを追跡し、BoundVar はこの placeholder がその binder 上のどのパラメータに対応するかを追跡します。 placeholder の等価性は、universe が等しく、かつ BoundVar が等しいかどうかのみによって決まります。 詳細については、Placeholders and Universes の章を参照してください。

他の rustc 開発者と話すときや、Debug フォーマットされた Ty/Const/Region を見るとき、Placeholder はしばしば '!UNIVERSE_BOUNDVARS と書かれます。 たとえば、ある型 for<'a> fn(&'a u32, for<'b> fn(&'b &'a u32)) が与えられた場合、 両方の binder をインスタンス化した後(事前に現在の InferCtxt 内の UniverseU0 だったと仮定します)、 &'b &'a u32 の型は &'!2_0 &!1_0 u32 と表現されます。

placeholder の universe が 0 の場合、debug 出力では完全に省略されます。つまり、!0_2!2 として出力されます。 ただし、placeholder で binder をインスタンス化するときには InferCtxt 内の universe を増やすため、実際にはこれはまれにしか起こりません。 したがって、通常遭遇しうる最小の universe の placeholder は U1 内のものです。

Binder は、InferCtxtenter_forall メソッドを介して placeholder でインスタンス化できます。 コンパイラが、binder の 1 つの具体的なインスタンス化ではなく、あらゆる可能なインスタンス化を考慮するべき場合に使用するべきです。

注: この章の元の例では、ローカル変数が型 &'^0 u32 を持つと推論するべきではないと述べました。 このコードは universes によってコンパイルが防止されます(リンク先の章で説明しているとおりです)。

なぜ RePlaceholderReBound の両方があるのか?

なぜこれら両方の variant があるのか疑問に思うかもしれません。結局のところ、Placeholder に格納されるデータは実質的に ReBound のデータと同等です。つまり、どの binder かを追跡するものと、Binder が導入したどのパラメータかを追跡する index です。

この主な理由は、Bound が束縛変数のより構文的な表現であるのに対し、Placeholder はより意味的な表現であることです。 具体例として、次を見てください。

#![allow(unused)]
fn main() {
impl<'a> Other<'a> for &'a u32 { }

impl<T> Trait for T
where
    for<'a> T: Other<'a>,
{ ... }

impl<T> Bar for T
where
    for<'a> &'a T: Trait
{ ... }
}

これらの trait implementation が与えられた場合、u32: Bar は成り立つべきでは_ありません_。 &'a u32Other<'a> を実装するのは、borrow のライフタイムと trait 上のライフタイムが等しい場合だけです。 しかし、ReBound だけを使用し、placeholder がなかった場合、その trait bound が成り立つと誤って信じてしまいやすいかもしれません。 これを説明するために、rustc に placeholder がない世界で u32: Bar を証明しようとする例を追ってみましょう。

  • まず u32: Bar を証明しようとする
  • impl<T> Bar for T impl を見つけ、EarlyBinderu32 でインスタンス化することになる(注: 実際には、まず binder を推論変数でインスタンス化し、その後それが u32 であると推論するため、これは_完全に_正確ではありませんが、ここではその違いはそれほど重要ではありません)
  • impl 上には where clause for<'a> &'^0 T: Trait があり、early binder を u32 でインスタンス化したため、実際には for<'a> &'^0 u32: Trait を証明する必要がある
  • impl<T> Trait for T impl を見つけ、EarlyBinder&'^0 u32 でインスタンス化することになる
  • where clause for<'a> T: Other<'^0> があり、early binder を &'^0 u32 でインスタンス化したため、実際には for<'a> &'^0 u32: Other<'^0> を証明する必要がある
  • impl<'a> Other<'a> for &'a u32 を見つけ、この impl は bound を証明するのに十分である。なぜなら borrow 上のライフタイムと trait 上のライフタイムがどちらも '^0 だからである

この最終結果は正しくありません。というのも、独自のジェネリックパラメータを導入する 2 つの別々の binder があったため、trait bound は for<'a1, 'a2> &'^1 u32: Other<'^0> のようなものになっているべきであり、これは impl<'a> Other<'a> for &'a u32 によって満たされ_ない_からです。 理論上はこれを動作させることもできますが、かなり込み入っており、現在の構成よりも複雑になります。次のことを行う必要があるでしょう。

  • Bound の ty/const/region で Binder/EarlyBinder をインスタンス化するたびに、束縛変数の DebruijnIndex がより高くなるように「書き換える」
  • 推論変数を束縛変数に推論する際、その束縛変数が、推論変数の作成後に入った binder に由来する場合は、その変数の DebruijnIndex を下げる必要がある。
  • 推論変数がどの binder の内側で作成されたかを別途追跡し、さらにその推論変数がどの最内の binder からパラメーターを名前指定できるかも追跡する(現在は後者だけを追跡すればよい)
  • 推論変数を解決する際、infcx の現在の binder 深さに応じて、あらゆる束縛変数を書き換える
  • ほかにもあるかもしれない(このリストを書いている間にも項目が増え続けたので、これが網羅的だと考えるのは単純すぎるように思える)

根本的に、この複雑さはすべて、Bound の ty/const/region では、Binder 上のあるパラメーターに対する表現が、そのパラメーターを導入する binder と、その使用箇所の間に存在する他の Binder の数によって異なるために生じます。 たとえば、次のコードがあるとします。

#![allow(unused)]
fn main() {
fn foo<T>()
where
    for<'a> T: Trait<'a, for<'b> fn(&'b T, &'a u32)>
{ ... }
}

この where 句は for<'a> T: Trait<'^0, for<'b> fn(&'^0 T, &'^1_0 u32)> と書かれます。 'a パラメーターへの参照が 2 つあるにもかかわらず、 それらは ^0^1_0 というように異なる形で表現されます。 これは、後者の使用箇所が、内側の関数ポインター型のための 2 つ目の Binder の下にネストされているためです。

これは Placeholder の ty/const/region とは対照的です。Placeholder にはこの制限がありません。なぜなら、Universe はパラメーターの使用箇所ではなく、現在の InferCtxt に固有のものだからです。

EarlyBinder をインスタンス化したり、推論変数を既存の Placeholder と単一化したりすることは自明に可能です。Placeholder がどのコンテキストにあっても、同じ表現を持つためです。 例として、上記の高ランク where 句上の binder をインスタンス化した場合、次のように表現されます。 T: Trait<'!1_0, for<'b> fn(&'^0 T, &'!1_0 u32)>. 'a の両方の使用箇所に対する RePlaceholder の表現は、一方が別の Binder の下にあるにもかかわらず同じです。

その後、関数ポインター上の binder をインスタンス化すると、次のような型が得られます。 fn(&'!2_0 T, ^'!1_0 u32) 'b パラメーターの RePlaceholder は、'a の binder の後にその binder がインスタンス化されたという事実を追跡するため、より高い universe にあります。

ReLateParam によるインスタンス化

型の表現に関する章で説明したように、RegionKind にはジェネリックパラメーターを表現するための 2 つのバリアント、ReLateParamReEarlyParam があります。 ReLateParam は概念的には、常にルート universe(U0)に存在する Placeholder です。 これは、関数/クロージャの内部にいる間に、それらの遅延束縛パラメーターをインスタンス化する際に使用されます。 その実際の表現は、ReEarlyParamRePlaceholder のどちらとも比較的異なっています。

  • 遅延束縛ジェネリックパラメーターを導入したアイテムの DefId
  • ジェネリックパラメーターの DefId とその名前(Symbol 経由)を指定するか、このプレースホルダーが Fn/FnMut クロージャの self borrow の匿名ライフタイムを表していることを示す BoundRegionKindBrAnon のためのバリアントもありますが、これは ReLateParam には使用されません。

たとえば、次のコードがあるとします。

impl Trait for Whatever {
    fn foo<'a>(a: &'a u32) -> &'a u32 {
        let b: &'a u32 = a;
        b
    }
}

関数本体内の型 &'a u32 におけるライフタイム 'a は、次のように表現されます。

ReLateParam(
    {impl#0}::foo,
    BoundRegionKind::BrNamed({impl#0}::foo::'a, "'a")
)

関数の本体内からその関数の遅延束縛ジェネリックパラメーターを参照するこの特定のケースでは、 これはどこかで Binder をインスタンス化する際に明示的に行われるのではなく、 hir_ty_lowering の間に暗黙的に行われます。 ただし場合によっては、BinderReLateParam で明示的にインスタンス化します。

一般に、関数/クロージャ上の遅延束縛パラメーターに対する Binder があり、 概念的にはすでにその binder の内側にいる場合、 liberate_late_bound_regions を使用して、それを ReLateParam でインスタンス化します。 これにより、この操作は EarlyBinderinstantiate_identity に相当する Binder の操作になります。

具体例として、型チェック中の関数のシグネチャにアクセスすると、それは EarlyBinder<Binder<FnSig>> として表現されます。 すでにこれらの binder の「内側」にいるため、instantiate_identity を呼び出した後に liberate_late_bound_regions を呼び出します。

早期束縛パラメーターと後期束縛パラメーター

注記: この章では、主に早期束縛/後期束縛を、関数アイテム型/関数定義について論じる場合にのみ関係するものとして扱います。これは完全には正しくない可能性があり、async ブロックとクロージャについても、この章である程度論じるべきでしょう。

早期束縛パラメーターと後期束縛パラメーターの区別が導入された当時の、これらのブログ記事も参照してください: Intermingled parameter lists および Intermingled parameter lists, take 2

「早期」束縛または「後期」束縛であるとはどういう意味か

すべての関数定義には、関数アイテム型として知られる Fn* トレイトを実装する、対応する ZST があります。 この章のこの部分では、早期束縛ジェネリックパラメーターと後期束縛ジェネリックパラメーターの違いを説明するうえで有用な文脈として、関数アイテム型の「脱糖」について少し説明します。

まず、ジェネリックパラメーターを含まない非常に単純な例から始めましょう。

#![allow(unused)]
fn main() {
fn foo(a: String) -> u8 {
    1
    /* 中略 */
}
}

foo に対応する関数アイテム型と、それに関連付けられた Fn impl の定義を明示的に書き出すと、次のようになります。

struct FooFnItem;

impl Fn<(String,)> for FooFnItem {
    type Output = u8;
    /* fn call(&self, ...) -> ... { ... } */
}

FnMut/FnOnce トレイトの組み込み impl、および CopyClone の impl は、簡潔にするために省かれています(ただし、これらのトレイトは関数アイテム型に対して実装されています)。

もう少し複雑な例では、関数にジェネリックパラメーターを導入します。

#![allow(unused)]
fn main() {
fn foo<T: Sized>(a: T) -> T {
    a
    /* 中略 */
}
}

定義を書き出すと、次のようになります。

struct FooFnItem<T: Sized>(PhantomData<fn(T) -> T>);

impl<T: Sized> Fn<(T,)> for FooFnItem<T> {
    type Output = T;
    /* fn call(&self, ...) -> ... { ... } */
}

関数アイテム型 FooFnItem は、関数 foo 上で定義された型パラメーター T に対してジェネリックであることに注意してください。 しかし、関数上で定義されたすべてのジェネリックパラメーターが、関数アイテム型上にも定義されるわけではありません。これは次の例で示されています。

#![allow(unused)]
fn main() {
fn foo<'a, T: Sized>(a: &'a T) -> &'a T {
    a
    /* 中略 */
}
}

その「脱糖」された形式は、次のようになります。

struct FooFnItem<T: Sized>(PhantomData<for<'a> fn(&'a T) -> &'a T>);

impl<'a, T: Sized> Fn<(&'a T,)> for FooFnItem<T> {
    type Output = &'a T;
    /* fn call(&self, ...) -> ... { ... } */
}

関数 foo のライフタイムパラメーター 'a は、関数アイテム型 FooFnItem には存在せず、代わりに引数型を表すためだけに、組み込み impl 上に導入されます。

ジェネリックパラメーターがすべて関数アイテム型上で定義されるわけではないということは、関数を呼び出す際にジェネリック引数が提供される段階が 2 つあることを意味します。

  1. 関数を名指しする段階(例: let a = foo;)では、FooFnItem に対する引数が提供されます。
  2. 関数を呼び出す段階(例: a(&10);)では、組み込み impl 上で定義された任意のパラメーターが提供されます。

この 2 段階の仕組みが、早期と後期という名前付けの由来です。早期束縛パラメーターは最も早い段階(関数を名指しする段階)で提供されるのに対し、後期束縛パラメーターは最も遅い段階(関数を呼び出す段階)で提供されます。

前の例の脱糖を見ると、T は早期束縛型パラメーターであり、'a は後期束縛ライフタイムパラメーターであることが分かります。なぜなら、T は関数アイテム型上に存在しますが、'a は存在しないからです。 各ジェネリックパラメーターに引数が提供される場所を注釈した、foo の呼び出し例を参照してください。

#![allow(unused)]
fn main() {
fn foo<'a, T: Sized>(a: &'a T) -> &'a T {
    a
    /* 中略 */
}

// ここでは、関数アイテム型上の型パラメーター `T` に
// 型引数 `String` を提供します
let my_func = foo::<String>;

// ここでは、組み込み impl 上のライフタイムパラメーター `'a` に
// (暗黙的に)ライフタイム引数が提供されます。
my_func(&String::new());
}

早期束縛パラメーターと後期束縛パラメーターの違い

高階ランク関数ポインターとトレイト境界

ジェネリックパラメーターが後期束縛であると、関数アイテムをより柔軟に使用できます。 たとえば、早期束縛ライフタイムパラメーターを持つ関数 foo と、後期束縛ライフタイムパラメーター 'a を持つ関数 bar がある場合、次のような組み込み Fn impl が得られます。

impl<'a> Fn<(&'a String,)> for FooFnItem<'a> { /* ... */ }
impl<'a> Fn<(&'a String,)> for BarFnItem { /* ... */ }

bar 関数は厳密により柔軟なシグネチャを持っています。なぜなら、その関数アイテム型は任意のライフタイムを持つ借用で呼び出せるのに対し、foo の関数アイテム型は、関数アイテム型上のライフタイムと同じライフタイムを持つ借用でしか呼び出せないからです。 これは、foo の関数アイテム型を異なるライフタイムで複数回呼び出そうとするだけで示せます。

#![allow(unused)]
fn main() {
// `'a: 'a` 境界により、このライフタイムは早期束縛になります。
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }

// 早期束縛ジェネリックパラメーターは、関数 `foo` を
// 名指しするときに、ここでインスタンス化されます。`'a` は早期束縛であるため、引数が提供されます。
let f = foo::<'_>;

// 両方の関数引数は同じライフタイムを持つ必要があります。なぜなら、
// ライフタイムパラメーターが早期束縛であるということは、`f` が
// 1 つの特定のライフタイムに対してのみ呼び出し可能であることを意味するからです。
//
// これは異なるライフタイムの借用で呼び出しているため、借用チェッカーは
// ここでエラーを出します。
f(&String::new());
f(&String::new());
}

foo 上のライフタイムパラメーターが早期束縛であるため、f のすべての呼び出し元は同じライフタイムを持つ借用を提供する必要があります。 この例では、foo の関数アイテム型を 2 回呼び出しており、そのたびに一時値の借用を渡しています。 これら 2 つの借用は、ライフタイムが重なり合うことはあり得ません。一時値は関数呼び出しの間だけ生存し、その後は生存しないため、コンパイルエラーになります。

foo 上のライフタイムパラメーターが後期束縛であれば、各呼び出し元が自分の借用に対して異なるライフタイム引数を提供できるため、これはコンパイル可能になります。 上で定義した bar 関数を使ってこれを示す、次の例を参照してください。

#![allow(unused)]
fn main() {
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }

// 早期束縛パラメーターはここでインスタンス化されますが、`'a` は
// 後期束縛であるため、ここでは提供されません。
let b = bar;

// 後期束縛パラメーターは各呼び出し箇所で個別にインスタンス化されるため、
// 各呼び出し元が異なるライフタイムを使用できます。
b(&String::new());
b(&String::new());
}

これは、関数アイテム型を高ランク関数ポインタへ型強制し、高ランクの Fn トレイト境界を証明できる能力に反映されています。 これは次の例で示せます。

#![allow(unused)]
fn main() {
// `'a: 'a` 境界により、このライフタイムは早期束縛になります。
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }

fn accepts_hr_fn(_: impl for<'a> Fn(&'a String) -> &'a String) {}

fn higher_ranked_trait_bound() {
    let bar_fn_item = bar;
    accepts_hr_fn(bar_fn_item);

    let foo_fn_item = foo::<'_>;
    // エラー
    accepts_hr_fn(foo_fn_item);
}

fn higher_ranked_fn_ptr() {
    let bar_fn_item = bar;
    let fn_ptr: for<'a> fn(&'a String) -> &'a String = bar_fn_item;

    let foo_fn_item = foo::<'_>;
    // エラー
    let fn_ptr: for<'a> fn(&'a String) -> &'a String = foo_fn_item;
}
}

これらのどちらの場合も、借用チェッカーは foo_fn_item が任意のライフタイムの借用で呼び出し可能だとはみなさないため、エラーになります。 これは、foo のライフタイムパラメータが早期束縛であり、その結果 foo_fn_itemFooFnItem<'_> という型を持つためです。この型は、脱糖された Fn impl が示すように、同じライフタイム '_ の借用でのみ呼び出し可能です。

遅延束縛パラメータが存在する場合の Turbofish

前述のように、早期束縛パラメータと遅延束縛パラメータの区別は、ジェネリックパラメータがインスタンス化される場所が 2 つあることを意味します。

  • 関数に名前を付けるとき(早期)
  • 関数を呼び出すとき(遅延)

現在、呼び出しステップ中に遅延束縛パラメータのジェネリック引数を明示的に指定する構文はありません。ジェネリック引数を指定できるのは、関数に名前を付けるときの早期束縛パラメータに対してのみです。 構文 foo::<'static>(); は、関数呼び出しの一部であるにもかかわらず、(foo::<'static>)(); として振る舞い、関数アイテム型上の早期束縛ジェネリックパラメータをインスタンス化します。

次の例を見てください。

#![allow(unused)]
fn main() {
fn foo<'a>(b: &'a u32) -> &'a u32 { b }

let f /* : FooFnItem<????> */ = foo::<'static>;
}

上の例はエラーになります。ライフタイムパラメータ 'a は遅延束縛であり、「関数に名前を付ける」ステップの一部としてインスタンス化できないためです。 ライフタイムパラメータを早期束縛にすると、このコードはコンパイルされるようになります。

#![allow(unused)]
fn main() {
fn foo<'a: 'a>(b: &'a u32) -> &'a u32 { b }

let f /* : FooFnItem<'static> */ = foo::<'static>;
}

現在のコンパイラ実装が目指しているのは、早期束縛ライフタイムパラメータと遅延束縛ライフタイムパラメータの両方を持つ関数に対してライフタイム引数を指定した場合にエラーにすることです。 実際には、過度な破壊的変更を避けるため、一部のケースは実際には将来互換性警告にとどまっています(#42868)。

  • ライフタイム引数の数が早期束縛ライフタイムパラメータの数と同じ場合、エラーではなく FCW が出力されます
  • メソッド呼び出し構文を使用している場合、エラーは常に FCW に格下げされます

これを示すために、さまざまな種類の関数を書き出し、それぞれに遅延束縛ライフタイムと早期束縛ライフタイムの両方を与えることができます。

fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}

struct Foo;

trait Trait: Sized {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}

impl Trait for Foo {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

impl Foo {
    fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

次に、最初のケースとして、各関数を単一のライフタイム引数(1 つの早期束縛ライフタイムパラメータに対応)で呼び出し、ハードエラーではなく FCW だけになることを確認できます。

#![allow(unused)]
#![deny(late_bound_lifetime_arguments)]

fn main() {
fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}

struct Foo;

trait Trait: Sized {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}

impl Trait for Foo {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

impl Foo {
    fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

// 早期束縛パラメータと同じ数の引数を指定することは、
// 常に将来互換性警告になります
Foo.trait_method::<'static>(&(), &());
Foo::trait_method::<'static>(Foo, &(), &());
Foo::trait_function::<'static>(&(), &());
Foo.inherent_method::<'static>(&(), &());
Foo::inherent_function::<'static>(&(), &());
free_function::<'static>(&(), &());
}

2 番目のケースでは、ライフタイムパラメータの数(早期束縛か遅延束縛かを問わず)より多いライフタイム引数で各関数を呼び出し、メソッド呼び出しは FCW になる一方で、自由関数や関連関数はハードエラーになることを確認します。

#![allow(unused)]
fn main() {
fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}

struct Foo;

trait Trait: Sized {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}

impl Trait for Foo {
    fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

impl Foo {
    fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
    fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}

// 早期束縛パラメータより多くの引数を指定することは、
// メソッド呼び出し構文を使用している場合には
// 将来互換性警告になります。
Foo.trait_method::<'static, 'static, 'static>(&(), &());
Foo.inherent_method::<'static, 'static, 'static>(&(), &());
// しかし、メソッド呼び出し構文を使用していない場合はハードエラーになります。
Foo::trait_method::<'static, 'static, 'static>(Foo, &(), &());
Foo::trait_function::<'static, 'static, 'static>(&(), &());
Foo::inherent_function::<'static, 'static, 'static>(&(), &());
free_function::<'static, 'static, 'static>(&(), &());
}

遅延束縛ライフタイムパラメータと早期束縛ライフタイムパラメータの両方に十分な数のライフタイム引数を指定した場合でも、これらの引数は、遅延束縛パラメータに提供されるライフタイムを注釈するために実際に使用されるわけではありません。 これは、非 static な借用を提供しながら、関数に 'static を turbofish することで示せます。

#![allow(unused)]
fn main() {
struct Foo;

impl Foo {
    fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b String ) {}
}

Foo.inherent_method::<'static, 'static>(&(), &String::new());
}

これは、&String::new() 関数引数が 'static ライフタイムを持たないにもかかわらずコンパイルされます。これは、関数を実際に呼び出すときに、「余分な」ライフタイム引数が遅延束縛パラメータについて考慮されるのではなく、破棄されるためです。

遅延束縛パラメータを持つ型のライブネス

関数アイテム型を含む型の outlives 境界をチェックするとき、早期束縛パラメータを考慮します。 例:

#![allow(unused)]
fn main() {
fn foo<T>(_: T) {}

fn requires_static<T: 'static>(_: T) {}

fn bar<T>() {
    let f /* : FooFnItem<T> */ = foo::<T>;
    requires_static(f);
}
}

型パラメーター T は早期束縛されるため、foo の関数アイテム型を脱糖すると、おおよそ struct FooFnItem<T> のようになります。 そのため、FooFnItem<T>: 'static が成り立つためには、T: 'static も成り立つことを要求しなければなりません。そうしないと、健全性バグにつながることになります。

残念ながら、コンパイラーのバグにより、早期束縛されたライフタイムを考慮していません。これが未解決の健全性バグ #84366 の原因です。 これは、生存性/型の outlives 境界について、早期束縛パラメーターと遅延束縛パラメーターの「違い」を示すことが不可能であることを意味します。遅延束縛できる唯一のジェネリックパラメーターの種類はライフタイムですが、それが正しく処理されていないためです。

それでも理論上は、#84366 が修正されれば、以下のコード例はそのような違いを示すはずです。

#![allow(unused)]
fn main() {
fn early_bound<'a: 'a>(_: &'a String) {}
fn late_bound<'a>(_: &'a String) {}

fn requires_static<T: 'static>(_: T) {}

fn bar<'b>() {
    let e = early_bound::<'b>;
    // これはエラーになる*べき*だが、ならない
    requires_static(e);

    let l = late_bound;
    // これは正しくエラーにならない
    requires_static(l);
}
}

パラメーターが遅延束縛されるための要件

ライフタイムパラメーターでなければならない

型パラメーターと Const パラメーターは遅延束縛できません。これは、dyn for<T> Fn(Box<T>)for<T> fn(Box<T>) のような型をサポートする方法がないためです。 そのような型を呼び出すには、基礎となる関数を単相化できる必要がありますが、動的ディスパッチによる間接参照を介してそれを行うことはできません。

where 句で使用されてはならない

現在、ジェネリックパラメーターが where 句で使用されている場合、そのパラメーターは早期束縛されなければなりません。 例:

#![allow(unused)]
fn main() {
trait Trait<'a> {}
fn foo<'a, T: Trait<'a>>(_: &'a String, _: T) {}
}

この例では、ライフタイムパラメーター 'a は where 句 T: Trait<'a> に現れるため、早期束縛されるものと見なされます。 これは、'a: 'a のような「自明な」where 句や、関数引数の wellformedness によって暗黙的に導かれる where 句についても当てはまります。例:

#![allow(unused)]
fn main() {
fn foo<'a: 'a>(_: &'a String) {}
fn bar<'a, T: 'a>(_: &'a T) {}
}

これら両方の関数において、ライフタイムパラメーター 'a は早期束縛されるものと見なされます。たとえ、それらが使用されている where 句が、実際には呼び出し側に何ら制約を課していないとも言える場合であってもです。

この制限の理由は、次の 2 つの組み合わせです。

  • 遅延束縛パラメーター上の境界は、それらがインスタンス化されるまで証明できません
  • 関数ポインターとトレイトオブジェクトには、基礎となる関数からの、まだ証明されていない where 句を表現する方法がありません

次の例を考えてみます。

#![allow(unused)]
fn main() {
trait Trait<'a> {}
fn foo<'a, T: Trait<'a>>(_: &'a T) {}

let f = foo::<String>;
let f = f as for<'a> fn(&'a String);
f(&String::new());
}

型チェック中のどこかの時点で、このコードに対してエラーが出力されるべきです。String はどのライフタイムについても Trait を実装していないためです。

ライフタイム 'a が遅延束縛されている場合、これをチェックするのは難しくなります。 foo を名指しする時点では、T: Trait<'a> トレイト境界の一部としてどのライフタイムを使用すべきかがわかりません。まだインスタンス化されていないためです。 関数アイテム型を関数ポインターに型強制するときには、関数を呼び出す際に証明されなければならない String: Trait<'a> トレイト境界を追跡する方法がありません。

ライフタイム 'a が早期束縛されている場合(rustc の現在の実装ではそうなっています)、関数 foo を名指しする時点でトレイト境界をチェックできます。 where 句で使用されるパラメーターを早期束縛にすることにより、関数に定義された where 句をチェックする自然な場所が得られます。

最後に、ライフタイムが暗黙の境界で使用されている場合には、早期束縛されることを要求しません。例:

#![allow(unused)]
fn main() {
fn foo<'a, T>(_: &'a T) {}

let f = foo;
f(&String::new());
f(&String::new());
}

このコードはコンパイルできるため、ライフタイムパラメーターが遅延束縛されていることが示されます。たとえ 'a が型 &'a T の中で使用されており、それによって T: 'a が成り立つことが暗黙的に要求されるとしてもです。 暗黙の境界は特別に扱うことができます。暗黙の境界を導入する型はいずれも関数ポインター型のシグネチャ内に存在するため、関数を呼び出すときに T: 'a を証明すべきであることがわかるからです。

引数型によって制約されていなければならない

関数アイテム型上の組み込み impl が、制約されていないジェネリックパラメーターを持つことにならないようにすることが重要です。これは非健全性につながる可能性があるためです。 これはユーザーが書いた impl に適用される制限と同じ種類のものです。たとえば、次のコードはエラーになります。

#![allow(unused)]
fn main() {
trait Trait {
    type Assoc;
}

impl<'a> Trait for u8 {
    type Assoc = &'a String;
}
}

関数アイテム上の組み込み impl に対応する例は、次のようになります。

fn foo<'a>() -> &'a String { /* ... */ }

ライフタイムパラメーター 'a が遅延束縛される場合、制約されていないライフタイムを持つ組み込み impl になることになります。'a が遅延束縛されているものとして、関数アイテム型とその impl の脱糖を手で書き下すことで、これを示すことができます。

// 注: これは単なるデモンストレーションであり、実際には `'a` は早期束縛される
struct FooFnItem;

impl<'a> Fn<()> for FooFnItem {
    type Output = &'a String;
    /* fn call(...) -> ... { ... } */
}

このような状況を避けるため、'a は早期束縛されるものと見なします。これにより、impl 上のライフタイムは self 型によって制約されます。

struct FooFnItem<'a>(PhantomData<fn() -> &'a String>);

impl<'a> Fn<()> for FooFnItem<'a> {
    type Output = &'a String;
    /* fn call(...) -> ... { ... } */
}

ty モジュール: 型の表現

ty モジュールは、Rust コンパイラが内部で型をどのように表現するかを定義します。 また、コンパイラにおける中心的なデータ構造である 型付けコンテキストtcx または TyCtxt)も定義します。

ty::Ty

rustc が型をどのように表現するかについて話すとき、通常は Ty と呼ばれる型を指します。 コンパイラには Ty に関するモジュールや型がかなり多くあります(Ty のドキュメント)。

ここで指している具体的な Tyrustc_middle::ty::Ty です (rustc_hir::Ty ではありません)。 この区別は重要なので、ty::Ty の詳細に入る前に、まずそれについて説明します。

rustc_hir::Tyty::Ty

rustc における HIR は、高レベル中間表現と考えることができます。 これは多かれ少なかれ AST(この章を参照)であり、ユーザーが書いた構文を表現し、構文解析といくつかの脱糖化の後に得られます。 HIR には型の表現がありますが、実際にはユーザーが書いたもの、つまりその型を表現するために書いたものをより強く反映しています。

対照的に、ty::Ty は型のセマンティクス、つまりユーザーが書いたものの意味を表します。 たとえば、rustc_hir::Ty はユーザーがプログラム内で u32 という名前を 2 回使ったという事実を記録しますが、 ty::Ty はその両方の使用が同じ型を指しているという事実を記録します。

例: fn foo(x: u32) → u32 { x }

この関数では、u32 が 2 回現れていることがわかります。 それが同じ型であること、つまりこの関数がある型の引数を受け取り、同じ型の引数を返すことはわかっています。 しかし HIR の観点からは、これらはプログラム内の 2 つの異なる場所に現れているため、2 つの別個の型インスタンスになります。 つまり、それぞれ異なる Span(位置)を持っています。

例: fn foo(x: &u32) -> &u32

さらに、HIR では情報が省かれている場合があります。 この型 &u32 は不完全です。完全な Rust の型には実際にはライフタイムが存在しますが、そのライフタイムを書く必要がなかったためです。 情報を挿入する省略規則もいくつかあります。 その結果は fn foo<'a>(x: &'a u32) -> &'a u32 のようになるかもしれません。

HIR レベルでは、これらは明示されておらず、かなり不完全な図になっていると言えます。 しかし、ty::Ty レベルでは、これらの詳細が追加され、完全になります。 さらに、u32 のような特定の型に対しては正確に 1 つの ty::Ty が存在し、その ty::Tyrustc_hir::Ty とは異なり、特定の使用箇所ではなく、プログラム全体のすべての u32 に対して使用されます。

まとめると次のようになります。

rustc_hir::Tyty::Ty
型の構文、つまりユーザーが書いたもの(いくつかの脱糖化を含む)を記述する。型のセマンティクス、つまりユーザーが書いたものの意味を記述する。
rustc_hir::Ty は、プログラム内の該当する場所に対応する独自の span を持つ。ユーザーのプログラム内の単一の場所には対応しない。
rustc_hir::Ty にはジェネリクスとライフタイムがある。ただし、それらのライフタイムの一部は LifetimeKind::Implicit のような特別なマーカーである。ty::Ty には、ユーザーが省いていたとしても、ジェネリクスとライフタイムを含む完全な型がある。
fn foo(x: u32) -> u32 { } - u32 の各使用を表す 2 つの rustc_hir::Ty があり、それぞれが独自の Span を持つ。また、rustc_hir::Ty は両方が同じ型であることを教えてくれない。fn foo(x: u32) -> u32 { } - プログラム全体にわたる u32 のすべてのインスタンスに対して 1 つの ty::Ty があり、ty::Tyu32 の両方の使用が同じ型を意味することを教えてくれる。
fn foo(x: &u32) -> &u32 { } - ここでも 2 つの rustc_hir::Ty がある。参照のライフタイムは、特別なマーカー LifetimeKind::Implicit を使って rustc_hir::Ty に現れる。fn foo(x: &u32) -> &u32 { }- 単一の ty::Ty。その ty::Ty には隠されたライフタイムパラメータがある。

順序

HIR は AST から直接構築されるため、ty::Ty が生成される前に行われます。 HIR が構築された後、いくつかの基本的な型推論と型チェックが行われます。 型推論の間に、あらゆるものの ty::Ty が何であるかを突き止め、また何かの型が曖昧でないかも確認します。 その後、ty::Ty は、すべてが期待される型を持っていることを確認しながら型チェックに使用されます。 hir_ty_lowering モジュールには、rustc_hir::Tyty::Ty に lowering する責任を持つコードがあります。 使用される主なルーチンは lower_ty です。 これは型チェックフェーズ中に発生しますが、「この関数はどのような引数型を期待しているのか」のような質問をしたいコンパイラの他の部分でも発生します。

セマンティクスが Ty の 2 つのインスタンスを駆動する仕組み

HIR は、最も少ない仮定を置く型情報の視点として考えることができます。 2 つのものが同じものだと証明されるまでは、それらは別個のものだと仮定します。 言い換えると、それらについて知っていることが少ないため、それらについての仮定も少なくすべきです。

構文的には、それらは 2 つの文字列です。N 行 20 列の "u32" と、N 行 35 列の "u32" です。 それらが同じであることはまだわかっていません。 そのため、HIR ではそれらを異なるものとして扱います。 後になって、それらがセマンティクス上は同じ型であると判断し、それが使用する ty::Ty になります。

別の例として、fn foo<T>(x: T) -> u32 を考えてみましょう。 誰かが foo::<u32>(0) を呼び出したとします。 これは、この呼び出しにおいて Tu32 が実際には同じ型であることが最終的に判明するという意味なので、最終的には同じ ty::Ty に到達することになりますが、rustc_hir::Ty は別個のものです。 (ただし、これは少し単純化しすぎています。型チェック中には関数をジェネリックにチェックし、u32 とは別個の T が依然として存在するためです。 後でコード生成を行うときには、各関数の「単相化された」(完全に置換された)バージョンを常に扱うことになり、そのため T が何を表しているか(そして具体的にはそれが u32 であること)がわかります。)

もう 1 つ例を示します。

#![allow(unused)]
fn main() {
mod a {
    type X = u32;
    pub fn foo(x: X) -> u32 { 22 }
}
mod b {
    type X = i32;
    pub fn foo(x: X) -> i32 { x }
}
}

ここでは、型 X は明らかにコンテキストによって変化します。 rustc_hir::Ty を見ると、 どちらの場合も X はエイリアスであると返されます(ただし、名前解決によって 別々のエイリアスに対応付けられます)。 しかし、ty::Ty シグネチャを見ると、fn(u32) -> u32 または fn(i32) -> i32 のいずれかになります(型エイリアスは完全に展開されます)。

ty::Ty の実装

rustc_middle::ty::Ty は実際には Interned<WithCachedTypeInfo<TyKind>> のラッパーです。 一般に Interned は無視してかまいません。基本的に明示的にアクセスすることはありません。 私たちは常にそれらを Ty の中に隠し、Deref の実装やメソッドを介して飛ばします。 TyKind は、多くの異なる Rust の型 (たとえばプリミティブ、参照、代数的データ型、ジェネリクス、ライフタイムなど)を表すバリアントを持つ大きな enum です。 WithCachedTypeInfo には、flagsouter_exclusive_binder のようないくつかのキャッシュされた値があります。 これらは 効率のための便利なハックであり、私たちが知りたい場合がある型に関する情報を要約していますが、 ここではそれほど重要ではありません。 最後に、Interned により、ty::Ty は薄いポインターのような 型になれます。 これにより、インターン化の他の利点に加えて、等価性の比較を低コストで行えます。

型の割り当てと操作

新しい型を割り当てるには、 Ty に定義されているさまざまな new_* メソッドを使用できます。 これらの名前は、おおむねさまざまな型の種類に対応しています。 例:

let array_ty = Ty::new_array_with_const_len(tcx, ty, count);

これらのメソッドはすべて Ty<'tcx> を返します。返されるライフタイムは、 この tcx がアクセスできるアリーナのライフタイムであることに注意してください。 型は常に正規化され、インターン化されます(そのため、まったく同じ型を二度割り当てることはありません)。

また、tcx 自体のフィールドにアクセスすることで、さまざまな一般的な型を見つけることもできます: tcx.types.booltcx.types.char などです(詳細は CommonTypes を参照してください)。

型の比較

型はインターン化されているため、== を使って等価性を効率的に比較できます — しかし、ハッシュ化して重複を探している場合でもない限り、これはほとんど決して望む操作ではありません。 これは、Rust では同じ型を表現する方法が複数あることが多く、 特に推論が関わるとそうなるためです。

たとえば、型 {integer}ty::Infer(ty::IntVar(..)) は整数推論変数で、 0 のような整数リテラルの型)と u8ty::UInt(..))は、 互いに代入可能かどうかをテストするときには等しいものとして扱われるべき場合がよくあります (これは診断コードで一般的な操作です)。 ただし、それらに対する ==false を返します。なぜなら、それらは異なる型だからです。

2 つの型を正しく比較する最も単純な方法には、推論コンテキスト(infcx)が必要です。 それがある場合、infcx.can_eq(param_env, ty1, ty2) を使用して、 型を等しくできるかどうかを確認できます。 これは通常、診断中に確認したいことです。診断で関心があるのは、 2 つの型を互いに代入できるかどうかのような問いであり、 コンパイラの型検査レイヤーで同一に表現されているかどうかではないためです。

推論コンテキストを扱うときは、型の内部にある可能性のある推論変数が、 実際にその推論コンテキストに属していることを確認するよう注意する必要があります。 すでに推論コンテキストにアクセスできる関数内にいる場合、これは成り立つはずです。 具体的には、これは HIR 型検査中や MIR 借用検査中に当てはまります。

もう 1 つ考慮すべき点は正規化です。 2 つの型は実際には同じであっても、一方が関連型の背後にある場合があります。 それらを正しく比較するには、まず型を正規化する必要があります。 これは主に、HIR 型検査中、および TyCtxt クエリから得られるすべての型 (たとえば tcx.type_of() から得られる型)で問題になります。

型検査中に FnCtxt または ObligationCtxt が利用可能な場合は、型を正規化するために それらに対して .normalize(ty) を使用するべきです。 型検査後、診断コードは tcx.normalize_erasing_regions(ty) を使用できます。

Ty に対して == を使って問題ない場合もあります。 これは、たとえば late lint の場合 やモノモーフィゼーション後に当てはまります。型検査が完了しているため、すべての推論変数が 解決され、すべての領域が消去されているからです。 このような場合、推論変数 または正規化が問題にならないことが分かっているなら、その lint を #[allow] または #[expect] することが推奨されます。

診断コードが推論コンテキストにアクセスできない場合、どこかで(型検査中などに) 利用可能な推論コンテキストがあるなら、それを関数呼び出しを通じて渡すべきです。

推論コンテキストがまったく利用できない場合は、 type-inference で説明されているように作成できます。 しかし、これは関係する型(たとえば、 tcx.type_of() のようなクエリから来た場合)が、実際に fresh_args_for_item を使用して 新しい推論変数で置換されている場合にのみ有用です。 これにより、「任意の T に対する Vec<T>Vec<u32> と単一化できるか?」のような問いに答えることができます。

ty::TyKind のバリアント

注: TyKind は関数型プログラミングにおける Kind の概念ではありません

コンパイラ内で Ty を扱うときは、その型の kind に対して match することが一般的です:

fn foo(x: Ty<'tcx>) {
  match x.kind {
    ...
  }
}

kind フィールドの型は TyKind<'tcx> であり、これはコンパイラ内のすべての異なる種類の 型を定義する enum です。

注意: 型推論中に型の kind フィールドを検査するのは危険な場合があります。 推論変数や考慮すべき他のものがある場合があり、また型がまだ分かっておらず、 後で分かるようになる場合があるためです。

関連する型はたくさんあり、いずれ扱います(たとえば領域/ライフタイム、 「置換」など)。

TyKind enum には多くのバリアントがあり、その ドキュメント を見ることで確認できます。 以下はその一部です:

  • 代数的データ型 (ADT) 代数的データ型は、structenum、またはunionです。 内部的には、structenumunionは実際には 同じ方法で実装されています。これらはすべてty::TyKind::Adtです。 基本的にはユーザー定義型です。 これらについては後で詳しく説明します。
  • Foreign extern type Tに対応します。
  • Str str型です。 ユーザーが&strと書いた場合、Strはその型のstr部分を表す方法です。
  • Slice [T]に対応します。
  • Array [T; n]に対応します。
  • RawPtr *mut Tまたは*const Tに対応します。
  • Ref Refは安全な参照、&'a mut Tまたは&'a Tを表します。 Refにはいくつかの 関連する部分があります。たとえば、Ty<'tcx>は参照が参照する型です。 Region<'tcx>は参照のライフタイムまたはリージョンであり、Mutabilityは参照が ミュータブルかどうかを表します。
  • Param 型パラメーター(例: Vec<T>T)を表します。
  • Error どこかで発生した型エラーを表し、よりよい診断を出力できるようにします。 これについては後で詳しく説明します。
  • そして他にも多数

インポート規約

厳密なルールはありませんが、tyモジュールは次のように使われる傾向があります。

use ty::{self, Ty, TyCtxt};

特に、非常によく使われるため、Ty型とTyCtxt型は直接インポートされます。 他の 型は、多くの場合、明示的なty::プレフィックス付きで参照されます(例: ty::TraitRef<'tcx>)。ただし、一部の モジュールでは、より多い、またはより少ない名前の集合を明示的にインポートすることを選びます。

型エラー

ユーザーが型エラーを起こしたときに生成されるTyKind::Errorがあります。 その考え方は、 この型を伝播させ、それによって発生する他のエラーを抑制することで、カスケードするコンパイラーエラーメッセージで ユーザーを圧倒しないようにする、というものです。

TyKind::Errorには重要な不変条件があります。 コンパイラーは、エラーがすでにユーザーに報告されていることを知っている場合を除き、決してErrorを生成すべきではありません。 これは通常、 (a) その場で報告したばかりであるか、(b) 既存のError型を伝播している(その 場合、そのエラー型が生成されたときにエラーが報告されているはずである)ためです。

この不変条件を維持することが重要なのは、Error型の要点が他のエラーを抑制すること、つまりそれらを報告しないことにあるためです。実際には ユーザーにエラーを出力せずにError型を生成してしまうと、その後のエラーが抑制される可能性があり、 コンパイルが意図せず成功してしまうかもしれません!

場合によっては第三のケースがあります。 エラーが報告されていると考えているものの、それは ローカルではなく、コンパイルのもっと前の段階で報告されていたはずだと考えている場合です。 その場合、delayed_bugまたはspan_delayed_bugで「遅延バグ」を作成できます。 これにより、コンパイルがエラーを生成することを期待している、という記録が残されます。ただし、 コンパイルが成功してしまった場合は、コンパイラーバグ報告がトリガーされます。

安全性を高めるため、実際にはrustc_middle::tyの外部で TyKind::Error値を生成することはできません。TyKind::Errorには、他の場所で構築できないようにする プライベートメンバーがあります。 代わりに、 Ty::new_errorメソッドまたはTy::new_error_with_messageメソッドを使うべきです。 これらのメソッドは、ErrorGuaranteedを受け取るか、 またはspan_delayed_bugを呼び出してから、種類がErrorのインターン済みTyを返します。 すでにspan_delayed_bugを使う予定だった場合は、冗長な遅延バグを避けるために、代わりに spanとメッセージをty_error_with_messageに渡すだけで済みます。

TyKindバリアントの省略記法

Tyのデバッグ出力を見るときや、単にコンパイラー内のさまざまな型について話すときに、有効なRustではないものの、型に関する内部情報を簡潔に表すために使われる構文に遭遇することがあります。 以下は、さまざまな構文が実際に何を意味するのかを示すクイックリファレンスのチートシートです。

  • ジェネリックパラメーター: {name}/#{index} 例: T/#0。ここでindexはジェネリックパラメーターのリスト内での位置に対応します
  • 推論変数: ?{id} 例: ?x/?0。ここでidは推論変数を識別します
  • バインダー由来の変数: ^{binder}_{index} 例: ^0_x/^0_2。ここでbinderindexは、どのバインダーのどの変数が参照されているかを識別します
  • プレースホルダー: !{id}または!{id}_{universe} 例: !x/!0/!x_2/!0_2。指定されたユニバース内の何らかの一意な型を表します。ユニバースが0の場合、多くの場合は省かれます

これらについては後の章でより詳しく扱われるはずです。

ADT とジェネリック引数

ADT という用語は “Algebraic data type”(代数的データ型)を表し、Rust では struct、enum、または union を指します。

ADT の表現

MyStruct<u32> のような型の例を考えてみましょう。ここで MyStruct は次のように定義されています。

struct MyStruct<T> { x: u8, y: T }

MyStruct<u32>TyKind::Adt のインスタンスになります。

Adt(&'tcx AdtDef, GenericArgs<'tcx>)
//  ------------  ---------------
//  (1)            (2)
//
// (1) は `MyStruct` の部分を表します
// (2) は `<u32>`、つまり「置換」/ ジェネリック引数を表します

2 つの部分があります。

  • AdtDef は struct/enum/union を参照しますが、その型 パラメーターに対する値は含みません。 この例では、これは引数 u32 なしMyStruct の部分です。 (HIR では struct、enum、union は異なる形で表現されますが、ty::Ty では、 それらはすべて TyKind::Adt を使って表現されることに注意してください。)
  • GenericArgs は、ジェネリックパラメーターに置換される値のリストです。 MyStruct<u32> の例では、[u32] のようなリストになります。 ジェネリクスと置換については、少し後で詳しく掘り下げます。

AdtDefDefId

ソースコード内で定義されたすべての型には、一意の DefId があります(この 章を参照)。 これには ADT とジェネリクスが含まれます。 上で示した MyStruct<T> の定義では、 2 つの DefId があります。1 つは MyStruct 用で、もう 1 つは T 用です。 上のコードは u32 用の新しい DefId を生成しないことに注意してください。 なぜなら、それはそのコード内で定義されていない(参照されているだけ)からです。

AdtDef は、多くの便利なヘルパーメソッドを持つ DefId のラッパーのようなものです。 AdtDefDefId の間には、基本的に 1 対 1 の関係があります。 tcx.adt_def(def_id) クエリを使って、DefId に対する AdtDef を取得できます。 AdtDef はすべて intern されており、これは 'tcx ライフタイムによって示されています。

質問: なぜ AdtDef の「内部」で置換しないのか?

ジェネリック struct を (AdtDef, args) で表現することを思い出してください。 では、なぜこの仕組みをわざわざ使うのでしょうか?

別の方法として、型を表現する際に、すべての型がすでに置換された、 完全に置換済みの AdtDef の形を常に新しく作成する、という選択もできたはずです。 これは手間が少ないように見えます。 しかし、(AdtDef, args) という仕組みには、これよりいくつか利点があります。

まず、(AdtDef, args) の仕組みには効率上の利点があります。

struct MyStruct<T> {
  ... 100s of fields ...
}

// やりたいこと: MyStruct<A> ==> MyStruct<B>

このような例では、A への 1 つの参照を B に置き換えるだけで、 MyStruct<A>MyStruct<B>(など)として非常に低コストにインスタンス化できます。 しかし、すべてのフィールドを eager にインスタンス化した場合、 AdtDef 内のすべてのフィールドを走査して、 それらすべての型を更新しなければならない可能性があるため、はるかに多くの作業になることがあります。

もう少し深く言うと、これは Rust における struct が nominalであることに対応します。 つまり、それらはその名前によって定義されるということです(そして、その内容はその名前の定義からインデックス付けされ、 型そのものの「内部」に一緒に保持されるわけではありません)。

GenericArgs

ジェネリック型 MyType<A, B, …> が与えられた場合、MyType のジェネリック引数のリストを保存する必要があります。

rustc では、これは GenericArgs を使って行われます。 GenericArgs は、ジェネリック項目に対するジェネリック引数のリストを表す GenericArg のスライスへの thin pointer です。 たとえば、2 つの型パラメーター KV を持つ struct HashMap<K, V> が与えられた場合、型 HashMap<i32, u32> を表すために使われる GenericArgs&'tcx [tcx.types.i32, tcx.types.u32] によって表されます。

GenericArg は概念的には 3 つのバリアントを持つ enum で、型引数、const 引数、ライフタイム引数にそれぞれ 1 つずつ対応します。 実際には、これは GenericArgKind によって表現され、GenericArg はそれを GenericArgKind に変換するメソッドを持つ、より空間効率の良いバージョンです。

実際の GenericArg struct は、型、ライフタイム、または const を、下位 2 ビットに discriminant を格納した intern 済みポインターとして保存します。 GenericArgs の実装そのものに取り組んでいる場合を除き、通常は GenericArg を直接扱う必要はなく、代わりに GenericArg::unpack() メソッドを介して取得できる安全な GenericArgKind 抽象を利用するべきです。

場合によっては GenericArg を構築しなければならないことがあります。これは Ty/Const/Region::into() または GenericArgKind::pack によって行えます。

// ジェネリック引数を unpack および pack する例。
fn deal_with_generic_arg<'tcx>(generic_arg: GenericArg<'tcx>) -> GenericArg<'tcx> {
    // 生の `GenericArg` を安全に扱うために unpack する。
    let new_generic_arg: GenericArgKind<'tcx> = match generic_arg.unpack() {
        GenericArgKind::Type(ty) => { /* ... */ }
        GenericArgKind::Lifetime(lt) => { /* ... */ }
        GenericArgKind::Const(ct) => { /* ... */ }
    };
    // `GenericArgKind` を pack してジェネリック args リストに格納する。
    new_generic_arg.pack()
}

すべてをまとめると、次のようになります。

struct MyStruct<T>(T);
type Foo = MyStruct<u32>

Foo 型エイリアス内に書かれた MyStruct<U> については、次のように表現します。

  • MyStruct に対する AdtDef(および対応する DefId)があります。
  • リスト [GenericArgKind::Type(Ty(u32))] を含む GenericArgs があります。
  • そして最後に、上に挙げた AdtDefGenericArgs を持つ TyKind::Adt があります。

ネストしたジェネリック args

struct MyStruct<T>(T);

impl<T> MyStruct<T> {
    fn func<T2, T3>() {}
}

fn main() {
    MyStruct::<u32>::func::<bool, char>();
}

構文 MyStruct::<u32>::func::<bool, char> はタプルによって表現されます。つまり、func を指す DefId と、 すべての包含するジェネリックパラメーターを「たどる」GenericArgs リストです。この場合、そのリストは [u32, bool, char] になります。

generics_of クエリによって返される ty::Generics 型は、ネストした階層がどのように リストへ平坦化されるかに関する情報を含み、GenericArgs リスト内のどのインデックスがどの ジェネリックに対応するかを把握できるようにします。 その動作の一般的な考え方は、外側から内側へ(例では TT2 より前)、左から右へ (T2T3 より前)というものですが、いくつかの複雑な点があります。

  • trait には暗黙の Self ジェネリックパラメーターがあり、これは最初(つまり 0 番目)のジェネリックパラメーターです。Self はすべての状況でジェネリックパラメーターを意味するわけではないことに注意してください。Res::SelfTyAliasRes::SelfCtor を参照してください。
  • early-bound ジェネリックパラメーターのみが含まれ、late-bound ジェネリクスは含まれません。
  • … などです …

平坦化がどのように動作するかの正確な詳細については、ty::Generics を確認してください。

パラメータの Ty/Const/Region

ジェネリックアイテムの内部では、スコープ内のジェネリックパラメータを使用する型を書くことができます。たとえば fn foo<'a, T>(_: &'a Vec<T>) です。 この具体的なケースでは、&'a Vec<T> 型は内部的には次のように表現されます。

TyKind::Ref(
  RegionKind::LateParam(DefId(foo), DefId(foo::'a), "'a"),
  TyKind::Adt(Vec, &[TyKind::Param("T", 0)])
)

ジェネリックパラメータの使用は、3 つの別々の方法で表現します。

この章では、TyKind::ParamConstKind::ParamRegionKind::EarlyParam のみを扱います。

Ty/Const パラメータ

TyKind::ParamConstKind::Param は同じように実装されているため、このセクションでは簡単のため TyKind::Param のみに言及します。 ただし、ここで述べることはすべて ConstKind::Param にも当てはまることを覚えておいてください。

TyKind::Param には、パラメータの名前とインデックスという 2 つのものが含まれます。

TyKind::Param の使用例として、次の具体例を見てください。

struct Foo<T>(Vec<T>);

Vec<T> 型は、TyKind::Adt(Vec, &[GenericArgKind::Type(Param("T", 0))]) として表現されます。

名前はある程度自明で、型パラメータの名前です。 型パラメータのインデックスは、スコープ内にあるジェネリックパラメータのリストにおける その順序を示す整数です。 これには、そのパラメータが定義されているアイテムよりも外側のスコープにあるアイテムで定義されたパラメータも含まれることに注意してください。 次の例を考えてみましょう。

struct Foo<A, B> {
  // A のインデックスは 0 になる
  // B のインデックスは 1 になる

  .. // いくつかのフィールド
}
impl<X, Y> Foo<X, Y> {
  fn method<Z>() {
    // ここでは、X、Y、Z はすべてスコープ内にある
    // X のインデックスは 0
    // Y のインデックスは 1
    // Z のインデックスは 2
  }
}

具体的には、パラメータが定義されているアイテムの ty::Generics が与えられたとき、 インデックスが 2 で、ルートの parent から始める場合、それは導入される 3 番目のパラメータになります。 たとえば上の例では、Z のインデックスは 2 であり、impl ブロックから始めて導入される 3 番目のジェネリックパラメータです。

インデックスによって Ty は完全に定義され、コンパイルしているコードについて推論する上で重要な TyKind::Param の唯一の部分です。

通常、名前が何であるかは気にせず、インデックスのみを使用します。 名前は診断とデバッグログのために含まれています。そうしなければ、 出力を理解することが非常に難しくなるからです。つまり、Vec<Param(0)>: SizedVec<T>: Sized の違いです。デバッグ出力では、パラメータ型は しばしば {name}/#{index} として出力されます。たとえば関数 fooVec<T> をデバッグ出力すると、Vec<T/#0> と書かれます。

代替表現として、名前だけを持つことも考えられます。 しかし、インデックスを使用する方が効率的です。いくつかの引数でジェネリックパラメータをインスタンス化するときに GenericArgs にインデックスアクセスできることを意味するからです。 そうしない場合、GenericArgsHashMap<Symbol, GenericArg> として保存し、ジェネリックアイテムを使用するたびにハッシュマップ検索を行う必要があります。

理論上は、インデックスを使うことで、同じ名前を使用する複数の別々のパラメータを持つことも可能になります。たとえば、 impl<A> Foo<A> { fn bar<A>() { .. } } です。 シャドーイングを禁止するルールによってこれは難しくなっていますが、そうした言語ルールは将来変わる可能性があります。

ライフタイムパラメータ

Ty/Const の単一の Param バリアントとは対照的に、ライフタイムにはリージョンパラメータを表現するための 2 つのバリアントがあります。RegionKind::EarlyParamRegionKind::LateParam です。 その理由は、関数が早期束縛パラメータと後期束縛パラメータを区別するためであり、これについては前の章で説明されています(リンクを参照)。

RegionKind::EarlyParam は、Ty/ConstParam バリアントと同じ構造です。単に u32 インデックスと Symbol です。 非関数アイテムで定義されたライフタイムパラメータには、常に ReEarlyParam を使用します。 関数では、早期束縛パラメータには ReEarlyParam を使用し、後期束縛パラメータには ReLateParam を使用します。 Ty および Const パラメータと同様に、これらはデバッグフォーマットでしばしば 'SYMBOL/#INDEX として出力されることに注意してください。

例:

// この関数のシグネチャは次のように表現される:
//
// ```
// fn(
//     T/#2,
//     Ref('a/#0, Ref(ReLateParam(...), u32))
// ) -> Ref(ReLateParam(...), u32)
// ```
fn foo<'a, 'b, T: 'a>(one: T, two: &'a &'b u32) -> &'b u32 {
    ...
}

RegionKind::LateParam については、バインダーのインスタンス化の章でさらに説明します。

TypeFoldableTypeFolder

前の章では、バインダーのインスタンス化について説明しました。 これには、束縛された変数の使用箇所を見つけて置換するために、Early(Binder) の内部にあるすべてを調べることが含まれます。 バインダーは Ty だけでなく、任意の Rust 型 T をラップできます。 では、Early/Binder 型に対して instantiate メソッドをどのように実装するのでしょうか?

答えは、2つのトレイトです。 TypeFoldableTypeFolder です。

  • TypeFoldable は、型情報を埋め込む型によって実装されます。これにより、TypeFoldable の内容を再帰的に 処理し、それらに対して何かを行うことができます。
  • TypeFolder は、TypeFoldable を処理している間に遭遇する型に対して何を行いたいかを定義します。

たとえば、TypeFolder トレイトには、型を入力として受け取り、結果として新しい型を返す fold_ty というメソッドがあります。 TypeFoldable は、自身に対して TypeFolderfold_foo メソッドを呼び出し、 TypeFolder がその内容(内部に含まれる型、リージョンなど)にアクセスできるようにします。

これは、Rust で私たちが愛用しているイテレーターコンビネーターになぞらえて考えることができます。

vec.iter().map(|e1| foo(e2)).collect()
//             ^^^^^^^^^^^^ `TypeFolder` に相当
//         ^^^ `TypeFoldable` に相当

繰り返すと、次のようになります。

  • TypeFolder は、“map” 操作を定義するトレイトです。
  • TypeFoldable は、型を埋め込むものによって実装されるトレイトです。

subst の場合、それが TypeFolder として実装されていることがわかります: ArgFolder。 その実装を見ると、実際の置換が行われている箇所がわかります。

ただし、この実装が super_fold_with というメソッドを呼び出していることにも気づくかもしれません。これは 何でしょうか?これは TypeFoldable のメソッドです。次の TypeFoldableMyFoldable を考えてみましょう。

struct MyFoldable<'tcx> {
  def_id: DefId,
  ty: Ty<'tcx>,
}

TypeFolder は、MyFoldable のフィールドの一部だけを新しい値に置き換えたい場合に、MyFoldable に対して super_fold_with を呼び出せます。 代わりに MyFoldable 全体を別のものに置き換えたい場合は、fold_with を呼び出します(これは TypeFoldable の別のメソッドです)。

ほとんどすべての場合、構造体全体を置き換えたいわけではありません。構造体内の ty::Ty だけを置き換えたいので、 通常は super_fold_with を呼び出します。MyFoldable が持ちうる典型的な実装は、次のようなものになるでしょう。

my_foldable: MyFoldable<'tcx>
my_foldable.subst(..., subst)

impl TypeFoldable for MyFoldable {
  fn super_fold_with(&self, folder: &mut impl TypeFolder<'tcx>) -> MyFoldable {
    MyFoldable {
      def_id: self.def_id.fold_with(folder),
      ty: self.ty.fold_with(folder),
    }
  }

  fn super_visit_with(..) { }
}

ここでは、super_fold_with を実装して MyFoldable のフィールドをたどり、それらに対して fold_with を呼び出していることに注目してください。つまり、folder は def_idty を置き換えることはできますが、 MyFoldable 構造体全体を置き換えることはできません。

物事をまとめるために、もう1つ例を見てみましょう。Vec<Vec<X>> のような型があるとします。 ty::TyAdt(Vec, &[Adt(Vec, &[Param(X)])]) のようになります。subst(X => u32) を行いたい場合、 まず型全体を見ます。外側のレベルでは行うべき置換がないことがわかるので、1レベル下に降りて Adt(Vec, &[Param(X)]) を見ます。ここでもまだ行うべき置換はないので、さらに下に降ります。 今度は Param(X) を見ており、これは置換できるので、u32 に置き換えます。これ以上下には降りられないので、 処理は完了し、全体の結果は Adt(Vec, &[Adt(Vec, &[u32])]) になります。

最後に1つ触れておくことがあります。TypeFoldable をフォールドするとき、多くの場合、ほとんどのものは変更したくありません。 型に到達したときだけ何かを行いたいのです。つまり、多くの TypeFoldable 型では、その実装が基本的に各フィールドの TypeFoldable 実装へ転送するだけになる場合があります。このような TypeFoldable の実装を手で書くのはかなり退屈になりがちです。 このため、#![derive(TypeFoldable)] を使えるようにする derive マクロがあります。これは ここで定義されています。

subst 置換の場合、実際の folder は、すでに述べたインデックス付けを行います。 そこでは Folder を定義し、TypeFoldable に対して fold_with を呼び出して自身を処理します。 次に、各型を処理するメソッドである fold_tyty::Param を探し、それらについては 置換のリストから得たものに置き換え、それ以外の場合は型を再帰的に処理します。 置き換えるために、ty_for_param を呼び出します。これは Param のインデックスを使って置換のリストにインデックスを付けるだけです。

エイリアスと正規化

エイリアス

Rust には、何らかの「基になる」型と等しいと見なされる型がいくつかあります。たとえば、固有関連型、トレイト関連型、自由型エイリアス(type Foo = u32)、不透明型(-> impl RPIT)などです。こうした型を「エイリアス」と見なし、エイリアス型は TyKind::Alias バリアントで表現され、エイリアスの種類は AliasTyKind enum によって追跡されます。

正規化とは、これらのエイリアス型を取得し、それらが等しい基になる型で置き換える処理です。たとえば、型エイリアス type Foo = u32 がある場合、Foo を正規化すると u32 になります。

エイリアスという概念はに固有のものではなく、この概念は定数/const generics にも適用されます。しかし、現在のコンパイラでは const エイリアスを「第一級の概念」として実際には扱っていないため、この章では主に型の文脈で説明します(ただし、概念自体は問題なく転用できます)。

リジッド、曖昧、および未正規化のエイリアス

エイリアスは「リジッド」、「曖昧」、または単に未正規化のいずれかです。

型の「形状」が変化しない場合、その型をリジッドと見なします。たとえば Box はリジッドです。どれだけ正規化しても Boxu32 に変えることはできないためです。一方、<vec::IntoIter<u32> as Iterator>::Itemu32 に正規化できるため、リジッドではありません。

エイリアスは、それ以上正規化できるようになることが決してない場合にリジッドです。リジッドなエイリアスの具体例は、T: Iterator<Item = ...> 境界がなく、単なる T: Iterator 境界だけがある環境における <T as Iterator>::Item です。

#![allow(unused)]
fn main() {
fn foo<T: Iterator>() {
    // このエイリアスは*リジッド*です
    let _: <T as Iterator>::Item;
}

fn bar<T: Iterator<Item = u32>>() {
    // このエイリアスは `u32` に正規化できるため、リジッド*ではありません*
    let _: <T as Iterator>::Item;
}
}

エイリアスがまだ正規化できないものの、現在の環境では最終的に正規化可能になるかもしれない場合、それを「曖昧」なエイリアスと見なします。これは、エイリアスに推論変数が含まれており、トレイトがどのように実装されているかを判断できない場合に発生することがあります。

#![allow(unused)]
fn main() {
fn foo<T: Iterator, U: Iterator>() {
    // このエイリアスは「曖昧」と見なされます
    let _: <_ as Iterator>::Item;
}
}

これらを「曖昧」なエイリアスと呼ぶ理由は、それがリジッドなエイリアスかどうかが曖昧だからです。

_: Iterator トレイト impl の由来は曖昧(つまり不明)です。それは何らかの impl Iterator for u32 かもしれませんし、何らかの T: Iterator トレイト境界かもしれません。まだ分かりません。_: Iterator が成り立つ理由に応じて、そのエイリアスは未正規化のエイリアスである場合もあれば、リジッドなエイリアスである場合もあります。このエイリアスがどの種類のエイリアスなのかは曖昧です。

最後に、エイリアスは単に未正規化である場合があります。<Vec<u32> as IntoIterator>::Iter は、すでに std::vec::IntoIter<u32> に正規化できるにもかかわらず、まだそれが行われていないため、未正規化のエイリアスです。


Free エイリアスと Inherent エイリアスは、リジッドにも曖昧にもなり得ないことに注意する価値があります。なぜなら、それらを名指しすることは、そのエイリアスの基になる型を指定するエイリアス定義が解決済みであることも意味するためです。

発散するエイリアス

エイリアスは、その定義が正規化先となる基になる非エイリアス型を指定していない場合、「発散する」と見なされます。発散するエイリアスの具体例は次のとおりです。

#![allow(unused)]
fn main() {
type Diverges = Diverges;

trait Trait {
    type DivergingAssoc;
}
impl Trait for () {
    type DivergingAssoc = <() as Trait>::DivergingAssoc;
}
}

この例では、DivergesDivergingAssoc はどちらも、自分自身と等しいものとして定義されている発散する型エイリアスの「自明な」ケースです。Diverges が正規化され得る基になる型は存在しません。

発散するエイリアスが定義されたときには、一般にはエラーにしようとしますが、これは完全に「ベストエフォート」のチェックです。前の例では、定義が検出できるほど「十分に単純」なので、エラーが出力されます。しかし、より複雑なケースや、ジェネリックパラメーターの一部のインスタンス化だけが発散するエイリアスを生じるケースでは、エラーを出力しません。

#![allow(unused)]
fn main() {
trait Trait {
    type DivergingAssoc<U: Trait>;
}
impl<T: ?Sized> Trait for T {
    // このエイリアスは常に発散しますが、コンパイラにはそれが
    // 「見えない」ため、エラーを出力しません。
    type DivergingAssoc<U: Trait> = <U as Trait>::DivergingAssoc<U>;
}
}

最終的に、これは型システム内のエイリアスが発散しないという保証がないことを意味します。エイリアスは一部の特定のジェネリック引数でのみ発散する場合があるため、エイリアスが発散するかどうかは、それが完全に具体化されて初めて分かるということでもあります。つまり、コード生成/const 評価も発散するエイリアスを扱わなければなりません。

trait Trait {
    type Diverges<U: Trait>;
}
impl<T: ?Sized> Trait for T {
    type Diverges<U: Trait> = <U as Trait>::Diverges<U>;
}

fn foo<T: Trait>() {
    let a: T::Diverges<T>;
}

fn main() {
    foo::<()>();
}

この例では、foo::<()> のコード生成中にのみ、発散するエイリアスによるエラーに遭遇します。foo への呼び出しが削除されると、コンパイルエラーは出力されません。

不透明型

不透明型は比較的特殊な種類のエイリアスであり、独自の章で扱われています: 不透明型

Const エイリアス

型エイリアスとは異なり、const エイリアスは型システム内で直接表現されません。代わりに、const エイリアスは常に、const アイテムへのパス式を含む匿名ボディです。これは、型システムにおける唯一の「const エイリアス」が、匿名の未評価 const 本体であることを意味します。

したがって、ConstKind::Alias(AliasCtKind::Projection/Inherent/Free, _) は存在せず、代わりに匿名定数を表現するために使用される ConstKind::Unevaluated だけがあります。

#![allow(unused)]
fn main() {
fn foo<const N: usize>() {}

const FREE_CONST: usize = 1 + 1;

fn bar() {
    foo::<{ FREE_CONST }>();
    // const 引数は何らかの匿名定数で表現されます:
    // ```pseudo-rust
    // const ANON: usize = FREE_CONST;
    // foo::<ConstKind::Unevaluated(DefId(ANON), [])>();
    // ```
}
}

これは const generics の機能が改善されるにつれて変わる可能性が高いです。たとえば、feature(associated_const_equality)feature(min_generic_const_args) はどちらも、const エイリアスを型と同様に扱うこと(すべての const 引数を匿名定数でラップしないこと)を必要とします。

正規化とは何か

構造的正規化と深い正規化

正規化には、構造的(浅いとも呼ばれることがあります)と深いものの 2 つの形式があります。構造的正規化は、型の「最外層」の部分だけを正規化するものと考えるべきです。一方、深い正規化は型内のすべてのエイリアスを正規化します。

実際には、構造的正規化によって型の外側の層だけでなくそれ以上が正規化される場合がありますが、この振る舞いに依存すべきではありません。束縛変数(for<'a>)を利用する、正規化不能な非リジッドエイリアスは、どちらの種類の正規化によっても正規化できません。 例として、概念的には、型 Vec<<u8 as Identity>::Assoc> を構造的に正規化しても no-op になりますが、深く正規化すると Vec<u8> になります。ただし実際には、構造的正規化でも Vec<u8> になりますが、繰り返しになりますが、これに依存すべきではありません。

エイリアスを束縛変数を使うように変更すると、異なる挙動になります。Vec<for<'a> fn(<&'a u8 as Identity>::Assoc)> は、構造的に正規化した場合は変化しませんが、深く正規化した場合は Vec<for<'a> fn(&'a u8)> になります。

コア正規化ロジック

エイリアスを構造的に正規化することは、そのエイリアスを定義内で等しいものとして定義されているものに置き換えるよりも、少し微妙です。エイリアスを正規化した結果は、剛性型か推論変数(後で剛性型に推論される)のいずれかであるべきです。これを実現するために、2 つのことを行います。

まず、曖昧なエイリアスを正規化する際には、それをそのまま残すのではなく推論変数に正規化します。これには主に 2 つの効果があります。

  • 推論変数は剛性型ではありませんが、常に剛性型推論されることになるため、正規化の結果を再度正規化する必要がないことを保証できます
  • 推論変数は、型が非剛性であるすべての場合に使用されるため、コンパイラの他の部分が曖昧なエイリアス推論変数の両方を扱う必要がなくなります

次に、正規化がエイリアスの定義で指定された型を直接返すのではなく、返す前にまずその型を正規化します1。これは、正規化が冪等であり、呼び出し元がループ内で実行する必要がないようにするためです。

#![allow(unused)]
#![feature(lazy_type_alias)]

fn main() {
type Foo<T: Iterator> = Bar<T>;
type Bar<T: Iterator> = <T as Iterator>::Item;

fn foo() {
    let a_: Foo<_>;
}
}

この例では次のようになります。

  • Foo<?x> を正規化すると Bar<?x> になりますが、Foo が等しいものとして定義されている型内のエイリアスを正規化したい
  • Bar<?x> を正規化すると <?x as Iterator>::Item になりますが、ここでも、Bar が等しいものとして定義されている型内のエイリアスを正規化したい
  • <?x as Iterator>::Item を正規化すると、<?x as Iterator>::Item は曖昧なエイリアスであるため、新しい推論変数 ?y になります
  • 最終結果として、Foo<?x> を正規化すると ?y になります

正規化の方法

型システムとやり取りする際には、型の正規化を要求する必要があることがよくあります。基礎となる正規化ロジックにはさまざまなエントリーポイントがあり、それぞれのエントリーポイントはコンパイラの特定の部分でのみ使用すべきです。

追加の複雑さとして、コンパイラは現在、古い trait ソルバーから新しい trait ソルバーへの移行中です。 この移行の一環として、コンパイラにおける正規化へのアプローチはかなり大きく変わっており、その結果、一部の正規化エントリーポイントは「古いソルバー専用」となり、新しいソルバーが安定化した後、長期的には削除される予定です。 この移行は、Github の WG-trait-system-refactor ラベルで追跡できます。

以下は、コンパイラにおける正規化のさまざまなエントリーポイントの大まかな概要です。

  • infcx.at.structurally_normalize
  • infcx.at.(deeply_)?normalize
  • infcx.query_normalize
  • tcx.normalize_erasing_regions
  • traits::normalize_with_depth(_to)
  • EvalCtxt::structurally_normalize

trait ソルバーの外部

InferCtxt 型は、解析中に正規化するための「主要な」方法である normalizedeeply_normalizestructurally_normalize を公開しています。これらの関数は、多くの場合、FnCtxtObligationCtxt のような各種 InferCtxt ラッパー型上でラップされ、いくつかの引数や戻り値の一部を自動的に扱うための小さな API 調整を加えて再公開されています。

構造的な InferCtxt 正規化

infcx.at.structurally_normalize は、推論変数とリージョンを扱える構造的正規化を公開しています。通常、型の kind を調べる場合には必ず使用すべきです。

HIR Typeck の内部には、関連する正規化メソッドである fcx.structurally_resolve があります。これは、解決対象の型が未解決の推論変数である場合にエラーになります。新しいソルバーが有効な場合は、その型を構造的に正規化することも試みます。

このため、HIR typeck には、型をまず normalize で正規化し(古いソルバーでのみ正規化)、その後 structurally_resolve する(新しいソルバーでのみ正規化)というパターンがあります。HIR typeck 中は、structurally_normalize を呼び出すよりもこのパターンを優先すべきです。なぜなら、structurally_resolve は goal を評価することで推論を進めようとする一方、structurally_normalize はそうしないためです。

深い InferCtxt 正規化

infcx.at.(deeply_)?normalize

InferCtxt で深く正規化する方法は、normalizedeeply_normalize の 2 つがあります。その理由は、normalize が古いソルバーでのみ使用される「レガシー」な正規化エントリーポイントである一方、deeply_normalize は長期的に深い正規化を行う方法として意図されているためです。これらのメソッドはいずれもリージョンを扱えます。

新しいソルバーが安定化すると、infcx.at.normalize 関数は削除され、すべてが新しい深い正規化または構造的正規化メソッドへ移行済みになります。このため、normalize 関数は新しいソルバーの下では no-op であり、古いソルバーでは正規化が必要だが新しいソルバーでは不要な場合にのみ適しています。

deeply_normalize を使用すると、曖昧なエイリアス2に遭遇したときにエラーが発行されます。これは、すべての曖昧なエイリアスを推論変数へ正規化することをサポートするのは不可能であるためです3deeply_normalize は通常、曖昧なエイリアスに遭遇することを想定しない場合、たとえばアイテムシグネチャ由来の型を扱う場合にのみ使用すべきです。

infcx.query_normalize

infcx.query_normalize はごくまれに使用されます。これは normalize_erasing_regions とほぼ同じ制限(推論変数を扱えない、診断サポートがない)を持ちますが、主な違いはライフタイム情報を保持することです。このため、ほとんどすべての状況では normalize_erasing_regions の方が適切な選択です。ライフタイムを消去したクエリをキャッシュするため、より効率的だからです。

実際には、query_normalize は borrow checker での正規化に使われ、また他の箇所では infcx.normalize よりも性能面で最適化するために使われています。新しいソルバーが安定化した後は、新しいソルバーの正規化実装が性能低下にならない程度に十分高性能であるはずなので、query_normalize はコンパイラから削除できると期待されています。

tcx.normalize_erasing_regions

normalize_erasing_regions は、一般に型システム解析を行っていないコンパイラの部分で使用されます。この正規化エントリポイントは、推論変数、ライフタイム、診断を扱いません。リントとコード生成では、通常、整形式であると仮定できる(または少なくとも、それに対してエラーを出す責任を持たない)完全に推論済みのエイリアスを扱うため、このエントリポイントが多用されます。

トレイトソルバーの内部

traits::normalize_with_depth(_to)EvalCtxt::structurally_normalize は、トレイトソルバーの内部(それぞれ旧ソルバーと新ソルバー)でのみ使用されます。これは、実質的に、各トレイトソルバーによって正規化がどのように実装されているかという内部への生のエントリポイントです。他の正規化エントリポイントは、トレイト解決の内部からは使用できません。そうすると、ゴールの循環と再帰深度を正しく扱えないためです。

いつ/どこで正規化するか(旧ソルバーと新ソルバー)

旧ソルバーと新ソルバーの大きな変更点の 1 つは、エイリアスがいつ正規化されるべきだと期待するかについてのアプローチです。

旧ソルバー

すべての型はできるだけ早く正規化されることが期待されます。これにより、型システムで遭遇するすべての型は、リジッドであるか、推論変数(後でリジッドな項へと推論される)のいずれかになります。

具体例として、エイリアスの等価性は、それらがリジッドであると仮定し、エイリアスのジェネリック引数を再帰的に等価化することで実装されています。

新ソルバー

すべての型は、曖昧なエイリアスや未正規化のエイリアスを含み得るものと期待されます。エイリアスが正規化されていることを必要とする操作が実行されるたびに、そのエイリアスを正規化する責任はそのロジックにあります(これは、ty.kind() でのマッチングは、ほぼ常に最初に構造的に正規化する必要があることを意味します)。

具体例として、エイリアスの等価性は、カスタムのゴール種別(PredicateKind::AliasRelate)によって実装されます。これにより、等価化されるすべてのエイリアス型がリジッドであると仮定する代わりに、エイリアスの正規化を自身で扱えるようになります。

このアプローチにもかかわらず、パフォーマンスと単純さのために、書き戻し中には依然として深く正規化します。これにより、MIR 内の型は引き続き深く正規化済みであると仮定できます。


新ソルバーで変更を行う動機となった、正規化に対する旧ソルバーのアプローチには、いくつかの主な問題がありました。

正規化呼び出しの欠落

正規化呼び出しが欠落していることはよくあり、その結果、すべてがすでに正規化されていることを期待する API に未正規化の型を渡してしまっていました。曖昧なエイリアスや未正規化のエイリアスをリジッドとして扱うと、エイリアス同士が等しいと見なされないことによるさまざまな奇妙なエラーや、未正規化エイリアスのジェネリック引数を等価化することによる予期しない推論の誘導が発生していました。

パラメータ環境の正規化

もう 1 つの問題は、旧ソルバーでは ParamEnv を正しく正規化できなかったことです。正規化自体が正しい結果を返すために、正規化済みの ParamEnv を期待するためです。詳細については、ParamEnv に関する章を参照してください: Typing/ParamEnvs: すべての境界の正規化

高ランク型における正規化不可能な非リジッドエイリアス

for<'a> fn(<?x as Trait<'a>::Assoc>) のような型が与えられた場合、正規化に対する旧ソルバーのアプローチでは、これを正しく扱うことはできません。

これを for<'a> fn(?y) に正規化し、for<'a> <?x as Trait<'a>>::Assoc -> ?y を正規化するゴールを登録すると、<?x as Trait<'a>>::Assoc&'a u32 に正規化されるケースでエラーになります。推論変数 ?y は、for<'a> バインダーをインスタンス化するときに作成されるプレースホルダーよりも低い[ユニバース]に存在することになります。

エイリアスを未正規化のままにしておくことも誤りです。旧ソルバーはすべてのエイリアスがリジッドであることを期待するためです。これは、新ソルバーがコヒーレンスで安定化される前には健全性バグでした: coherence 中に projection substs を関連付けるのは不健全である

最終的に、これは値の内部にあるすべてのエイリアスがリジッドであることを常に保証できるわけではない、ということを意味します。

発散するエイリアスの使用の扱い

発散するエイリアスは、曖昧なエイリアスと同様に、推論変数へと正規化されます。発散するエイリアスを正規化するとトレイトソルバーの循環が発生するため、旧ソルバーでは常にエラーになります。新ソルバーでは、現在のコンテキストですべてのゴールが成立することを要求するに至った場合にのみエラーになります。たとえば、HIR typeck 中に発散するエイリアスを正規化すると、どちらのソルバーでもエラーになります。

エイリアスの整形式性は、そのエイリアスが発散しないことを要求しません4。これは、エイリアスが整形式であることを確認するだけでは、発散するエイリアスに対してエラーを発行させるには十分ではないことを意味します。実際にそのエイリアスを正規化しようとする必要があります。

発散するエイリアスをエラーにすることが正規化の副作用であるということは、実際にエラーを発行するかどうかが非常に 恣意的 であることを意味します。また、現在は正規化する箇所が減っているため、旧ソルバーと新ソルバーの間でも異なります。 発散するエイリアスをエラーにすることが「問題」を引き起こす場当たり的な性質の例:

#![allow(unused)]
fn main() {
trait Trait {
    type Diverges<D: Trait>;
}

impl<T> Trait for T {
    type Diverges<D: Trait> = D::Diverges<D>;
}

struct Bar<T: ?Sized = <u8 as Trait>::Diverges<u8>>(Box<T>);
}

この例では発散するエイリアスが使われていますが、ジェネリックパラメーターのデフォルトを明示的に正規化することがないため、たまたまエラーを出力しません。?Sized によるオプトアウトを削除すると、<u8 as Trait>::Diverges<u8>: Sized ゴールをたまたま正規化することになり、その副作用として発散するエイリアスに関するエラーが発生します。

const エイリアスはここで型エイリアスと少し異なります。const エイリアスの整形式性には、それらが正常に評価できることが必要です(ConstEvaluatable ゴールによって)。つまり、const 引数の整形式性を単にチェックするだけで、それらが評価に失敗する場合にはエラーにするのに十分です。これを型エイリアスにも採用することに意味があるのか、それとも const エイリアスが整形式性のためにこれを要求するのをやめるべきなのかは、やや不明確です5


  1. 新しいソルバーでは、これは暗黙的に行われます

  2. バインダー内の曖昧なエイリアスの扱い方には、古いソルバーと新しいソルバーの間で微妙な違いがあります。古いソルバーでは、高階ランク型の内部にある一部の曖昧なエイリアスでエラーを出せませんが、新しいソルバーは正しくエラーにします。

  3. バインダー内の曖昧なエイリアスは推論変数へ正規化できません。これについては後ほど詳しく扱います。

  4. エイリアスが発散しないことのチェックは、それらが完全に具象化されるまで行えないため、これは、コード生成/定数評価の前にエイリアスが整形式であることをチェックできないか、単相化の後にエイリアスが整形式から非整形式へ変わるかのどちらかを意味します。

  5. これをやめたとしても、const エイリアスが型エイリアスより 安全性が低く なることは確かにありません

型付け/パラメーター環境

型付け環境

型システムとやり取りする際には、トレイト解決の結果に影響し得るいくつかの変数を考慮する必要があります。 スコープ内の where 句の集合と、コンパイラーの型システム操作がどのフェーズで実行されているか(それぞれ [ParamEnv][penv] 構造体と [TypingMode][tmode] 構造体)です。

型システム操作を実行するための環境がまだ作成されていない場合、 [TypingEnv][tenv] を使用して、必要なすべての外部コンテキストを単一の型にまとめることができます。

型システム操作を実行するためのコンテキストが作成されると(例: ObligationCtxtFnCtxt)、通常 TypingEnv はどこにも保存されません。これは TypingMode だけが環境全体のプロパティである一方で、 異なる ParamEnv はゴールごとに使用できるためです。

パラメーター環境

ParamEnv とは何か

[ParamEnv][penv] は、スコープ内の where 句のリストです。 これは通常、特定のアイテムの where 句に対応します。 一部の句は明示的には書かれず、代わりに predicates_of クエリで暗黙的に追加されます。 たとえば ConstArgHasType や(一部の)implied bounds です。

ほとんどの場合、ParamEnv は最初に param_env クエリを介して作成されます。このクエリは、指定されたアイテムの where 句から派生した ParamEnv を返します。 ParamEnv は、特定のアイテムから派生したものではない任意の句の集合で作成することもできます。 たとえば compare_method_predicate_entailment では、impl の where 句とトレイト定義の関数の where 句からなるハイブリッドな ParamEnv を作成しています。


次のような関数がある場合:

#![allow(unused)]
fn main() {
// `foo` は次の `ParamEnv` を持つことになります:
// `[T: Sized, T: Trait, <T as Trait>::Assoc: Clone]`
fn foo<T: Trait>()
where
    <T as Trait>::Assoc: Clone,
{}
}

概念的に foo の内部にいる場合(たとえば、型チェックや lint を行っている場合)、型システムとやり取りするすべての箇所でこの ParamEnv を使用します。 これにより、正規化、ジェネリック定数の評価、 where 句/ゴールの証明などが、T が sized であり、Trait を実装している、などの事実に依存できるようになります。

より具体的な例:

#![allow(unused)]
fn main() {
// `foo` は次の `ParamEnv` を持つことになります:
// `[T: Sized, T: Clone]`
fn foo<T: Clone>(a: T) {
    // `foo` を型チェックするとき、`requires_clone` 上のすべての where 句が
    // 呼び出しを合法にするために成り立つことを要求します。これは、
    // `T: Clone` を証明しなければならないことを意味します。`foo` を型チェックしているため、
    // `T: Clone` が成り立つことを確認しようとするときには `foo` の
    // 環境を使用します。
    //
    // `[T: Sized, T: Clone]` の `ParamEnv` で `T: Clone` を証明しようとすると、
    // 証明したい境界が環境内にあるため自明に成功します。
    requires_clone(a);
}
}

あるいは、コンパイルされない例:

#![allow(unused)]
fn main() {
// `foo2` は次の `ParamEnv` を持つことになります:
// `[T: Sized]`
fn foo2<T>(a: T) {
    // `foo2` を型チェックするとき、`T: Clone` を証明しようとします。
    // `foo2` を型チェックしているため、`T: Clone` を証明しようとするときには
    // `foo2` の環境を使用します。
    //
    // `[T: Sized]` の `ParamEnv` で `T: Clone` を証明しようとすると、
    // トレイトソルバーに `T` が `Clone` を実装していることを伝えるものが
    // 環境内に何もなく、適用可能なユーザー記述の impl も存在しないため、
    // 失敗します。
    requires_clone(a);
}
}

ParamEnv の取得

型システムとやり取りする際に誤った [ParamEnv][penv] を使用すると、ICE、 不正形式のプログラムのコンパイル成功、またはエラーにすべきでない箇所でのエラーにつながる可能性があります。 コンパイラーを正しい param env を使用するように変更し、その過程で ICE を修正した PR の例として、#82159#82067 を参照してください。

大多数の場合、ParamEnv が必要なときには、それはすでにスコープ内のどこかに存在しているか、 呼び出しスタックの上位にあり、下位へ渡されるべきものです。 既存の ParamEnv が見つかる可能性がある場所の非網羅的なリスト:

  • typeck 中は、FnCtxt に [param_env フィールド][fnctxt_param_env]があります
  • late lint を書くときは、LateContext に [param_env フィールド][latectxt_param_env]があります
  • well-formedness チェック中は、WfCheckingCtxt に [param_env フィールド][wfckctxt_param_env]があります
  • MIR Typeck に使用される TypeChecker には [param_env フィールド][mirtypeck_param_env]があります
  • 次世代トレイトソルバーでは、すべての Goal に、そのゴールをどの環境で証明するかを指定する [param_env フィールド][goal_param_env]があります
  • 既存の [TypeRelation][typerelation] を編集していて、それが [PredicateEmittingRelation][predicate_emitting_relation] を実装している場合、[param_env メソッド][typerelation_param_env]が利用可能です。

使用できる ParamEnv がスコープ内のどこかにあるかどうかわからない場合は、[#t-compiler/help][compiler_help] Zulip チャンネルでスレッドを開く価値があります。そこでは、誰かが ParamEnv をどこから取得できるかを指摘してくれるかもしれません。

手動で ParamEnv を構築する必要があるのは、通常、何らかのトップレベル解析(例: hir typeck や borrow checking)の開始時だけです。 そのような場合、これを行う方法は 3 つあります:

  • [tcx.param_env(def_id) クエリ][param_env_query]を呼び出す。これは、指定された定義に関連付けられた環境を返します。
  • [ParamEnv::empty][env_empty] で空の環境を作成する。
  • [ParamEnv::new][param_env_new] を使用して、任意の where 句の集合を持つ env を構築する。 その後、traits::normalize_param_env_or_error を呼び出して、env 内のすべての where 句の正規化と elaboration を処理する。

ほとんどの場合、コンパイラーは特定の定義の一部として解析を実行しているため、ParamEnv を構築する最も一般的な方法は、圧倒的に param_env クエリを使用することです。

ParamEnv::empty で空の環境を作成するのは、通常、コード生成([TypingEnv::fully_monomorphized][tenv_mono] を介して間接的に)で行う場合か、 ジェネリックパラメーターに遭遇することが決してないと想定している解析の一部として行う場合だけです (例: coherence/orphan チェックのさまざまな部分)。

任意の where 句の集合から env を作成することは通常不要であり、必要な環境がソースコード内の実際のアイテムに対応していない場合にのみ行うべきです(例: compare_method_predicate_entailment)。 [param_env_new]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.ParamEnv.html#method.new normalize_env_or_error: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_trait_selection/traits/fn.normalize_param_env_or_error.html [fnctxt_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_typeck/fn_ctxt/struct.FnCtxt.html#structfield.param_env [latectxt_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint/context/struct.LateContext.html#structfield.param_env [wfckctxt_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_analysis/check/wfcheck/struct.WfCheckingCtxt.html#structfield.param_env [goal_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_infer/infer/canonical/ir/solve/struct.Goal.html#structfield.param_env [typerelation_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_infer/infer/trait.PredicateEmittingRelation.html#tymethod.param_env [typerelation]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/relate/trait.TypeRelation.html [mirtypeck_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_borrowck/type_check/struct.TypeChecker.html#structfield.param_env [env_empty]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.ParamEnv.html#method.empty [param_env_query]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_typeck/fn_ctxt/struct.FnCtxt.html#structfield.param_env method_pred_entailment: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_analysis/check/compare_impl_item/fn.compare_method_predicate_entailment.html [predicate_emitting_relation]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/relate/combine/trait.PredicateEmittingRelation.html [tenv_mono]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.TypingEnv.html#method.fully_monomorphized [compiler_help]: https://rust-lang.zulipchat.com/#narrow/channel/182449-t-compiler.2Fhelp

ParamEnv はどのように構築されるか

ParamEnv の作成は、ユーザーが記述したとおりにアイテム上で定義されたwhere句のリストを単純に使用するよりも複雑です。 スーパーtraitを環境へ展開し、すべてのエイリアスを完全に正規化する必要があります。 このロジックは traits::normalize_param_env_or_error によって処理されます(ただし、その名前には展開について何も言及されていません)。

スーパーtraitの展開

fn foo<T: Copy>() のような関数がある場合、Copy traitには Clone スーパーtraitがあるため、関数内で T: Clone を証明できるようにしたいと考えます。 ParamEnv を構築する際には、環境内のすべてのtrait境界を見て、それらのtrait上に見つかったスーパーtraitについて、新しいwhere句を ParamEnv に明示的に追加します。

具体的な例は次の関数です。

#![allow(unused)]
fn main() {
trait Trait: SuperTrait {}
trait SuperTrait: SuperSuperTrait {}

// `bar` の展開前の `ParamEnv` は次のようになります:
// `[T: Sized, T: Copy, T: Trait]`
fn bar<T: Copy + Trait>(a: T) {
    requires_impl(a);
}

fn requires_impl<T: Clone + SuperSuperTrait>(a: T) {}
}

環境を展開しなければ、T: CloneT: SuperSuperTrait を証明できないため、requires_impl 呼び出しは型チェックに失敗します。 実際には環境を展開するため、barParamEnv は実際には次のようになります。 [T: Sized, T: Copy, T: Clone, T: Trait, T: SuperTrait, T: SuperSuperTrait] これにより、bar の型チェック時に T: CloneT: SuperSuperTrait を証明できます。

Clone traitには Sized スーパーtraitがありますが、環境内に2つの T: Sized 境界(1つはスーパーtrait由来、もう1つは暗黙に追加される T: Sized 境界由来)が存在することにはなりません。これは、展開プロセス(util::elaborate によって実装)がwhere句を重複排除するためです。

この副作用として、スーパーtraitの実際の展開が行われない場合でも、 環境内の既存のwhere句も重複排除されます。 次の例を参照してください。

#![allow(unused)]
fn main() {
trait Trait {}
// 展開前の `ParamEnv` は次のようになります:
// `[T: Sized, T: Trait, T: Trait]`
// しかし展開後は次のようになります:
// `[T: Sized, T: Trait]`
fn foo<T: Trait + Trait>() {}
}

次世代traitソルバー でも、この展開が行われる必要があります。

すべての境界の正規化

古いtraitソルバーでは、ParamEnv に格納されるwhere句は完全に正規化されている必要があります。そうでない場合、traitソルバーは正しく機能しません。 ParamEnv の正規化が必要になる具体例は次のとおりです。

#![allow(unused)]
fn main() {
trait Trait<T> {
    type Assoc;
}

trait Other {
    type Bar;
}

impl<T> Other for T {
    type Bar = u32;
}

// `foo` の正規化前の `ParamEnv` は次のようになります:
// `[T: Sized, U: Sized, U: Trait<T::Bar>]`
fn foo<T, U>(a: U) 
where
    U: Trait<<T as Other>::Bar>,
{
    requires_impl(a);
}

fn requires_impl<U: Trait<u32>>(_: U) {}
}

人間には、<T as Other>::Baru32 と等しいことが分かるため、U 上のtrait境界は U: Trait<u32> と同等です。 実際には、この環境で古いソルバーを使って U: Trait<u32> を証明しようとすると、<T as Other>::Baru32 と等しいことを判断できないため失敗します。

これを回避するため、ParamEnv を構築した後に正規化します。そのため、fooParamEnv は実際には [T: Sized, U: Sized, U: Trait<u32>] となり、traitソルバーは ParamEnv 内の U: Trait<u32> を使用して、trait境界 U: Trait<u32> が成立することを判断できるようになります。

この回避策はすべての場合に機能するわけではありません。関連型の正規化には ParamEnv が必要であり、それがブートストラップ問題を引き起こすためです。 正規化が正しい結果を返すには正規化済みの ParamEnv が必要ですが、その ParamEnv を得るには正規化が必要です。 現在は、正規化前のparam envを使用して ParamEnv を一度だけ正規化しており、これが壊れる例はいくつかあるものの(example)、実際にはおおむね問題ない結果を返す傾向があります。

次世代traitソルバーでは、ParamEnv 内のすべてのwhere句が完全に正規化されている必要はないため、ParamEnv を構築する際に正規化は行いません。

型付けモード

型システム操作をどのコンテキストで実行しているかに応じて、 必要となる振る舞いが異なる場合があります。 たとえばコヒーレンス中には、ゴールが成立しないと見なせるタイミングや、型が等しくないと見なせるタイミングについて、より強い要件があります。

コンパイラの型システム操作がどの「フェーズ」で実行されているかの追跡は、[TypingMode][tmode] enumによって行われます。 TypingMode enumに関するドキュメントはかなり優れているため、ここでそのまま繰り返す代わりに、APIドキュメントを直接読むことをおすすめします。 [penv]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.ParamEnv.html [tenv]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.TypingEnv.html [tmode]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/type.TypingMode.html

型推論

型推論とは、式の型を自動的に検出するプロセスです。

これにより、Rust では型注釈を少なく、またはまったく書かずに済むため、ユーザーにとって扱いやすくなります。

fn main() {
    let mut things = vec![];
    things.push("thing");
}

ここでは、things にプッシュしている値により、things の型は Vec<&str> であると推論されます。

型推論は標準的な Hindley-Milner(HM)型推論アルゴリズムに基づいていますが、サブタイピング、リージョン推論、高階ランク型に対応するために、さまざまな方法で拡張されています。

用語に関する注記

推論変数を指すために ?T という表記を使用します。これは存在変数とも呼ばれます。

「リージョン」と「ライフタイム」という用語は同じ意味で使用します。どちらも &'a T における 'a を指します。

「束縛リージョン」という用語は、関数シグネチャ内で束縛されるリージョンを指します。たとえば、for<'a> fn(&'a u32) における 'a です。リージョンが束縛されていない場合、そのリージョンは「自由」です。

推論コンテキストの作成

推論コンテキストは、たとえば次のようにして作成します。

let infcx = tcx.infer_ctxt().build();
// ここで推論コンテキスト `infcx` を使用します。

infcx の型は InferCtxt<'tcx> であり、構築元の tcx と同じ 'tcx ライフタイムを持ちます。

tcx.infer_ctxt メソッドは実際にはビルダーを返します。つまり、infcx が作成される前に、いくつかの種類の設定を行うことができます。詳細については InferCtxtBuilder を参照してください。

推論変数

推論コンテキストの主な目的は、多数の推論変数を保持することです。これらは、正確な値がまだわかっていないものの、型チェックを実行するにつれて明らかになる型やリージョンを表します。

H-M 型システムにおける単一化の基本的な考え方や、Prolog のような論理型言語に詳しい場合、これは同じ概念です。そうでない場合は、H-M 型推論がどのように機能するかについてのチュートリアル、あるいは Chalk プロジェクトにおけるunification in the Chalk projectに関するこのブログ記事を読むとよいかもしれません。

全体として、推論コンテキストは 5 種類の推論変数を格納します( 2023 年 3 月時点)。

  • 型変数。これには 3 種類があります。
    • 一般型変数(最も一般的なもの)。これは任意の型と単一化できます。
    • 整数型変数。これは整数型とのみ単一化でき、22 のような整数リテラル式から生じます。
    • 浮動小数点型変数。これは浮動小数点型とのみ単一化でき、22.0 のような浮動小数点リテラル式から生じます。
  • リージョン変数。これはライフタイムを表し、至るところで生じます。
  • const 変数。これは定数を表します。

すべての型変数はほぼ同じように機能します。新しい型変数を作成すると、未解決の型 ?T を表す Ty<'tcx> が得られます。その後、等価性やサブタイピングなど、推論器がサポートするさまざまな操作を適用できます。その結果として、その ?T が特定の値にインスタンス化(または束縛)される可能性があります。

リージョン変数はやや異なる動作をし、別のセクションで後述します。

等価性 / サブタイピングの強制

型推論器で実行できる最も基本的な操作は等価性です。これは 2 つの型 TU が同じであることを強制します。等価制約を追加する推奨方法は、概ね次のように at メソッドを使用することです。

infcx.at(...).eq(t, u);

最初の at() 呼び出しは、多少のコンテキスト、つまりなぜこの単一化を行うのか、どの環境で行うのかを提供し、eq メソッドが実際の等価制約を実行します。

もの同士を等価にすると、それらが厳密に等しいことを強制します。等価化は InferResult を返します。Err(err) を返した場合、等価化は失敗しており、内包される TypeError が何が問題だったのかを示します。

成功の場合は、もう少し興味深いものです。eq の「主要な」戻り値の型は () です。つまり、成功した場合、特に関心のある値は返しません。むしろ、型変数を制約するなどの副作用のために実行されます。ただし、実際の戻り値の型は () ではなく、InferOk<()> です。InferOk 型は追加のトレイト義務を運ぶために使用されます。これらが満たされるようにするのはあなたの役割です(通常は、それらを充足コンテキストに登録します)。この点に関する背景については、trait chapterを参照してください。

同様に、infcx.at(..).sub(..) を通じてサブタイピングを強制できます。上記と同じ基本概念が適用されます。

等価性を「試す」

場合によっては、2 つの型をエラーなしで等価にすることが可能かどうかを知りたいことがあります。これは infcx.can_eq(サブタイピングの場合は infcx.can_sub)でテストできます。これが Ok を返す場合、等価性は可能です。ただし、いずれの場合も、副作用はすべて巻き戻されます。

ただし、これらのメソッドの成功または失敗は常にリージョンを法としていることに注意してください。つまり、2 つの型 &'a u32&'b u32 は、たとえ 'a != 'b であっても、can_eq に対して Ok を返します。これは、リージョン制約を解く方法の「2 フェーズ」の性質から生じます。

スナップショット

前のセクションの can_eq で説明したように、一連の操作を実行し、その後でそれらの副作用をロールバックできると便利なことがよくあります。これはさまざまな理由で行われます。その 1 つはバックトラックできるようにするためであり、どのパスを取るか決定する前に複数の可能性を試すことができます。もう 1 つは、一連の小さな変更がアトミックに行われるか、まったく行われないかを保証するためです。

これを可能にするため、推論コンテキストは snapshot メソッドをサポートしています。これを呼び出すと、実行する操作によって発生する変更の記録を開始します。完了したら、それらの変更を取り消す rollback_to を呼び出すか、変更を永続化する confirm を呼び出すことができます。スタックのような規律に従う限り、スナップショットはネストできます。

スナップショットを直接使用するよりも、より高レベルのパターンをカプセル化した commit_if_okprobe のようなメソッドを使用すると役立つことがよくあります。

サブタイピング義務

議論する価値があるものの 1 つに、サブタイピング義務があります。?T <: i32 のように 2 つの型をサブタイプ関係に強制する場合、それらを等価制約に変換できることがよくあります。これは Rust のサブタイピングの概念がかなり限定的であることに由来します。したがって、上記の場合、?T <: i32?T = i32 と等価です。

ただし、場合によってはより慎重になる必要があります。たとえば、リージョンが関与する場合です。したがって、?T <: &'a i32 がある場合、まず &'a i32 をリージョン変数を持つ型、つまり &'?b i32 に「一般化」し、その後 ?T をそれと単一化します(?T = &'?b i32)。次に、この新しい変数を元の境界と関連付けます。

&'?b i32 <: &'a i32

これにより、'?b: 'a というリージョン制約(後述)が生じます。 最後に興味深いケースとして、?T <: ?U のように、2 つの未束縛の型変数を関連付ける場合があります。この場合、処理を進めることができないため、Subtype(?T, ?U) というオブリゲーションをキューに入れ、InferOk メカニズムを通じてそれを返します。?T または ?U についてより多くの詳細が判明したときに、再度試す必要があります。

リージョン制約

リージョンは型とはやや異なる方法で推論されます。先行して統一を行うのではなく、処理の過程で制約を収集するだけで、リージョンを解決しようとすることは(ほとんど)ありません。これらの制約は「outlives」制約の形を取ります。

'a: 'b

実際には、コードはこれらをサブリージョン関係として扱う傾向がありますが、考え方は同じです。

'b <= 'a

(「verifys」など、他にもさまざまな種類の制約があります。詳細については、region_constraints モジュールを参照してください。)

あるケースでは、ある程度の先行した統一を行います。2 つのリージョンの間に等価制約がある場合、

'a = 'b

その事実を統一テーブルに記録します。その後、opportunistic_resolve_var を使用して 'b'a に変換できます(またはその逆も可能です)。これは、固定点アルゴリズムの停止性を保証するために必要になることがあります。

リージョン制約の解決

リージョン制約は、型検査の最後の最後、つまり他のすべての制約が判明し、他のすべてのオブリゲーションが証明された後にのみ解決されます。現在、リージョン制約を解決する方法は 2 つあります。レキシカルと非レキシカルです。最終的には 1 つだけになります。

ここでの例外は、トレイト解決中に使用され、高ランクリージョンを含むリージョン制約に依存するリークチェックです。ルートユニバース内のリージョン制約(つまり for<'a> から生じたものではない制約)は、トレイトシステムに影響を与えてはなりません。これらのリージョンはコード生成時にすべて消去されるためです。

レキシカルなリージョン制約を解決するには、resolve_regions_and_report_errors を呼び出します。これにより、リージョン制約プロセスが「クローズ」され、lexical_region_resolve コードが呼び出されます。これが完了すると、それ以降に等価関係を設定したり、サブタイピング関係を作成したりしようとすると ICE が発生します。

NLL ソルバー(実際には MIR 型チェッカー)は、少し異なる方法で処理します。トレイト解決には canonical query を使用し、その最後に take_and_reset_region_constraints を使用します。これにより、canonical query の間に追加されたすべての outlives 制約が抽出されます。これは、NLL ソルバーが、どのリージョンが互いに outlive するかだけでなく、どこでそうなるかも知る必要があるためです。最後に、NLL ソルバーは get_region_var_infos を呼び出し、すべてのリージョン変数をソルバーに提供します。

レキシカルリージョンの解決

レキシカルリージョンの解決は、最初に各リージョン変数に空の値を割り当てることで行われます。その後、各 outlives 制約を繰り返し処理し、固定点に到達するまでリージョン変数を拡張します。リージョン変数は、リージョン束上の最小上界関係を使用して、かなり素直な方法で拡張できます。

トレイト解決(旧式)

この章では、トレイト解決 の一般的なプロセスについて説明し、 いくつかの分かりにくい点を指摘します。

注: この章(およびそのサブチャプター)では、トレイト ソルバーが現在どのように動作しているかを説明します。ただし、私たちは新しい トレイトソルバーの設計を進めています。そちらについて読みたい場合は、 このサブチャプターを参照してください。

主要な概念

トレイト解決とは、トレイトへの各参照に対して impl を対応付けるプロセスです。 したがって、たとえば次のようなジェネリック関数があるとします。

fn clone_slice<T:Clone>(x: &[T]) -> Vec<T> { ... }

そして、その関数への呼び出しがあるとします。

let v: Vec<isize> = clone_slice(&[1, 2, 3])

この場合、(この例では)isize : Clone の impl が存在するかどうかを判断するのが トレイト解決の役割です。

ジェネリック関数のように、場合によっては特定の impl を 見つけられないことがありますが、呼び出し元が impl を 提供しなければならないことは判断できます。たとえば、clone_slice の本体を考えてみます。

fn clone_slice<T:Clone>(x: &[T]) -> Vec<T> {
    let mut v = Vec::new();
    for e in &x {
        v.push((*e).clone()); // (*)
    }
}

(*) が付けられた行は、T*e の型)が Clone トレイトを実装している場合にのみ正当です。当然、T が何であるかは 分からないため、具体的な impl を見つけることはできません。しかし、境界 T:Clone に基づいて、 呼び出し元が提供しなければならない impl が存在すると言えます。

私たちは、impl を必要とするトレイト参照を指すために オブリゲーション という用語を使います。 基本的に、トレイト解決システムは、適切な impl が実際に存在することを証明することで オブリゲーションを解決します。

型チェック中には、トレイト選択の結果は保存しません。 単にトレイト選択が成功することを検証したいだけです。その後、 コード生成時にすべての具体的な型が利用可能になったとき、 実際の実装を選択するためにトレイト選択を繰り返すことができます。 その実装は出力バイナリに生成されます。

概要

トレイト解決は、3つの主要な部分で構成されます。

  • 選択: 特定のオブリゲーションをどのように解決するかを決定します。 たとえば、選択は、特定のオブリゲーションを Self 型に一致する impl を 用いることで解決できる、またはパラメータ境界(例: T: Trait)を使って 解決できる、と判断することがあります。impl の場合、1つの オブリゲーションを選択すると、その impl 自体にある where 句のために ネストしたオブリゲーション が作成されることがあります。また、曖昧さを解消するために、 それらのネストしたオブリゲーションを評価する必要がある場合もあります。

  • 充足: 充足コードは、オブリゲーションが完全に充足されていることを追跡するものです。 基本的には、選択されるべきオブリゲーションのワークリストです。 選択が成功すると、そのオブリゲーションはワークリストから削除され、 ネストしたオブリゲーションがキューに追加されます。 充足は推論変数に制約を与えます。

  • 評価: 推論変数に一切制約を与えずに、オブリゲーションが成り立つかどうかを確認します。 選択によって使用されます。

選択

選択とは、オブリゲーションを解決できるかどうか、そして解決できる場合には どのように解決するか(impl、where 句など)を決定するプロセスです。 主なインターフェイスは select() 関数で、これはオブリゲーションを受け取り、 SelectionResult を返します。起こり得る結果は3つあります。

  • Ok(Some(selection)) – はい、そのオブリゲーションは解決でき、 selection がその方法を示します。impl によって解決された場合、 selection はその impl によって要求されるネストしたオブリゲーションも示すことがあります。

  • Ok(None) – そのオブリゲーションを解決できるかどうか、まだ確信が持てません。 これは、オブリゲーションに未束縛の型変数が含まれている場合に最もよく起こります。

  • Err(err) – 型エラーのため、または適用できる可能性のある impl が存在しないため、 そのオブリゲーションは確実に解決できません。

選択の基本的なアルゴリズムは、大きく2つのフェーズに分かれます。 候補の組み立てと確認です。

ライフタイム推論の仕組み上、ライフタイム間の単一化やサブタイプ関係が 成り立つかどうかについて即座にフィードバックを返すことはできないことに注意してください。 したがって、ライフタイムの照合は選択中には考慮されません。 これは、サブリージョン代入が失敗しないという事実に反映されています。 その結果、後でエラーであることが判明するライフタイム制約が生じる場合があります (対照的に、ライフタイム以外の制約は選択中にすでにチェックされており、 それ自体がエラーを引き起こすことは決してありませんが、当然ながら下流で別のエラーにつながることはあります)。

候補の組み立て

TODO: なぜ異なる候補が存在するのか、そしてなぜそれをプローブ内で行う必要があるのかについて説明する。

オブリゲーションを満たすために使用できる可能性のある impl、where 句などを検索します。それらのそれぞれを候補と呼びます。 曖昧さを避けるため、明確に適用可能な候補をちょうど1つ見つけたいと考えます。 場合によっては、impl や where 句が適用されるかどうか分からないことがあります。 これは、オブリゲーションに未束縛の推論変数が含まれている場合に起こります。

特定の impl、where 句などが特定のオブリゲーションに適用されるかどうかを決定するサブルーチンは、 総称して 照合 のプロセスと呼ばれます。impl 候補の場合 、 これは、ネストしたオブリゲーションを無視しつつ、impl ヘッダー(Self 型とトレイト引数)を 単一化することに相当します。照合が成功した場合、それを候補の集合に追加します。 CopySizedCoerceUnsized などの組み込みトレイトの候補を組み立てる際には、 他にも規則があります。

この最初のパスが完了すると、候補の集合を調べることができます。 それが単集合であれば、完了です。これは、適用できる可能性のあるスコープ内で唯一の impl です。 そうでなければ、where 句やその他の条件を使って候補の集合を絞り込むことができます。 絞り込みでは、ネストしたオブリゲーションが適用され得るかどうかを確認するために evaluate_candidate を使用します。それでもなお2つ以上の候補が残る場合は、 fn candidate_should_be_dropped_in_favor_of を使って、一部の候補を他の候補より優先します。

この縮小された集合が単一で曖昧でないエントリを生成するなら問題ありません。 そうでなければ、結果は曖昧であるとみなされます。

絞り込み: 曖昧さの解決

しかし、すべての型が単一化される複数の impl が存在する場合はどうなるのでしょうか。 次の例を考えてみます。

trait Get {
    fn get(&self) -> Self;
}

impl<T: Copy> Get for T {
    fn get(&self) -> T {
        *self
    }
}

impl<T: Get> Get for Box<T> {
    fn get(&self) -> Box<T> {
        Box::new(<T>::get(self))
    }
}

たとえば get(&Box::new(1_u16)) を呼び出すとどうなるでしょうか。 この場合、Self 型は Box<u16> です。これは両方の impl と単一化されます。 なぜなら、最初の impl はすべての型 T に適用され、2つ目はすべての Box<T> に適用されるためです。これを曖昧でないものにするために、コンパイラは where 句を考慮し、 候補を削除しようとする 絞り込み パスを実行します。この場合、最初の impl は Box<u16> : Copy の場合にのみ適用されますが、これは成り立ちません。したがって絞り込み後には、 候補が1つだけ残るため、先に進むことができます。

where

impl 以外で義務を解決するもう 1 つの主要な方法は、 where 句によるものです。選択プロセスには常に パラメーター 環境 が与えられます。これは where 句のリストを含んでおり、 基本的には満たせると仮定できる義務です。私たちはそのリストを反復処理し、 現在の義務がそのリスト内に見つかるかどうかを確認します。 見つかった場合、それは満たされたと見なされます。より正確には、 同じトレイト(または何らかのサブトレイト)に対する where 句の義務で、 その義務とマッチできるものが存在するかどうかを確認したいのです。

この簡単な例を考えてみましょう。

trait A1 {
    fn do_a1(&self);
}
trait A2 : A1 { ... }

trait B {
    fn do_b(&self);
}

fn foo<X:A2+B>(x: X) {
    x.do_a1(); // (*)
    x.do_b();  // (#)
}

foo の本体では、明らかに変数 x に対して A1A2、または B のメソッドを使用できます。(*) で示された行は義務 X: A1 を発生させ、 一方 (#) で示された行は義務 X: B を発生させます。同時に、 パラメーター環境には 2 つの where 句、X : A2X : B が含まれます。 したがって各義務について、この where 句のリストを検索します。 義務 X: B は where 句 X: B と自明にマッチします。 義務 X:A1 を解決するには、X:A2X:A1 を含意することに注目します。

確認

確認 は、トレイトの出力型パラメーターを義務内で見つかった値と単一化し、 型エラーを生じさせる可能性があります。

前のセクションの Convert の例について、次のような変形を考えてみましょう。

trait Convert<Target> {
    fn convert(&self) -> Target;
}

impl Convert<usize> for isize { ... } // isize -> usize
impl Convert<isize> for usize { ... } // usize -> isize

let x: isize = ...;
let y: char = x.convert(); // 注: `y: char` になりました!

確認では、impl が Targetusize と指定しているのに対し、 義務は char と報告しているため、エラーが報告されます。したがって、 選択の結果はエラーになります。

候補 impl は Self 型に基づいて選ばれますが、 確認は(この場合)Target 型パラメーターに基づいて行われることに注意してください。

codegen 中の選択

上で述べたように、型チェック中にはトレイト選択の結果を保存しません。 codegen 時には、各メソッド呼び出しに対して特定の impl を選ぶために、 トレイト選択を繰り返します。これは fn codegen_select_candidate を使用して行われます。 この 2 回目の選択では、スコープ内にある where 句を一切考慮しません。 なぜなら、各解決が特定の impl に解決されることがわかっているからです。

1 つ興味深いひねりは、ネストした義務に関するものです。一般に、codegen では、 どの候補が適用されるかを判断するだけでよく、ネストした義務については気にしません。 それらはすでに真であると仮定されているからです。それにもかかわらず、現在はそれらをすべて実際に満たしています。 これは、それが型推論の結果に影響を与える場合があるためです。 つまり、impl の型変数に関する完全な代入が利用できるわけではないため、 すべてを把握するにはトレイト選択を実行しなければなりません。

高階トレイト境界

トレイト解決におけるより微妙な概念の 1 つに、高階トレイト 境界があります。そのような境界の例は for<'a> MyTrait<&'a isize> です。 高階トレイト参照に対する選択がどのように機能するかを見ていきましょう。

基本的なマッチングとプレースホルダーリーク

Foo というトレイトがあるとします。

#![allow(unused)]
fn main() {
trait Foo<X> {
    fn foo(&self, x: X) { }
}
}

任意の 'a について Foo<&'a isize> を実装する型を必要とする 関数 want_hrtb があるとしましょう。

fn want_hrtb<T>() where T : for<'a> Foo<&'a isize> { ... }

ここで、任意の 'a について Foo<&'a isize> を実装する構造体 AnyInt があるとします。

struct AnyInt;
impl<'a> Foo<&'a isize> for AnyInt { }

そして問題は、AnyInt : for<'a> Foo<&'a isize> が成り立つかどうかです。 答えは yes であってほしいところです。これを判断するアルゴリズムは、 高階型のサブタイピング(こちらで説明されており、 SPJ による論文にも記載されています。高階サブタイピングを理解したい場合は、 その論文を読むことをお勧めします)と密接に関連しています。いくつかの部分があります。

  1. 義務内の束縛領域をプレースホルダーに置き換える。
  2. impl をプレースホルダー義務とマッチさせる。
  3. _プレースホルダーリーク_を確認する。

では、この例を順に見ていきましょう。

  1. 最初に行うことは、義務内の束縛領域をプレースホルダーに置き換えることで、 AnyInt : Foo<&'0 isize> が得られます(ここで '0 はプレースホルダー領域 #0 を表します)。 この時点で量化子がなくなっていることに注意してください。 コンパイラの型で言えば、これは ty::PolyTraitRef から TraitRef へ変わるということです。次に impl から TraitRef を作成し、 その束縛領域には新しい変数を使用します(したがって Foo<&'$a isize> が得られます。ここで '$a'a の推論変数です)。

  2. 次に、2 つのトレイト参照を関連付けると、 '0 == '$a という制約を持つグラフが得られます。

  3. 最後に、プレースホルダーの「リーク」を確認します。リークとは基本的に、 プレースホルダー領域を別のプレースホルダー領域、または impl のマッチ以前から存在していた 任意の領域に関連付けようとするあらゆる試みのことです。 リークチェックは、プレースホルダー領域から探索して、それが何らかの形で関連付けられている 領域の集合を見つけることで行われます。これは「汚染」集合と呼ばれます。 チェックに合格するには、その集合はそれ自身と impl 由来の領域変数のみで 構成されていなければなりません。汚染集合に他の領域が含まれている場合、 マッチは失敗します。この場合、'0 の汚染集合は {'0, '$a} であり、 したがってチェックは成功します。

失敗するケースを考えてみましょう。次のような構造体もあるとします。

struct StaticInt;
impl Foo<&'static isize> for StaticInt;

義務 StaticInt : for<'a> Foo<&'a isize> は満たされていないと 見なされてほしいところです。チェックは以前と同じように始まります。'a は プレースホルダー '0 に置き換えられ、impl のトレイト参照は Foo<&'static isize> としてインスタンス化されます。この 2 つを関連付けると、 'static == '0 のような制約が得られます。これは、'0 の汚染集合が {'0, 'static} であることを意味し、リークチェックに失敗します。

TODO: これは、'static が領域変数ではないにもかかわらず 汚染集合に含まれているため、という理解で正しいですか?

高階トレイト義務

基本的なマッチングが終わると、次の興味深い話題に移ります。 impl 義務をどのように扱うかです。ここでは単純な例を順に見ていきます。 FooBar というトレイトと、関連する impl があるとします。

#![allow(unused)]
fn main() {
trait Foo<X> {
    fn foo(&self, x: X) { }
}

trait Bar<X> {
    fn bar(&self, x: X) { }
}

impl<X,F> Foo<X> for F
    where F : Bar<X>
{
}
}

ここで、義務 Baz: for<'a> Foo<&'a isize> があり、この impl とマッチするとしましょう。 その結果としてどのような義務が生成されるでしょうか。得たいものは Baz: for<'a> Bar<&'a isize> ですが、それはどのようにして起こるのでしょうか。

マッチング後、X => &'0 isize のようなプレースホルダー置換を持つ状態になります。 この置換を impl 義務に適用すると、F : Bar<&'0 isize> が得られます。 もちろん、これは直接使用できません。なぜならプレースホルダー領域 '0 は この計算の外へリークできないからです。

そこで、'0 の汚染集合から、それの元になった元の束縛領域(ここでは 'a)への 逆マッピングを作成します。(これは higher_ranked::plug_leaks で行われます。) リークチェックに合格していることは分かっているので、この汚染集合はプレースホルダー領域自身と さまざまな中間領域変数のみで構成されています。次にトレイト参照を走査し、 その汚染集合内のすべての領域を後期束縛領域に戻します。そのため、この場合は最終的に Baz: for<'a> Bar<&'a isize> になります。

キャッシュとそれに関する微妙な考慮事項

一般に、私たちはトレイト選択の結果をキャッシュしようとします。これは いくらか複雑なプロセスです。その理由の一部は、トレイト参照内のすべての型が 完全には分かっていない場合でも結果をキャッシュできるようにしたいからです。 その場合、トレイト選択プロセスが型変数にも影響を与えていることがあるため、 選択プロセスの結果をキャッシュするだけでなく、型変数に対するその効果を 再生できる必要があります。

キャッシュがどのように機能するかの大まかな考え方は、まずすべての未束縛の 推論変数をプレースホルダー版に置き換えるというものです。したがって、 トレイト参照 usize : Foo<$t> があり、$t が未束縛の推論変数である場合、 それを usize : Foo<$0> に置き換えるかもしれません。ここで $0 は プレースホルダー型です。その後、これをキャッシュで検索します。

ヒットが見つかった場合、そのヒットは選択プロセスで直ちに次に取るべき手順 (たとえば impl #22 を適用する、または where 句 X : Foo<Y> を適用する) を教えてくれます。

一方、ヒットがない場合は、selection process を最初から実行する必要があります。たとえば、可能な impl は def-id 22 を持つ次のものだけである、という結論に達したとします。

impl Foo<isize> for usize { ... } // 実装 #22

その後、キャッシュに usize : Foo<$0> => ImplCandidate(22) を記録します。次に ImplCandidate(22)confirm します。これにより、副作用として $tisize と単一化されます。

さて、後のある時点で、usize : Foo<$u> に出くわしたとします。プレースホルダーに置き換えると、これは以前と同じく usize : Foo<$0> となり、そのためキャッシュ検索は成功し、 ImplCandidate(22) が得られます。私たちは ImplCandidate(22) を confirm し、 これにより、副作用として $uisize と単一化されます。

where 句とローカルキャッシュ対グローバルキャッシュ

微妙な相互作用の 1 つは、トレイト検索の結果が、どの where 句がスコープ内に あるかによって変わるということです。したがって、実際には2 つのキャッシュ、 ローカルキャッシュとグローバルキャッシュがあります。ローカルキャッシュは ParamEnv に付属し、グローバルキャッシュは tcx に付属します。 結果がスコープ内の where 句に依存する可能性がある場合には、ローカルキャッシュを 使用します。どちらのキャッシュを使用するかの決定は、select.rspick_candidate_cache メソッドによって行われます。現時点では、非常に単純で 保守的な規則を使用しています。スコープ内に where 句が 1 つでもあれば、 ローカルキャッシュを使用します。以前は、より細かい区別を試みていましたが、 それにより #22019#18290 のような厄介で奇妙なバグが相次ぎました。 この単純な規則はかなり明確に安全であるように思われ、また非常に高いヒット率 (rustc のコンパイル時に約 95%)も維持しています。

TODO: pick_candidate_cache はもう存在しないようです。一般に、このセクションは まだ少しでも正確なのでしょうか?

暗黙的に含意される境界

現在、明示的なアノテーションを避けるために、暗黙的に含意されるリージョン境界を追加しています。たとえば、 fn foo<'a, T>(x: &'a T) では、それを指定しなくても T: 'a が成り立つと自由に仮定できます。

暗黙的に含意される境界には、明示的なものと暗黙的なものの 2 種類があります。明示的に含意される境界は、関連するアイテムの fn predicates_of に追加されますが、暗黙的なものは……まあ……暗黙的に扱われます。

明示的に含意される境界

明示的に含意される境界は、fn inferred_outlives_of で計算されます。ADT と遅延型エイリアスだけが、fn inferred_outlives_crate クエリの不動点アルゴリズムによって計算される、明示的に含意される境界を持ちます。

クレート内のすべての ADT のすべてのフィールドに対して、fn insert_required_predicates_to_be_wf を使用します。この関数は、別個の実装を使用して、フィールドの各構成要素に対する outlives 境界を計算します。

ADT、トレイトオブジェクト、および関連型については、最初に必要となる述語が fn check_explicit_predicates で計算されます。これは単に fn explicit_predicates_of を使用し、それらを詳細化しません。

リージョン述語は fn insert_outlives_predicate によって追加されます。この関数は outlives 述語を受け取り、それを分解し、存続される側のリージョンがリージョンパラメーターである場合にのみ、その構成要素を明示的な述語として追加します。'static 要件は追加しません

暗黙的に含意される境界

まだバインダー内の含意を扱えないため、impl や関数の outlives 要件を単純に明示的な述語として追加することはできません。

暗黙的に含意される境界を仮定として使用する

これらの境界は、影響を受けるアイテム自身の ParamEnv には追加されません。レキシカルリージョン解決では、fn OutlivesEnvironment::from_normalized_bounds を使用して追加されます。同様に、MIR borrowck 中には fn UniversalRegionRelationsBuilder::add_implied_bounds を使用して追加します。

MIR borrowck では、関数シグネチャと impl ヘッダーに対して暗黙的に含意される境界を追加します。MIR borrowck の外部では、fn assumed_wf_types クエリによって返される型の outlives 要件を追加します。

暗黙的な境界に対して仮定される outlives 制約は、 fn implied_outlives_bounds クエリを使用して計算されます。これは、 fn wf::obligations から必要な outlives 境界を直接抽出します

MIR borrowck は、正規化済みの型と未正規化の型の両方について outlives 制約を追加しますが、レキシカルリージョン解決は未正規化の型だけを使用します

暗黙的に含意される境界を証明する

暗黙的に含意される境界は fn predicates_of に含まれないため、それらが実際に成り立つことを別途確認する必要があります。一般には、WellFormed 述語を発行して、使用されるすべての型が well-formed であることをチェックすることでこれを扱います。

impl をインスタンス化するときには WellFormed 述語を発行できません。そうすると、現在はしばしば帰納的になるトレイトソルバーのサイクルが発生するためです。また、高階ランクのリージョンを含む制約も発行しません。これは、それらのバインダーから得られる暗黙的に含意される境界が不足しているためです。

これにより、複数の不健全性が生じます。

  • サブタイピングを使用することによるもの: #25860
  • 高階トレイト境界に対するスーパートレイトのアップキャストを使用することによるもの: #84591
  • impl を使用するときには射影を正規化できる一方で、その impl をチェックするときには正規化できないことによるもの: #100051

特殊化

TODO: Chalk はどこに位置付けられるか? ここで言及/議論すべきか?

specialize モジュールで定義されています。

基本戦略は、コヒーレンスチェック中に特殊化グラフを構築していくことです(コヒーレンスチェックは重複する impl を探します)。 グラフへの挿入では、特殊化階層内で impl を置くべき適切な場所を特定します。適切な場所がない場合(部分的に重複しているが包含関係がないため)、重複エラーになります。特殊化は impl を選択するときに参照されるのはもちろんですが、デフォルトを特殊化階層の下位へ伝播するときにもグラフが参照されます。

実際に特殊化を行うとき、つまり選択中に特殊化グラフが使われると予想するかもしれません。これは、次の 2 つの理由から行われていません。

  • これは単なる最適化です。適用される候補の集合が与えられた場合、グラフを参照するのではなく、それらを特殊化について直接比較することで、最も特殊化されたものを判定できます。さらに選択結果もキャッシュしていることを考えると、この最適化の利点は疑わしいものです。

  • そもそも特殊化グラフを構築するには、選択を使う必要があります(一方の impl がもう一方を特殊化しているかどうかを判定する必要があるためです)。この再入性に対処するには、選択のために何らかの追加のモード切り替えが必要になります。いずれにせよグラフを使う強い理由はないように思われるため、選択ではより単純なアプローチを採用し、グラフはデフォルト実装の伝播にのみ使用します。

トレイト impl の選択は、複数の impl が適用可能な場合でも、それらが同じ特殊化ファミリーの一部である限り成功できます。その場合、成功時には単一の impl を返します。これは、適用されることが分かっている最も特殊化された impl です。ただし、推論変数が関与している場合、返された impl はコード生成時に実際に使用する impl ではない可能性があります。したがって、(1) 関連型が default を使用しておらず、そのためオーバーライドできない場合、または (2) すべての入力型が具体的に分かっている場合のいずれかでない限り、関連型を投影しないように特別な注意を払っています。

追加リソース

@sunjay によるこの講演が役に立つかもしれません。この講演は、問題と解決策の大まかな概要を示しているだけである点に留意してください(@sunjay の作業の中間あたりで発表されたものです)。また、これは 2018 年 6 月に行われたものであり、あなたが視聴する時点ではいくつかの点が変わっている可能性があります。

Chalk ベースのトレイト解決

Chalk は Rust 向けの実験的なトレイトソルバーであり、 ( 2022 年 5 月時点で) Types team によって開発中です。 その目標は、実装が難しい多くのトレイトシステム機能やバグ修正 (例: GAT や specialization) を可能にすることです。新しいソルバーの開発に 協力したい場合は、rust-lang Zulip の #t-types チャンネルに立ち寄って、 ぜひ声をかけてください!

新しいスタイルのトレイトソルバーは、chalk で行われた作業に基づいています。Chalk は Rust のトレイトシステムを、論理プログラミングの観点から明示的に捉え直します。これは、 Rust コードを一種の論理プログラムへ「低水準化」し、そのプログラムに対して クエリを実行できるようにすることで行われます。

ここで重要な観察は、Rust のトレイトシステムが基本的には一種の論理であり、 標準的な論理推論規則に対応付けられるということです。その後、たとえば Prolog ソルバーが動作する方法と非常によく似た形で、それらの推論規則の解を 探すことができます。実際には、Prolog の規則 (Horn 節とも呼ばれます) を そのまま 使用することはできず、もう少し表現力の高い変種が必要であることが わかっています。

Chalk 自体については、 Chalk book セクションでさらに詳しく読むことができます。

進行中の作業

新しいスタイルのトレイト解決の設計は、2 つの場所で行われています。

chalkchalk リポジトリは、トレイトシステムの新しいアイデアや設計を 実験する場所です。

rustc。論理規則に満足したら、それらを rustc に実装する段階へ進みます。 struct、trait、impl 宣言を、rustc の lowering モジュール内で論理推論規則へ 対応付けます。

ロジックへの変換

ここでの重要な観察は、Rust のトレイトシステムが基本的に 一種の論理であり、標準的な論理的推論規則へ対応付けられる、 という点です。そのうえで、たとえば Prolog ソルバーが動作する方法と 非常によく似た形で、それらの推論規則に対する解を探すことができます。 ただし、Prolog の規則(Horn 節とも呼ばれます)をそのまま使うことは 完全にはできず、やや表現力の高い変種が必要になることがわかっています。

Rust のトレイトと論理

最初に得られる観察の 1 つは、Rust のトレイトシステムが 基本的に一種の論理であるということです。そのため、struct、trait、 impl の宣言を論理的推論規則へ対応付けることができます。大部分において、 これらは基本的に Horn 節ですが、Rust の表現力を完全に捉えるには、 特にジェネリックプログラミングをサポートするには、標準的な Horn 節より 少し先へ進む必要があることを見ていきます。

この対応付けがどのように機能するかを見るために、例から始めましょう。 次のようにトレイトといくつかの impl を宣言するとします。

#![allow(unused)]
fn main() {
trait Clone { }
impl Clone for usize { }
impl<T> Clone for Vec<T> where T: Clone { }
}

これらの宣言は、Prolog 風の記法で書かれたいくつかの Horn 節へ、 次のように対応付けることができます。

Clone(usize).
Clone(Vec<?T>) :- Clone(?T).

// `A :- B` という記法は「B が真なら A は真である」ことを意味します。
// 別の言い方をすれば、B は A を含意します。

Prolog の用語では、Clone(Foo)(ここで Foo は何らかの Rust の型です)は、型 FooClone を実装しているという考えを表す 述語である、と言えるでしょう。これらの規則はプログラム節です。 つまり、その述語を証明できる(すなわち、真とみなせる)条件を述べています。 そのため、最初の規則は単に「usize に対して Clone が実装されている」と 言っています。次の規則は「任意の型 ?T について、?T に対して clone が 実装されているなら、Vec<?T> に対して Clone が実装されている」と 言っています。したがって、たとえば Clone(Vec<Vec<usize>>) を証明したい場合、 規則を再帰的に適用することでそれを行います。

  • Clone(Vec<Vec<usize>>) が証明可能であるのは、次の場合です。
    • Clone(Vec<usize>) が証明可能である場合で、それは次の場合です。
      • Clone(usize) が証明可能である場合です。(これは証明可能なので、問題ありません。)

しかし今度は、Clone(Vec<Bar>) を証明しようとしたとしましょう。 これは失敗します(結局のところ、Bar に対する Clone の impl は 与えていません)。

  • Clone(Vec<Bar>) が証明可能であるのは、次の場合です。
    • Clone(Bar) が証明可能である場合です。(しかし、適用可能な規則がないため、これは証明可能ではありません。)

上の例は、複数の入力型を持つジェネリックなトレイトを扱うように 簡単に拡張できます。そこで、Eq<T> トレイトを考えてみましょう。 これは、Self が型 T の値と等価比較可能であることを宣言します。

trait Eq<T> { ... }
impl Eq<usize> for usize { }
impl<T: Eq<U>> Eq<Vec<U>> for Vec<T> { }

これは次のように対応付けることができます。

Eq(usize, usize).
Eq(Vec<?T>, Vec<?U>) :- Eq(?T, ?U).

ここまでは順調です。

通常の関数の型チェック

さて、トレイトがいつ実装されるかを表現し、関連型を扱うことができる いくつかの論理規則を定義したので、少し焦点を型チェックへ 移しましょう。型チェックが興味深いのは、証明する必要があるゴールを 与えてくれるものだからです。つまり、ここまで見てきたことはすべて、 プログラム内のトレイトと impl から、ゴールを証明するための規則を どのように導出するかに関するものでした。しかし、私たちは証明する必要がある ゴールをどのように導出するかにも関心があります。そしてそれらは 型チェックから得られます。

ここで、関数 foo() の型チェックを考えてみましょう。

fn foo() { bar::<usize>() }
fn bar<U: Eq<U>>() { }

もちろん、この関数は非常に単純です。行っているのは bar::<usize>() を 呼び出すことだけです。さて、bar() の定義を見ると、 U: Eq<U> という where 節が 1 つあることがわかります。つまり、 foo() が型引数として usize を指定して bar() を呼び出せることを 示すには、usize: Eq<usize> を証明する必要がある、ということです。

望むなら、bar() を呼び出せる条件を定義する Prolog の述語を書くことも できます。ここでは、その条件を「well-formed」であることと呼ぶことにします。

barWellFormed(?U) :- Eq(?U, ?U).

すると、bar::<usize> への参照(つまり、型 usize に適用された bar())が well-formed であれば、foo() は型チェックに通る、と 言うことができます。

fooTypeChecks :- barWellFormed(usize).

ゴール fooTypeChecks を証明しようとすると、成功します。

  • fooTypeChecks が証明可能であるのは、次の場合です。
    • barWellFormed(usize) の場合で、これは次の場合に証明可能です。
      • Eq(usize, usize) の場合で、これは impl があるため証明可能です。

ここまでは順調です。より複雑な関数の型チェックへ進みましょう。

ジェネリック関数の型チェック: Horn 節を超えて

前のセクションでは、標準的な Prolog の Horn 節(Rust の型等価性の概念で 拡張したもの)を使用して、いくつかの単純な Rust 関数を型チェックしました。 しかし、それが機能するのは、非ジェネリック関数を型チェックしている場合に 限られます。ジェネリック関数を型チェックしたい場合、Prolog が提供できるものより 強いゴールの概念が必要になることがわかります。私が何を言っているのかを見るために、 前の例を改造して foo をジェネリックにしてみましょう。

fn foo<T: Eq<T>>() { bar::<T>() }
fn bar<U: Eq<U>>() { }

foo の本体を型チェックするには、型 T を「抽象的」に保てる必要があります。 つまり、特定の何らかの型についてだけではなく、すべての型 T に対して foo の本体が型安全であることをチェックする必要があります。これは次のように 表現できるでしょう。

fooTypeChecks :-
  // すべての型 T について...
  forall<T> {
    // ...Eq(T, T) が証明可能であると仮定するなら...
    if (Eq(T, T)) {
      // ...`barWellFormed(T)` が成り立つことを証明できる。
      barWellFormed(T)
    }
  }.

ここで使っている記法は、私がプロトタイプ実装で使ってきた記法です。 標準的な数学の記法に似ていますが、少し Rust 風になっています。いずれにせよ、 問題は、標準的な Horn 節ではゴール内で全称量化(forall)や含意(if)を 許可しないことです(ただし、多くの Prolog エンジンは拡張としてそれらを サポートしています)。このため、「一階 hereditary Harrop」(FOHH)節と呼ばれるものを 受け入れる必要があります。この長い名前は、基本的には「本体内に forallif を 持つ標準的な Horn 節」という意味です。しかし、正式な名前を知っておくのはよいことです。 FOHH 節を効率的に扱う方法を説明した研究が数多くあるからです。たとえば、 Gopalan Nadathur による優れた 「A Proof Procedure for the Logic of Hereditary Harrop Formulas」Chalk Book の参考文献で参照してください。

FOHH のサポートは、実際にはそれほど難しいものではないことがわかっています。 そして、それができるようになれば、foo のようなジェネリック関数の 型チェック規則を、私たちのロジックで簡単に記述できます。

出典

このページは、 Nicholas Matsakis によるブログ記事を軽く改変したものです。

ゴールと節

論理プログラミングの用語では、ゴールとは証明しなければならないものであり、とは真であることが分かっているものです。論理への lowering の章で説明したように、Rust のトレイトソルバーは hereditary harrop (HH) 節の拡張に基づいています。これは、従来の Prolog のホーン節にいくつかの新しい強力な機能を加えたものです。

ゴールと節のメタ構造

Rust のソルバーでは、ゴールは次の形式を取ります(2 つの定義が互いを参照していることに注意してください)。

Goal = DomainGoal           // 下のセクションで定義
        | Goal && Goal
        | Goal || Goal
        | exists<K> { Goal }   // 存在量化
        | forall<K> { Goal }   // 全称量化
        | if (Clause) { Goal } // 含意
        | true                 // 自明に真であるもの
        | ambiguous            // 決して証明できないもの

Clause = DomainGoal
        | Clause :- Goal     // Goal を証明できるなら、Clause は真
        | Clause && Clause
        | forall<K> { Clause }

K = <type>     // 「種別」
    | <lifetime>

この種のゴールに対する証明手続きは、実際にはかなり単純です。本質的には、深さ優先探索の一種です。論文 “A Proof Procedure for the Logic of Hereditary Harrop Formulas” に詳細があります。

コード上では、これらの型は rustc では rustc_middle/src/traits/mod.rs に、chalk では chalk-ir/src/lib.rs に定義されています。

ドメインゴール

ドメインゴールは、トレイト論理の原子です。上で示した定義から分かるように、一般的なゴールは基本的にドメインゴールの組み合わせで構成されます。

さらに、前述の節の定義を少し平坦化すると、節は常に次の形式であることが分かります。

forall<K1, ..., Kn> { DomainGoal :- Goal }

したがって、ドメインゴールは実際には節の左辺です。つまり、最も粒度の細かいレベルでは、ドメインゴールこそがトレイトソルバーが最終的に証明しようとするものです。

このシステムにおけるドメインゴールの集合を定義するには、まずいくつかの単純な定式化を導入する必要があります。トレイト参照は、トレイトの名前と、適切な入力の集合 P0..Pn から構成されます。

TraitRef = P0: TraitName<P1..Pn>

たとえば、u32: Display はトレイト参照であり、Vec<T>: IntoIterator も同様です。Rust の表層構文では、関連型束縛(Vec<T>: IntoIterator<Item = T>)のような追加のものも許可されていますが、それらはトレイト参照の一部ではないことに注意してください。

射影は、関連アイテム参照とその入力 P0..Pm から構成されます。

Projection = <P0 as TraitName<P1..Pn>>::AssocItem<Pn+1..Pm>

これらを踏まえると、DomainGoal は次のように定義できます。

DomainGoal = Holds(WhereClause)
            | FromEnv(TraitRef)
            | FromEnv(Type)
            | WellFormed(TraitRef)
            | WellFormed(Type)
            | Normalize(Projection -> Type)

WhereClause = Implemented(TraitRef)
            | ProjectionEq(Projection = Type)
            | Outlives(Type: Region)
            | Outlives(Region: Region)

WhereClause は、Rust ユーザーが実際に Rust プログラムで記述できる where 節を指します。この抽象化は、実質的に Rust で記述可能なドメインゴールだけを扱いたい場合があるため、便宜上存在しています。

これらを 1 つずつ分解して見ていきましょう。

Implemented(TraitRef)

例: Implemented(i32: Copy)

与えられたトレイトが、与えられた入力型およびライフタイムに対して実装されている場合に真です。

ProjectionEq(Projection = Type)

例: ProjectionEq<T as Iterator>::Item = u8

与えられた関連型 ProjectionType と等しい、ということです。これは正規化、またはプレースホルダー関連型を使用して証明できます。Chalk Book の関連型に関するセクションを参照してください。

Normalize(Projection -> Type)

例: ProjectionEq<T as Iterator>::Item -> u8

与えられた関連型 ProjectionType正規化できます。

Chalk Book の関連型に関するセクションで説明されているように、NormalizeProjectionEq を含意しますが、その逆は成り立ちません。一般に、Normalize(<T as Trait>::Item -> U) を証明するには、Implemented(T: Trait) の証明も必要です。

FromEnv(TraitRef)

例: FromEnv(Self: Add<i32>)

内側の TraitRef が真であると仮定されている場合、つまり、スコープ内の where 節から導出できる場合に真です。

たとえば、次の関数があるとします。

#![allow(unused)]
fn main() {
fn loud_clone<T: Clone>(stuff: &T) -> T {
    println!("cloning!");
    stuff.clone()
}
}

この関数の本体内では、FromEnv(T: Clone) を持つことになります。スコープ内の where 節は入れ子になるため、impl 本体の内側にある関数本体は、その impl 本体の where 節も継承します。

このルールと次のルールは、implied bounds を実装するために使用されます。lowering に関するセクションで見るように、FromEnv(TraitRef)Implemented(TraitRef) を含意しますが、その逆は成り立ちません。この区別は implied bounds にとって重要です。

FromEnv(Type)

例: FromEnv(HashSet<K>)

内側の Type が well-formed であると仮定されている場合、つまり、それが関数または impl の入力型である場合に真です。

たとえば、次のコードがあるとします。

struct HashSet<K> where K: Hash { ... }

fn loud_insert<K>(set: &mut HashSet<K>, item: K) {
    println!("inserting!");
    set.insert(item);
}

HashSet<K>loud_insert 関数の入力型です。したがって、これは well-formed であると仮定されるため、この関数の本体内では FromEnv(HashSet<K>) を持つことになります。lowering に関するセクションで見るように、HashSet 宣言は K: Hash という where 節とともに記述されているため、FromEnv(HashSet<K>)Implemented(K: Hash) を含意します。したがって、loud_insert 関数でその bound を繰り返す必要はありません。むしろ、それが真であると自動的に仮定します。

WellFormed(Item)

これらのゴールは、与えられたアイテムが well-formed であることを含意します。

well-formed であるアイテムには、さまざまな種類があります。

  • 、たとえば Rust では真である WellFormed(Vec<i32>) や、真ではない WellFormed(Vec<str>)strSized ではないため)などです。

  • TraitRef、たとえば WellFormed(Vec<i32>: Clone) などです。

well-formedness は implied bounds にとって重要です。特に、loud_clone の例で FromEnv(T: Clone) を仮定してよい理由は、loud_clone の各呼び出し箇所について also WellFormed(T: Clone) を検証するためです。同様に、loud_insert の例で FromEnv(HashSet<K>) を仮定してよい理由は、loud_insert の各呼び出し箇所について WellFormed(HashSet<K>) を検証するためです。

Outlives(Type: Region), Outlives(Region: Region)

例: Outlives(&'a str: 'b), Outlives('a: 'static) 与えられた左側の型またはリージョンが、右側のリージョンより長く存続する場合に真です。

余帰納的ゴール

私たちのシステムにおけるほとんどのゴールは「帰納的」です。帰納的ゴールでは、循環論法は許可されません。次の例の節を考えてみましょう。

    Implemented(Foo: Bar) :-
        Implemented(Foo: Bar).

帰納的に考えると、この節は役に立ちません。Implemented(Foo: Bar) を証明しようとすると、その後再帰的に Implemented(Foo: Bar) を証明しなければならず、その循環は無限に続くことになります(trait solver はここで終了しますが、単に Implemented(Foo: Bar) が真であることはわかっていないと見なします)。

しかし、一部のゴールは余帰納的です。簡単に言えば、これは循環しても問題ないという意味です。そのため、もし Bar が余帰納的トレイトであれば、上のルールは完全に妥当であり、Implemented(Foo: Bar) が真であることを示します。

Auto traits は、Rust で余帰納的ゴールが使われる一例です。Send トレイトを考え、次の構造体があると想像してください。

#![allow(unused)]
fn main() {
struct Foo {
    next: Option<Box<Foo>>
}
}

auto traits のデフォルトルールでは、フィールドの型が Send であれば FooSend であるとされます。したがって、次のようなルールがあります。

Implemented(Foo: Send) :-
    Implemented(Option<Box<Foo>>: Send).

想像できると思いますが、Option<Box<Foo>>: Send を証明しようとすると、結局 Foo: Send をもう一度証明することが循環的に必要になります。したがって、これは循環に陥る例です。しかし、それで問題ありません。自己参照しているにもかかわらず、Foo: Send が成り立つと見なします

一般に、余帰納的トレイトは、可能性の固定集合を列挙したい場合に Rust のトレイト解決で使用されます。auto traits の場合、与えられた開始点から到達可能な型の集合を列挙しています(つまり、Foo は型 Option<Box<Foo>> の値に到達でき、それは型 Box<Foo> の値に到達できることを意味し、さらに型 Foo の値に到達でき、そこで循環が完了します)。

auto traits に加えて、WellFormed 述語も余帰納的です。これらは、暗黙の境界のセクションで説明されているように、同様の「すべてのケースを列挙する」パターンを実現するために使用されます。

未完成の章

今後記述する予定のトピック:

  • 証明手続きを詳しく説明する
  • SLG 解法 – 否定的推論を導入する

正準クエリ

トレイトシステムの「出発点」は正準クエリです(これは、 より一般的な意味でのクエリ、つまり答えを知りたいもの、であると同時に、 rustc 固有の意味でのクエリでもあります)。考え方としては、 型チェッカーやシステムの他の部分が、それぞれの処理を行う過程で、 あるトレイトがある型に対して実装されているかどうかを知りたくなる場合があります (たとえば、u32: Debug は真か?)。あるいは、何らかの関連型を正規化したくなる場合もあります。

このセクションでは、かなり高い抽象レベルでクエリを扱います。 サブセクションでは、これらの考え方が rustc でどのように実装されているかを、 もう少し詳しく見ていきます。

伝統的な対話型 Prolog クエリ

伝統的な Prolog システムでは、クエリを開始すると、ソルバーは処理を進め、 見つけられるすべての可能な答えを提示し始めます。したがって、次のようなものが与えられた場合:

?- Vec<i32>: AsRef<?U>

ソルバーは次のように答えるかもしれません:

Vec<i32>: AsRef<[i32]>
    continue? (y/n)

この continue の部分は興味深いものです。Prolog における考え方は、 ソルバーが、真となるクエリのすべての可能なインスタンス化を見つける、というものです。 この場合、?U = [i32] とインスタンス化すれば、そのクエリは真になります (伝統的な Prolog インターフェイスは、直接には ?U の値を教えてくれないことに注意してください。 ただし、応答を元のクエリと単一化することで、それを推測できます – Rust のソルバーは代わりに代入を返します)。 もし y を押すと、ソルバーは別の可能な答えを返すかもしれません:

Vec<i32>: AsRef<Vec<i32>>
    continue? (y/n)

この答えは、AsRef に対して反射的な impl (impl<T> AsRef<T> for T)が存在するという事実に由来します。 もう一度 y を押すと、今度は否定的な応答が返ってくるかもしれません:

no

当然ながら、場合によっては可能な答えが存在しないこともあり、そのためソルバーはすぐに no を返してきます:

?- Box<i32>: Copy
    no

場合によっては、応答が無限に存在することもあります。たとえば、次のクエリを与えて y を押し続けると、 ソルバーは答えを返し続けて止まることがありません:

?- Vec<?U>: Clone
    Vec<i32>: Clone
        continue? (y/n)
    Vec<Box<i32>>: Clone
        continue? (y/n)
    Vec<Box<Box<i32>>>: Clone
        continue? (y/n)
    Vec<Box<Box<Box<i32>>>>: Clone
        continue? (y/n)

想像できるように、ソルバーは私たちが止めるよう求めるか、メモリを使い果たすまで、 嬉々として Box の層をもう 1 つ追加し続けます。

もう 1 つ興味深い点は、クエリにはまだ変数が含まれている場合があるということです。 たとえば:

?- Rc<?T>: Clone

は、次の答えを生成するかもしれません:

Rc<?T>: Clone
    continue? (y/n)

結局のところ、Rc<?T>?T がどのような型であっても真です。

rustc におけるトレイトクエリ

rustc のトレイトクエリは、やや異なる仕組みで動作します。 すべての可能な答えを列挙しようとするのではなく、曖昧でない答えを探します。 特に、型変数の値を教えてくれる場合、それは、現在の impl と where 句の集合のもとで証明可能となる、 使用できる唯一の可能なインスタンス化であることを意味します。

rustc におけるトレイトクエリへの応答は、通常 Result<QueryResult<T>, NoSolution> です (ここで T はクエリ自体に応じて多少変わります)。Err(NoSolution) の場合は、 クエリが偽であり、答えがなかったことを示します(たとえば Box<i32>: Copy)。 それ以外の場合、QueryResult は、見つかった可能な答えについての情報を返します。 これは 4 つの部分から構成されます:

  • 確実性: この答えについて、どれほど確信しているかを示します。これは 2 つの値を取り得ます:
    • Proven は、結果が真であることが既知であることを意味します。
      • これは、たとえば Vec<i32>: CloneRc<?T>: Clone を証明しようとした結果かもしれません。
    • Ambiguous は、まだ真または偽のいずれであるかを証明できないものがあったことを意味します。 通常は、より多くの型情報が必要だったためです。(すぐに例を見ます。)
      • これは、Vec<?T>: Clone を証明しようとした結果かもしれません。
  • 変数値: 元のクエリに現れた未束縛の推論変数(?T のようなもの)それぞれの値です。 (Prolog では、これらを推測しなければならなかったことを思い出してください。)
    • 下の例で見るように、Ambiguous の場合でも変数値が返ってくることがあります。
  • リージョン制約: これは、入力として与えたライフタイム間で成り立たなければならない関係です。 ここでは無視します。
  • 値: クエリ結果には、型 T の値も付属します。 関連型の正規化のような特殊なクエリでは、追加の結果を返すためにこれが使われますが、 多くの場合は単に () です。

各部分が何を意味するのかを理解するために、例のクエリを順に見ていきましょう。 Borrow トレイトを考えます。このトレイトにはいくつもの impl があります。 その中には、次の 2 つがあります(わかりやすさのため、Sized 境界を明示的に書いています):

impl<T> Borrow<T> for T where T: ?Sized
impl<T> Borrow<[T]> for Vec<T> where T: Sized

例 1。 次の(やや人工的な)コード片を型チェックしているところを想像してください:

fn foo<A, B>(a: A, vec_b: Option<B>) where A: Borrow<B> { }

fn main() {
    let mut t: Vec<_> = vec![]; // 型: Vec<?T>
    let mut u: Option<_> = None; // 型: Option<?U>
    foo(t, u); // 例 1: `Vec<?T>: Borrow<?U>` を要求する
    ...
}

コメントが示すように、まず 2 つの変数 tu を作成します。 t は空のベクターであり、uNone オプションです。 これらの変数はいずれも、その型に未束縛の推論変数を持っています。 ?T はベクター t 内の要素を表し、?U はオプション u に格納される値を表します。 次に、foo を呼び出します。foo のシグネチャをその引数と比較すると、 最終的に A = Vec<?T> および B = ?U になります。 したがって、foo の where 句は Vec<?T>: Borrow<?U> を要求します。 これが、最初のトレイトクエリの例です。

クエリ Vec<?T>: Borrow<?U> には、多くの可能な解があります。 たとえば:

  • ?U = Vec<?T>,
  • ?U = [?T],
  • ?T = u32, ?U = [u32]
  • などです。

したがって、返ってくる結果は次のようになります(リージョン制約と「値」は無視します):

  • 確実性: Ambiguous – これが成り立つかどうかはまだわかりません
  • 変数値: [?T = ?T, ?U = ?U] – 変数の値について何もわかりませんでした

要するに、このクエリ結果は、このトレイトが証明されるかどうかについて多くを語るには早すぎる、 ということを示しています。型チェック中、これは即座のエラーではありません。 代わりに、型チェッカーはこの要件(Vec<?T>: Borrow<?U>)を保持して待機します。 次の例で見るように、?T?U が他のソースから制約されることがあり、 その場合はトレイトクエリを再度試すことができます。

例 2。 ここで前の例を少し拡張し、 u に値を代入できます:

```rust,ignore
fn foo<A, B>(a: A, vec_b: Option<B>) where A: Borrow<B> { }

fn main() {
    // 以前見たもの:
    let mut t: Vec<_> = vec![]; // 型: Vec<?T>
    let mut u: Option<_> = None; // 型: Option<?U>
    foo(t, u); // `Vec<?T>: Borrow<?U>` => 曖昧

    // 新しい内容:
    u = Some(vec![]); // ?U = Vec<?V>
}

この代入の結果、u の型は強制的に Option<Vec<?V>> になります。ここで、?V はベクターの要素型を表します。これはさらに、?UVec<?V>単一化されることを意味します。

型チェッカーが、以前見た「まだ証明されていない」トレイト義務 Vec<?T>: Borrow<?U> を再検討することにしたと仮定しましょう。?U はもはや未束縛の推論変数ではなく、値 Vec<?V> を持っています。したがって、その値でクエリを「更新」すると、次のようになります。

Vec<?T>: Borrow<Vec<?V>>

今回は、適用される impl は 1 つだけで、再帰的な impl です。

impl<T> Borrow<T> for T where T: ?Sized

したがって、トレイトチェッカーは次のように答えます。

  • 確実性: Proven
  • 変数の値: [?T = ?T, ?V = ?T]

ここでは、この義務が実際に成り立つことを証明済みであり、さらに ?T?V が同じ型であることもわかっている、と言っています(ただし、その型が何であるかはまだわかっていません!)。

(実際には、ここで関数が終了するため、型チェッカーはこの時点でエラーを出します。tu の要素型は、同じであることはわかっていても、まだわかっていないからです。)

正準化

注記: FIXME: この章の内容は 次世代トレイト解決の正準化の章と一部重複しています。 将来的にこれらの内容を再編成することが推奨されています。

正準化とは、推論値をそのコンテキストから分離するプロセスです。 これは正準クエリを実装するうえで重要な部分であり、より多くの背景を得るために親の章を読んでおくとよいでしょう。

正準化は、実際には非常に単純な概念に基づいています。すべての推論変数は常に 2 つの状態のいずれかにあります。つまり、まだそれがどの型であるかわからない未束縛の状態か、わかっている束縛済みの状態です。したがって、型/リージョンを含む何らかのデータ構造 T をその環境から分離するには、単に下へたどって T に現れる未束縛変数を見つけます。それらの変数は、0 から始まり固定された順序で番号付けされた「正準変数」に置き換えられます(ほとんどの場合は左から右ですが、実際には一貫している限り重要ではありません)。

たとえば、型 X = (?T, ?U) があり、?T?U が互いに異なる未束縛の推論変数である場合、X の正準形は (?0, ?1) になります。ここで ?0?1 はこれらの正準プレースホルダーを表します。型 Y = (?U, ?T)(?0, ?1) に正準化されることに注意してください。しかし、型 Z = (?T, ?T)(?0, ?0) に正準化されます((?U, ?U) も同様です)。言い換えると、推論変数の正確な同一性は重要ではありません。ただし、それらが繰り返される場合を除きます。

これを使って、キャッシュを改善したり、トレイト解決中にサイクルやその他のものを検出したりします。大まかに言えば、2 つのトレイトクエリが同じ正準形を持つ場合、それらは同じ答えを得るという考え方です。その答えは正準変数(?0, ?1)の形で表現され、それを元の変数(?T, ?U)へ対応付けて戻すことができます。

クエリの正準化

これがどのように機能するかを見るために、次のトレイトクエリを解決しようとしていると想像してください: ?A: Foo<'static, ?B>。ここで ?A?B は未束縛です。このクエリには 2 つの未束縛変数が含まれていますが、ライフタイム 'static も含まれています。トレイトシステムは一般にすべてのライフタイムを無視し、それらを等しく扱うため、正準化するときには、任意の自由ライフタイムも正準変数に置き換えます('static はここでは実際には_自由_ライフタイム変数であることに注意してください。私たちはそれをプログラム全体の型付けコンテキストではなく、このトレイト参照のコンテキストでのみ考慮しています。数学的には、プログラム全体にわたって量化しているのではなく、この義務についてのみ量化しています)。したがって、次の結果が得られます。

?0: Foo<'?1, ?2>

これを次のように別の形で書くこともあります。

for<T,L,T> { ?0: Foo<'?1, ?2> }

この for<> は、その中の各正準変数に関する情報を与えます。この場合、各 T は型変数を示すため、?0?2 は型です。L はライフタイム変数を示すため、?1 はライフタイムです。canonicalize メソッドは、各正準化された変数の「元の値」を持つ CanonicalVarValues 配列 OV も返します。

[?A, 'static, ?B]

クエリ応答を処理するときに、後でこのベクター OV が必要になります。

クエリの実行

正準クエリを構築したら、それを解決してみることができます。そのためには、最終的に新しい推論コンテキストを作成し、そのコンテキストで正準クエリをインスタンス化します。考え方としては、各正準変数に対応する新しい推論変数(適切な種類のもの)を含む、正準形からの置換 S を作成します。したがって、例のクエリでは次のようになります。

for<T,L,T> { ?0: Foo<'?1, ?2> }

置換 S は次のようになるかもしれません。

S = [?A, '?B, ?C]

その後、束縛された正準変数(?0 など)をこれらの推論変数に置き換えることで、次の完全にインスタンス化されたクエリが得られます。

?A: Foo<'?B, ?C>

ただし、置換 S を覚えておいてください。後でそれが必要になります。

さて、新しい推論コンテキストとインスタンス化されたクエリができたので、それを解決してみることができます。トレイトソルバー自体については別のセクションでより詳しく説明していますが、ここでは、それが確実性の値Proven または Ambiguous)を計算し、作成した推論変数に副作用を与えると言えば十分です。たとえば、Foo の impl が次のように 1 つだけだった場合:

impl<'a, X> Foo<'a, X> for Vec<X>
where X: 'a
{ ... }

その場合、確実性の値として Proven を得るとともに、新しい推論変数 '?D?E(impl 上のパラメータを表すため)を作成し、次のように単一化することになるかもしれません。

  • '?B = '?D
  • ?A = Vec<?E>
  • ?C = ?E

また、where 句により、リージョン制約 ?E: '?D も蓄積します。

最終的なクエリ結果を作成するには、これらの値をクエリの推論コンテキストから「持ち上げ」、元の推論コンテキストで再適用できるものにする必要があります。これは、正準化を再適用することによって行いますが、対象はクエリ結果です。

クエリ結果の正準化

親セクションで説明したように、ほとんどのトレイトクエリは、「確実性の値」certainty、結果の置換 var_values、およびいくつかのリージョン制約をまとめた結果になります。これを作成するために、最初にクエリをインスタンス化したときに作成した置換 S を再利用することになります。記憶を新たにすると、次のクエリがありました。

for<T,L,T> { ?0: Foo<'?1, ?2> }

これに対して置換 S を作成しました。

S = [?A, '?B, ?C]

その後、これらの変数のいくつかを他のものと単一化する作業を行いました。最新の結果で S を「更新」すると、次のようになります。

S = [Vec<?E>, '?D, ?E]

これらはまさに、元のクエリからの 3 つの入力変数に対する新しい値です。ただし、これらにはいくつかの新しい変数(?E など)が含まれていることに注意してください。もう一度正準化することで、それらを消すことができます。とはいえ、S だけを正準化するのではなく、クエリ応答 QR 全体を正準化します。

QR = {
    certainty: Proven,             // または任意のもの
    var_values: [Vec<?E>, '?D, ?E] // これは S
    region_constraints: [?E: '?D], // impl から
    value: (),                     // ここでの目的においては単に () だが、
                                   // 場合によっては型やその他の情報を
                                   // 持つことがある
}

結果は次のようになります。

Canonical(QR) = for<T, L> {
    certainty: Proven,
    var_values: [Vec<?0>, '?1, ?0]
    region_constraints: [?0: '?1],
    value: (),
}

(微妙な点が 1 つあります。クエリ結果を正準化するときには、自由ライフタイムに対して特別な扱いは行いません。たとえば、'?D への両方の参照は同じ正準変数(?1)に変換されています。これは、元のクエリで、すべての自由ライフタイムを新しい正準変数へ正準化していたこととは対照的です。)

この結果は、必要とされる各コンテキストで再適用されなければなりません。

正準化されたクエリ結果の処理

前のセクションでは、正準クエリ結果を生成しました。次に、その結果を元のコンテキストに適用する必要があります。思い出してみると、かなり最初のほうで、私たちは次のクエリを証明しようとしていました。

?A: Foo<'static, ?B>

これを次のように正準化しました。

for<T,L,T> { ?0: Foo<'?1, ?2> }

そして今、次の正準レスポンスが返ってきました。

for<T, L> {
    certainty: Proven,
    var_values: [Vec<?0>, '?1, ?0]
    region_constraints: [?0: '?1],
    value: (),
}

次に、このレスポンスを自分たちのコンテキストに適用したいと考えています。概念的には、そのために行うことは、(a) 結果内の各正準変数を新しい推論変数でインスタンス化し、(b) 結果内の値を元の値と単一化し、そして (c) リージョン制約を後で使うために記録する、というものです。ステップ (a) を実行すると、次のような結果になります。

{
      certainty: Proven,
      var_values: [Vec<?C>, '?D, ?C]
                       ^^   ^^^ fresh inference variables
      region_constraints: [?C: '?D],
      value: (),
}

ステップ (b) では、次を単一化します。

?A with Vec<?C>
'static with '?D
?B with ?C

そして最後に、リージョン制約 ?C: 'static が後で検証できるように記録されます。

(私たちが実際に行うのは、これを少し最適化した変種です。結果内の正準値をすべて変数で先行してインスタンス化するのではなく、値のベクターを走査し、その値が単なる正準変数であるケースを探します。この例では、values[2]?C なので、?C := ?B および '?D := 'static と推論できることを意味します。これにより、値の部分的な集合が得られます。値を見つけられなかったものについては、推論変数を作成します。)

トレイト解決(新)

この章では、rustc_trait_selection/solve にある新しい WIP ソルバーで トレイト解決がどのように機能するかを説明します。必要に応じて、 現在のソルバーおよび chalk ソルバーのドキュメントも参照してください。

コア概念

トレイトシステムの目的は、与えられたトレイト境界が満たされるかどうかを確認することです。 特に、ジェネリックである可能性のある関数の本体を型チェックするときに重要です。 例:

#![allow(unused)]
fn main() {
fn uses_vec_clone<T: Clone>(x: Vec<T>) -> (Vec<T>, Vec<T>) {
    (x.clone(), x)
}
}

ここで x.clone() の呼び出しでは、T: Clone が真であるという仮定のもとで、 Vec<T>Clone を実装していることを証明する必要があります。 この関数の呼び出し元によって証明されるため、T: Clone を仮定できます。

T: Clone を仮定して Vec<T>: Clone を証明する」という概念は Goal と呼ばれます。 Vec<T>: CloneT: Clone はどちらも Predicate を使って表現されます。他にも述語があり、 特に関連アイテムに対する等値境界があります: <Vec<T> as IntoIterator>::Item == T。 網羅的な一覧については PredicateKind enum を参照してください。Goal は、証明する必要がある predicate と、この述語が成立しなければならない param_env として表現されます。

与えられたゴールに対して、各可能な Candidate が適用できるかどうかを、 そのネストされたゴールを再帰的に証明することで確認し、ゴールを証明します。 例を含む可能な候補の一覧については CandidateSource を参照してください。 最も重要な候補は Impl 候補、すなわちユーザーによって書かれたトレイト実装と、 ParamEnv 候補、すなわち現在の環境における仮定です。

上記の例を見ると、Vec<T>: Clone を証明するには、まず impl<T: Clone> Clone for Vec<T> を使用します。この impl を使用するには、 T: Clone が成立するというネストされたゴールを証明する必要があります。 これは、ネストされたゴールを持たない ParamEnv の仮定 T: Clone を使用できます。 したがって Vec<T>: Clone は成立します。

トレイトソルバーは、CanonicalResponse として成功、曖昧性、またはエラーのいずれかを返すことができます。 成功と曖昧性の場合は、推論制約とリージョン制約も返します。

型システムの不変条件

FIXME: このファイルでは、ソルバーだけでなく、型システム全体の不変条件について説明します

型システムが常に真であることを保証するもの、つまり他の言語や型システムから望まれたり期待されたりする不変条件は数多くあります。残念ながら、そのかなり多くは現在の Rust では成り立ちません。これは設計上の根本的な理由による場合もあれば、バグによる場合もあり、将来変更される可能性があります。

型システムに取り組むとき、また型システムを利用するときに何を仮定できるのかを知っておくことは重要です。そのため、ここではコア型システムの不変条件について、網羅的ではなく非公式なリストを示します。

  • ✅: この不変条件はおおむね成り立ちますが、いくつか奇妙な例外や現在のバグがあります
  • ❌: この不変条件は成り立たず、将来成り立つ可能性も低いです。健全性のために依存してはならず、依存する場合は非常に注意する必要があります

wf(X)wf(normalize(X)) を含意する ✅

エイリアスを含む型が整形式である場合、そのエイリアスを正規化した後も整形式であるべきです。そうでなければ、これらの型について整形式性を再検査する必要があるため、私たちはこれに依存しています。

これは現在、型システムの健全性の破れにより成り立っていません: #84533

リージョンを法とした構造的等価性は意味的等価性を含意する ✅

ある型があり、左辺と右辺の両方で任意のリージョンを一意な推論変数に置き換えた後でそれ自身と等価にした場合、結果として構造的に異なる可能性のある型同士は、それでも互いに等しいべきです。

これは、ゴールが HIR typeck で成功した後に MIR borrowck で失敗するのを防ぐために必要です。この不変条件が破られると、MIR typeck は最終的に ICE で失敗します。

ゴールから得られた推論結果を適用しても、その結果は変わらない ❌

TODO: この不変条件は奇妙な形で定式化されており、詳述する必要があります。 だいたいのところ、私はこの検査がソルバーのバグがある場合にのみ失敗するようにしたいです: https://github.com/rust-lang/rust/blob/2ffeb4636b4ae376f716dc4378a7efb37632dc2d/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs#L391-L407。 この検査を再追加して、どこで壊れるかを見るべきです :3

あるゴールを証明する、型を等価にする、その他何らかの処理を行った後、結果として得られた推論制約を適用し、元の操作をやり直した場合、結果は同じであるべきです。

残念ながら、少なくとも新しいソルバーでは、いくつかの厄介な理由によりこれは成り立ちません。

トレイトソルバーは局所的に健全でなければならない ✅

これは、対応する impl が存在しないゴールに対して、決して成功を返してはならないことを意味します。そうしてしまうと、実装されていないにもかかわらずトレイトが実装されていると仮定することになり、実際の健全性の破れにつながる可能性が非常に高くなります。ゴールを証明するために where 境界を使用する場合、impl はそのアイテムのユーザーによって提供されます。

この不変条件は、リージョン制約を検査する場合にのみ成り立ちます。コヒーレンスにおける暗黙の負のオーバーラップ検査ではリージョン制約を検査しないため、この不変条件はそこで破られています。この検査はトレイトソルバーの完全性に依存しているため、現在のリージョン制約検査である InferCtxt::resolve_regions を使用できません。これは、type outlives ゴールの扱いが不完全であるためです。

空の環境において意味的に等しいエイリアスを正規化すると、一意な型になる ✅

エイリアス型/定数の正規化は、一意な結果を持たなければなりません。そうでないと、安全なコードで簡単に transmute を実装できてしまいます。次の関数があるとき、入力型と出力型が常に同じ具象型へ正規化されることを保証しなければなりません。

#![allow(unused)]
fn main() {
fn foo<T: Trait>(
    x: <T as Trait>::Assoc
) -> <T as Trait>::Assoc {
    x
}
}

現在知られている健全性の破れの問題の多くは、最終的にこの不変条件が破られていることに依存しています。しかし、この不変条件なしに健全な型システムを想像することは非常に困難です。したがって問題は、この不変条件が破られていることであり、私たちが誤ってこれに依存していることではありません。

型システムは完全である ❌

型システムは完全ではありません。 不要な推論制約を追加したり、ゴールが成り立ち得るにもかかわらずエラーにしたりすることがよくあります。

  • メソッド選択
  • opaque 型推論
  • type outlives 制約の扱い
  • トレイトソルバーの候補選択中に、Impl 候補よりも ParamEnv 候補を優先すること

ゴールは HIR typeck 以降も結果を保持する ✅

HIR typeck 中に成功したゴールが、MIR borrowck 中に再評価されたときに失敗すると、ICE が発生します。例: #140211

HIR typeck 中に成功したゴールが、インスタンス化された後に失敗すると、健全性の破れになります。例: #140212

この制約を維持しながらも、トレイトソルバーにある程度の不完全性を許容しているのは興味深いことです。「許容される不完全性」と、この不変条件を破る振る舞いとを明確に分離する方法があれば望ましいでしょう。

正規化は結果を変えてはならない

この不変条件は、ジェネリックエイリアスの正規化を可能にするために依存されています。これを破ると、容易に健全性の破れにつながる可能性があります。例: #57893

ゴールはインスタンス化後もオーバーフローする可能性がある

これは、それらが再帰制限に達し始めると起こります。 また、問題のある発散エイリアスもあります。 これらをどのように扱うべきかは明確ではありません :3

空の環境におけるトレイトゴールは、一意な impl によって証明される ✅

空の環境でトレイトゴールが成り立つ場合、そのゴールを証明するために使用される、一意な impl が存在するべきです。それはユーザー定義の場合も組み込みの場合もあります。これは、一意なメソッドや関連アイテムを選択するために必要です。

ただし、いくつかの場合にはこの不変条件を破っています。その一部はバグによるものであり、一部は設計によるものです。

  • marker traits は、関連アイテムを持たないため、オーバーラップが許可されています
  • specialization は、特殊化 impl がその親とオーバーラップすることを許可します
  • 組み込みのトレイトオブジェクトのトレイト実装は、ユーザー定義 impl とオーバーラップすることがあります: #57893

空でない環境で証明できるゴールは、モノモーフィゼーション中にも成り立つ ✅

あるゴールがジェネリック環境で証明できる場合、完全に具象的な型でインスタンス化し、スコープ内に where 句がない状態でも、そのゴールは成り立つべきです。

これは codegen によって仮定されており、codegen はオーバーフローではない曖昧性に遭遇すると ICE します。この不変条件は現在、specialization(#147507)および marker traits(#149502)によって破られています。

コヒーレンスにおける暗黙の負のオーバーラップ検査中、型システムは完全である ✅

オーバーラップ検査の詳細については、Coherence chapter を参照してください。

コヒーレンスにおける暗黙の負のオーバーラップ検査中、 証明可能なゴールに対して決してエラーを返してはなりません。 そうすると、関連アイテムが異なる可能性のあるオーバーラップした impl が許可され、 他の多くの不変条件が破られます。

この不変条件は、私たちが実際に依存しているものであるにもかかわらず、現在さまざまな形で破られています。 これは非常に壊しやすいため、注意する必要があります。

  • エイリアスの一般化
  • サブタイピングバインダー中の一般化(幸い、コヒーレンスでは悪用できません)

トレイト解決は、ライフタイムが異なることに依存してはならない ✅

あるゴールがライフタイムが異なる場合に成り立つなら、そのライフタイムが同じである場合にも成り立たなければなりません。そうでないと、コード生成中にモノモーフィゼーション後エラーが発生したり、不正な vtable による健全性の破綻が生じたりします。

最初に異なるライフタイムでゴールを証明し、それらが後から等しいと制約される場合にも、一貫しない動作が発生する可能性があります。

本体内のトレイト解決は、ライフタイムが等しいことに依存してはならない ✅

トレイトソルバーでリージョンの等価性に依存する場合にも注意が必要です。 これはコード生成に関しては問題ありません。消去されたリージョンはすべて等しいものとして扱うためです。しかし、 HIR から MIR typeck への過程で等価性の情報を失う可能性があります。

これは現在、新しいソルバーでは成り立っていません: trait-system-refactor-initiative#27

曖昧性を取り除くと、コンパイルできるものが厳密に増える ❌

理想的には、ものごとがコンパイルできるために曖昧性に依存すべきではありません。 そうしないと、将来の改善が破壊的変更になる原因となります。

不完全性 のため、これは成り立っておらず、 推論を改善すると推論の変更が生じ、既存のプロジェクトを壊す可能性があります。

意味的等価性は構造的等価性を含意する ✅

型システムにおいて 2 つの型が等しいということは、それらのジェネリックパラメーターを具体的な 引数でインスタンス化した後に同じ TypeId を持つことを意味しなければなりません。そうでない場合、それらの異なる TypeId を使ってトレイト選択に影響を与えることができます。

コード生成中は構造的等価性を使って型を検索しますが、これは必ずしも健全性の破綻につながるとは限りません

  • 冗長なメソッドコード生成やバックエンドの型チェックエラーが発生する可能性がある?
  • CTFE のアサーションでもこれに依存しています

意味的に異なる型は異なる TypeId を持つ ✅

意味的に異なる 'static 型は、transmute を避けるために異なる TypeId を必要とします。 たとえば for<'a> fn(&'a str)fn(&'static str) は異なる TypeId を持たなければなりません。

const 項目の評価は決定的である ✅

const 項目の値は型システムに入力され得るため、const 項目の値がすべてのクレートで常に同じであることが重要です。そうでない場合、「等しい」const 引数を持つ関連型が存在し、それゆえ「等しい」関連型であるにもかかわらず、異なるクレートでコード生成中に正規化されたとき、実際には異なる型になってしまう可能性があります。

特に、これは const 関数 には拡張されません。型システムは const 項目 の結果だけを扱うため、const 項目の最終的な値に影響しない限り、const 関数が非決定的であっても実際には問題ありません。

ソルバー

Chalk の再帰的ソルバーのドキュメントも読むことを検討してください。 この実装と非常によく似ており、このアプローチの制限についても説明されています。

大まかな流れ

ソルバーのエントリーポイントは InferCtxtEvalExt::evaluate_root_goal です。この関数はルートの EvalCtxt をセットアップし、その後 EvalCtxt::evaluate_goal を呼び出して、実際にトレイトソルバーに入ります。

EvalCtxt::evaluate_goal正準化、キャッシュ、オーバーフロー、ソルバーサイクルを処理します。それが完了すると、別個のローカルな InferCtxt を持つネストされた EvalCtxt を作成し、EvalCtxt::compute_goal を呼び出します。これは「実際のソルバーの挙動」を担当します。PredicateKind でマッチし、それぞれについて別の関数に委譲します。

Vec<T>: Clone のようなトレイトゴールの場合、EvalCtxt::compute_trait_goal は、このゴールを証明できる可能性のあるすべての方法を EvalCtxt::assemble_and_evaluate_candidates によって収集する必要があります。各候補は、推論制約が他の候補に漏れないよう、別々の「プローブ」で処理されます。その後、組み立てられた候補を EvalCtxt::merge_candidates によってマージしようとします。

重要な概念と設計パターン

EvalCtxt::add_goal

ネストされたゴールを証明するために、EvalCtxt::compute_goal を直接呼び出すのではなく、代わりに EvalCtxt::all_goal でそのゴールを EvalCtxt に追加します。その後、EvalCtxt::try_evaluate_added_goals または EvalCtxt::evaluate_added_goals_and_make_canonical_response のいずれかですべてのネストされたゴールをまとめて証明します。これにより、後続のゴールからの推論制約を扱えるようになります。

たとえば、ネストされたゴールとして ?x: Debug(): ConstrainToU8<?x> の両方がある場合、?x: Debug を証明することは最初は曖昧ですが、(): ConstrainToU8<?x> を証明した後は ?xu8 に制約され、u8: Debug の証明に成功します。

TyKind でのマッチング

ソルバーでは型を遅延正規化するため、あらゆる型と定数が潜在的に未正規化であると常に仮定する必要があります。これは、TyKind でのマッチングが簡単に誤りになり得ることを意味します。

正規化は 2 つの異なる方法で扱います。関連型を正規化するときに Trait ゴールを証明する場合、self 型と構造的に一致するかどうかに応じて、候補を別々に組み立てます。self 型に一致する候補は EvalCtxt::assemble_candidates_via_self_ty で処理され、これは EvalCtxt::assemble_candidates_after_normalizing_self_ty を介して再帰し、self 型を 1 レベル正規化します。それ以外のすべての場合で TyKind にマッチする必要があるときは、まず EvalCtxt::try_normalize_ty を使用して、その型を可能な限り正規化します。

高階ランクのゴール

ゴールが高階ランクである場合、たとえば for<'a> F: FnOnce(&'a ()) では、EvalCtxt::compute_goal は即座に 'a をプレースホルダーでインスタンス化し、その後 F: FnOnce(&'!a ()) をネストされたゴールとして再帰的に証明します。

選択への対処

一部のゴールは複数の方法で証明できます。このような場合、それぞれの選択肢を別々の「プローブ」で試し、その後 EvalCtxt::try_merge_responses を使用して結果のレスポンスをマージしようとします。レスポンスのマージに失敗した場合は、代わりに EvalCtxt::flounder を使用して曖昧性を返します。一部のゴールについては、EvalCtxt::try_merge_responses が失敗した場合に、いくつかの選択肢を他より不完全に優先しようとします。

さらに学ぶ

ソルバーはかなり自己完結しているはずです。上記の情報が、コード自体を見るときの良い基礎になることを願っています。その際に行き詰まった場合や、不明確だったり、より良いコメントが必要だったり、ここで言及すべき癖や設計判断がある場合は、Zulip で連絡してください。

候補の優先順位

Trait ゴールと NormalizesTo ゴールを証明する方法は複数あります。 そのような各選択肢は Candidate と呼ばれます。 適用可能な候補が複数ある場合、一部の候補を他の候補より優先します。 関連情報はそれらの CandidateSource に保存します。

この優先順位は、不正な推論やリージョン制約を引き起こす可能性があるため、コヒーレンス中には不健全になります。 そのため、コヒーレンスでは単にすべての候補のマージを試みます。

Trait ゴール

Trait ゴールは、適用可能な候補を fn merge_trait_candidates でマージします。 このドキュメントでは、現在の優先順位ルールが なぜ 存在するのかを説明するための追加の詳細と参照を提供します。

CandidateSource::BuiltinImpl(BuiltinImplSource::Trivial))

自明な builtin impl は、well-formed な型に対して常に適用可能であることが分かっている builtin impl です。 これは、もしそれが存在するなら、別の候補を使用しても制約が少なくなることは決してない、ということを意味します。 現在、自明であると見なしているのは SizedMetaSized の impl のみです。

これは、次のパターンでライフタイムエラーを防ぐために必要です。

#![allow(unused)]
fn main() {
trait Trait<T>: Sized {}
impl<'a> Trait<u32> for &'a str {}
impl<'a> Trait<i32> for &'a str {}
fn is_sized<T: Sized>(_: T) {}
fn foo<'a, 'b, T>(x: &'b str)
where
    &'a str: Trait<T>,
{
    // `&'a str: Trait<T>` の where 境界を展開すると、
    // `&'a str: Sized` の where 境界になります。これを builtin impl より
    // 優先したくありません。
    is_sized(x);
}
}

この優先順位は、builtin impl が非 param の where 節に依存するネストされたゴールを持つ場合には正しくありません。

#![allow(unused)]
fn main() {
struct MyType<'a, T: ?Sized>(&'a (), T);
fn is_sized<T>() {}
fn foo<'a, T: ?Sized>()
where
    (MyType<'a, T>,): Sized,
    MyType<'static, T>: Sized,
{
    // where 境界は自明ですが、タプルに対する builtin の `Sized` impl は
    // `MyType<'a, T>: Sized` の証明を要求し、これは where 節を使用することでしか
    // 証明できず、不要な `'static` 制約を追加します。
    is_sized::<(MyType<'a, T>,)>();
    //~^ ERROR lifetime may not live long enough
}
}

CandidateSource::ParamEnv

少なくとも 1 つの 非グローバルParamEnv 候補が存在すると、すべての ParamEnv 候補を他の候補種別より優先します。 where 境界がグローバルであるとは、それが高ランクでなく、かつジェネリックパラメーターを一切含まない場合です。 それには 'static を含めることができます。

where 境界はユーザーが最も制御しやすい傾向があるため、他の候補よりも where 境界を適用しようとします。これにより、候補の優先順位が誤っている場合でも、ユーザーが最も簡単に調整できます。

Impl 候補より優先する理由

これは、次の例でリージョンエラーを回避するために必要です。

#![allow(unused)]
fn main() {
trait Trait<'a> {}
impl<T> Trait<'static> for T {}
fn impls_trait<'a, T: Trait<'a>>() {}
fn foo<'a, T: Trait<'a>>() {
    impls_trait::<'a, T>();
}
}

シャドウされた impl は、現在あいまいなソルバーサイクルを引き起こす可能性があるため、これも必要です: trait-system-refactor-initiative#76。 優先順位がない場合、不完全性を避けるために where 境界がリージョン制約を生じさせるなら、あいまい性エラーで失敗せざるを得ません。

#![allow(unused)]
fn main() {
trait Super {
    type SuperAssoc;
}

trait Trait: Super<SuperAssoc = Self::TraitAssoc> {
    type TraitAssoc;
}

impl<T, U> Trait for T
where
    T: Super<SuperAssoc = U>,
{
    type TraitAssoc = U;
}

fn overflow<T: Trait>() {
    // 展開された `Super<SuperAssoc = Self::TraitAssoc>` の where 境界を使用して、
    // `T: Trait` 実装の where 境界を証明できます。これは現在、
    // オーバーフローを引き起こします。
    let x: <T as Trait>::TraitAssoc;
}
}

この優先順位は多くの問題を引き起こします。 #24066 を参照してください。 問題の大部分は、where 境界が型推論を誘導する場合であっても、impl より where 境界を優先することによって引き起こされます。

#![allow(unused)]
fn main() {
trait Trait<T> {
    fn call_me(&self, x: T) {}
}
impl<T> Trait<u32> for T {}
impl<T> Trait<i32> for T {}
fn bug<T: Trait<U>, U>(x: T) {
    x.call_me(1u32);
    //~^ ERROR mismatched types
}
}

しかし、where 境界が推論を誘導しない場合にのみこの優先順位を適用するとしても、それでも不正なライフタイム制約を引き起こす可能性があります。

#![allow(unused)]
fn main() {
trait Trait<'a> {}
impl<'a> Trait<'a> for &'a str {}
fn impls_trait<'a, T: Trait<'a>>(_: T) {}
fn foo<'a, 'b>(x: &'b str)
where
    &'a str: Trait<'b>
{
    // `'b: 'x` を伴って `&'x str: Trait<'b>` を証明する必要があります。
    impls_trait::<'b, _>(x);
    //~^ ERROR lifetime may not live long enough
}
}

AliasBound 候補より優先する理由

これは、次の例でリージョンエラーを回避するために必要です。

#![allow(unused)]
fn main() {
trait Bound<'a> {}
trait Trait<'a> {
    type Assoc: Bound<'a>;
}

fn impls_bound<'b, T: Bound<'b>>() {}
fn foo<'a, 'b, 'c, T>()
where
    T: Trait<'a>,
    for<'hr> T::Assoc: Bound<'hr>,
{
    impls_bound::<'b, T::Assoc>();
    impls_bound::<'c, T::Assoc>();
}
}

これは不要な制約を引き起こすこともあります。

#![allow(unused)]
fn main() {
trait Bound<'a> {}
trait Trait<'a> {
    type Assoc: Bound<'a>;
}

fn impls_bound<'b, T: Bound<'b>>() {}
fn foo<'a, 'b, T>()
where
    T: for<'hr> Trait<'hr>,
    <T as Trait<'b>>::Assoc: Bound<'a>,
{
    // `<T as Trait<'a>>::Assoc: Bound<'a>` に where 境界を使用すると、
    // `<T as Trait<'a>>::Assoc` が env からの
    // `<T as Trait<'b>>::Assoc` と不要に等価とされます。
    impls_bound::<'a, <T as Trait<'a>>::Assoc>();
    // `<T as Trait<'b>>::Assoc: Bound<'b>` については、where 境界の self 型は
    // 一致しますが、trait 境界の引数は一致しません。
    impls_bound::<'b, <T as Trait<'b>>::Assoc>();
}
}

グローバルな where 境界を優先しない理由

グローバルな where 境界は、impl に完全に含意されるか、満たせないかのどちらかです。 満たせない場合、何が起きるかは実際には気にしません。 where 境界が完全に含意される場合、trait ゴールを証明するために impl を使用しても追加の制約が発生することはありません。 trait ゴールにおいて、これは 'static を使用する where 境界に対してのみ有用です。

#![allow(unused)]
fn main() {
trait A {
    fn test(&self);
}

fn foo(x: &dyn A)
where
    dyn A + 'static: A, // この境界を使用するとライフタイムエラーにつながります。
{
    x.test();
}
}

さらに重要なこととして、ここで impl を使用することで、関連型を正規化するときにグローバルな where 境界が impl をシャドウするのを防ぎます。 グローバルな where 境界より impl を優先することによる既知の問題はありません。

それでもグローバルな where 境界を考慮する理由

グローバルな where 境界が存在していても単に impl を使用するのであれば、なぜこれらのグローバルな where 境界を完全に無視しないのか、と疑問に思うかもしれません。非グローバルな where 境界からの推論の誘導を弱めるために、それらを使用します。

グローバルな where 境界がない場合、適用可能な impl も存在するにもかかわらず、現在は非グローバルな where 境界を優先します。 非グローバルな where 境界を追加することで、この不要な推論の誘導が無効化され、次のコードがコンパイルできるようになります。

#![allow(unused)]
fn main() {
fn check<Color>(color: Color)
where
    Vec: Into<Color> + Into<f32>,
{
    let _: f32 = Vec.into();
    // グローバルな `Vec: Into<f32>` 境界がなければ、
    // ここで非グローバルな `Vec: Into<Color>` 境界を
    // 積極的に使用してしまい、これが失敗します。
}

struct Vec;
impl From<Vec> for f32 {
    fn from(_: Vec) -> Self {
        loop {}
    }
}
}

CandidateSource::AliasBound

alias-bound 候補を impl より優先します。 現在、この優先順位を型推論の誘導に使用しており、その結果、以下がコンパイルされます。 個人的には、この優先順位が望ましいとは思いません 🤷

#![allow(unused)]
fn main() {
pub trait Dyn {
    type Word: Into<u64>;
    fn d_tag(&self) -> Self::Word;
    fn tag32(&self) -> Option<u32> {
        self.d_tag().into().try_into().ok()
        // `Self::Word: Into<?0>` を証明してから、`?0` 上の
        // メソッドを選択する。eager な推論が必要。
    }
}
}
fn impl_trait() -> impl Into<u32> {
    0u16
}

fn main() {
    // `x` には 2 つの可能な型がある:
    // - `impl Into<u32>` の「alias bound」を使うことによる `u32`
    // - `impl<T> From<T> for T` を使うことによる `impl Into<u32>`、すなわち `u16`
    //
    // `x` の型を `u32` と推論する。ただし、これは厳密には
    // 必要ではなく、驚くようなエラーにつながることさえある。
    let x = impl_trait().into();
    println!("{}", std::mem::size_of_val(&x));
}

この優先により、リージョン制約による曖昧さも回避されますが、実際に人々がこれに依存しているかはわかりません。

#![allow(unused)]
fn main() {
trait Bound<'a> {}
impl<T> Bound<'static> for T {}
trait Trait<'a> {
    type Assoc: Bound<'a>;
}

fn impls_bound<'b, T: Bound<'b>>() {}
fn foo<'a, T: Trait<'a>>() {
    // これを `'a` と推論すべきか、それとも `'static` と推論すべきか。
    impls_bound::<'_, T::Assoc>();
}
}

CandidateSource::BuiltinImpl(BuiltinImplSource::Object(_))

組み込みの trait object impl を、ユーザーが書いた impl より優先します。 これは 不健全 であり、将来削除されるべきです。 詳細については #57893#141347 を参照してください。

NormalizesTo ゴール

正規化中の候補の優先順位の挙動は fn assemble_and_merge_candidates に実装されています。

AliasBound 候補を常に考慮します

where-bound が関連アイテムを指定していない場合、trait ゴールが ParamEnv 候補によって証明されていたとしても、エイリアスを固定的なものとして扱う代わりに AliasBound 候補を考慮します。

#![allow(unused)]
fn main() {
trait Super {
    type Assoc;
}
trait Bound {
    type Assoc: Super<Assoc = u32>;
}
trait Trait: Super {}

// 環境を展開すると、`T::Assoc: Super` where-bound が得られる。
// この where-bound は、`Super<Assoc = u32>`
// item bound による正規化を妨げてはならない。
fn heck<T: Bound<Assoc: Trait>>(x: <T::Assoc as Super>::Assoc) -> u32 {
    x
}
}

このようなエイリアスを使用すると、追加のリージョン制約が生じる可能性があります。cc #133044

#![allow(unused)]
fn main() {
trait Bound<'a> {
    type Assoc;
}
trait Trait {
    type Assoc: Bound<'static, Assoc = u32>;
}

fn heck<'a, T: Trait<Assoc: Bound<'a>>>(x: <T::Assoc as Bound<'a>>::Assoc) {
    // 関連型を正規化するには `T::Assoc: Bound<'static>` が必要になる。これは、
    // エイリアスを固定的なままにせず、`Bound<'static>` alias-bound を使うため。
    drop(x);
}
}

ParamEnv 候補を AliasBound より優先します

where-bound が関連型を指定していない場合は AliasBound 候補を使用しますが、指定している場合は where-bound を優先します。 これは以下の例で必要です:

#![allow(unused)]
fn main() {
// `I::IntoIterator: Iterator<Item = ()>`
// where-bound を、`I::Intoiterator: Iterator<Item = I::Item>`
// alias-bound より優先することを確認する。

trait Iterator {
    type Item;
}

trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;
}

fn normalize<I: Iterator<Item = ()>>() {}

fn foo<I>()
where
    I: IntoIterator,
    I::IntoIter: Iterator<Item = ()>,
{
    // `I::IntoIterator: Iterator<Item = ()>`
    // where-bound を、`I::Intoiterator: Iterator<Item = I::Item>`
    // alias-bound より優先する必要がある。
    normalize::<I::IntoIter>();
}
}

where-bound を常に考慮します

trait ゴールが impl によって証明されていたとしても、ParamEnv 候補が存在するなら、依然としてそれらを優先します。

「orphaned」where-bound を優先します

fn check_type_bounds で GAT と RPITIT の item bound を正規化するとき、「orphaned」Projection 句を ParamEnv に追加します。 これらの ParamEnv 候補を、impl や他の where-bound より優先する必要があります。

#![allow(unused)]
#![feature(associated_type_defaults)]
fn main() {
trait Foo {
    // 以下の impl によって `i32: Baz<Self>` を証明できるはずである。
    // その impl は `Self::Bar<()>: Eq<i32>` を必要とするが、
    // `for<T> Self::Bar<T> = i32` と仮定するため、これは真である。
    type Bar<T>: Baz<Self> = i32;
}
trait Baz<T: ?Sized> {}
impl<T: Foo + ?Sized> Baz<T> for i32 where T::Bar<()>: Eq<i32> {}
trait Eq<T> {}
impl<T> Eq<T> for T {}
}

この優先が実際に必要になるケースを完全には理解しておらず、まだ面白い形でこれを悪用できてもいませんが 🤷

グローバルな where-bound を impl より優先します

以下がコンパイルされるためにはこれが必要です。 実際に何かがこれに依存しているかはわかりません 🤷

#![allow(unused)]
fn main() {
trait Id {
    type This;
}
impl<T> Id for T {
    type This = T;
}

fn foo<T>(x: T) -> <u32 as Id>::This
where
    u32: Id<This = T>,
{
    x
}
}

これは、正規化が追加のリージョン制約を生じさせる可能性があることを意味します。cc #133044

#![allow(unused)]
fn main() {
trait Trait {
    type Assoc;
}

impl Trait for &u32 {
    type Assoc = u32;
}

fn trait_bound<T: Trait>() {}
fn normalize<T: Trait<Assoc = u32>>() {}

fn foo<'a>()
where
    &'static u32: Trait<Assoc = u32>,
{
    trait_bound::<&'a u32>(); // ok、impl によって証明される
    normalize::<&'a u32>(); // エラー、where-bound によって証明される
}
}

Trait の where-bound は impl をシャドウします

対応する trait ゴールが ParamEnv または AliasBound 候補によって証明されている場合、関連アイテムの正規化は impl を考慮しません。 これは、関連型を制約しない where-bound については、関連型が 固定的 なままになることを意味します。

impl を使用すると異なるリージョン制約が生じます

これは、impl を適用することによる不要なリージョン制約を避けるために必要です。

#![allow(unused)]
fn main() {
trait Trait<'a> {
    type Assoc;
}
impl Trait<'static> for u32 {
    type Assoc = u32;
}

fn bar<'b, T: Trait<'b>>() -> T::Assoc { todo!() }
fn foo<'a>()
where
    u32: Trait<'a>,
{
    // 戻り値の型を正規化すると impl が使用され、
    // `T: Trait` where-bound の証明では where-bound が使用されるため、
    // 異なるリージョン制約が生じる。
    bar::<'_, u32>();
}
}

RPITIT type_of サイクル

現在、RPITIT のクエリサイクルを避けるため、where-bound がある場合は impl 候補を避ける必要があります。#139762 を参照してください。 この問題を取り除くために、RPITIT の計算中に auto-trait leakage に依存するのをやめるのが望ましいように感じます。#139788 を参照してください。

#![allow(unused)]
fn main() {
use std::future::Future;
pub trait ReactiveFunction: Send {
    type Output;

    fn invoke(self) -> Self::Output;
}

trait AttributeValue {
    fn resolve(self) -> impl Future<Output = ()> + Send;
}

impl<F, V> AttributeValue for F
where
    F: ReactiveFunction<Output = V>,
    V: AttributeValue,
{
    async fn resolve(self) {
        // ここでは `<V as AttributeValue>::{synthetic#0}` を await している。
        // 現在入っている impl を介してこれを正規化するには
        // `collect_return_position_impl_trait_in_trait_tys` に依存するが、これは
        // 最終的に、この関数の opaque return type がトレイト定義の `Send` item
        // bound を実装していることを確認する際の auto-trait leakage に依存する。
        self.invoke().resolve().await
    }
}
}

トレイト定義では、常に適用可能な impl の associated type を使用できない

トレイト定義内の T: Trait という仮定により、blanket impl を使用して <Self as Trait>::AssocT に正規化することはできません。 これは、非常に強いものではないにせよ、ある程度望ましい制約のように感じられます。

#![allow(unused)]
fn main() {
trait Eq<T> {}
impl<T> Eq<T> for T {}
struct IsEqual<T: Eq<U>, U>(T, U);

trait Trait: Sized {
    type Assoc;
    fn foo() -> IsEqual<Self, Self::Assoc> {
        //~^ ERROR トレイト境界 `Self: Eq<<Self as Trait>::Assoc>` が満たされていません
        todo!()
    }
}

impl<T> Trait for T {
    type Assoc = T;
}
}

正準化

正準化は、値をそのコンテキストから隔離するプロセスであり、推論変数を含むゴールをグローバルにキャッシュするために必要です。

考え方としては、ゴール u32: Trait<?x>u32: Trait<?y> が与えられ、ここで ?x?y が現在は制約されていない 2 つの異なる推論変数である場合、両方のゴールに対して同じ結果を得るべきです。したがって、正準クエリ exists<T> u32: Trait<T> を一度証明し、その結果を再利用できます。

まず正準クエリがどのように動作するかを見てから、正準化がどのように機能するかの詳細に入ります。

正準クエリのウォークスルー

少しわかりやすくするため、トレイトゴール u32: Trait<?x> を例として使い、関連する impl は impl<T> Trait<Vec<T>> for u32 だけであると仮定します。

入力の正準化

まずゴールを正準化し、推論変数を存在束縛変数に、プレースホルダーを全称束縛変数に置き換えます。これにより、正準ゴール exists<T> u32: Trait<T> が得られます。

すべての束縛変数の元の値を、元のコンテキスト内で覚えておきます。ここでは、これは T?x に対応付けます。これらの元の値は、後でクエリ応答を扱うときに使用されます。

次に、正準ゴールを使って正準クエリを呼び出します。

クエリ内での正準ゴールのインスタンス化

実際に正準ゴールを証明しようとするために、束縛変数を再び推論変数とプレースホルダーでインスタンス化します。

これは、クエリ内の完全に別の InferCtxt の中で行われます。クエリ内では、ゴール u32: Trait<?0> があります。また、正準ゴール内の束縛変数をインスタンス化するために使った値も覚えておきます。これは T?0 に対応付けます。

次に、ゴール u32: Trait<?0> を計算し、これが成り立つことを突き止めますが、?0Vec<?1> に制約されています。最後に、この結果を呼び出し元にとって有用なものへ変換します。

クエリ応答の正準化

ゴールが成り立つかどうかと、クエリ内からの推論制約の両方を呼び出し元に返す必要があります。

推論結果を呼び出し元に返すために、束縛変数からクエリ内でインスタンス化された値へのマッピングを正準化します。つまり、クエリ応答は Certainty::Yes と、T から exists<U> Vec<U> へのマッピングになります。

クエリ応答のインスタンス化

呼び出し元は、クエリによって返された制約を適用する必要があります。そのために、まず正準応答の束縛変数を再び推論変数とプレースホルダーでインスタンス化します。そのため、応答内のマッピングは T から Vec<?z> へのものになります。

次に、T の元の値(?x)を、応答内の T に対する値(Vec<?z>)と等価にします。これにより、?x は正しく Vec<?z> に制約されます。

ExternalConstraints

トレイトゴールの計算は、推論変数を制約するだけでなく、リージョン義務を追加することもあります。たとえば、ゴール (): AOutlivesB<'a, 'b> が与えられた場合、'a: 'b が成り立たなければならないという事実を返したいとします。

これは、束縛変数からクエリ内でインスタンス化された値へのマッピングを返すだけでなく、応答を構築する際に InferCtxt コンテキストから追加の ExternalConstraints を抽出することで行われます。

正準化は正確にはどのように機能するか

TODO: PR がマージされたらコードへリンクして詳述する

  • 型と const: ユニバースを考慮し、推論変数は存在束縛変数へ、プレースホルダーは全称束縛変数へ
  • 入力内のジェネリックパラメーターは、ルートユニバース内のプレースホルダーとして扱われる
  • 入力内のすべてのリージョンは、すべて存在束縛変数にマッピングされ、それらを「一意化」する。 &'a (): Trait<'a>exists<'0, '1> &'0 (): Trait<'1> に正準化される。これらのユニバースについては気にせず、すべてのリージョンを入力の最も高いユニバースに単純に入れる。
  • 出力では、呼び出し元のユニバース内のすべてのものがルートユニバースに入れられ、呼び出し元の元の値と変数値を単一化するときにのみ正しいユニバースを得る
  • 応答内のリージョンは一意化せず、'static は正準化しない

余帰納法

トレイトソルバーは、ゴールを証明するときに余帰納法を使うことがあります。 余帰納法はかなり繊細なので、独立した章として扱います。

余帰納法と帰納法

帰納法では、有限の証明木に到達するまで再帰的に証明を適用します。 次の木になる Vec<Vec<Vec<u32>>>: Debug の例を考えてみましょう。

  • Vec<Vec<Vec<u32>>>: Debug
    • Vec<Vec<u32>>: Debug
      • Vec<u32>: Debug
        • u32: Debug

この木は有限です。しかし、成り立ってほしいすべてのゴールが有限の証明木を持つわけではありません。 次の例を考えてみましょう。

#![allow(unused)]
fn main() {
struct List<T> {
    value: T,
    next: Option<Box<List<T>>>,
}
}

List<T>: Send が成り立つには、そのすべてのフィールドも再帰的に Send を実装していなければなりません。 これは次の証明木になります。

  • List<T>: Send
    • T: Send
    • Option<Box<List<T>>>: Send
      • Box<List<T>>: Send
        • List<T>: Send
          • T: Send
          • Option<Box<List<T>>>: Send
            • Box<List<T>>: Send

この木は無限に大きくなりますが、まさにこれこそが余帰納法の扱う対象です。

ゴールを帰納的に証明するには、そのゴールに対して有限の証明木を提示する必要があります。 ゴールを余帰納的に証明する場合、提示される証明木は無限であってもかまいません。

なぜ余帰納法は正しいのか

あるトレイトゴールが成り立つかどうかを確認するとき、私たちは「この境界を満たす impl は存在するか」を問うています。ネストしたゴールの無限の連鎖がある場合でも、使用されるべき一意の impl は依然として存在します。

余帰納法を実装する方法

私たちの実装では、無限の木を構築しようとして余帰納法を確認することはできません。 それには無限のリソースが必要になるためです。それでも、この観点から余帰納法を考えることには意味があります。

無限の木を確認することはできないため、代わりに、無限の証明木になると分かっているパターンを探します。現在検出しているパターンは(正準)サイクルです。T: SendT: Send に依存しているなら、それが永遠に続くだけであることはかなり明らかです。

サイクルを扱う場合、キャッシュには注意が必要です。リージョンと推論変数の正準化により、サイクルに遭遇したからといって、無限の証明木が得られるとは限らないためです。 次の例を見てみましょう。

#![allow(unused)]
fn main() {
trait Foo {}
struct Wrapper<T>(T);

impl<T> Foo for Wrapper<Wrapper<T>>
where
    Wrapper<T>: Foo
{} 
}

Wrapper<?0>: Foo を証明すると、impl<T> Foo for Wrapper<Wrapper<T>> という impl が使われ、これによって ?0Wrapper<?1> に制約され、その後 Wrapper<?1>: Foo が要求されます。正準化により、これはサイクルとして検出されます。

解決の考え方は、サイクルを検出するたびに暫定結果を返し、そのゴールの暫定結果が最終結果と等しくなるまでゴールを繰り返し再試行することです。最初は制約なしの Yes を結果として使用し、再実行が必要になるたびに前回の反復の結果へ更新します。

TODO: ここを詳述する。余帰納的サイクルについては chalk と同じアプローチを使用しています。 なお、帰納的サイクルの扱いは現在、単に Overflow を返すという点で異なります。 chalk book の関連する章を参照してください。

今後の作業

現在は、自動トレイト、Sized、および WF ゴールのみを余帰納的と見なしています。 将来的には、ほぼすべてのゴールを余帰納的にするつもりです。 まず、より多くの余帰納的証明を許可することがなぜ望ましいのかを詳しく説明します。

再帰的データ型はすでに余帰納法に依存している…

…ただし、トレイトソルバー内ではそれを避ける傾向があります。

#![allow(unused)]
fn main() {
enum List<T> {
    Nil,
    Succ(T, Box<List<T>>),
}

impl<T: Clone> Clone for List<T> {
    fn clone(&self) -> Self {
        match self {
            List::Nil => List::Nil,
            List::Succ(head, tail) => List::Succ(head.clone(), tail.clone()),
        }
    }
}
}

この impl では tail.clone() を使用しています。このためには Box<List<T>>: Clone を証明する必要があり、それには List<T>: Clone が必要ですが、それは現在確認している impl に依存しています。 その要件を impl の where 句に追加すると、これは perfect derive で行うことですが、そのサイクルがトレイトソルバーに移動し、エラーが発生します

再帰的データ型

射影を含む再帰型について推論するためにも余帰納法が必要です。 たとえば、次のコードは有効であるべきにもかかわらず、現在はコンパイルに失敗します。

#![allow(unused)]
fn main() {
use std::borrow::Cow;
pub struct Foo<'a>(Cow<'a, [Foo<'a>]>);
}

この問題は少なくとも 2015 年から知られています。詳しく知りたい場合は #23714 を参照してください。

明示的に確認される暗黙の境界

impl を確認するとき、impl ヘッダー内の型は整形式であると仮定します。 これは、その impl をインスタンス化して使うときに、それが実際にそうであることを証明しなければならないことを意味します。 #100051 は、これが成り立っていないことを示しています。 これを修正するには、impl ヘッダー内の型に対して WF 述語を追加する必要があります。 すべてのトレイトに対する余帰納法がなければ、これは core さえ壊してしまいます。

#![allow(unused)]
fn main() {
trait FromResidual<R> {}
trait Try: FromResidual<<Self as Try>::Residual> {
    type Residual;
}

struct Ready<T>(T);
impl<T> Try for Ready<T> {
    type Residual = Ready<()>;
}
impl<T> FromResidual<<Ready<T> as Try>::Residual> for Ready<T> {}
}

FromResidual の impl が整形式であることを確認すると、次のサイクルが発生します。

impl は、<Ready<T> as Try>::ResidualReady<T> が整形式である場合に整形式です。

  • wf(<Ready<T> as Try>::Residual) は次を要求します
  • Ready<T>: Try。これはスーパートレイトにより次を要求します
  • Ready<T>: FromResidual<Ready<T> as Try>::Residual>impl に対する暗黙の境界のため
  • wf(<Ready<T> as Try>::Residual) :tada: サイクル

余帰納法をより多くのゴールへ拡張する際の問題

余帰納法を拡張する際には、留意すべき追加の問題がいくつかあります。 ここでの問題は、現在のソルバーには関係ありません。

暗黙のスーパートレイト境界

現在のトレイトシステムは、trait Trait: SuperTrait のようなスーパートレイトを、1) Trait を実装するすべての型について SuperTrait が成り立つことを要求し、2) Trait が成り立つなら SuperTrait も成り立つと仮定する、という形で扱っています。

  1. を証明している間に 2) に依存するのは健全ではありません。これは余帰納的サイクルの場合にのみ観測できます。 サイクルがなければ、2) に依存するたびに、使用された Trait の impl について、2) に依存せずに 1) も証明しているはずだからです。
#![allow(unused)]
fn main() {
trait Trait: SuperTrait {}

impl<T: Trait> Trait for T {}

// 余帰納法について現在の構成を維持すると
// これのコンパイルが許可されてしまいます。うーん :<
fn sup<T: SuperTrait>() {}
fn requires_trait<T: Trait>() { sup::<T>() }
fn generic<T>() { requires_trait::<T>() }
}

これは余帰納法に本質的なものというよりも、余帰納法によって不健全になる既存の性質です。

考えられる解決策

これを解決する最も簡単な方法は、2) を完全に削除し、常にトレイトソルバーの外側で T: TraitT: TraitT: SuperTrait に elaboration することです。 これにより 1) も削除できるようになりますが、トレイト上の通常の where 境界は依然として証明する必要があるため、それは追加の作業にすぎません。

  1. をチェックするときに 2) の循環的な使用を無効化する方法は想像できるかもしれませんが、 少なくとも私(@lcnr)の考えでは、どれも合理的とは言えないほど複雑すぎます。

normalizes_to ゴールと進捗

normalizes_to ゴールは、<T as Trait>::Assoc が何らかの U に正規化されるという要件を表します。 これは事実上、まず <T as Trait>::Assoc を正規化し、次にその結果の型を U と等置することで実現されます。 各射影はちょうど 1 つの型に正規化されるべきなので、これはマッピングであるべきです。 単に無限の証明木を許可すると、次のような振る舞いになります。

#![allow(unused)]
fn main() {
trait Trait {
    type Assoc;
}

impl Trait for () {
    type Assoc = <() as Trait>::Assoc;
}
}

ここで normalizes_to(<() as Trait>::Assoc, Vec<u32>) を計算すると、impl が解決され、 関連型 <() as Trait>::Assoc が得られます。次に、それを期待される型と等置するため、 再び normalizes_to(<() as Trait>::Assoc, Vec<u32>) をチェックすることになります。 これは永遠に続き、結果として無限の証明木になります。

これは、<() as Trait>::Assoc が他の任意の型と等しくなってしまうことを意味し、健全ではありません。

これを解決する方法

警告: これは微妙であり、間違っている可能性があります

トレイトゴールとは異なり、normalizes_toproductive1 でなければなりません。 normalizes_to ゴールは、射影が rigid な型コンストラクタに正規化された時点で productive になります。 したがって、<() as Trait>::AssocVec<<() as Trait>::Assoc> に正規化される場合は productive になります。

normalizes_to ゴールには 2 種類のネストしたゴールがあります。 射影を実際に正規化するために必要なネストした要件と、正規化された射影と 期待される型との等価性です。productive でなければならないのは等価性だけです。 証明木のある分岐は、それが有限であるか、エイリアスが rigid な型コンストラクタに解決される normalizes_to を少なくとも 1 つ含む場合に productive です。

あるいは、単純に normalizes_to の equate 分岐を常に帰納的として扱うこともできます。 どのような循環も無限型をもたらすはずですが、無限型はいずれにせよサポートされておらず、 codegen のために深く正規化する際にオーバーフローを引き起こすだけです。

実験と例: https://hackmd.io/-8p0AHnzSq2VAE6HE_wX-w?view

要約の別の試み。

  • projection eq では、rhs を制約することで進捗を得なければならない
  • 循環が許容されるのは、等置中に正規化後の lhs 上に少なくとも一度 rigid ty がある場合だけである
  • normalizes_to の再帰的な eq 呼び出しの外側にある循環は常に問題ない

  1. 関連: https://coq.inria.fr/refman/language/core/coinductive.html#top-level-definitions-of-corecursive-functions

新しいトレイトソルバーにおけるキャッシュ

トレイトソルバーの結果をキャッシュすることは、パフォーマンスのために必要です。 それが健全であることを確認しなければなりません。キャッシュは SearchGraph によって処理されます。

グローバルキャッシュ

中核において、キャッシュはかなり単純です。ゴールを評価するとき、 それがグローバルキャッシュにあるかを確認します。ある場合は、そのエントリを再利用します。ない場合は、 ゴールを計算し、その結果をキャッシュに格納します。

インクリメンタルコンパイルを扱うため、ゴールの計算は DepGraph::with_anon_task の内部で行われます。これは、この計算内で使用されたすべてのクエリに依存する新しい DepNode を作成します。 その後、グローバルキャッシュにアクセスするときに、この DepNode を読み取り、使用されたすべてのクエリへの依存関係エッジを手動で追加します: ソース

オーバーフローへの対処

新しいトレイトソルバーで再帰制限に達することは致命的ではなく、代わりに単に 曖昧性を返します: ソース。そのため、再帰制限に達したかどうかは、 コンパイル失敗を引き起こすことなく結果を変化させる可能性があります。つまり、 キャッシュ結果にアクセスするときには、残りの利用可能な深さを考慮しなければなりません。

これは、キャッシュエントリにより多くの情報を格納することで行います。評価が 再帰制限に達しなかったゴールについては、単にその到達深さを格納します: ソース。 これらの結果は、現在の available_depth がその reached_depth より大きい限り、自由に使用できます: ソース。その後、 グローバルキャッシュエントリを使用したかどうかが観測可能にならないように、 現在のゴールの到達深さを更新します: ソース

再帰制限に達するゴールについては、現在のところ、利用可能な深さがエントリの深さと 正確に一致する 場合にのみキャッシュ結果を使用します。そのため、各ゴールのキャッシュエントリには、 残りの深さごとに別個の結果が含まれます: ソース1

サイクルの処理

トレイトソルバーはサイクルをサポートしなければなりません。これらのサイクルは、 参加するゴールに応じて帰納的または共帰納的です。詳細については chapter on coinduction を参照してください。サイクルヘッドとサイクルルートを区別します。スタックエントリは、 再帰的にアクセスされた場合にサイクルヘッドです。ルート は、いずれかのサイクルに 関与しているスタック上で最も深いゴールです。次の依存関係ツリーでは、AB はどちらも サイクルヘッドですが、ルートは A だけです。

graph TB
    A --> B
    B --> C
    C --> B
    C --> A

サイクル参加者の結果は、まだスタック上にあるゴールの結果に依存します。 しかし、現在その結果を計算しているところなので、その結果はまだ不明です。これは、 不動点に達するまでサイクルヘッドを評価することで処理されます。最初のイテレーションでは、 サイクルが共帰納的かどうかに応じて、制約なしの成功またはオーバーフローのいずれかを 返します: ソース。サイクルのヘッドを評価した後、 その provisional_result がこのイテレーションの結果と等しいかどうかを確認します。等しい場合、 このサイクルの評価は完了しており、その結果を返します。等しくない場合は、暫定結果を更新し、 ゴールを再評価します: ソース。最初のイテレーションの後は、 サイクルが共帰納的か帰納的かは問題ではありません。常に暫定結果を使用します。

サイクルルートのみをキャッシュする

サイクルルートの評価が完了するまで、どのサイクル参加者の結果もグローバルキャッシュに移動することはできません。 しかし、サイクルを完全に評価した後であっても、ルート自体を除く すべての参加者の結果を破棄せざるを得ません。

すべてのグローバルキャッシュエントリのクエリ依存関係を追跡します。これにより、 サイクル参加者のキャッシュは自明ではなくなります。サイクルルートの DepNode を単純に再利用することはできません。2 サイクル A -> B -> A がある場合、ADepNode には A -> B からの依存関係が含まれます。 このエントリを B に再利用すると、ソースが変更された場合に壊れる可能性があります。B -> A の エッジはもはや存在しないかもしれず、A は完全に削除されているかもしれません。これは容易に ICE につながる可能性があります。

しかし、サイクルの結果はどのゴールがルートであるかによって変化する可能性があるため、さらに悪いです: 。これにより、キャッシュをさらに弱める必要があります。 そのルートを含むサイクルの参加者であったスタックエントリが存在する場合、 サイクルルートのキャッシュエントリを使用してはなりません。これは、指定されたルートのすべてのサイクル参加者を そのグローバルキャッシュエントリに格納し、それがスタックの要素を含まないことを確認することで行います: ソース

暫定キャッシュ

TODO: これを書く :3

  • 暫定結果のスタック依存性
  • エッジケース: 暫定キャッシュが挙動に影響する

  1. これは過度に制限的です。すべてのネストされたゴールが、ある利用可能な深さ n で オーバーフロー応答を返す場合、それらの結果は n より小さい任意の深さで同じであるべきです。 この最適化は将来実装できます。

  2. 関連する Zulip thread の要約

証明木

トレイトソルバー自体は、ゴールが成立するかどうかと必要な制約のみを返しますが、その証明を試みる過程で何が起きたかも知りたい場合があります。トレイトソルバーは通常、コンパイラーの他の部分からはブラックボックスとして扱われるべきですが、その内部を完全に無視することはできないため、そのためのインターフェイスとして「証明木」を提供しています。これを使用するには、ProofTreeVisitor トレイトを実装します。例については既存の実装を参照してください。特に重要な用途として、coherence エラーに対するクレート間の曖昧性の原因トレイトソルバーのエラー改善、および クロージャーシグネチャの先行推論の計算があります。

証明木の計算

トレイトソルバーは Canonicalization を使用し、ネストされた各ゴールに対して完全に独立した InferCtxt を使用します。診断と rustdoc の auto-traits はどちらも、「ネストされたゴールの中を見る」処理を正しく扱う必要があります。Vec<Vec<?x>>: Debug のようなゴールが与えられた場合、これを exists<T0> Vec<Vec<T0>>: Debug に正規化し、そのゴールを Vec<Vec<?0>>: Debug としてインスタンス化し、ネストされたゴール Vec<?0>: Debug を取得し、これを正規化して exists<T0> Vec<T0>: Debug を取得し、これを Vec<?0>: Debug としてインスタンス化します。その結果、曖昧な ?0: Debug ゴールがネストされます。

証明木は、ProofTreeBuilder を検索グラフに渡すことで計算します。これは、トレイトソルバーの評価ステップを木に変換します。推論変数またはプレースホルダーを使用する任意のデータを保存する際、そのデータは、この計算中に作成されたすべての未制約の推論変数のリストとともに正規化されます。この CanonicalState はその後、証明木をたどる際に親の推論コンテキストでインスタンス化され、推論変数のリストを使用して、この評価中に作成されたすべての正規化された値を接続します。

ソルバーのデバッグ

以前は、ソルバー実装のデバッグにも証明木を使おうとしていました。これには、プログラムから解析する場合とは異なる設計要件があります。トレイトソルバーをデバッグする推奨方法は、tracing を使用することです。トレイトソルバーは、その一般的な「形状」には debug トレーシングレベルのみを使用し、追加の詳細には trace を使用します。したがって、RUSTC_LOG=rustc_next_trait_solver=debug によって大まかな概要が得られ、より正確な情報が必要な場合は RUSTC_LOG=rustc_next_trait_solver=trace を使用できます。

新しいソルバーにおける opaque 型

新しいソルバーでの opaque 型 の扱い方は、古い実装とは異なります。 これは、新しいソルバーにおける挙動についての自己完結した説明であるべきです。

非定義使用 vs 定義使用

非定義使用と定義使用の区別は、推論の挙動を決定し、抽象化の厳密さを制御します。定義使用は背後にある hidden type を明らかにしますが、非定義使用は剛性を強制し、その型を抽象的なプレースホルダーとして扱います。

非定義使用

この概念により、opaque 型は剛性を持つようになります。背後にある型を未知として扱うことで、ソルバーは抽象化を維持します。

#![allow(unused)]
fn main() {
fn foo() -> impl Copy {
    let x: u32 = 56;
    x 
}

fn bar() {
    let x = foo();
    // 'foo' が u32 を返すことは分かっていても、ソルバーはここで
    // 剛性を強制し、これは非定義使用です。
}
}

定義使用

ここで opaque 型が実際に定義されます。定義使用は、抽象化の背後にある具象型が正確に何であるかをコンパイラに伝えます。

#![allow(unused)]
fn main() {
fn foo() -> impl Copy {
    let x: u32 = 56;
    // 戻り値 x は hidden type を u32 として定義し、これは定義使用です。
    x
}
}

opaque は alias 型である

opaque 型は、可能な限り、他の alias、特に associated type と同じように扱われます。 挙動の相違はできるだけ少なくあるべきです。

これは望ましいことです。なぜなら、opaque 型は他の alias 型と非常によく似ており、hidden type へ正規化でき、完全性に関しても同じ要件を持つためです。 このように扱うことで、コードを共有して型システムの複雑さも低減できます。 opaque 型を個別に扱わなければならない場合、より複雑なルールや新しい種類の相互作用が生じます。 implicit-negative モードでは opaque 型を他の alias と同様に扱う必要があるため、モード間に大きな差異があることも複雑さを増します。

未解決の疑問: ここに代替アプローチはあるか。たとえば、opaque 型をより剛性のある型のように扱い、インスタンス化できる場所をより限定する方法はあるか。coherence の間は、それでも通常の alias である必要がある

opaque に対する normalizes-to

source

normalizes-to は、新しいソルバーにおいて alias の 1 ステップ正規化の挙動を定義するために使用されます。<<T as IdInner>::Assoc as IdOuter>::Assoc はまず <T as IdInner>::Assoc に正規化され、その後 T に正規化されます。これは、正規化される AliasTy と期待される Term の両方を受け取ります。実際の正規化に normalizes-to を使用するには、期待される項を単に未制約の推論変数にできます。

定義スコープ内および implicit-negative coherence モード内の opaque 型については、これは常に 2 ステップで行われます。定義スコープ外では、opaque に対する normalizes-to は常に Err(NoSolution) を返します。

まず、期待される型を hidden type として割り当てようとします。

implicit-negative coherence モードでは、これは現在、opaque 型ストレージと相互作用することなく、常に曖昧性になります。代わりに、すべての opaque 型を「定義」できるようにし、最後にそれらの推論された型を破棄することもできます。この場合、coherence 中に opaque 型が複数回使用されたときの挙動が変わります: example

定義スコープ内では、まず opaque の型引数と const 引数がすべてプレースホルダーであるかどうかを確認します: source。このチェックが曖昧であれば曖昧性を返し、失敗した場合は Err(NoSolution) を返します。このチェックは、borrowck の最後でのみ確認される領域を無視します。成功した場合は続行します。

次に、opaque のジェネリック引数を、opaque 型ストレージにすでに存在する任意の opaque 型の引数と 意味論的に 単一化できるかどうかを確認します。できる場合は、以前に格納された型を、この normalizes-to 呼び出しの期待される型と単一化します: source1

できない場合は、期待される型を opaque 型ストレージに挿入します: source2。 最後に、opaque の item bounds が期待される型に対して成り立つかどうかを確認します: source

正規化可能な alias の alias-bounds を使用する

https://github.com/rust-lang/trait-system-refactor-initiative/issues/77

正規化可能な alias に対して AliasBound 候補を使用することは、一般にはできません。なぜなら、associated type は ParamEnv 候補を介して正規化した結果の型よりも強い境界を持つことがあるためです。

これらの候補は、正確な正規化戦略をユーザーから見えるものにしてしまいます。そうでなければ、正規化を積極的に行うかどうかは、ほとんど観測できません。どこで正規化するかは、古いソルバーのサポートを削除した後に変更したい可能性が高いため、それは望ましくありません。

opaque 型はどこでも定義できる

opaque 型は、その定義スコープ内であれば、単に型を関連付ける場合でも trait ソルバー内でも、どこでも定義できます。これにより、順序依存性と不完全性が取り除かれます。これがない場合、ゴールの結果は微妙な理由によって異なることがあります。たとえば、opaque の最初の定義使用より前に、その opaque を使ってゴールを評価しようとするかどうかなどです。

定義スコープ内の higher ranked opaque 型

これらはサポートされておらず、現時点で定義しようとすると常にエラーになるべきです。

FIXME: opaque 型ストレージで opaque 型を検索すると、今では領域を単一化できるため、opaque 型がプレースホルダーを参照していないことを積極的に確認する必要があります。そうしないと、プレースホルダーが漏出してしまいます。

member constraints

member constraints の扱いは新しいソルバーでも変わりません。それについては 関連する既存の章 を参照してください。

opaque 型に対するメソッド呼び出し

FIXME: 定義スコープ内で、まだ未制約の opaque 型に対してメソッドを呼び出すことを引き続きサポートする必要があります。これをどのように行うのが最善かは不明です。

#![allow(unused)]
fn main() {
use std::future::Future;
use futures::FutureExt;

fn go(i: usize) -> impl Future<Output = ()> + Send + 'static {
    async move {
        if i != 0 {
            // これは定義スコープ内で `impl Future<Output = ()>` を返します。
            // この時点では、その opaque の具象型は分かっていません。
            // 現在は opaque を既知の型として扱い成功しますが、
            // 「健全に実装するのが最も容易」という観点からは、
            // これが曖昧であるとよいでしょう。
            go(i - 1).boxed().await;
        }
    }
}
}

  1. FIXME: args がプレースホルダーであることを要求し、領域は常に推論変数であることを考えると、理想的にはこれは一意の候補のみをもたらすべきです

  2. FIXME: なぜ期待される型が剛性を持つかどうかを確認するのでしょうか。

重要な変更点と癖

重要な変更点と癖

以下の項目の一部はすでに個別に言及されていますが、このページでは古いトレイトシステム実装からの主な変更点を追跡します。また、ソルバーが理想化された実装から大きく逸脱しているいくつかの点についても言及します。このドキュメントは単純化しており、エッジケースを無視しています。各文に暗黙の「ほとんどの場合」を加えて読むことを推奨します。

正準化

新しいソルバーは、ネストされたゴールを評価するときに正準化を使用します。複数の候補が存在する可能性がある場合、各候補は即座に正準化されます。その後、それらの正準応答のマージを試みます。これは、トレイトシステムの内部で正準化を使用しない古い実装とは異なります。

これは、両方のソルバーの設計にいくつかの大きな影響を与えます。候補の制約を退避するために正準化を使用しない場合、候補選択では各候補の制約を破棄する必要があり、選択後に候補を再評価することでのみ制約を適用します: source。正準化を使用しない場合、ゴールの評価から得られる推論制約をキャッシュすることもできません。そのため、古い実装には evaluatefulfill という 2 つのシステムがあります。Evaluation はキャッシュされ、推論制約を適用せず、候補選択時に使用されます。Fulfillment は推論制約とリージョン制約を適用し、キャッシュされず、推論制約を適用します。

正準化を使用することで、新しい実装は evaluationfulfillment をマージでき、複雑さや挙動の微妙な違いを避けられます。これによりキャッシュが大幅に単純化され、追跡されていない情報に誤って依存することを防げます。選択後に候補を再評価することを避けられ、複数の候補の応答をマージできるようになります。しかし、評価中にゴールを正準化するため、新しい実装ではトレイト解決中にサイクルに遭遇したときに不動点アルゴリズムを使用する必要があります: source

遅延されたエイリアス等価性

新しい実装は、エイリアスを関連付けるときに AliasRelate ゴールを発行しますが、古い実装は代わりにエイリアスを構造的に関連付けます。これにより、新しいソルバーは関連付けられたエイリアスを正規化できるようになるまで等価性を停止できます。

古いソルバーの挙動は不完全で、曖昧なエイリアスを推論変数に置き換える即時正規化に依存しています。これは束縛変数を含むエイリアスでは不可能であるため、古い実装はバインダー内のエイリアスを正しく処理しません。例: #102048。詳細については、正規化の章を参照してください。

ネストされたゴールの即時評価

新しい実装は、ネストされたゴールを呼び出し元に返すのではなく即座に処理します。古い実装はその両方を行います。評価ではネストされたゴールは即座に処理されますが、フルフィルメントでは単に後で処理するために返されます

新しい実装は候補選択のためにネストされたゴールを即座に処理できる必要があるため、常にそうすることで複雑さが軽減されます。また、将来的により多くの候補をマージできるようになる可能性もあります。

ネストされたゴールは不動点に達するまで評価される

新しい実装は、常に不動点に達するまでループ内でゴールを評価します。古い実装は fulfillment ではこれを行いますが、evaluation では行いません。常にそうすることで推論が強化され、トレイトソルバーの順序依存性が低減されます。trait-system-refactor-initiative#102 を参照してください。

証明木と診断情報の提供

新しい実装は診断情報を直接追跡せず、代わりに関連情報を遅延計算するために使用される証明木を提供します。これはまだ完全には具体化されておらず、ややハック的です。目的は、パフォーマンスを向上させるために正常系でこの情報を追跡することを避け、挙動のために診断データへ誤って依存することを避けることです。

新しい実装の主な癖

env 候補が存在する場合に impl を隠す

ある Trait ゴールを証明するための ParamEnv または AliasBound 候補が少なくとも 1 つ存在する場合、TraitProjection の両方のゴールについて、すべての impl 候補を破棄します: source。これにより、ユーザーが where 境界によって完全に覆われている impl を使用することを防ぎ、古い実装の挙動に一致させ、いくつかの奇妙なエラーを回避します。例: trait-system-refactor-initiative#76

NormalizesTo ゴールは関数である

正規化の章を参照してください。正規化に影響を与えないように、NormalizesTo ゴールを計算する前に、期待される項を制約のない推論変数に置き換えます。これは、NormalizesTo ゴールが他のすべてのゴール種別とは多少異なる方法で処理され、追加のソルバーサポートが必要であることを意味します。特に、その曖昧なネストされたゴールは呼び出し元に返され、呼び出し元がそれらを評価します。詳細については #122687 を参照してください。

トレイトソルバーを rust-analyzer と共有する

rust-analyzer はコンパイラフロントエンドと見なすことができます。つまり、コード生成の前に実行される rustc の各部分と同様のタスク、たとえばパース、字句解析、AST の構築と lowering、HIR の lowering、さらには限定的な MIR 構築と const 評価を行います。

しかし、rust-analyzer は主に言語サーバーであるため、そのアーキテクチャは rustc のものとは いくつかの重要な点で異なります。 こうした違いにもかかわらず、その責務のかなりの部分、特に型推論とトレイト解決は コンパイラと重なっています。

重複を避け、2 つの実装間の一貫性を維持するために、rust-analyzer は rustc の複数の crate を再利用し、可能な限り共有された抽象化に依存しています。

共有 crate

現在、rust-analyzer はコンパイラの複数の rustc_* crate に依存しています。

  • rustc_abi
  • rustc_ast_ir
  • rustc_index
  • rustc_lexer
  • rustc_next_trait_solver
  • rustc_parse_format
  • rustc_pattern_analysis
  • rustc_type_ir

これらの crate は、コンパイラの通常の配布プロセスの一部として crates.io に公開されていないため、 rust-analyzer は独自の公開パイプラインを維持しています。 rustc-auto-publish スクリプトを使用して、これらの crate を ra-ap-rustc_* というプレフィックス付きで crates.io に公開しています (例: https://crates.io/crates/ra-ap-rustc_next_trait_solver)。 その後、rust-analyzer は自身のビルドでこれらの再公開された crate に依存します。

トレイト解決に関しては、主要な共有 crate は rustc_type_irrustc_next_trait_solver です。これらは、両方のコンパイラフロントエンドで使用される 中核的な IR とソルバーロジックを提供します。

抽象化レイヤー

rust-analyzer は言語サーバーであるため、頻繁に変更されるソースコードや、 部分的に不正または不完全なソースコードを扱う必要があります。 そのため、特にソースコードと HIR の間のレイヤー、たとえば Ty とそれを支えるインターナーにおいて、 rustc とはかなり異なるインフラストラクチャが必要になります。

こうした違いを橋渡しするために、コンパイラは rustc と rust-analyzer で共有される 抽象化レイヤーとして rustc_type_ir を提供しています。 この crate は、型、述語、およびトレイトソルバーに必要なコンテキストを表現するために使用される 基本的なインターフェイスを定義します。 rustc と rust-analyzer はどちらも、それぞれ独自の具体的な型表現に対してこれらのトレイトを実装し、 rustc_next_trait_solver はこれらの抽象化に対してジェネリックに書かれています。

これらのインターフェイスに加えて、rustc_type_ir には、抽象化レイヤーの上に構築された いくつかの重要なコンポーネントも含まれています。たとえば、elaboration ロジックや、ソルバーが使用する 探索グラフ機構などです。

設計コンセプト

rustc_next_trait_solver は、rustc_type_ir で定義された抽象インターフェイスのみに依存することを 意図しています。 これをサポートするため、rustc_type_ir の型システムトレイトは、ソルバーが必要とするすべての インターフェイスを公開しなければなりません。たとえば、新しい推論型変数の作成rustcrust-analyzer)です。 コンパイラ固有の表現を必要としない項目については、rustc_type_ir がこれらのトレイトで パラメータ化された struct または enum として直接定義します。たとえば、TraitRef です。

以下は、rustc_type_ir crate の注目すべき項目の一部です。

trait Interner

この設計における中心的なトレイトは Interner であり、rustc と rust-analyzer の両方について、 すべての実装固有の詳細を規定します。 その重要な責務には、次のようなものがあります。

  • 関連型を通じて、実装で使用される具体的な型を指定すること。これらは、各コンパイラフロントエンドが 共有 IR をインスタンス化する方法の中核を形成します。
  • ソルバーが必要とするコンテキストを提供すること(例: lang item の問い合わせ、 あるトレイトに対するすべての blanket impl の列挙)。
  • そして、フォーマットとトレースのために IrPrint を実装しなければならないこと。
    実際には、これらの IrPrint impl は単に rustc または rust-analyzer 内の既存のフォーマットロジックへ ルーティングします。

rustc では、TyCtxtInterner を実装しています。これは rustc のクエリメソッドを公開し、 必要な Interner トレイトメソッドはそれらのクエリを呼び出すことで実装されています。 rust-analyzer では、実装する型は DbInterner という名前です(多くのインターン処理を salsa データベースを通じて行うため)。また、そのメソッドの大半は rustc のクエリではなく salsa のクエリによって 支えられています。

mod inherent

rustc_type_ir におけるもう 1 つの注目すべき項目は inherent モジュールです。 このモジュールは、TyGenericArg のようなコンパイラ固有の型に存在するメソッドに対応する、 トレイトとして表現された inherent method の前方定義を提供します。
これらの定義により、ジェネリックな crate(rustc_next_trait_solver など)は、rustc と rust-analyzer で 異なる形で実装されるメソッドを呼び出せるようになります。

ジェネリックな crate 内のコードでは、これらの定義を次のようにインポートする必要があります。

#![allow(unused)]
fn main() {
use inherent::*;
}

これらの前方定義は、具体的な実装自体の内部では決して使用してはなりませんmod inherent のトレイトを実装する crate は、それらの具象型が名前で参照可能になった時点で、 その具象型上の実際の inherent method を呼び出すべきです。

これらのトレイトに対する rustc の実装は、 rustc_middle::ty::inherent モジュールで確認できます。 rust-analyzer の場合、対応する実装は hir_ty::next_solver::region など、 hir_ty::next_solver 配下の複数のモジュールにあります。

trait InferCtxtLiketrait SolverDelegate

これら 2 つのトレイトは、rustc における InferCtxt の役割に対応します。

InferCtxtLike は、coherence 制約(orphan rule)のために rustc_infer で定義されなければなりません。 その結果、rustc_trait_selection に存在する機能を提供することはできません。 代わりに、トレイト解決ロジックに依存する振る舞いは、別のトレイトである SolverDelegate に抽象化されます。 rustc におけるその実装者は、rustc_trait_selection 内の InferCtxt 上の単なる newtype struct です。

(rust-analyzer でも、主に rustc の構造を反映するために、独自の InferCtxt 上の newtype ラッパーに対して実装されています。ただし、すべてのソルバー関連ロジックは すでに hir-ty crate 内に存在するため、これは厳密には必要ではありません。)

長期的には、理想的な設計は、現在 SolverDelegate を通じて表現されているすべてのロジックを rustc_next_trait_solver に移し、必要な中核操作を InferCtxtLike に直接追加することです。 これにより、ソルバーの振る舞いのより多くを、共有ソルバー crate の内部に完全に置けるようになります。

rustc_type_ir::search_graph::{Cx, Delegate}

抽象化トレイト CxDelegate は、 すでに rustc_next_trait_solver 自体の内部で実装されています。 したがって、共有 crate のユーザーである rustc と rust-analyzer のどちらも、独自の実装を提供する必要はありません。 これらのトレイトは主に、完全なトレイトソルバーから独立して探索グラフのファジングを サポートするために存在します。 このインフラストラクチャは、外部のファジングプロジェクトで使用されています: https://github.com/lcnr/search_graph_fuzz.

rust-analyzer のサポートに関する長期計画

一般に、これらの共有クレートでは rustc と同程度に rust-analyzer をサポートすることを目指しています。ただし、 そうすることで rustc のパフォーマンスや保守性が大きく損なわれないことが前提です。 (例: #145377#146111#146182、および #147723

nightly 専用機能を必要とする共有クレートは、そのようなコードを nightly 機能フラグの背後で ガードしなければなりません。rust-analyzer は stable ツールチェーンでビルドされるためです。

今後は、より多くの共有ロジックを rustc_type_ir に取り込む予定です。 rustc と rust-analyzer の間には、ObligationCtxtrustcrust-analyzer)や型強制ロジック (rustcrust-analyzer)など、まだ重複した実装があり、時間をかけて統一したいと考えています。

CoerceUnsized

CoerceUnsized は主にデータコンテナに関係します。構造体 (通常はスマートポインター)が CoerceUnsized を実装している場合、それは その構造体が指すデータが unsize されることを意味します。

CoerceUnsized を実装している型には、以下のようなものがあります。

  • &T
  • Arc<T>
  • Box<T>

このトレイトは、(最終的には)ユーザーが作成したスマートポインターによって 実装されることが意図されており、ある型が CoerceUnsized を実装できる条件に 関するルールは、このトレイトのドキュメントで説明されています。

Unsize

対照的に、Unsize トレイトは、unsize できる実際の型に関係します。

これはユーザーが実装することを意図したものではありません。なぜなら Unsize は、 型を どのように unsize するかをコンパイラー(すなわち codegen)に指示する ものではなく、単に unsize できるかどうかを示すだけだからです。これは、型がどのように 表現され、unsize されるかを理解していなければならない codegen とかなり密接に 組み合わされています。

プリミティブな unsize 実装

組み込み実装は以下に対して提供されています。

  • T: Trait の場合の T -> dyn Trait + 'a(かつ T: Sized + 'a であり、Trait が dyn 互換1である場合)。
  • [T; N] -> [T]

構造的実装

構造的と考えられる Unsize の実装が 1 つあります。

  • TailField<Pi, .., Pj>: Unsize<Ui, .. Uj> が成り立つ場合の Struct<.., Pi, .., Pj, ..>: Unsize<Struct<.., Ui, .., Uj, ..>>。これにより、 構造体のテールフィールドがジェネリックパラメーター Pi, .., Pj に言及する 唯一のフィールドである場合、そのテールフィールドを unsize できます (これらのパラメーターは連続している必要はありません)。

構造体の unsize に関するルールは少し複雑です。というのも、それらは 複数のパラメーターの変更(必ずしも unsize とは限りません)を許可する場合があり、 構造体のテールフィールドという観点で述べるのが最も適しているからです。

(タプルの unsize は以前、feature gate unsized_tuple_coercion の背後で 実装されていましたが、その実装は #137728 によって削除されました。)

アップキャスト実装

内部的には、2 つのものが「アップキャスト」と呼ばれています。

  1. 真のアップキャスト dyn SubTrait -> dyn SuperTrait(これは、以下のように 自動トレイトの削除とライフタイムの調整も許可します)。
  2. dyn trait の自動トレイトを削除し、ライフタイムを調整すること (principal2 は変更しない): dyn Trait + AutoTraits... + 'a -> dyn Trait + NewAutoTraits... + 'b ここで AutoTraitsNewAutoTraits であり、'a: 'b です。

これらは異なる操作のように見えるかもしれません。なぜなら (1.) は dyn trait の vtable の調整を含む一方で、(2.) は no-op だからです。しかし、型システムにとっては、 これらはほぼ同じコードで処理されます。

この Unsize の組み込み実装は、特に関連型の複雑さをサポートするために 再設計された後、 最も入り組んだものになっています。

具体的には、アップキャストアルゴリズムには次の処理が含まれます。ソース dyn trait の principal の各 supertrait(それ自身を含む)について…

  1. super trait ref をターゲットの principal と単一化します(真の supertrait にのみ アップキャストし、決して impl 経由ではアップキャストしないことを確認します)。
  2. ターゲット内のすべての自動トレイトについて、それがソースに存在することを確認します (これにより自動トレイトを削除できますが、新しいものを得ることはできません)。
  3. ターゲット内のすべての射影について、それがソース内の単一の射影と単一化されることを 確認します(trait Sub: Sup<.., A = i32> + Sup<.., A = u32> が与えられている場合のように、 複数存在する可能性があるためです)。

具体的には、(3.) は、推論を不必要に導くために射影境界を選択することを防ぎますが、 曖昧でない場合には推論を導くことがあります。


  1. 以前は「object safe」として知られていました。

  2. principal は、dyn Trait の 1 つの非自動トレイトです。

Trait 境界と Projection 境界を分離すること

T: Foo<AssocA = u32, AssocB = i32> という where 境界が与えられた場合、現在はこれを Trait(Foo<T>) と、別々の Projection(<T as Foo>::AssocA, u32) および Projection(<T as Foo>::AssocB, i32) 境界へ lower しています。 代わりに、なぜこれを単一の Trait(Foo[T], [AssocA = u32, AssocB = u32] 境界として表現しないのでしょうか?

Projection 境界を証明する方法は、対応する Trait 境界を証明することに直接依存しています: 旧ソルバー 新ソルバー

trait が実装されているかどうかをチェックし、その関連型(を計算する方法)を返す単一の実装だけを持つほうが理にかなっているように感じられます。

残念ながら、これはかなり困難です。対応する trait 境界とは異なる候補を正規化に使う可能性があるためです。 alias-bound と where-bound および グローバル where 境界と impl を参照してください。

そうできない理由には、他にも微妙なものがいくつかあります。 最も愚かなものは rigid alias に関するものです。 それらを正規化しようとしても、trait 境界を証明する際の lifetime 制約は一切考慮されません。 これは binder に関する仮定が不足しているために必要です - https://github.com/rust-lang/trait-system-refactor-initiative/issues/177 - そして長期的には修正されるべきです。

別の問題として、現時点では、 Trait ゴールや shadow された Projection 候補で関連型の type_of を取得すると、RPITIT でクエリサイクルが発生する可能性があります。 https://github.com/rust-lang/trait-system-refactor-initiative/issues/185 を参照してください。

また、一部の builtin impl の候補間にはわずかな違いがあります。これらはどれも一般的には望ましくないように見え、ここで統一されたアプローチがあれば修正されるバグだと私は考えています。

最後に、この分割がないと where-clause の lowering がより面倒になります。 現在のシステムでは、重複した where-clause があっても問題にはならず、super trait 境界を展開するときに簡単に発生し得ます。 その場合、すべての関連型制約をマージするようにしなければなりません。例:

#![allow(unused)]
fn main() {
trait Super {
    type A;
    type B;
}

trait Trait: Super<A = i32> {}
// Trait<B = u32> をどのように展開するか
}

あるいは、さらに悪い例です

#![allow(unused)]
fn main() {
trait Super<'a> {
    type A;
    type B;
}

trait Trait<'a>: Super<'a, A = i32> {}
// どのように展開するか
// T: Trait<'a> + for<'b> Super<'b, B = u32>
}

型パラメータとライフタイムパラメータの変性

変性に関するより一般的な背景については、background 付録を参照してください。

型チェックの際には、型パラメータとライフタイム パラメータの変性を推論しなければなりません。このアルゴリズムは、PLDI’11 で 発表され、Altidor らによって書かれた論文 “Taming the Wildcards: Combining Definition- and Use-Site Variance” の セクション 4 から採用しており、以降は The Paper と呼びます。

この推論は、コード内での型の使用を明示的に考慮しないように 設計されています。型 X に定義された型パラメータの 変性を決定するには、型 X の定義と、 それが参照する任意の型の定義のみを考慮します。

struct や enum のような データ型 に見られる型パラメータについてのみ 変性を推論します。このような場合、変性が何を意味するかについては、 かなり単純な説明があります。型パラメータまたはライフタイム パラメータの変性は、AB の関係 (それぞれ 'a'b の関係)に基づいて、T<A>T<B> の サブタイプであるかどうか(それぞれ T<'a>T<'b>)を定義します。

トレイト、関数、または impl に見られる型パラメータについては、変性を 推論しません。トレイトパラメータにおける変性には確かに意味があり得ます (かつてはそれを計算していました)が、実際には意味がかなり微妙で、 実用上それほど有用ではないため、削除しました。詳細については addendum を参照してください。一方、関数/impl パラメータにおける変性は 意味をなしません。これらのパラメータはインスタンス化された後に忘れられ、 型やコンパイル済みの副産物には永続しないからです。

表記

この章全体で The Paper の表記を使用します:

  • +共変性 です。
  • -反変性 です。
  • *双変性 です。
  • o不変性 です。

アルゴリズム

基本的な考え方は非常に単純です。定義された型を反復処理し、 型パラメータ X の各使用について、X の変性がその使用箇所の 変性に対して有効でなければならないことを示す制約を 蓄積します。その後、すべての制約が満たされるまで、X の変性を 反復的に洗練します。解は常に存在します。なぜなら、極限では すべての型パラメータを不変と宣言でき、その場合すべての制約が 満たされるからです。

簡単な例として、次を考えてみましょう:

enum Option<A> { Some(A), None }
enum OptionalFn<B> { Some(|B|), None }
enum OptionalMap<C> { Some(|C| -> C), None }

ここでは、次の制約を生成します:

1. V(A) <= +
2. V(B) <= -
3. V(C) <= +
4. V(C) <= -

これらは、(1) A の変性が最大でも共変でなければならないこと、 (2) B の変性が最大でも反変でなければならないこと、そして (3, 4) C の 変性が最大でも共変かつ反変でなければならないことを示しています。 これらの結果はすべて、次のように定義される変性束に基づいています:

   *      Top (bivariant)
-     +
   o      Bottom (invariant)

この束に基づくと、解 V(A)=+, V(B)=-, V(C)=o が 最適解です。なお、すべての変数を不変と宣言するだけの 単純な解は常に存在します。

なぜ固定点反復が必要なのか疑問に思うかもしれません。その理由は、 使用箇所の変性が、それ自体、他の型パラメータの変性の 関数であり得るからです。完全に一般化すると、制約は次の形を取ります:

V(X) <= Term
Term := + | - | * | o | V(X) | Term x Term

ここで表記 V(X) は、型/リージョンパラメータ X の、 それを定義しているクラスに対する変性を示します。Term x Term は、 論文で定義されている「変性変換」を表します:

型変数 X の型式 E における変性が V2 であり、 クラス C の対応する型パラメータの定義箇所における変性が V1 である場合、型式 C<E> における X の変性は V3 = V1.xform(V2) です。

制約

where 句を持つ struct または enum がある場合:

struct Foo<T: Bar> { ... }

Bar に対する T の変性が、Foo に対する T の変性に影響するか 疑問に思うかもしれません。私はそうではないと主張します。理由は次のとおりです。 TBar に対して不変だが、Foo に対して共変であると仮定します。そして、 X <: Y であるとき、Foo<X>Foo<Y> にアップキャストされるとします。 しかし、X : Bar であっても、Y : Bar は成り立ちません。その場合、 アップキャストは不正になりますが、それは変性の失敗によるものではなく、 むしろターゲット型 Foo<Y> 自体が整形式ではないからです。基本的に、 変性を考慮する前に、関係するすべての型の整形式性を仮定できます。

依存グラフの管理

変性はクレート全体に対する推論であるため、注意しなければ、その依存グラフは 非常に混乱したものになり得ます。これを解決するため、2 つのクエリへ リファクタリングします:

  • crate_variances は、現在のクレート内のすべての項目について変性を計算します。
  • variances_of は、個別の読み取り対象に対する変性にアクセスします。これは crate_variances を要求し、関連するデータを抽出することで機能します。

variances_of の読み取りに限定すれば、コードはその特定の項目の推論に のみ依存することになります。

最終的に、この構成は red-green algorithm に依存しています。特に、 すべての変性クエリは、実質的に(crate_variances を通じて)クレート全体の すべての型定義に依存しますが、ほとんどの変更は変性推論の実際の結果に 変更をもたらさないため、variances_of クエリは再評価された後に green と見なされることになります。

補遺: トレイトにおける変性

上で述べたように、以前はトレイトにおける変性を許可していました。これは、 メソッドシグネチャにおけるトレイト型パラメータの出現に基づいて計算され、 トレイトオブジェクト内の vtable(およびトレイト境界内の「仮想」vtable や 辞書)の互換性を表すために使用されていました。1 つの複雑な点は、 関連型の変性がそれほど明らかではないことでした。関連型は射影して取り出され、 多種多様な用途に使われ得るため、いつ X<A>::Bar の変動を許可して 安全なのか(あるいは実際、それが何を意味するのかさえ)明確ではありません。 さらに(以下で扱うように)、関連型を持つ任意のトレイト上のすべての入力は 不変でなければならず、適用可能性が制限されていました。最後に、すべての トレイト型パラメータが変性を持つことを保証するために必要な注釈 (MarkerTrait, PhantomFn)は、わずかな利益の割に混乱を招き、 煩わしいものでした。

歴史的な参考のために、変性とトレイトマッチングをどのように解釈できるかを 示すテキストをいくらか保存しておきます。

変性とオブジェクト型

struct や enum と同様に、AB の関係に基づいて、 2 つのオブジェクト型 &Trait<A>&Trait<B> の間のサブタイピング 関係を決定できます。オブジェクト型では、Self 型パラメータを無視する点に 注意してください。これは未知であり、動的ディスパッチの性質により、 適切な Self 型を期待する関数を常に呼び出すことが保証されます。しかし、 他の型パラメータについては注意しなければなりません。そうしないと、 ある型を期待している関数を、別の型を提供して呼び出してしまう可能性があります。

私の意味するところを見るために、次のようなトレイトを考えてみましょう:

```rust
trait ConvertTo<A> {
    fn convertTo(&self) -> A;
}

直感的には、1つのオブジェクト O=&ConvertTo<Object> と、別の S=&ConvertTo<String> がある場合、String <: Object なので S <: O になります (Java 風の「string」と「object」型を想定しています。これは私がサブタイピングの例としてよく使うものです)。 実際のアルゴリズムは、(明示的な)型パラメーターを、それぞれの分散を考慮しながらペアごとに比較するものになります。 ここでは、型パラメーター A は共変です(戻り値の位置にのみ現れます)。したがって、String <: Object であることを要求します。

ただし、(暗黙の)Self 型パラメーターの束縛は考慮していないことに気づくでしょう。 実際、それは未知なので、それで問題ありません。 そのパラメーターを無視できる理由は、呼び出しが発生するまでその値を知る必要がないからです。 そしてその時点では、(あなたが言ったように)仮想ディスパッチの動的な性質により、実行されるコードは、メソッドを呼び出した特定のオブジェクトについて Self がたまたま束縛されているどの値に対しても正しいものになります。 したがって SelfA とは異なります。呼び出し元は、メソッド convertTo() の戻り値の型を知るために、A が既知であることを必要とするからです。 (余談ですが、Self がレシーバー位置以外に現れるメソッドをオブジェクト経由で呼び出せないようにする規則があります。)

トレイトの分散と vtable 解決

しかし、トレイトはオブジェクトと一緒に使われるだけではありません。 ある impl が特定のトレイト境界を満たすかどうかを判断する際にも使われます。 ここで状況を設定するために、次のような関数があると想像してください。

fn convertAll<A,T:ConvertTo<A>>(v: &[T]) { ... }

ここで、Object に対する ConvertTo の実装があると想像してください。

impl ConvertTo<i32> for Object { ... }

そして、文字列の配列に対して convertAll を呼び出したいとします。 さらに、何らかの理由で型パラメーター T の値として String を明示的に指定するとします。

let mut vector = vec!["string", ...];
convertAll::<i32, String>(vector);

これは合法でしょうか? 別の言い方をすると、Object 用の implString 型に適用できるでしょうか? 答えは yes ですが、その理由を見るには、何が起こるかを展開してみる必要があります。

  • convertAll は、ベクター内のエントリの1つへのポインターを作成し、それは &String 型になります

  • その後、オブジェクトとともに使用することを意図した convertTo() の impl を呼び出します。 これは fn(self: &Object) -> i32 という型を持ちます。

    &String <: &Object であるため、self&String 型の値を渡しても問題ありません。

OK、直感的にはこれを合法にしたいので、これを分散の話に戻し、正しい結果を計算しているかどうかを見てみましょう。 まず、「Object,i32 用の impl は、String,i32 用の impl が期待される場所で使用可能か?」という問いをどのように表現するかを考えなければなりません。

型クラスの辞書渡し実装を考えるとわかりやすいかもしれません。 その場合、convertAll() は impl を表す暗黙のパラメーターを受け取ります。 要するに、私たちは次の型の impl を持っています

V_O = ConvertTo<i32> for Object

そして関数プロトタイプは、次の型の impl を期待します。

V_S = ConvertTo<i32> for String

どの引数でもそうであるように、これは、与えられた値の型(V_O)が期待される型(V_S)のサブタイプであれば合法です。 では、V_O <: V_S でしょうか? 答えは、さまざまなパラメーターの分散に依存します。 この場合、Self パラメーターは反変であり、A は共変なので、次のことを意味します。

V_O <: V_S iff
    i32 <: i32
    String <: Object

これらの条件は満たされているので、問題ありません。

分散と関連型

関連型を持つトレイト、または少なくとも射影式を持つトレイトは、それらのすべての入力に関して不変でなければなりません。 これがなぜ理にかなっているかを見るために、トレイト参照に対するサブタイピングが何を意味するかを考えてみましょう。

<T as Trait> <: <U as Trait>

これは、T as Trait であることを知っているなら、U as Trait であることも知っている、という意味です。 さらに、それを辞書渡しスタイルとして考えるなら、<T as Trait> の辞書は、<U as Trait> の辞書が期待される場所で安全に使用できる、という意味です。

問題は、<T as Trait> から型を射影できる場合、T==U でない限り、<U as Trait> から射影される型との関係がまったく不明であることです(詳細は #21726 を参照してください)。 Trait を不変にすることで、これが真であることを保証します。

関連するもう1つの理由は、関連型を持つトレイトを不変にしなかった場合、射影が単一の結果を持つ関数ではなくなることです。 次を考えてみてください。

trait Identity { type Out; fn foo(&self); }
impl<T> Identity for T { type Out = T; ... }

ここで <&'static () as Identity>::Out がある場合、これは任意の 'a について &'a () として有効に導出できます。

<&'a () as Identity> <: <&'static () as Identity>
if &'static () < : &'a ()   -- Identity is contravariant in Self
if 'static : 'a             -- Subtyping rules for relations

一方、この変更により、<'static () as Identity>::Out は常に &'static () になります(これは別途 'a () にアップキャストされるかもしれません)。 これは #21750 の解決に役立ちました。

コヒーレンス

注記: これは @lcnr によるメモに基づいています

コヒーレンスチェックは、トレイト impl と固有 impl の両方が他と重複していることを検出するものです。 (リマインダー: 固有 implimpl MyStruct {} のような具象型の impl です)

重複するトレイト impl は常にエラーを生成しますが、 重複する固有 impl は、同じ名前のメソッドを持つ場合にのみエラーになります。

重複のチェックは 2 つの部分に分かれています。 まず overlap check があります。 これは、コンパイラが現在把握しているトレイト実装と固有実装の間の重複を見つけます。

しかし、コヒーレンスは、現在は未知であっても、他の impl が存在し得る場合にもエラーになります。 これは、後方互換性を保った形で上流クレートに追加される可能性がある impl と、 下流クレートの impl に影響します。 これは Orphan チェックと呼ばれます。

Overlap checks

Overlap check は、固有 impl とトレイト impl の両方に対して実行されます。 これは同じ重複チェックコードを使用しますが、実際には 2 つの別々の解析として行われます。 Overlap check は常に実装のペアを考慮し、それらを互いに比較します。

固有 impl ブロックに対する重複チェックは fn check_item(coherence/inherent_impls_overlap.rs 内)を通じて行われます。 そこでは、(少なくとも小さい n については)チェックが実際に impl 間で n^2 回の 比較を実行していることが非常に明確にわかります。

トレイトの場合、このチェックは現在、特殊化グラフの構築の一部として行われています。 これは、特殊化する impl がその親と重複することを扱うためですが、将来変更される可能性があります。

どちらの場合も、すべての impl のペアについて重複がチェックされます。

重複は部分的に許可されることがあります。

  1. マーカートレイトの場合
  2. 特殊化のもとで

通常は許可されません。

Overlap check にはさまざまなモードがあります(OverlapMode を参照)。 重要なのは、明示的な negative impl チェックと、暗黙的な negative impl チェックがあることです。 どちらも、重複が絶対に不可能であることを証明しようとします。

明示的な negative impl チェック

このチェックは impl_intersection_has_negative_obligation で行われます。

このチェックは negative trait implementation を見つけようとします。 例:

#![allow(unused)]
fn main() {
struct MyCustomErrorType;

// どちらも自分のクレート内
impl From<&str> for MyCustomErrorType {}
impl<E> From<E> for MyCustomErrorType where E: Error {}
}

この例では、次のようになります: MyCustomErrorType: From<&str>MyCustomErrorType: From<?E> が得られ、?E = &str となります。

したがって、これら 2 つの実装は重複します。 しかし、libstd は &str: !Error を提供しており、そのため &str: Error の positive implementation が決して存在しないことを保証します。したがって重複はありません。

この種の negative impl チェックでは、明示的な negative implementation が提供されていなければならないことに注意してください。 これは現在 stable ではありません。

暗黙的な negative impl チェック

このチェックは impl_intersection_has_impossible_obligation で行われます。 これは negative trait implementation に依存せず、stable です。

次のものがあるとします。

#![allow(unused)]
fn main() {
impl From<MyLocalType> for Box<dyn Error> {}  // 自分のクレート内
impl<E> From<E> for Box<dyn Error> where E: Error {} // std 内
}

これにより、Box<dyn Error>: From<MyLocalType>Box<dyn Error>: From<?E> が得られ、 ?E = MyLocalType となります。

自分のクレートには MyLocalType: Error はなく、下流クレートは MyLocalType(リモート型)に対して Error(リモートトレイト)を実装できません。 したがって、これら 2 つの impl は重複しません。 重要なのは、これは impl !Error for MyLocalType が存在しない場合でも機能するということです。

HIR の型検査

hir_analysis クレートには、「型収集」のソースと 関連する多くの機能が含まれています。 関数本体の検査は hir_typeck クレートで実装されています。 これらのクレートは、型推論トレイト解決 に大きく依存しています。

型収集

型「収集」とは、ユーザーが書いた構文上のものを表す HIR 内の型 (hir::Ty)を、コンパイラが使用する内部表現Ty<'tcx>)へ変換するプロセスです。 where 句や関数シグネチャのその他の部分についても、同様の変換を行います。

違いの感覚をつかむために、次の関数を考えてみましょう。

struct Foo { }
fn foo(x: Foo, y: self::Foo) { ... }
//        ^^^     ^^^^^^^^^

これら 2 つのパラメーター xy は、それぞれ同じ型を持ちます。しかし、それらは 異なる hir::Ty ノードを持ちます。これらのノードは異なる span を持ち、当然ながら パスのエンコード方法も多少異なります。しかし、いったん Ty<'tcx> ノードへ「収集」されると、 まったく同じ内部型として表現されます。

収集は、コンパイル対象のクレート内にあるさまざまな関数、トレイト、およびその他のアイテムに関する情報を計算するための クエリの束として定義されています。 これらの各クエリは 手続き間 の事柄を扱うことに注意してください。 たとえば、関数定義については、収集によって関数の型とシグネチャが判明しますが、 関数の本体をいかなる形でも訪問することはなく、ローカル変数の型注釈を調べることもありません (それは型検査の仕事です)。

詳細については、collect モジュールを参照してください。

TODO: 型検査について実際に説明する… #1161

型強制

型強制は、値を別の型へ変換する暗黙的な操作です。型強制サイトとは、型強制を暗黙的に実行できる位置のことです。型強制サイトには次の 2 種類があります。

  • 1対1
  • LUB(Least-Upper-Bound、最小上界)
#![allow(unused)]
fn main() {
let one_to_one_coercion: &u32 = &mut 8;

let lub_coercion = match my_bool {
    true => &mut 10,
    false => &12,
};
}

どのような型強制が存在するか、およびどの式が型強制サイトであるかについては、型強制に関する Reference ページを参照してください: https://doc.rust-lang.org/reference/type-coercions.html

1対1の型強制

1対1の型強制では、単一の型から既知のターゲット型へ型強制します。上の例では、これは &mut u32 から &u32 への型強制です。

1対1の型強制は、FnCtxt::coerce を呼び出すことで実行できます。

LUB 型強制

LUB 型強制では、複数のソース型を何らかの未知のターゲット型へ型強制します。1対1の型強制とは異なり、LUB 型強制は、すべてのソース型が型強制されるターゲット型を生成します。

上の例では、これは &mut i32&i32 の両方に対する LUB 型強制であり、ターゲット型 &i32 を生成します。

「LUB 型強制」(Least-Upper-Bound 型強制)という名前は、この型強制が型の集合を受け取り、両方のソース型を型強制/サブタイプ化できる、型強制/サブタイプ化された最小の型を計算する方法に由来します。

LUB 型強制を実行する一般的な処理は次のとおりです。

// * 1
let mut coerce = CoerceMany::new(intial_lub_ty);
for expr in exprs {
    // * 2
    let expr_ty = fcx.check_expr_with_expectation(expr, expectation);
    coerce.coerce(fcx, &cause, expr, expr_ty);
}
// * 3
let final_ty = coerce.complete(fcx);

ここにはいくつかの重要なステップがあります。

  1. CoerceMany 値を作成し、初期 lub を選ぶ
  2. 各式を型検査し、その型を LUB 型強制の一部として登録する
  3. LUB 型強制を完了させ、結果として得られる LUB 化された型を取得する

ステップ 1

まず CoerceMany 値を作成します。これは LUB 型強制に必要なすべての状態を格納します。1対1の型強制とは異なり、LUB 型強制は単一の関数呼び出しではありません。これは、型検査と LUB 型強制の進行を織り交ぜたいからです。

CoerceMany の作成には、何らかの initial_lub 型を渡します。これは型強制のターゲットとは異なります。ターゲットは、(1対1の型強制とは異なり)入力ではなく、LUB 型強制の出力です。

初期 lub ty は、この LUB 型強制の対象となる式の Expectation から導出されるべきです。これにより、LUB 型強制の計算から生じる推論制約を、その LUB 型強制に参加する後続の式を型検査するために使われる Expectation へ伝播できます。

これが及ぼす効果についての詳細は、“unnecessary inference constraints” 見出しを参照してください。

使用する Expectation がない場合は、初期 lub ty 用に新しい推論変数を作成するべきです。

ステップ 2

次に、LUB 型強制に参加する各式について、それを型検査してから、その型を指定して CoerceMany::coerce を呼び出します。

場合によっては、LUB 型強制に参加する式が HIR に実際には存在しないことがあります。たとえば、オペランドを持たない break 式や return 式を処理するときには、() が LUB 型強制に参加する必要があります。

このような場合には、CoerceMany::coerce_forced_unit メソッドを使用できます。

CoerceMany::coerce メソッドと coerce_forced_unit メソッドはいずれも、新しい型によって LUB 型強制が充足不能になる場合にエラーを出力します。この場合、LUB 型強制の最終的な型はエラー型になります。

ステップ 3

最後に、すべての式が型強制されたら、CoerceMany::complete を呼び出すことで LUB 型強制の最終的な型を取得できます。

LUB 型強制の結果として得られる型は、CoerceMany の構築時に渡された初期 lub 型とは意味のある形で異なります。常に LUB 型強制の結果として得られる型を受け取り、それに対して必要なチェックを実行するべきです。

実装上のニュアンス

調整

型強制操作が成功したときには、それがどの種類の型強制であったかを記録します。たとえば、unsize 型強制や autoderef などです。これは型強制操作の一部として処理され、進行中の TypeckResults調整のリストを書き込みます。

THIR を構築するときには、TypeckResults に格納されている調整を取り出し、すべての型強制ステップを明示的にします。コンパイラのこの時点以降では、型強制という概念は実質的には存在せず、MIR における明示的なキャストとサブタイプ化だけがあります。

TODO: ここに調整の章を書いてリンクする

CoerceMany はどのように動作するか

CoerceMany は、現在の lub ty と何らかの新しいソース型を繰り返し受け取り、両方の型を型強制できる新しい lub ty を計算することで動作します。型のペアを受け取り、何らかの新しい第 3 の型を計算する中核ロジックは、try_find_coercion_lub にあります。

#![allow(unused)]
fn main() {
fn foo() {}
fn bar() {}

let a = match my_bool {
    true => foo,
    true if other_bool => foo,
    false => bar,
}
}

この例では、match 式を型検査するときに LUB 型強制が実行されます。この LUB 型強制は、let 文に既知の型がないため、何らかの推論変数 ?x を初期 lub ty として開始します。

この LUB 型強制に参加する式は 3 つあります。LUB 型強制の最初の式は特別で、既存の初期 lub ty とともに新しい型を計算するのではなく、最初の式から初期 lub ty へ直接型強制します。

  1. true => foo, を型検査した後、最終的に型 FnDef(Foo) が得られます。次に CoerceMany::coerce を呼び出します。これは FnDef(Foo) から ?x への1対1の型強制を実行します。これにより ?x=FnDef(Foo) が推論され、LUB 型強制の新しい lub ty が得られます。
  2. true if other_bool => foo, を型検査した後、再び最終的に型 FnDef(Foo) が得られます。次に CoerceMany::coerce を呼び出します。これは、以前の lub ty(FnDef(Foo))とこの式の型(FnDef(Foo))から新しい lub ty を計算しようとします。これにより lub ty FnDef(Foo) が得られます。
  3. false => bar, を型検査した後、最終的に型 FnDef(Bar) が得られます。次に CoerceMany::coerce を呼び出します。これは、以前の lub ty(FnDef(Foo))とこの式の型(FnDef(Bar))から新しい lub ty を計算しようとします。この場合、両方の関数アイテム型を関数ポインタへ型強制することを選ぶため、型 fn() -> () が得られます。

これにより、LUB 型強制の最終的な型として fn() -> () が得られます。

推移的な型強制

CoerceMany の、現在のターゲット型を新しい型へ型強制しようと繰り返し試みるアルゴリズムは、現在のところ「推移的な型強制」をもたらします。LUB 型強制のあるステップで式を型強制し、その後のステップでその式をさらに型強制することが可能です。

```rust
struct Foo;

use std::ops::Deref;

impl Deref for Foo {
    type Target = [u8; 2];
    
    fn deref(&self) -> &[u8; 2] {
        &[1; _]
    }
}

fn main() {
    match () {
        _ if true => &Foo,
        _ if true => &[1_u8; 2],
        _ => &[1_u8; 2] as &[u8],
    };
}

ここでは、初期 lub 型が ?x である LUB 型強制があります。最初のステップでは、&Foo から ?x への 1 対 1 の型強制を行います(最初のステップは特別であることを思い出してください)。

2 番目のステップでは、現在の lub 型である &Foo と、新しい型である &[u8; 2] から、新しい lub 型を計算します。この新しい lub 型は、最初の式に対して &Foo から &[u8; 2] への deref 型強制を行うことで、&[u8; 2] になります。

3 番目のステップでは、現在の lub 型である &[u8; 2] と、新しい型である &[u8] から、新しい lub 型を計算します。この新しい lub 型は、最初の 2 つの式に対して &[u8; 2] から &[u8] への unsizing 型強制を行うことで、&[u8] になります。

最初の式が 2 回型強制されている点に注意してください。1 回目は &Foo から &[u8; 2] への deref 型強制で、次に &[u8; 2] から &[u8] への unsizing 型強制です。

推移的な型強制の現在の実装は壊れており、前の例は実際には stable で ICE します。LUB 型強制を実行するロジックは推移的な型強制を問題なく生成できますが、コンパイラの残りの部分はそれらを処理できるようになっていません。

1 対 1 の型強制は、LUB 型強制が生成できる多くの種類の推移的な型強制も生成できません。たとえば、前の例を 1 対 1 の型強制に変えると、コンパイルエラーになります。

struct Foo;

use std::ops::Deref;

impl Deref for Foo {
    type Target = [u8; 2];
    
    fn deref(&self) -> &[u8; 2] {
        &[1; _]
    }
}

fn main() {
    let a: &[u8] = &Foo;
}

ここでは &Foo から &[u8] への 1 対 1 の型強制を実行しようとしていますが、これは失敗します。deref 型強制か unsizing 型強制のどちらかしか実行できず、その 2 つを合成することはできないためです。

try_find_coercion_lub はどのように動作するか

LUB 型強制の新しい lub 型を計算する方法は 3 つあります。

  1. 現在の lub 型と新しい型の両方を関数ポインターに型強制する
  2. 現在の lub 型を新しい型に型強制する(またはその逆)
  3. 現在の lub 型と新しい型の相互上位型を計算する

残念ながら、実際の実装はこの点をかなり分かりにくくしています。

相互上位型の計算は、型強制が失敗した場合にすでに部分型付けを処理する 1 対 1 の型強制のロジックを再利用しているため、暗黙的に行われます。

さらに、現在の lub 型と新しい型の両方を関数ポインターに型強制しようとする際には、不要な型強制を避けるため、相互上位型の計算を積極的に試みます。

この関数の構造を改善して、概念モデルにより近づける余地はおそらくあります。

1 対 1 の型強制における use_lub フィールド

1 対 1 の型強制の実装は、LUB 型強制の一部として再利用されています。

シグネチャを関連付ける場合や、型強制が不可能な場合に部分型付けへフォールバックする場合に、LUB 型強制が一方向の部分型付けを使うのは誤りです。代わりに、2 つの型の相互上位型を計算したいのです。

Coerceuse_lub フィールドは、通常の部分型付けを行うか(1 対 1 の型強制の場合)、相互上位型を計算するか(LUB 型強制の場合)を切り替えるために存在します。

Lubbing

理論上、相互上位型の計算は、新しい推論変数 ?mutual_sup を作成し、lub_ty <: ?mutual_supnew_ty <: ?mutual_sup を要求するだけの単純なものであるはずです。実際には、LUB 型強制は特別な TypeRelation である LatticeOp を使用します。

これは主に、高ランク型に対する部分型付け/汎化がかなり壊れていることを回避するためです。通常の部分型付けとは異なり、高ランク型に遭遇すると、lub 型関係は不変性に切り替わります。

これにより、高ランク型のバインダーが等価であることが強制され、「最も一般的な」バインダーを選ぶ必要がなくなります。そのようなバインダーを選ぶのは非常に困難です。

また、相互上位型を計算するプロセスが順序依存になることも避けられます。型 ab が与えられたとき、ab の相互上位型を計算した結果が、ba の相互上位型を計算した結果と同じになると望ましいかもしれません。

高ランク型と部分型付けに関する現在の問題により、相互上位型を計算する素朴な方法を使った場合、この性質は成り立たなくなります。

型強制は MIR 構築中に明示的な MIR 操作へ変換されるため、LUB 型強制の最終的な型を計算するプロセスは HIR typeck 中にのみ発生します。これはまた、相互上位型を計算する挙動が型推論にのみ関係し、健全性には関係しないことを意味します。

注意事項

Probe

probe の内部から型強制する場合は注意が必要です。1 対 1 の型強制と LUB 型強制のどちらにも、probe によってロールバックできない副作用があるためです。

LUB 型強制は、型強制ステップが失敗するとエラーを出力します。これにより、probe の内部での使用に完全に適したものになります。

1-to-1 型強制と LUB 型強制はどちらも、成功時に型強制された式へ調整を適用します。つまり、probe の内部で型強制の試行が成功した場合、その probe は何もロールバックしてはなりません。

したがって、FnCtxt::coerce 呼び出しを commit_if_ok の内部にラップするのは正しいですが、coerce 呼び出しの後に Err を返す場合にそうするのは誤りです。また、probe の内部から FnCtxt::coerce を呼び出すのも誤りです。

CoerceMany は、probe または commit_if_ok の内部から決して使用すべきではありません。

Never-to-Any 型強制

never 型(!)から推論変数へ型強制すると、ターゲット型がその推論変数である NeverToAny 型強制になります。これは、その推論変数を never 型と単一化することとは微妙に異なります。

ある推論変数 ?x! と単一化するには、?x が実際に !等しい必要があります。しかし、NeverToAny 型強制では、?x は任意の可能な型に推論されることが許されます。

この違いは、型強制の初期 lub 型が推論変数である場合(たとえば、初期 lub 型に使用する Expectation がない場合)でも、部分型付けではなく型強制を使用することが重要であることを意味します。

never 型に推論してしまうのではなく、型強制を経由すべきだった箇所で誤って never 型に推論していたバグを修正する PR #147834 を参照してください。

部分型付けへのフォールバック

部分型付けは型強制ではありませんが、FnCtxt::coerceCoerceMany::coerce/coerce_forced_unit はどちらも、部分型付けによって成功することがあります。

1 対 1 の型強制では、ソース型がターゲット型の部分型であることを強制しようとします。LUB 型強制では、既存のすべての型の上位型である型を計算しようとします。

たとえば、?x から u32 への 1 対 1 の型強制を実行すると、部分型付けにフォールバックし、?x eq u32 と推論します。これは、型強制が失敗した場合、その後に部分型付けを試みる必要がないことを意味します。

不要な推論制約

Expectation の型を初期 lub ty として使用すると、LUB 型強制に参加する式の型によって推論変数が制約される可能性があります。これらの推論変数は、実際には LUB 型強制の最終的な型によってのみ制約されればよいため、これは常に望ましいとは限りません。

#![allow(unused)]
fn main() {
fn foo<T>(_: T) {}

fn a() {}
fn b() {}

foo::<?x>(match my_bool {
    true => a,
    false => b,
})
}

ここでは、最初の式の型が FnDef(a) で、2 つ目の式の型が FnDef(b) である LUB 型強制があります。LUB 型強制の初期 lub ty として ?x を使用すると、次のような挙動になります。

  • 式 1: ?x=FnDef(a) と推論する
  • 式 2: FnDef(a), FnDef(b) の間の型強制 lub を見つけ、その結果 fn() -> () になる
  • LUB 型強制の最終的な型は fn() -> () です。?x eq fn() -> () を等置しますが、?x は実際にはすでに FnDef(a) と推論されているため、これは実際には FnDef(a) eq fn() -> () を等置していることになり、これは成立しません

これらの望ましくない推論制約の一部(ただしすべてではありません)を避けるため、LUB 型強制の Expectation が推論変数である場合、それを初期 lub ty として使用しません。代わりに新しい推論変数を作成します。たとえば、上記のコードスニペットでは、実際には ?x を使用する代わりに、初期 lub ty 用に何らかの新しい推論変数 ?y を作成します。

  • 式 1: ?y=FnDef(a) と推論する
  • 式 2: FnDef(a), FnDef(b) の間の型強制 lub を見つけ、その結果 fn() -> () になる
  • LUB 型強制の最終的な型は fn() -> () であり、?x=fn() -> () と推論する

新しい推論変数を作成しなかったことにより望ましくない推論制約が生じた事例については、#140283 を参照してください。

これは すべて のケースで不要な制約を避けるわけではなく、Expectation が推論変数であるという最も一般的なケースのみを避けます。理論上は、すべてのケースでこれらの制約を避けることが望ましいですが、それを行うにはかなり複雑な作業が必要になります。

メソッド検索

メソッド検索は、self 型、自動参照外し、トレイト検索など、多数の要因が相互作用するため、かなり複雑になることがあります。このファイルでは、そのプロセスの概要を説明します。より詳細なメモは、当然ながらコード自体にあります。

メソッド検索の考え方の 1 つは、receiver.method(...) という形式の式を、より明示的な完全修飾構文(以前は UFCS と呼ばれていました)に変換するというものです。

  • トレイト呼び出しの場合は Trait::method(ADJ(receiver), ...)
  • 固有メソッド呼び出しの場合は ReceiverType::method(ADJ(receiver), ...)

ここで ADJ は何らかの調整であり、通常は一連の自動参照外しの後に、必要に応じて自動参照が続くものです(例: &**receiver)。ただし、途中で他の調整や型強制を行うこともあり、特にサイズ変更があります(例: [T; n] から [T] への変換)。

メソッド検索は、大きく 2 つのフェーズに分かれます。

  1. プロービング(probe.rs)。プローブフェーズでは、どのメソッドを呼び出すか、およびレシーバーをどのように調整するかを決定します。
  2. 確認(confirm.rs)。確認フェーズは、この選択を「適用」し、サイドテーブルを更新し、型変数を単一化し、その他の副作用を伴う処理を行います。

この分割の理由の 1 つは、キャッシュにより適したものにするためです。プローブフェーズは「選択結果」(probe::Pick)を生成します。これはメソッド呼び出し箇所間でキャッシュ可能になるように設計されています。そのため、推論変数やその他の情報は含まれません。

プローブフェーズ

ステップ

プローブフェーズが最初に行うことは、一連のステップを作成することです。これは、レシーバー型をそれ以上参照外しできなくなるまで段階的に参照外しし、さらに任意の「サイズ変更」ステップを適用することで行われます。したがって、レシーバーの型が Rc<Box<[T; 3]>> の場合、次のようになる可能性があります。

  1. Rc<Box<[T; 3]>>
  2. Box<[T; 3]>
  3. [T; 3]
  4. [T]

候補の組み立て

次に、それらのステップに沿って検索し、候補のリストを作成します。Candidate は、呼び出されているメソッドである可能性が十分にあるメソッド項目です。各候補について、明示的な self を考慮した「変換後の self 型」を導出します。

候補は、固有と拡張の 2 種類に分類されます。

固有候補は、レシーバー自体の型から導出されるものです。つまり、何らかの名目型 Foo(例: 構造体)のレシーバーがある場合、impl Foo のような impl 内で定義されたメソッドはすべて固有メソッドです。固有メソッドを使用するために何かをインポートする必要はありません。それらは型自体に関連付けられています(なお、固有 impl は型自体と同じクレート内でしか定義できません)。

拡張候補は、インポートされたトレイトから導出されます。トレイト ToString をインポートしていて、to_string() をメソッドとして呼び出した場合、ToString の各 impl にある to_string() 定義を候補として列挙します。この種のメソッド呼び出しは「拡張メソッド」と呼ばれます。

では、例を続けましょう。レシーバー Rc<Box<[T; 3]>> でメソッド foo を呼び出しており、それを型 Rc<U> に対して &self で定義するトレイト Foo があり、さらに foo を定義する型 Box 上のメソッドがあるものの、それは &mut self であるとします。この場合、2 つの候補が存在する可能性があります。

  • 拡張候補としての &Rc<U>
  • 固有候補としての &mut Box<U>

候補検索

最後に、実際にメソッドを選択するために、ステップを順に下りながら検索し、レシーバー型を候補型と照合しようとします。各ステップでは、自動参照と自動可変参照も考慮し、それによって候補のいずれかが一致するかどうかを確認します。結果として得られる各レシーバー型について、拡張候補より先に固有候補を考慮します。グループ内に一致する候補が複数ある場合はエラーを報告します。ただし、同じトレイトの複数の impl は単一の一致として扱われます。それ以外の場合は、最初に見つかった一致を選択します。

この例の場合、最初のステップは Rc<Box<[T; 3]>> であり、それ自体はどの候補にも一致しません。しかし、自動参照すると、&Rc<Box<[T; 3]>> という型が得られ、これは &Rc<U> に一致します。その後、impl に現れるすべての where 句を再帰的に考慮します。それらが一致する場合(または一致しないと断定できない場合)、これが選択するメソッドになります。そうでなければ、一連のステップをさらに進み続けます。

constジェネリクス

const引数の種類

存在する ty::Const の種類のほとんどは、存在する型の種類と直接対応しています。たとえば ConstKind::ParamTyKind::Param に相当します。

ここで主に興味深い点は次のとおりです。

  • ConstKind::Unevaluated。これは TyKind::Alias に相当し、長期的には名前を変更するべきです(また、ty::AliasKind に対応する AliasConstKind も導入するべきです)。
  • ConstKind::Value。これは単相化後の ty::Const の最終的な値です。 これは TyKind::StrTyKind::ADT のような完全に具体的なものにやや似ています。

const引数のすべての種類と、それらが型システムで実際にどのように表現されるかの完全な一覧については、ConstKind 型を参照してください。

推論変数はかなり面白みがなく、ほとんどあらゆる場所で型推論変数と同等に扱われます。 constパラメータも同様に面白みがなく、ほとんどあらゆる場所で型パラメータの使用と同等です。 ただし、構文解析、名前解決、AST lowering中にそれらがどのように扱われるかには、いくつか興味深い微妙な点があります: ambig-unambig-ty-and-consts

Anon Consts

Anon Consts(anonymous const itemsの略)は、constジェネリクスで任意の式を表現する方法です。たとえば、配列長の 1 + 1foo()、あるいは単なる 0 などです。 これらはconstジェネリクスに固有であり、型における実質的な相当物はありません。

脱糖

#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias = [u8; 1 + 1];
}

この例では、1 + 1 というconst引数(配列長)があり、これは anon const として表現されます。脱糖すると、おおよそ次のようになります。

#![allow(unused)]
fn main() {
struct Foo<const N: usize>;

const ANON: usize = 1 + 1;
type Alias = [u8; ANON];
}

ここで、[u8; ANON] の配列長は、ANON の使用を含むanon constそのものではなく、ANON constアイテムの「直接的」な使用の一種です(ConstKind::Unevaluated)。

anon constは、それが含まれているアイテムのジェネリックパラメータを継承しません。

#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias<T: Sized> = [T; 1 + 1];

// 脱糖すると次のようになる;

struct Foo<const N: usize>;

const ANON: usize = 1 + 1;
type Alias<T: Sized> = [T; ANON];
}

Alias には型パラメータ T とwhere句 T: Sized の両方がありますが、ANON constにはジェネリックパラメータもwhere句もないことに注目してください。 この脱糖は、anon constがジェネリックパラメータを利用できないことを強制する仕組みの一部です。

anon constを実際のconstアイテムへ脱糖されるものとして考えるのは有用ですが、コンパイラは実際にはこの方法で実装していません。

AST loweringの時点では、anon constのがまだわかっていないため、明示的に書かれた型を持つ実際のHIRアイテムへ脱糖することはできません。 これを回避するために、DefKind::AnonConsthir::Node::AnonConst があります。 これらは、実際には脱糖できないこのような匿名constアイテムを表現するために使われます。

これらのanon constの型は、type_of クエリから取得できます。 しかし、type_of クエリには実際には型を計算するロジックは含まれていません(実際、呼び出されると単にICEします)。 代わりに、HIR Ty loweringが、loweringされた任意のanon constについて type_of クエリの値を供給する責任を負います。 HIR Ty loweringは、anon constが引数として渡されるconstパラメータの型を見ることで、そのanon constの型を決定できます。

TODO: query feedingに関する章を書いてここにリンクする

ある意味では、前の例の脱糖は次のようなものです。

#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias = [u8; 1 + 1];

// だいたい次のような擬似Rustへ脱糖される:
struct Foo<const N: usize>;

const ANON = 1 + 1;
type Alias = [u8; ANON];
}

Alias 内の配列型についてHIR ty loweringを通るとき、配列長もloweringし、type_of(ANON) -> usize を供給します。 これにより、HIRを構築するときではなく、コンパイラの後の段階で ANON constアイテムの型を実質的に設定することになります。

この脱糖がすべて行われた後、型システム内の最終的な表現(すなわち ty::Const として)は、AnonConstDefId を持つ ConstKind::Unevaluated になります。これは、anon constを経由せずに実際のconstアイテムの使用を表現する場合(たとえば min_generic_const_args が有効な場合)に、それを表現する方法と同等です。

これにより、constの「エイリアス」の表現を TyKind::Alias の表現と同じにできます。適切なHIR本体があることにより、大量のコード再利用も可能になります。たとえば、HIR型検査や、MIRへのすべてのloweringステップを再利用でき、そこでさらにconst評価を再利用できます。

ジェネリックパラメータを使用できないことの強制

anon constがジェネリックパラメータを使用できないことを強制する方法は3つあります。

  1. 名前解決は、anon const内にいるとき、ジェネリックパラメータへのパスを解決しません
  2. HIR Ty loweringは、ジェネリックパラメータを参照する型への Self 型エイリアスにanon const内で遭遇したときにエラーにします
  3. Anon Constsは、親定義からwhere句やジェネリクスを継承しません(すなわち generics_of はanon constについて親を含みません)
#![allow(unused)]
fn main() {
// *1* 名前解決でエラー
type Alias<const N: usize> = [u8; N + 1];
//~^ ERROR: ジェネリックパラメータはconst演算で使用できません

// *2* HIR Ty loweringでエラー:
struct Foo<T>(T);
impl<T> Foo<T> {
    fn assoc() -> [u8; { let a: Self; 0 }] {}
    //~^ ERROR: ジェネリックな`Self`型は現在、匿名定数では許可されていません
}

// *3* 脱糖されたanon constにwhere句がないことによるエラー
trait Trait<T> {
    const ASSOC: usize;
}
fn foo<T>() -> [u8; <()>::ASSOC]
//~^ ERROR: ユニット型`()`に`ASSOC`という名前の関連アイテムが見つかりません
where
    (): Trait<T> {}
}

2つ目の点は特に微妙です。HIR Ty loweringを誤って実装し、anon constがジェネリックパラメータを使用できないことを適切に強制し損ねるのは非常に簡単だからです。 既存のチェックは保守的すぎる一方で、誤って一部のジェネリックパラメータがanon constの本体に入り込むことを許してしまいます #144547

anon const内でジェネリックパラメータを誤って許可すると、ICEにつながることもありますが、不正な形式のプログラムを受理することにもつながり得ます。

3つ目の点もやや微妙です。親アイテムのwhere句を一切継承しないことで、スコープ内のwhere句がジェネリックパラメータに言及している場合に、トレイト解決がそのwhere句に基づいて推論変数をジェネリックパラメータへ推論してしまうことを避けられます。 たとえば、式 <() as Trait<?x>>::ASSOC と、スコープ内の (): Trait<T> というwhere句から ?x=T を推論するような場合です。

これにより、仮にanon const内でジェネリックパラメータを誤って許可してしまった場合でも、コンパイラがICEするか、少なくとも偶発的に何らかのエラーを出す可能性がずっと高くなります。なぜなら、そのanon constには、ジェネリックパラメータを適切に扱うために環境内で必要な情報がまったく存在しないからです。

配列の繰り返し式

上記すべてに対する唯一の例外は、配列式の繰り返し回数です。 後方互換性のためのハックとして、繰り返し回数のconst引数がジェネリックパラメータを使用することを許可しています。

#![allow(unused)]
fn main() {
fn foo<T: Sized>() {
    let a = [1_u8; size_of::<T>()];
}
}

ただし、anon const の const 引数でジェネリックパラメータを許可することに伴う問題の大半を避けるため、定数はモノモーフィゼーション前(たとえば型チェック中)に評価される必要があります。ある意味では、ここでジェネリックパラメータを許可するのは、それらが意味的に未使用である場合に限られます。

前の例では、サイズ付き型への raw pointer は常に同じサイズ(たとえば 64bit プラットフォームでは 8)を持つため、anon const は任意の型パラメータ T に対して評価できます。

構文上はジェネリックパラメータを含んでいるものの、評価が成功するために実際にはそれらに依存していない anon const を評価したことを検出すると、const_evaluatable_unchecked FCW を出力します。 これは、const 引数でジェネリックパラメータを使用する方法、たとえば min_generic_const_args や(現在は廃止された)generic_const_exprs がさらに安定化された時点で、hard error になることが意図されています。

この FCW の実装はここにあります: const_eval_resolve_for_typeck

generic_const_parameter_types との非互換性

const N: [u8; M]const N: Foo<T> のような const パラメータのサポートは、現在の anon const の仕組みとはあまり相性がよくありません。 これには 2 つの理由があります:

  1. anon const はジェネリックパラメータを使用できないため、その型もジェネリックパラメータを参照できません。 つまり、型がまだジェネリックパラメータを参照している const パラメータへの引数として anon const を使用することは、根本的に不可能です。

    #![allow(unused)]
    #![feature(adt_const_params, generic_const_parameter_types)]
    
    fn main() {
    fn foo<const N: usize, const M: [u8; N]>() {}
    
    fn bar<const N: usize>() {
        // `M` への const 引数を指定する方法はない
        foo::<N, { [1_u8; N] }>();
    }
    }
  2. 現在、HIR ty lowering 中に anon const を lowering する際、anon const の型を知っている必要があります。 generic const parameter types では、現在わかっている型に推論変数が含まれている(つまり、まだ完全にはわかっていない)場合があります。

    #![allow(unused)]
    #![feature(adt_const_params, generic_const_parameter_types)]
    
    fn main() {
    fn foo<const N: usize, const M: [u8; N]>() {}
    
    fn bar() {
        // 推論できるにもかかわらず、`N` への const 引数は
        // 明示的に指定する必要がある
        foo::<_, { [1_u8; 3] }>();
    }
    }

generic_const_parameter_types を const generics の残りの部分とうまく連携させる正しい方法は、現時点では明らかではありません。

generic_const_exprs では、ジェネリックパラメータを参照する型を持つ anon const が許可されるはずでしたが、その設計は最終的に実行不可能であることがわかりました。

min_generic_const_args では、一部の式(たとえば配列の構築)を anon const なしで表現できるようになり、そのためこれらの問題に遭遇しなくなります。ただし、これで 十分 かどうかはまだ判断されていません。

Const 引数の型チェック

const 引数が well formed であるためには、その引数の対象である const パラメータと同じ型を持っている必要があります。 たとえば、配列長に対する型 bool の const 引数は well formed ではありません。配列の長さパラメータの型は usize だからです。

#![allow(unused)]
fn main() {
type Alias<const B: bool> = [u8; B];
//~^ ERROR:
}

これをチェックするために ClauseKind::ConstArgHasType(ty::Const, Ty) があり、 item 上で定義された各 Const Parameter について、 等価な ConstArgHasType 節をその where 節のリストに desugar します。 これにより、すべての節を証明することで何かの wellformedness をチェックするたびに、 すべての Const Arguments が正しい型を持っていることもたまたまチェックされるようになります。

#![allow(unused)]
fn main() {
fn foo<const N: usize>() {}

// pseudo-rust では次のように desugar される

fn foo<const N>()
where
//  ConstArgHasType(N, usize)
    N: usize, {}
}

ConstArgHasType goals の証明は、まず const 引数の型を計算し、それを与えられた型と等価にすることで実装されています。 Const Argument の型を計算する方法の大まかな概要:

  • ConstKind::Param(N)ParamEnv で検索して ConstArgHasType(N, ty) 節を見つけることができます
  • ConstKind::Value は値の型を自身の内部に保持しているため、簡単にアクセスできます
  • ConstKind::Unevaluatedtype_of query を呼び出すことで型を計算できます
  • より詳細な情報については、ConstArgHasType goals の証明の実装を参照してください

ConstArgHasType は、Const Arguments が正しい型を持つことをチェックする、健全性にとって重要な唯一の方法です。 ただし、一部の場合には、別の方法で Const Arguments の型を 間接的に チェックしています。

#![allow(unused)]
fn main() {
type Alias = [u8; true];

// 次のように desugar される

const ANON: usize = true;
type Alias = [u8; ANON];
}

anon const の型に Const Parameter の型を与えることで、 anon const を含む ConstArgHasType goal が成功することを保証します。 anon const の型が Const Parameter の型と一致しない場合、 実際に起こるのは、anon const の本体を型チェックするときの 型チェック エラーです。

上の例を見ると、これは ANON が型 usize を持つため [u8; ANON] は well formed な型である一方、ANON本体 は illformed であり、型 usize の const item から true を返すことはできないため型チェックエラーになる、ということに対応します。

不透明型(型エイリアス impl Trait

不透明型は、特定のトレイト集合のみをインターフェイスとして公開する不透明な型エイリアスを宣言するための構文です。背後にある具体的な型は、不透明型の特定の使用箇所の集合から推論されます。

これは、型エイリアス内で impl Trait を使用することで表現されます。例:

type Foo = impl Bar;

これは Foo という名前の不透明型を宣言します。この型について分かる唯一の情報は、Bar を実装しているということです。したがって、Bar のインターフェイスはいずれも Foo に対して使用できますが、それ以外は使用できません(具体的な型が他のトレイトを実装しているかどうかにかかわらず)。

背後に具体的な型が必要であるため、2025年5月時点()では、その型を不透明型の「定義使用箇所」で不透明型を使用することによって表現できます。

struct Struct;
impl Bar for Struct { /* 処理 */ }
#[define_opaque(Foo)]
fn foo() -> Foo {
    Struct
}

他の「定義使用箇所」は、まったく同じ型を生成する必要があります。

不透明型への型エイリアスを定義することは、不安定な機能であることに注意してください。 これを使用するには、nightly と、ファイル上の #![feature(type_alias_impl_trait)] アノテーション、および不透明型を具体的な型に結び付けるメソッド上の #[define_opaque(Foo)] が必要です。 完全な例:

#![allow(unused)]
#![feature(type_alias_impl_trait)]

fn main() {
trait Bar { /* 処理 */ }

type Foo = impl Bar;

struct Struct;

impl Bar for Struct { /* 処理 */ }

#[define_opaque(Foo)]
fn foo() -> Foo {
    Struct
}
}

定義使用箇所

現在、不透明型の定義使用箇所になれるのは関数の戻り値のみです(かつ、その関数の戻り値の型が不透明型を含んでいる場合に限ります)。

不透明型の定義使用は、不透明型定義の親の内部にある任意のコードにできます。これには、不透明型の兄弟要素、および兄弟要素のすべての子要素が含まれます。

*「型システムが何をしているのか理解しようとしている間に、開発者が頭の中で誤って無限ループを実行してしまうことで致命的な脳損傷を引き起こさない」*ための取り組みにより、不透明型の子要素を定義使用箇所にすることは認めないことになりました。

関連不透明型

関連不透明型は、同じトレイト impl 上の他の任意の関連アイテム、またはそれらの関連アイテムの子要素によって定義できます。たとえば:

trait Baz {
    type Foo;
    fn foo() -> Self::Foo;
}

struct Quux;

impl Baz for Quux {
    type Foo = impl Bar;
    fn foo() -> Self::Foo { ... }
}

このためには、nightly と、(異なる)#![feature(impl_trait_in_assoc_type)] アノテーションも使用する必要があります。 不透明型が(関連型を介して)関数シグネチャに記載されているため、メソッド上の #[define_opaque(Foo)] はもう必要ないことに注意してください。 完全な例:

#![feature(impl_trait_in_assoc_type)]

trait Bar {}
struct Zap;

impl Bar for Zap {}

trait Baz {
    type Foo;
    fn foo() -> Self::Foo;
}

struct Quux;

impl Baz for Quux {
    type Foo = impl Bar;
    fn foo() -> Self::Foo { Zap }
}

不透明型(impl Trait)の推論

このページでは、コンパイラが不透明型隠れた型をどのように推論するかを説明します。 この種の型推論は特に複雑です。 なぜなら、他の種類の型推論とは異なり、 関数や関数本体をまたいで機能できるためです。

実行例

仕組みを説明するために、例を考えてみましょう。

#![feature(type_alias_impl_trait)]
mod m {
    pub type Seq<T> = impl IntoIterator<Item = T>;

    #[define_opaque(Seq)]
    pub fn produce_singleton<T>(t: T) -> Seq<T> {
        vec![t]
    }

    #[define_opaque(Seq)]
    pub fn produce_doubleton<T>(t: T, u: T) -> Seq<T> {
        vec![t, u]
    }
}

fn is_send<T: Send>(_: &T) {}

pub fn main() {
    let elems = m::produce_singleton(22);

    is_send(&elems);

    for elem in elems {
        println!("elem = {:?}", elem);
    }
}

このコードでは、不透明型Seq<T> です。 その定義スコープはモジュール m です。 その隠れた型Vec<T> で、 これは m::produce_singletonm::produce_doubleton から推論されます。

main 関数では、不透明型はその定義スコープの外にあります。 mainm::produce_singleton を呼び出すと、不透明型 Seq<i32> への参照が返されます。 is_send 呼び出しは Seq<i32>: Send であることを検査します。 Send は impl trait の境界には列挙されていませんが、 auto-trait leakage により、それが成り立つことを推論できます。 for ループの脱糖では Seq<T>: IntoIterator であることが必要で、 これは Seq<T> に宣言された境界から証明できます。

main の型チェック

まず、main を型チェックするときに何が起こるかを見てみましょう。 最初に produce_singleton を呼び出し、戻り値の型は不透明型 OpaqueTy になります。

for ループの型チェック

for ループは、in elems の部分を IntoIterator::into_iter(elems) に脱糖します。 elems の型は Seq<T> なので、型チェッカーは Seq<T>: IntoIterator 義務を登録します。 この義務は自明に満たされます。 なぜなら、Seq<T> はそのトレイトに対する境界を持つ不透明型(impl IntoIterator<Item = T>)だからです。 U: Foo という where 境界によって U が自明に Foo を満たせるのと同様に、 不透明型の境界は型チェッカーから利用可能であり、義務を満たすために使われます。

for ループ内の elem の型は <Seq<T> as IntoIterator>::Item、つまり T と推論されます。 型チェッカーが隠れた型に関心を持つことは一切ありません。

is_send 呼び出しの型チェック

auto trait 境界を証明しようとするとき、 まず上記と同じプロセスを繰り返し、 その auto trait が不透明型の境界リストに含まれているかを確認します。 それが失敗した場合、不透明型の隠れた型を明らかにしますが、 これはこの特定のトレイト境界を証明するためだけであり、一般的に明らかにするわけではありません。 明らかにする処理は、不透明型の DefId に対して type_of クエリを呼び出すことで行われます。 クエリは内部で、定義関数から隠れた型を要求し、 それを返します(詳細は type_of に関するセクションを参照してください)。

型チェック手順のフローチャート

flowchart TD
    TypeChecking["`main` の型チェック"]
    subgraph TypeOfSeq["type_of(Seq<T>) クエリ"]
        WalkModuleHir["モジュール `m` の HIR をたどり、\nその中にある各 function/const/static から\n隠れた型を見つける"]
        VisitProduceSingleton["`produce_singleton` を訪問"]
        InterimType["`produce_singleton` の隠れた型は `Vec<T>`\n検索を続ける"]
        VisitProduceDoubleton["`produce_doubleton` を訪問"]
        CompareType["`produce_doubleton` の隠れた型も Vec<T>\nこれは以前に見たものと一致する ✅"]
        Done["スコープ内に見るべきアイテムはもうない\n`Vec<T>` を返す"]
    end

    BorrowCheckProduceSingleton["`borrow_check(produce_singleton)`"]
    TypeCheckProduceSingleton["`type_check(produce_singleton)`"]

    BorrowCheckProduceDoubleton["`borrow_check(produce_doubleton)`"]
    TypeCheckProduceDoubleton["`type_check(produce_doubleton)`"]

    Substitute["`T => u32` を代入し、\n隠れた型として `Vec<i32>` を得る"]
    CheckSend["`Vec<i32>: Send` であることを検査 ✅"]

    TypeChecking -- auto trait 用のトレイトコード --> TypeOfSeq
    TypeOfSeq --> WalkModuleHir
    WalkModuleHir --> VisitProduceSingleton
    VisitProduceSingleton --> BorrowCheckProduceSingleton
    BorrowCheckProduceSingleton --> TypeCheckProduceSingleton
    TypeCheckProduceSingleton --> InterimType
    InterimType --> VisitProduceDoubleton
    VisitProduceDoubleton --> BorrowCheckProduceDoubleton
    BorrowCheckProduceDoubleton --> TypeCheckProduceDoubleton
    TypeCheckProduceDoubleton --> CompareType --> Done
    Done --> Substitute --> CheckSend

type_of クエリの内部

不透明型 O に適用された type_of クエリは、隠れた型を返します。 その隠れた型は、O の定義スコープ内にある各制約関数からの結果を 組み合わせることで計算されます。

flowchart TD
    TypeOf["type_of クエリ"]
    TypeOf -- find_opaque_ty_constraints --> FindOpaqueTyConstraints
    FindOpaqueTyConstraints --> Iterate
    Iterate["定義スコープ内の各アイテムを反復処理する"]
    Iterate -- 各アイテムについて --> TypeCheck
    TypeCheck["typeck(I) を検査して、それが O を制約するか確認する"]
    TypeCheck -- I は O を\n制約しない --> Iterate
    TypeCheck -- I は O を制約する --> BorrowCheck
    BorrowCheck["mir_borrowck(I) を呼び出し、I によって計算された\nO の隠れた型を取得する"]
    BorrowCheck --> PreviousType
    PreviousType["I からの隠れた型は\nこれまでに見つかった以前の隠れた型と\n同じか?"]
    PreviousType -- はい --> Complete
    PreviousType -- いいえ --> ReportError
    ReportError["エラーを報告する"]
    ReportError --> Complete["アイテム I 完了"]
    Complete --> Iterate

    FindOpaqueTyConstraints -- すべての制約が見つかった --> Done
    Done["完了"]

不透明型を別の型に関連付ける

不透明型がその隠れた型によって制約される中心的な場所が 1 つあり、 それが handle_opaque_type 関数です。 面白いことに、この関数は 2 つの型を受け取るので、任意の 2 つの型を渡せますが、 そのうちの 1 つは不透明型である必要があります。 順序が重要なのは診断に対してのみです。

flowchart TD
    subgraph typecheck["type check comparison routines"]
        equate.rs
        sub.rs
        lub.rs
    end

    typecheck --> TwoSimul

    subgraph handleopaquetype["infcx.handle_opaque_type"]

        TwoSimul["Defining two opaque types simultaneously?"]

        TwoSimul -- Yes --> ReportError["Report error"]

        TwoSimul -- No --> MayDefine -- Yes --> RegisterOpaqueType --> AlreadyHasValue

        MayDefine -- No --> ReportError

        MayDefine["In defining scope OR in query?"]

        AlreadyHasValue["Opaque type X already has\na registered value?"]

        AlreadyHasValue -- No --> Obligations["Register opaque type bounds\nas obligations for hidden type"]

        RegisterOpaqueType["Register opaque type with\nother type as value"]

        AlreadyHasValue -- Yes --> EquateOpaqueTypes["Equate new hidden type\nwith old hidden type"]
    end

クエリとの相互作用

クエリが opaque 型を処理するとき、 それらは自分が定義スコープ内にいるかどうかを判断できないため、 単にそうであると仮定します。

登録された隠れた型は、QueryResponse 構造体の opaque_types フィールドに格納されます(関数 take_opaque_types_for_query_response がそれらを読み出します)。

QueryResponsequery_response_substitution_guess で 周囲の infcx にインスタンス化されるとき、 各隠れた型の制約を、(上記のように)handle_opaque_type を呼び出すことで変換します。

「奇妙」な点が 1 つあります。 インスタンス化された opaque 型には順序があります (ある opaque 型が別の opaque 型と比較され、 どちらの opaque 型を、その隠れた型が割り当てられるものとして使うかを選ばなければならない場合)。 「期待される」と見なされるものを使用します。 しかし実際には、両方の opaque 型に定義使用がある可能性があります。 クエリ結果がインスタンス化されると、 そのクエリを使用しているコンテキストから再評価されます。 最終的なコンテキスト(関数の typeck、mir borrowck、または wf-checks)は、 どの opaque 型を実際にインスタンス化できるかを把握し、 それを正しく処理します。

MIR 借用チェッカー内

MIR 借用チェッカーは nll_relate を介して物事を関連付け、リージョンだけを考慮します。 あらゆる型関係は隠れた型の束縛をトリガーするため、 借用チェッカーは型チェッカーと同じことをしていますが、 明らかに到達不能なコード(例: panic の後)は無視します。 また、隠れた型に関しては借用チェッカーが信頼できる情報源でもあります。 なぜなら、隠れた型上のどのライフタイムが opaque 型宣言上のどのライフタイムに対応するかを適切に把握できるのは、 借用チェッカーだけだからです。

後方互換性ハック

戻り値位置の impl Trait には、どの RFC にも含まれておらず、 偶発的に安定化された可能性が高いさまざまな癖があります。 これらをサポートするために、 以前の挙動を再導入する目的で replace_opaque_types_with_inference_vars が使われています。

後方互換性ハックは 3 つあります。

  1. すべての return 箇所が同じ推論変数を共有するため、 ある return 箇所は、別の return 箇所が具象型を使っている場合にのみコンパイルできることがあります。

    #![allow(unused)]
    fn main() {
    fn foo() -> impl Debug {
        if false {
            return std::iter::empty().collect();
        }
        vec![42]
    }
    }
  2. impl Trait の関連型等価制約は、 隠れた型が関連型上のトレイト境界を満たしている限り使用できます。 opaque な impl Trait シグネチャは、それらを満たす必要はありません。

    #![allow(unused)]
    fn main() {
    trait Duh {}
    
    impl Duh for i32 {}
    
    trait Trait {
        type Assoc: Duh;
    }
    
    // `R` が `F` 上の `::Output` 射影であるという事実により、
    // 中間の推論変数が生成され、その後、実際に見つかった
    // `Assoc` 型と比較されます。
    impl<R: Duh, F: FnMut() -> R> Trait for F {
        type Assoc = R;
    }
    
    // ここでの `impl Send` は、後で作成された推論変数と比較され、
    // 推論変数が隠れた型ではなく `impl Send` に設定されます。
    // 推論変数には、`Trait::Assoc` 上の `: Duh` 境界を守らせるための
    // obligation がすでに登録されています。opaque 型は、その隠れた型が
    // `Duh` を実装している場合でも、`Duh` を実装しません。
    // Lazy TAIT ではエラーになりますが、再び動作するようにするためにハックを挿入し、
    // 後方互換性を維持しました。
    fn foo() -> impl Trait<Assoc = impl Send> {
        || 42
    }
    }
  3. クロージャは、親関数の impl Trait の隠れた型を作成できません。 この点はほとんど問題になりません。 なぜなら、1 の点によって推論変数が導入されるため、 クロージャが見るのは推論変数だけだからです。ただし、1 を修正した場合、これは問題になります。

トレイト内の戻り位置 Impl Trait

トレイト内の戻り位置 impl Trait(RPITIT)は、概念的には(そして #112988 の時点では文字どおり)、トレイトメソッド内の RPIT を、ユーザーが トレイト側または impl 側のどちらでもその GAT を定義しなくてもよい ジェネリック関連型(GAT)へ変換する糖衣構文です。

RPITIT は当初 #101224 で実装されました。これは トレイト内の async fn(AFIT)のサポートを追加したもので、RPITIT の実装は、 以前に RFC 化されていた AFIT の実装の一部として 付随的に得られたためです。その後、RFC 3425 で独立して RFC 化され、 最近 T-lang によって承認されました。

どのように動作するか?

このドキュメントは、主にコンパイルパイプラインに沿って並んでいます:

  1. AST lowering(AST -> HIR)
  2. HIR ty lowering(HIR -> rustc_middle::ty データ型)
  3. typeck

AST lowering

RPITIT の AST lowering は、RPIT の lowering とほぼ同じです。私たちは 引き続きそれらを hir::ItemKind::OpaqueTy として lowering します。 2つの違いは次のとおりです:

不透明型に対して in_trait を記録します。これは、その不透明型が HIR ty lowering や HIR を扱う診断などにおいて RPITIT であることを示します。

不透明型に対して lifetime_mapping を記録します。これについては以下で説明します。

補足: 不透明型のライフタイムの重複

すべての不透明型(RPITIT だけではありません)は、捕捉した ライフタイムを、不透明型にローカルな新しいライフタイムパラメータへ 重複させることになります。これを行う主な理由は、RPIT が、捕捉した 任意の late-bound 引数を「具象化」1 できる、つまり early-bound なものに できる必要があるためです。これは、それらを不透明型のジェネリック引数として使い、 後で隠れた型をインスタンス化できるようにするためです。AST lowering の時点では、 どのライフタイムが early-bound で、どれが late-bound か分からないため、 すべてのライフタイムに対してこれを行います。

RPITIT における主な追加点は、lowering 中に、捕捉されたライフタイムと 対応して重複されたライフタイムとの関係を追加のフィールド OpaqueTy::lifetime_mapping で追跡することです。 このライフタイムの対応付けは、後で predicates_of で、これらの重複された ライフタイムとその元のライフタイムとの等価性を強制する境界を設定し、 これらの GAT を正しく型チェックするために使用します。これについては以下で説明します。

重複なしで lowering できるなら、その方がよいかもしれません。そのためには、 early-bound ライフタイムと late-bound ライフタイムの区別をやめる必要があると 私は考えています。したがって、ジェネリクスにおいて late-bound ライフタイムを考慮する #103448 のような解決策と、さらに impl-trait に関数ライフタイムを継承させる #103449 に類似した PR が必要になるでしょう。

HIR ty lowering

HIR ty lowering における主な変更点は、RPITIT の hir::TyKind::OpaqueDef を 不透明型ではなく射影に lowering することです。このとき、トレイト内の 新しい関連型のために新たに合成された def-id を使用します。次のセクションで、 この def-id を正確にどのように取得するかを説明します。

これは、RPITIT に対して lower_ty を呼び出すたびに、 不透明型ではなく射影が返ってくることを意味します。この射影はその後、 正しい値へ正規化できます。つまり、トレイト内にいる場合は元の不透明型へ、 impl 内にいる場合は RPITIT の推論された型へ正規化されます。

合成された関連型への lowering

query feeding を使用して、メソッド内に現れる RPITIT のために、 トレイト側と impl 側の両方で新しい関連型を合成します。

トレイト内の RPITIT の lowering

tcx.associated_item_def_ids(trait_def_id) が、トレイトのすべての関連型を 集めるためにトレイトに対して呼び出されると、以前のクエリは単に そのトレイトの子である HIR アイテムの def-id を返していました。 #112988 以降は、それに加えて、トレイト内の各メソッドについて、 tcx.associated_types_for_impl_traits_in_associated_fn(trait_method_def_id) が返す def-id を追加します。 これは各トレイトメソッドを走査し、シグネチャに現れる RPITIT をすべて集め、 その後、各 RPITIT に対して associated_type_for_impl_trait_in_trait を呼び出し、それが新しい関連型を合成します。

impl 内の RPITIT の lowering

同様に、impl の HIR アイテムとともに、各 impl メソッドについて、 その impl メソッドに対応する associated_types_for_impl_traits_in_associated_fn をすべて追加します。 これは associated_type_for_impl_trait_in_impl を呼び出し、 対応するトレイトメソッドに由来する各 RPITIT について、関連型定義を合成します。

新しい関連型の合成

query feeding (TyCtxtAt::create_def) を使用して、各 RPITIT の合成 GAT のために新しい def-id を合成します。

ローカルでは、rustc のほとんどのクエリは、値を計算するためにアイテムの HIR に 対してマッチします。RPITIT には実際には関連付けられた HIR がない、 少なくとも関連型に対応する HIR はないため、多くのクエリを先行して計算し、 それらを feed しなければなりません。たとえば opt_def_kindassociated_itemvisibility、および defaultness です。

これらのクエリのほとんどについて、その値は明らかです。RPITIT は概念的に その情報の大半を親関数から継承する(例: visibility)か、 関連型であるため自明に分かる(opt_def_kind)ためです。

その他のいくつかのクエリはより込み入っているか、feed できません。 そのうち興味深いものを以下に記します:

トレイトに対する generics_of

RPITIT の GAT は概念的に、その由来となる RPIT と同じジェネリクスを継承します。 ただし、ジェネリクスの親がメソッドになるのではなく、トレイトが親になります。

現在のところ、RPIT のジェネリクスとメソッドのジェネリクスを取り、 それらをどちらも新しいジェネリクスリストへフラット化し、各パラメータの def-id を保持することで済ませています。(これにより def-id の親が誤っている 問題が発生する可能性がありますが、最悪の場合でも診断の問題を引き起こすだけです。 これが問題になる場合は、親が GAT であるジェネリックパラメータのために 新しい def-id を合成できます。)

図解例
#![allow(unused)]
fn main() {
trait Foo {
    fn method<'early: 'early, 'late, T>() -> impl Sized + Captures<'early, 'late>;
}
}

次のように脱糖されます…

#![allow(unused)]
fn main() {
trait Foo {
    //       vvvvvvvvv メソッドのジェネリクス
    //                  vvvvvvvvvvvvvvvvvvvvvvvv 不透明型のジェネリクス
    type Gat<'early, T, 'early_duplicated, 'late>: Sized + Captures<'early_duplicated, 'late>;

    fn method<'early: 'early, 'late, T>() -> Self::Gat<'early, T, 'early, 'late>;
}
}
impl に対する generics_of

impl の GAT のジェネリクスは、少し興味深いものです。それらは、 RPITIT 自身のジェネリクス(トレイト定義由来)を、 impl のメソッドのジェネリクスに追加したもので構成されます。これには上記と同じ問題があり、 GAT のジェネリクスには def-id の親が誤っているパラメータが含まれますが、 これは診断においてのみ問題を引き起こすはずです。 もし新しいジェネリクスの def-id を合成するなら、同様にこれを修正できますが、これは後から将来互換のある方法で行えます。おそらく、関心を持った新しいコントリビューターによって行われるでしょう。

opt_rpitit_info

一部のクエリは、explicit_predicates_of のように、先行して与えるとサイクルを引き起こす情報の計算に依存しています。 そのため、RPITIT の GAT に対して正しい値を返す処理は predicates_of プロバイダーに委ねます。これは、クエリの早い段階で opt_rpitit_info を使って関連型が合成されたものかどうかを検出することで行います。このメソッドは、関連型が合成されたものである場合に Some を返します。

その後、explicit_predicates_of のようなクエリの中で、関連型が合成されたものかどうかを次のように検出できます。

#![allow(unused)]
fn main() {
fn explicit_predicates_of(tcx: TyCtxt<'_>, def_id: LocalDefId) -> ... {
    if let Some(rpitit_info) = tcx.opt_rpitit_info(def_id) {
        // RPITIT のための特別な処理を行う...
        return ...;
    }

    // `def_id` の HIR へのアクセスに依存する通常の計算。
}
}
explicit_predicates_of

RPITIT は、それを定義したメソッドの述語を、trait 側と impl 側の両方でコピーすることから始まります。

さらに、「双方向の outlives」述語をインストールします。 具体的には、キャプチャされた各 early-bound ライフタイムについて、両方向の region-outlives 述語を追加し、それが lowering によって生じる複製された early-bound ライフタイムと等しくなるように制約します。これは例で示すのが最も分かりやすいです。

#![allow(unused)]
fn main() {
trait Foo<'a> {
    fn bar() -> impl Sized + 'a;
}

// 次のように脱糖される...

trait Foo<'a> {
    type Gat<'a_duplicated>: Sized + 'a
    where
        'a: 'a_duplicated,
        'a_duplicated: 'a;
    //~^ 具体的には、複製された `'a_duplicated` ライフタイムが
    // 常に `'a` ライフタイムと同期していると
    // 仮定できるべきである。

    fn bar() -> Self::Gat<'a>;
}
}
assumed_wf_types

trait と impl の両方にある GAT は、RPITIT を定義する trait メソッドの assumed_wf_types を継承します。これは、次のコードが lowering されたときに well-formed であることを保証するためです。

#![allow(unused)]
fn main() {
trait Foo {
    fn iter<'a, T>(x: &'a [T]) -> impl Iterator<Item = &'a T>;
}

// 次のように lowering される...

trait FooDesugared {
    type Iter<'a, T>: Iterator<Item = &'a T>;
    //~^ 仮定された WF: `&'a [T]`
    // 仮定された WF 型がなければ、GAT はそれ自体では well-formed にならない。

    fn iter<'a, T>(x: &'a [T]) -> Self::Iter<'a, T>;
}
}

assumed_wf_types はローカル def id に対してのみ定義されているため、外部 trait の RPIT を持つ impl に対して assumed_wf_types を適切に実装するには、RPITIT の仮定された WF 型を extern クエリ assumed_wf_types_for_rpitit にエンコードする必要があります。

型検査

RPITIT 推論アルゴリズム

RPITIT 推論アルゴリズムは collect_return_position_impl_trait_in_trait_tys に実装されています。

高レベル: impl メソッドと trait メソッドが与えられると、trait メソッドを取り、そのシグネチャ内の各 RPITIT を推論変数でインスタンス化します。次に、この trait メソッドのシグネチャを impl メソッドのシグネチャと等置し、その結果として発生するすべての obligation を処理して、メソッド内のすべての RPITIT の型を推論します。

このメソッドは、各 RPITIT の hidden type が impl Trait の境界を実際に満たすこと、つまり impl Trait = Foo と推論した場合に Foo: Trait が成り立つことを確認する役割も担います。

例...
#![allow(unused)]
#![feature(return_position_impl_trait_in_trait)]

fn main() {
use std::ops::Deref;

trait Foo {
    fn bar() -> impl Deref<Target = impl Sized>;
             // ^- RPITIT ?0        ^- RPITIT ?1
}

impl Foo for () {
    fn bar() -> Box<String> { Box::new(String::new()) }
}
}

最終的に、fn() -> ?0 のような trait シグネチャと、ネストされた obligation ?0: Deref<Target = ?1>?1: Sized が得られます。impl シグネチャは fn() -> Box<String> です。

これらのシグネチャを等置すると ?0 = Box<String> が得られ、その後 obligation Box<String>: Deref<Target = ?1> を処理すると ?1 = String が得られ、もう一方の obligation String: Sized は true と評価されます。

アルゴリズムの終了時には、関連型の def-id からシグネチャから推論された具象型へのマッピングが得られます。このマッピングは各 RPITIT について type Assoc = ...= の後に来るべき型を記述しているため、これを使って impl 内の合成関連型に対する type_of を実装できます。

RPITIT hidden type 推論における implied bounds

collect_return_position_impl_trait_in_trait_tys は fulfillment とリージョン解決を行うため、compare_method_predicate_entailment と同じ期待される implied bounds によってリージョン obligation を証明できるよう、これに assumed_wf_types を提供しなければなりません。

メソッドの戻り値型は仮定された WF 型の 1 つであると理解されており、さらに opaque type 推論を行うために戻り値型を推論変数で先行して fold するため、opaque type 推論の後、戻り値型は RPITIT の hidden type を含むように解決されます。これは、RPITIT の hidden type が、それ自体が well-formed であることを独立して証明されることなく、well-formed であると仮定されることを意味します。これにより、 微妙な unsoundness バグ が発生しました。この循環的な推論を防ぐため、代わりにメソッドの戻り値型に含まれる RPITIT の hidden type を placeholder に置き換えます。これにより、implied well-formedness bounds は発生しません。

デフォルト trait 本体

次のようなデフォルト trait 本体を型検査するには、

#![allow(unused)]
fn main() {
trait Foo {
    fn bar() -> impl Sized {
        1i32
    }
}
}

興味深いハックが 1 つ必要です。Foo::bar の param-env に projection 述語をインストールし、RPITIT の GAT が RPITIT の opaque type に正規化されると仮定できるようにする必要があります。これは、trait メソッドと RPITIT の GAT が常に「同期している」という観察に依存しています。つまり、一方がオーバーライドされる場合、もう一方も必ずオーバーライドされます。

これを、上のコードの類似した脱糖と比較してください。次のコードは同じ仮定に依存できないため失敗します。

#![allow(unused)]
#![feature(impl_trait_in_assoc_type)]
#![feature(associated_type_defaults)]

fn main() {
trait Foo {
    type RPITIT = impl Sized;

    fn bar() -> Self::RPITIT {
        01i32
    }
}
}

下流の impl が、理論上は bar の実装を提供せずに RPITIT の実装を提供できるため、失敗します。

error[E0308]: mismatched types
--> src/lib.rs:8:9
 |
5 |     type RPITIT = impl Sized;
 |     ------------------------- associated type defaults can't be assumed inside the trait defining them
6 |
7 |     fn bar() -> Self::RPITIT {
 |                 ------------ expected `<Self as Foo>::RPITIT` because of return type
8 |         01i32
 |         ^^^^^ expected associated type, found `i32`
 |
 = note: expected associated type `<Self as Foo>::RPITIT`
                       found type `i32`

整形式性検査

通常の関連型と同様に、RPITIT の整形式性を検査します。

複製された早期束縛ライフタイムを元のライフタイムに結び付けるライフタイム境界を predicates_of に追加し、RPITIT の由来となるメソッドの WF 型を継承する assumed_wf_types を実装したため(#113704)、通常の GAT であるかのように GAT の WF 検査を行っても問題はありません。

壊れているもの、奇妙なもの、その他

特殊化はかなり壊れています

上で説明した「デフォルト trait メソッド」は特殊化とうまく相互作用しません。なぜなら、これらの射影境界は trait のデフォルトメソッドにのみインストールし、impl メソッドにはインストールしないためです。特殊化はすでにかなり壊れているので詳細には踏み込みませんが、現在これは以下で追跡されているバグです。 * tests/ui/impl-trait/in-trait/specialization-broken.rs

射影には分散がありません

射影には分散がないため、このコードは失敗します。

#![allow(unused)]
#![feature(return_position_impl_trait_in_trait)]

fn main() {
trait Foo {
    // 以下の RPITIT は `'lt` をキャプチャしないことに注意してください。
    fn bar<'lt: 'lt>() -> impl Eq;
}

fn test<'a, 'b, T: Foo>() -> bool {
    <T as Foo>::bar::<'a>() == <T as Foo>::bar::<'b>()
    //~^ ERROR
    // (`'a == 'b` が必要)
}
}

これは、ライフタイムをキャプチャしていない場合でも、<T as Foo>::Rpitit<'a><T as Foo>::Rpitit<'b> を関連付けることができないためです。通常の opaque 型を使用していれば、これは機能します。なぜなら、そのライフタイムパラメーターに関して双変になるためです。

#![allow(unused)]
#![feature(return_position_impl_trait_in_trait)]

fn main() {
fn bar<'lt: 'lt>() -> impl Eq {
    ()
}

fn test<'a, 'b>() -> bool {
    bar::<'a>() == bar::<'b>()
}
}

ただし、RPITIT はいずれにせよ、スコープ内のすべてのライフタイムをキャプチャするようにキャプチャ動作が変更される可能性が高いため、おそらくこれは問題ありません。射影を関連付ける際に RPITIT の分散を考慮するようにすれば、後で前方互換な方法で緩和することもできます。


  1. これは compiler-errors の用語であり、正確だと主張しているわけではありません :^)

Opaque 型におけるリージョン推論の制限

この章では、opaque 型の隠れた型 Opaque<'a, 'b, .., A, B, ..> := SomeHiddenType を定義する際に、そのジェネリック引数へ課しているさまざまな制限について説明します。

これらの制限は、opaque 型推論の最終ステップであるため、借用検査(ソース)で実装されています。

背景: 型および const ジェネリック引数

型引数については、2 つの制限が必要です。各型引数は (1) 型パラメーターであり、 (2) ジェネリック引数の中で一意でなければなりません。 同じことが const 引数にも適用されます。

ケース (1) の例:

#![allow(unused)]
fn main() {
type Opaque<X> = impl Sized;

// `T` は型パラメーターです。
// Opaque<T> := ();
fn good<T>() -> Opaque<T> {}

// `()` は型パラメーターではありません。
// Opaque<()> := ();
fn bad() -> Opaque<()> {} //~ ERROR
}

ケース (2) の例:

#![allow(unused)]
fn main() {
type Opaque<X, Y> = impl Sized;

// `T` と `U` はジェネリック引数内で一意です。
// Opaque<T, U> := T;
fn good<T, U>(t: T, _u: U) -> Opaque<T, U> { t }

// `T` はジェネリック引数内に 2 回出現しています。
// Opaque<T, T> := T;
fn bad<T>(t: T) -> Opaque<T, T> { t } //~ ERROR
}

動機: 最初のケース Opaque<()> := () では、隠れた型は 2 つの異なる解釈、すなわち Opaque<X> := XOpaque<X> := () の両方に適合するため曖昧です。 同様に、2 番目のケース Opaque<T, T> := T では、Opaque<X, Y> := X として解釈すべきか、Opaque<X, Y> := Y として解釈すべきかが曖昧です。 この曖昧さのため、どちらのケースも無効な定義用法として拒否されます。

一意性の制限

各ライフタイム引数は、引数リスト内で一意でなければならず、'static であってはなりません。 これは、型パラメーターの場合と同様に、隠れた型の推論における曖昧さを避けるためです。 たとえば、以下の無効な定義用法 Opaque<'static> := Inv<'static> は、 Opaque<'x> := Inv<'static>Opaque<'x> := Inv<'x> の両方に適合します。

#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
type Inv<'a> = Option<*mut &'a ()>;

fn good<'a>() -> Opaque<'a> { Inv::<'static>::None }

fn bad() -> Opaque<'static> { Inv::<'static>::None }
//~^ ERROR
}
#![allow(unused)]
fn main() {
type Opaque<'x, 'y> = impl Trait<'x, 'y>;

fn good<'a, 'b>() -> Opaque<'a, 'b> {}

fn bad<'a>() -> Opaque<'a, 'a> {}
//~^ ERROR
}

意味的なライフタイム等価性: 型パラメーターと比較した場合のライフタイムの複雑さの 1 つは、 構文上は異なる 2 つのライフタイムが、意味的には等しい場合があることです。 したがって、ライフタイムが一意であることを検証する際には注意が必要です。

#![allow(unused)]
fn main() {
// これも無効です。なぜなら `'a` は *意味的に* `'static` と等しいためです。
fn still_bad_1<'a: 'static>() -> Opaque<'a> {}
//~^ エラーになるはずです!

// これも無効です。なぜなら `'a` と `'b` は *意味的に* 等しいためです。
fn still_bad_2<'a: 'b, 'b: 'a>() -> Opaque<'a, 'b> {}
//~^ エラーになるはずです!
}

一意性ルールの例外

上記の一意性ルールの例外は、opaque 型の定義における境界が、あるライフタイムパラメーターを別のもの、または 'static ライフタイムと等しくすることを要求している場合です。

#![allow(unused)]
fn main() {
// 定義は `'x` が `'static` と等しいことを要求しています。
type Opaque<'x: 'static> = impl Sized + 'x;

fn good() -> Opaque<'static> {}
}

動機: RPIT に対して一意性の制限を実装しようとしたところ、 crater によって発見された破壊的影響が発生しました。 これは、このルールの例外によって緩和できます。 そうしないと破壊的影響を受けるコードの例:

#![allow(unused)]
fn main() {
struct Type<'a>(&'a ());
impl<'a> Type<'a> {
    // `'b == 'a`
    fn do_stuff<'b: 'a>(&'b self) -> impl Trait<'a, 'b> {}
}
}

これが正しい理由: Opaque<'a, 'a> := &'a str のような定義用法については、 Opaque<'x, 'y> := &'x str としても、Opaque<'x, 'y> := &'y str としても解釈でき、どちらでも問題ありません。なぜなら、Opaque のすべての使用は、well-formedness ルールに従って、両方のパラメーターが等しいことを保証するからです。

ユニバーサルライフタイムの制限

opaque 型引数では、普遍量化されたライフタイムのみが許可されます。 これにはライフタイムパラメーターとプレースホルダーが含まれます。

#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;

fn test<'a>() -> Opaque<'a> {
    // `Opaque<'empty> := ()`
    let _: Opaque<'_> = ();
    //~^ ERROR
}
}

動機: これによりライフタイム引数と型引数の振る舞いに一貫性が生まれますが、それは副次的な利点にすぎません。 この制限の本当の理由は純粋に技術的なものであり、メンバー制約アルゴリズムが根本的な制約に直面しているためです。 opaque 型定義 Opaque<'?1> := &'?2 u8 に遭遇すると、 メンバー制約 '?2 member-of ['static, '?1] が登録されます。 アルゴリズムが正しい選択肢を選ぶには、選択肢リージョン ['static, '?1] 間の “outlives” 関係の完全な集合が、リージョン推論を行う前にすでに分かっていなければなりません。 これは、各選択肢リージョンが次のいずれかである場合にのみ満たせます。

  1. ユニバーサルリージョン、すなわち RegionKind::Re{EarlyParam,LateParam,Placeholder,Static}。 なぜなら、ユニバーサルリージョン間の関係は、明示的境界と暗黙的境界から、リージョン推論に先立って完全に分かっているためです。
  2. または、ユニバーサルリージョンと「厳密に等しい」存在リージョン。 厳密なライフタイム等価性は以下で定義され、完全なリージョン推論より前に評価できる唯一の種類の等価性であるため、ここで必要になります。

厳密なライフタイム等価性: 2 つのライフタイムの間に双方向の outlives 制約がある場合、それらは厳密に等しいと言います。 NLL の用語では、これはライフタイムが同じ SCC の一部であることを意味します。 重要なのは、この種類の等価性は完全なリージョン推論の前に評価できることです (ただしもちろん、制約収集の後です)。 もう 1 つの種類の等価性は、リージョン推論の結果、2 つのライフタイム変数が、厳密には等しくなくても同じ値を与えられる場合です。 以前はこの違いを混同していたことについては、#113971 を参照してください。

“once modulo regions” 制限との相互作用 上の例では、シグネチャ内の opaque 型は Opaque<'a> であり、無効な定義用法内のものは Opaque<'empty> であることに注意してください。 提案されている MiniTAIT 計画、すなわち “once modulo regions” ルールでは、 これはすでに許可されていません。 「ユニバーサルライフタイム」の制限は「MiniTAIT」の制限から論理的に導かれるため冗長になるように見えるかもしれませんが、ライフタイム等価性とクロージャに関する後続の関連議論は引き続き重要です。

クロージャの制限

opaque 型がクロージャ/コルーチン/inline-const 本体内で定義されている場合、そのクロージャに対して「外部」であるユニバーサルライフタイムは、opaque 型引数では許可されません。 外部リージョンは [RegionClassification::External][source-external-region] で定義されています。 [source-external-region]: https://github.com/rust-lang/rust/blob/caf730043232affb6b10d1393895998cb4968520/compiler/rustc_borrowck/src/universal_regions.rs#L201.

例: (これは現在の nightly ではたまたまコンパイルされますが、より実用的な例は すでに分かりにくいエラーで reject されています。)

#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;

fn test<'a>() -> Opaque<'a> {
    let _ = || {
        // `'a` はクロージャーの外部にある
        let _: Opaque<'a> = ();
        //~^ エラーになるべき!
    };
    ()
}
}

動機: クロージャー本体では、外部ライフタイムは「ユニバーサル」ライフタイムとして分類されるものの、 それらの間の関係が事前には分からないという点で、存在ライフタイムに近い振る舞いをします。 代わりに、それらの値は存在ライフタイムと同様に推論され、要件は親 fn に伝播されます。 これは上で説明したメンバー制約アルゴリズムを壊します:

アルゴリズムが正しい選択肢を選べるようにするには、選択肢のリージョン ['static, '?1] 間の 「outlives」関係の完全な集合が、リージョン推論を行う前にすでに分かっていなければならない

これがどのように起こるかを詳しく示す例を以下に示します:

#![allow(unused)]
fn main() {
type Opaque<'x, 'y> = impl Sized;

//
fn test<'a, 'b>(s: &'a str) -> impl FnOnce() -> Opaque<'a, 'b> {
    move || { s }
    //~^ ERROR `Opaque<'_, '_>` の隠れた型が、境界に現れないライフタイムをキャプチャしている
}

// 上のクロージャー本体は、おおよそ次のように desugar される:
fn test::{closure#0}(_upvar: &'?8 str) -> Opaque<'?6, '?7> {
    return _upvar
}

// ここで `['?8, '?6, ?7]` はクロージャーの *外部* にあるユニバーサルライフタイムです。
// クロージャーの *内部* では、それらの間に既知の関係はありません。
// しかし親 fn では、`'?6: '?8` であることが分かっています。
//
// opaque 定義 `Opaque<'?6, '?7> := &'8 str` に遭遇したとき、
// メンバー制約アルゴリズムには `?8 = '?6` と安全に判断するための十分な情報がありません。
// このため、妥当なメッセージでエラーになります:
// "hidden type captures lifetime that does not appear in bounds".
}

これらの制限がないと、エラーメッセージが分かりにくくなり、さらに重要なこととして、 クロージャー内のメンバー制約は非常に壊れているため、将来壊れる可能性が高いコードを 受け入れてしまうリスクがあります。

出力型: これが実際のコードで問題を引き起こす最も一般的なシナリオは、 クロージャー/async ブロックの出力型だと思います。クロージャーと async ブロックの間には、 この問題をさらに示す不一致があることに注意する価値があります。これは future にのみ適用される replace_opaque_types_with_inference_vars のハック に起因しています。

#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> impl FnOnce() -> Opaque<'a> {
    // クロージャーの出力型は Opaque<'a>
    // -> 隠れた型の定義はクロージャーの *内部* で起こる
    // -> reject される。
    move || {}
    //~^ ERROR ジェネリックなライフタイムパラメータが期待されたが、`'_` が見つかった
}
}
#![allow(unused)]
fn main() {
use std::future::Future;
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> impl Future<Output = Opaque<'a>> {
    // async ブロックの出力型はユニット `()`
    // -> 隠れた型の定義は親 fn で起こる
    // -> 受け入れられる。
    async move {}
}
}

エフェクト、const trait、および const 条件チェック

HostEffect 述語

HostEffectPredicate は、[const] Tr または const Tr 境界に由来する述語の一種です。 これは trait 参照と、境界に応じて Maybe または Const になり得る constness を持ちます。 [const] Tr、より正確には Maybe 境界は、 それが置かれているコンテキストに応じて異なる適用のされ方をするため、通常の境界とは異なる 振る舞いをします。 T: Tr のような関数上の通常の trait 境界は、関数が呼び出されるときに証明され、 関数内で仮定されるものとして predicates_of クエリ内に収集されますが、 T: [const] Tr のような境界は通常の trait 境界として振る舞い、predicates_of の結果に T: Tr を追加する一方で、const_conditions クエリにも HostEffectPredicate を追加します。

一方、T: const Tr 境界はコンテキストをまたいでも意味が変わらないため、 predicates_of には HostEffect(T: Tr, const) が追加され、 const_conditions には追加されません。

const_conditions クエリ

predicates_of は、ある item を使用するために証明する必要がある述語の集合を表します。 たとえば、以下の例で foo を使用するには次のようにします。

#![allow(unused)]
fn main() {
fn foo<T>() where T: Default {}
}

TDefault を実装していることを証明できなければなりません。 同様に、 const_conditions は、ある item を const コンテキスト内で 使用するために 証明する必要がある述語の集合を表します。上の例を const trait 境界を使うように調整すると、次のようになります。

#![allow(unused)]
fn main() {
const fn foo<T>() where T: [const] Default {}
}

この場合、fooconst_conditions クエリ内に HostEffect(T: Default, maybe) を持つことになり、 const コンテキストから foo を呼び出すためには、TDefault の const 実装を持つことを 証明しなければならないことを示します。

const_conditions の強制

const_conditions は現在、さまざまな場所でチェックされています。

const コンテキスト(const fnconst item を含む)からの HIR 内のすべての呼び出しでは、 呼び出している関数の const_conditions が成り立つことをチェックします。 これは FnCtxt::enforce_context_effects で行われます。 以下のコードはコンパイルされる必要があるため、 関数が参照されているだけで呼び出されていない場合はチェックしないことに注意してください。

#![allow(unused)]
fn main() {
const fn hi<T: [const] Default>() -> T {
    T::default()
}
const X: fn() -> u32 = hi::<u32>;
}

trait impl が well-formed であるためには、impl の環境から trait の const_conditions を証明できなければなりません。 これは wfcheck::check_impl でチェックされます。

例を示します。

#![allow(unused)]
fn main() {
const trait Bar {}
const trait Foo: [const] Bar {}
// `const_conditions` には `HostEffect(Self: Bar, maybe)` が含まれる

impl const Bar for () {}
impl const Foo for () {}
// ^ ここで impl が well-formed であるための `const_conditions` をチェックする
}

trait impl のメソッドは、それが実装している trait のメソッドよりも厳しい境界を持ってはなりません。 メソッドに互換性があることをチェックするために、 impl の述語に trait メソッドの述語を加えたハイブリッド環境が構築され、 impl メソッドの述語を証明しようとします。 const_conditions についても同じことを行います。

#![allow(unused)]
fn main() {
const trait Foo {
    fn hi<T: [const] Default>();
}

impl<T: [const] Clone> Foo for Vec<T> {
    fn hi<T: [const] PartialEq>();
    // ^ `T: [const] Clone` と `T: [const] Default` が与えられても
    // `T: [const] PartialEq` を証明できないため、impl 上のメソッドが
    // trait 上のメソッドよりも厳しいことがわかる。
}
}

これらのチェックは compare_method_predicate_entailment で行われます。 関連型に対して同じチェックを行う類似の関数は compare_type_predicate_entailment と呼ばれます。 これらはどちらも、const コンテキスト内では const_conditions を考慮する必要があります。

MIR では、const チェックの一部として、呼び出される item の const_conditionsChecker::revalidate_conditional_constness で再度検証されます。

関連型と trait における explicit_implied_const_bounds

以下のような関連型、不透明型、および supertrait 上の境界は、 その境界が異なる形で表現されます。

#![allow(unused)]
fn main() {
trait Foo: [const] PartialEq {
    type X: [const] PartialEq;
}

fn foo() -> impl [const] PartialEq {
    // ^ 未実装の構文
}
}

const_conditions は呼び出し元に対して証明される必要があり、 定義内(たとえば関数上の trait 境界)では仮定できますが、これらの境界は定義時(impl 内、 または opaque を返すとき)に証明される必要があり、呼び出し元では仮定できます。 これらの境界の非 const 版は explicit_item_bounds と呼ばれます。

これらの境界は、HIR typeck では compare_impl_item::check_type_bounds、古い solver では evaluate_host_effect_from_item_bounds、 新しい solver では consider_additional_alias_assumptions でチェックされます。

HostEffectPredicate の証明

HostEffectPredicate は [old solver] と [new trait solver] の両方で実装されています。 一般に、次のいずれかの条件を満たす場合に HostEffect 述語を証明できます。

* 述語は呼び出し元の境界から仮定できます。
* 型がそのトレイトに対する `const` `impl` を持ち、*かつ* その impl 上の const 条件が
  成立し、*かつ* そのトレイトの `explicit_implied_const_bounds` が成立する場合。または
* 型が const コンテキストにおいてそのトレイトの組み込み実装を持つ場合。
  たとえば、`Fn` は、その const 条件が満たされていれば関数アイテムによって実装されることがあり、
  また `Destruct` は、その型をコンパイル時にドロップできる場合に const コンテキストで実装されます。

[old solver]: https://doc.rust-lang.org/nightly/nightly-rustc/src/rustc_trait_selection/traits/effects.rs.html
[new trait solver]: https://doc.rust-lang.org/nightly/nightly-rustc/src/rustc_next_trait_solver/solve/effect_goals.rs.html

## const トレイトの詳細

後で拡充予定です。

### `#[rustc_non_const_trait_method]` 属性

これは内部(標準ライブラリ)での使用のみを意図しています。
この属性をトレイトメソッドに適用すると、コンパイラはこのメソッドのデフォルト本体が
コンパイル時に実行可能かどうかをチェックしません。
そのトレイトのユーザーも、このトレイトメソッドを const コンテキストで使用することは許可されません。
この属性は主に、
`Iterator` のような大規模なトレイトを、そのすべてのメソッドを同時に `const` にすることなく
const 化するために使用されます。

この属性は、トレイトを `const` として安定化する際には存在していてはなりません。

パターンと網羅性の検査

Rust では、パターンマッチングと束縛に、いくつかの非常に役立つ性質があります。 コンパイラは、束縛が行われるときにそれが反駁不能であることと、match アームが 網羅的であることを検査します。

パターンの有用性

有用性検査が答える中心的な問いは次のものです。 「この match 式において、その分岐は冗長か?」。 より正確には、すでに見たパターンのリストが与えられたときに、 ある新しいパターンが何らかの新しい値にマッチする可能性があるかを 計算することに帰着します。

たとえば、次の match 式では、 各パターンが、その上にあるパターンにマッチしなかった何かに マッチする可能性があるかを順に尋ねます。 ここでは、4 番目のパターンが 1 番目のパターンと冗長であることがわかります。 その分岐には「到達不能」の警告が出ます。 3 番目のパターンが有用かどうかは、 FooBar 以外のバリアントがあるかどうかに依存します。 最後に、ワイルドカードパターン(_)が その match 内のすべてのパターンのリストに対して有用かどうかを尋ねることで、 match 全体が網羅的かどうかを尋ねることができます。 ここでは _ が有用である((false, None) を捕捉する)ことがわかります。 したがって、この式には「網羅的でない match」エラーが出ます。

#![allow(unused)]
fn main() {
// x: (bool, Option<Foo>)
match x {
    (true, _) => {} // 1
    (false, Some(Foo::Bar)) => {} // 2
    (false, Some(_)) => {} // 3
    (true, None) => {} // 4
}
}

したがって、有用性は 2 つの目的で使われます。 到達不能コードの検出(これはユーザーにとって有用です)と、 match が網羅的であることの保証(これは健全性にとって重要です。 なぜなら match 式は値を返すことができるためです)。

どこで行われるか

この検査は、パターンを書ける場所ならどこでも行われます。match 式、if letlet else、 通常の let、関数引数です。

#![allow(unused)]
fn main() {
// `match`
// 有用性は到達不能な分岐を検出し、網羅的でない match を禁止できます。
match foo() {
    Ok(x) => x,
    Err(_) => panic!(),
}

// `if let`
// 有用性は到達不能な分岐を検出できます。
if let Some(x) = foo() {
    // ...
}

// `while let`
// 有用性は無限ループとデッドループを検出できます。
while let Some(x) = it.next() {
    // ...
}

// 分解 `let`
// 有用性は網羅的でないパターンを禁止できます。
let Foo::Bar(x, y) = foo();

// 関数引数の分解
// 有用性は網羅的でないパターンを禁止できます。
fn foo(Foo { x, y }: Foo) {
    // ...
}
}

アルゴリズム

網羅性検査は、MIR の構築前に check_match で実行されます。 これは rustc_pattern_analysis クレートに実装されており、 アルゴリズムの中核は usefulness モジュールにあります。 そのファイルには、アルゴリズムの詳細な説明が含まれています。

重要な概念

コンストラクターとフィールド

Pair(Some(0), true) において、Pair はその値のコンストラクターと呼ばれ、Some(0)true はそのフィールドです。マッチ可能なすべての値は、この方法で分解できます。 コンストラクターの例には、SomeNone(,)(2 タプルコンストラクター)、 Foo {..}(構造体 Foo のコンストラクター)、および 2(数値 2 のコンストラクター)があります。

各コンストラクターは固定数のフィールドを取ります。これはそのアリティと呼ばれます。Pair(,) の アリティは 2、Some のアリティは 1、None42 のアリティは 0 です。各型には既知の コンストラクター集合があります。一部の型には多くのコンストラクター(u64 など)があり、さらには 無限個のコンストラクター(&str&[T] など)を持つものもあります。

パターンも似ています。Pair(Some(_), _) はコンストラクター Pair と 2 つのフィールドを持ちます。 違いは、いくつかのパターン専用コンストラクターが追加されることです。具体的には、ワイルドカード _、 変数束縛、0..=10 のような整数範囲、[_, .., _] のような可変長スライスです。 or パターンは別に扱います。

さて、値 v がパターン p にマッチするかを検査するには、v のコンストラクターが p の コンストラクターにマッチするかを検査し、必要であればそれらのフィールドを再帰的に比較します。 代表的な例をいくつか示します。

  • matches!(v, _) := true
  • matches!((v0, v1), (p0, p1)) := matches!(v0, p0) && matches!(v1, p1)
  • matches!(Foo { a: v0, b: v1 }, Foo { a: p0, b: p1 }) := matches!(v0, p0) && matches!(v1, p1)
  • matches!(Ok(v0), Ok(p0)) := matches!(v0, p0)
  • matches!(Ok(v0), Err(p0)) := false(互換性のないバリアント)
  • matches!(v, 1..=100) := matches!(v, 1) || ... || matches!(v, 100)
  • matches!([v0], [p0, .., p1]) := false(互換性のない長さ)
  • matches!([v0, v1, v2], [p0, .., p1]) := matches!(v0, p0) && matches!(v2, p1)

この概念は、パターン解析において絶対的に中心となるものです。constructor モジュールは、 コンストラクターを抽出、列挙、操作するための関数を提供します。これは十分に有用な概念であるため、 その変種はコンパイラの他の場所にも見られます。たとえば、match 式の MIR lowering や いくつかの clippy lint などです。

コンストラクターのグループ化と分割

パターン専用コンストラクター(_、範囲、可変長スライス)は、それぞれ通常のコンストラクターの集合を表します。 たとえば、_: Option<T> は集合 {None, Some} を表し、[_, .., _] は アリティ >= 2 のスライスコンストラクターの無限集合 {[,], [,,], [,,,], …} を表します。

これらのコンストラクターを管理するために、可能な限りグループ化したまま保持します。たとえば次のようにします。

#![allow(unused)]
fn main() {
match (0, false) {
    (0 ..=100, true) => {}
    (50..=150, false) => {}
    (0 ..=200, _) => {}
}
}

この例では、01、..、49 はすべて同じアームにマッチするため、1 つのグループとして扱うことができます。 実際、この match で考慮する必要がある範囲は 0..5050..=100101..=150151..=200201.. だけです。同様に次の場合です。

#![allow(unused)]
fn main() {
enum Direction { North, South, East, West }
let wind = (Direction::North, 0u8);
match wind {
    (Direction::North, 50..) => {}
    (_, _) => {}
}
}

ここでは、North ではないすべてのコンストラクターを 1 つのグループとして扱えるため、扱うべきケースは North と、それ以外のすべてのもの、の 2 つだけになります。

これは「コンストラクター分割」と呼ばれ、網羅性検査を妥当な時間で実行するために不可欠です。

空の型が存在する場合の有用性と到達可能性

これはおそらく網羅性において最も微妙な側面です。完全に正確に言えば、match は 値に対して動作するのではなく、場所に対して動作します。特定の unsafe な状況では、場所が その型に対して有効なデータを含まないことがあり得ます。これは空の型に対して微妙な結果をもたらします。 次を見てください。

#![allow(unused)]
fn main() {
enum Void {}
let x: u8 = 0;
let ptr: *const Void = &x as *const u8 as *const Void;
unsafe {
    match *ptr {
        _ => println!("Reachable!"),
    }
}
}

この例では、ptr は無効なデータを持つ場所を指している有効なポインターです。_ パターンは 場所 *ptr の内容を見ないため、このコードは問題なく、アームが選択されます。言い換えると、 検査している場所の型が Void であるにもかかわらず、到達可能なアームがあります。一方で、 そのアームに束縛がある場合は次のようになります。

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
enum Void {}
let x: u8 = 0;
let ptr: *const Void = &x as *const u8 as *const Void;
unsafe {
match *ptr {
    _a => println!("Unreachable!"),
}
}
}

ここでは、その束縛が *ptr という場所から型 Void の値をロードします。この例では、データが有効ではないため、 これは UB を引き起こします。一般の場合では、これは *ptr にあるデータの妥当性を表明します。 いずれにせよ、このアームが選択されることは決してありません。 最後に、空の match match *ptr {} について考えてみましょう。これが網羅的であると考えるなら、 *ptr に無効なデータがあることは無効です。言い換えると、空の match は意味論的には _a => ... match と同等です。明示性の観点から、アームがあるケースを好むため、 ユーザーに _a アームを削除するようには伝えません。言い換えると、_a アームは 到達不能ではあるものの、冗長ではありません。これが、lint が “unreachable” と言っているにもかかわらず、 到達不能なアームではなく冗長なアームに対して lint を行う理由です。

これらの考慮事項は、特定の場所、すなわち UB なしに非有効なデータを含み得る場所にのみ影響します。 それらは、ポインタのデリファレンス、参照のデリファレンス、union フィールドアクセスです。網羅性チェック中に、 与えられた場所が有効なデータを含むことが既知かどうかを追跡します。

以上を踏まえても、現在の網羅性チェックの実装は上記の考慮事項に従っていません。stable では、 空型はほとんどの場合、非空として扱われます。 exhaustive_patterns 機能は逆方向に誤っています。unsafe な状況で到達可能になり得るアームの省略を 許可してしまいます。never_patterns 実験的機能は、これを修正し、 パターン内の空型の正しい振る舞いを許可することを目指しています。

unsafe 性チェック

Rust の特定の式はメモリ安全性に違反する可能性があるため、unsafe ブロックまたは関数の内部に置く必要があります。 コンパイラは、対応する unsafe 操作がないのに unsafe ブロックが使用されている場合にも警告します。

概要

unsafe 性チェックは check_unsafety モジュールにあります。 これは、関数とそのすべてのクロージャおよびインライン定数の THIR を走査します。 unsafe コンテキスト、つまり unsafe ブロックに入ったかどうかを追跡します。 unsafe 操作が unsafe ブロックの外で使用された場合は、エラーが報告されます。 unsafe 操作が unsafe ブロック内で使用された場合、 そのブロックは unused_unsafe lint のために使用済みとしてマークされます。

unsafe 性チェックには型情報が必要なので、 HIR 上で typeck の結果、THIR、または MIR を利用して行うことも潜在的には可能です。 THIR が選ばれているのは、 HIR よりも考慮すべきケースが少ないためです。たとえば unsafe 関数呼び出しと unsafe メソッド呼び出しは THIR では同じ表現になります。 このチェックが MIR 上で行われないのは、安全性チェックが制御フローに依存しないため、 MIR を使用する必要がないからです。 また、MIR には一部の式について十分に正確な span がありません。

ほとんどの unsafe 操作は、THIR の ExprKind を確認し、 引数の型を確認することで識別できます。 たとえば、生ポインタのデリファレンスは、 生ポインタ型を持つ引数を伴う ExprKind::Deref に対応します。

unsafe な Union フィールドアクセスを探すのは、少し複雑です。union のフィールドへの書き込みは安全だからです。 チェッカーは、代入式の左辺を訪問しているタイミングを追跡し、 union フィールドがそこに直接現れることを許可する一方で、 それ以外のすべての場合にはエラーにします。 Union フィールドアクセスはパターン内にも現れる可能性があるため、それらも走査する必要があります。

unused_unsafe lint

unused_unsafe lint は、削除可能な unsafe ブロックを報告します。 unsafe 性チェッカーは、unsafe を必要とする操作を見つけるたびに記録します。 その後、次のいずれかの場合に lint が報告されます。

  • unsafe ブロックに unsafe 操作が含まれていない
  • unsafe ブロックが別の unsafe ブロック内にあり、外側のブロックが未使用と見なされていない
#![allow(unused)]
#![deny(unused_unsafe)]
fn main() {
let y = 0;
let x: *const u8 = core::ptr::addr_of!(y);
unsafe { // このブロックに対して lint が報告される
    unsafe {
        let z = *x;
    }
    let safe_expr = 123;
}
unsafe {
    unsafe { // このブロックに対して lint が報告される
        let z = *x;
    }
    let unsafe_expr = *x;
}
}

unsafe を伴うその他のチェック

Unsafe traits は実装するために unsafe impl を必要とし、このチェックは coherence の一部として行われます。 unsafe_code lint は、unsafe ブロック、関数、実装、および特定の unsafe 属性を検索する ast 上の lint パスとして実行されます。

データフロー解析

MIR に取り組む場合、さまざまな種類の データフロー解析に頻繁に出くわすことになります。rustc はデータフローを使用して、未初期化変数を見つけたり、generator の yield 文をまたいでどの変数が生存しているかを判定したり、制御フローグラフ内の特定の時点でどの Place が借用されているかを計算したりします。データフロー解析は現代のコンパイラにおける基本的な概念であり、この主題に関する知識は、将来のコントリビューターにとって役立つでしょう。

ただし、このドキュメントはデータフロー解析の一般的な入門ではありません。 これは、rustc でこれらの解析を定義するために使用されるフレームワークの説明にすぎません。このドキュメントは、読者が中核となる考え方や、「転送関数」、「不動点」、「束」といった基本的な用語に精通していることを前提としています。 これらの用語になじみがない場合や、手早く復習したい場合は、Anders Møller と Michael I. Schwartzbach による Static Program Analysis が、無料で利用できる優れた教科書です。視聴覚による学習を好む方には、以前は Goethe University Frankfurt による YouTube の短い講義シリーズを推奨していましたが、その後削除されました。 背景についてはこの PR を、代替講義についてはこのコメント を参照してください。

データフロー解析の定義

データフロー解析は Analysis トレイトによって定義されます。データフロー状態の型に加えて、このトレイトは各ブロックへの入口におけるその状態の初期値と、解析の方向(順方向または逆方向)を定義します。データフロー解析のドメインは、適切に振る舞う join 演算子を持つ(厳密には結合半束)でなければなりません。詳細については、lattice モジュールのドキュメント、および JoinSemiLattice トレイトを参照してください。

転送関数と効果

rustc のデータフローフレームワークでは、基本ブロック内の各文(および終端子)が独自の転送関数を定義できます。簡潔にするため、これら個々の転送関数は「効果」と呼ばれます。各効果はデータフロー順に順次適用され、それらが合わさって基本ブロック全体の転送関数を定義します。一部の終端子の特定の出力エッジに対して効果を定義することも可能です(例: Call 終端子の success エッジに対する apply_call_return_effect)。これらはまとめて「エッジごとの効果」と呼ばれます。

「前」効果

ドキュメントを注意深く読んだ読者は、各文と終端子には実際には 2 つ の可能な効果、すなわち「前」効果と、接頭辞なし(または「主要」)効果があることに気づくかもしれません。「前」効果は、解析の方向に関係なく、接頭辞なし効果の直前に適用されます。 言い換えると、逆方向解析は、順方向解析と同様に、基本ブロックの転送関数を計算するときに「前」効果を適用し、その後「主要」効果を適用します。

解析の大多数は、接頭辞なし効果のみを使用すべきです。各文に複数の効果があると、利用者にとってどこを見るべきかが分かりにくくなります。ただし、「前」バリアントは、代入文の右辺の効果を左辺とは別に考慮しなければならない場合など、いくつかのシナリオで有用です。

収束

解析は「不動点」に収束しなければなりません。そうでなければ、永遠に実行され続けます。 不動点に収束するとは、「平衡に達する」と言い換えることができます。 平衡に達するためには、解析はいくつかの法則に従わなければなりません。従わなければならない法則の 1 つは、ボトム値1 と他の値を結合すると、その 2 つ目の値に等しくなるというものです。式で表すと、次のようになります。

bottom join x = x

もう 1 つの法則は、解析が「トップ値」を持たなければならないというもので、次のようになります。

top join x = top

トップ値があることで、半束が有限の高さを持つことが保証されます。また、上に述べた法則により、データフロー状態がいったんトップに達すると、それ以上変化しなくなることが保証されます(不動点はトップになります)。

簡単な例

このセクションでは、単純なデータフロー解析の簡単な例を高レベルで示します。 知っておく必要のあるすべてを説明するわけではありませんが、このページの残りの部分をより明確に理解できるようになることを期待しています。

プログラム内のある時点までに mem::transmute が呼び出された可能性があるかどうかを調べる、単純な解析を行いたいとしましょう。解析ドメインは、これまでに transmute が呼び出されたかどうかを記録する bool だけにします。デフォルトでは transmute は呼び出されていないため、ボトム値は false になります。transmute が呼び出されたと判定した時点で解析は完了するため、トップ値は true になります。結合演算子は単に論理 OR(||)演算子にします。AND ではなく OR を使用するのは、次のようなケースがあるためです。

#![allow(unused)]
fn main() {
unsafe fn example(some_cond: bool) {
let x = if some_cond {
    std::mem::transmute::<i32, u32>(0_i32) // transmute が呼び出された!
} else {
    1_u32 // transmute は呼び出されていない
};

// この時点までに transmute は呼び出されたか? 保守的に yes と近似する。
// そのため、OR 演算子を使用する。
println!("x: {}", x);
}
}

データフロー解析の結果を調べる

解析を構築したら、iterate_to_fixpoint を呼び出す必要があります。これは Results を返し、Results には各ブロックの入口における不動点でのデータフロー状態が含まれています。Results が得られたら、CFG 内の任意の時点で、不動点におけるデータフロー状態を調べることができます。少数の位置(例: 各 Drop 終端子)でのみ状態が必要な場合は、ResultsCursor を使用します。すべての位置で状態が必要な場合は、ResultsVisitor の方が効率的です。

                         Analysis
                            |
                            | iterate_to_fixpoint()
                            |
                         Results
                         /     \
 into_results_cursor(…) /       \  visit_with(…)
                       /         \
               ResultsCursor  ResultsVisitor

たとえば、次のコードは ResultsVisitor を使用します…

// `MyVisitor` が `ResultsVisitor<FlowState = MyAnalysis::Domain>` を実装していると仮定する...
let mut my_visitor = MyVisitor::new();

// RPO 内のすべてのブロック内のすべての位置について、不動点状態を調べる。
let results = MyAnalysis::new()
    .iterate_to_fixpoint(tcx, body, None);
results.visit_with(body, &mut my_visitor);`

一方、このコードは ResultsCursor を使用します。

let mut results = MyAnalysis::new()
    .iterate_to_fixpoint(tcx, body, None);
    .into_results_cursor(body);

// 各 `Drop` 終端子の直前の不動点状態を調べる。
for (bb, block) in body.basic_blocks().iter_enumerated() {
    if let TerminatorKind::Drop { .. } = block.terminator().kind {
        results.seek_before_primary_effect(body.terminator_loc(bb));
        let state = results.get();
        println!("state before drop: {:#?}", state);
    }
}

Graphviz 図

データフロー解析の結果が期待どおりでない場合、それらを可視化すると役に立つことがよくあります。これは Debugging MIR で説明されている -Z dump-mir フラグを使って行えます。 まずは -Z dump-mir=F -Z dump-mir-dataflow から始めてください。ここで F は “all”、または関心のある MIR 本体の名前です。

これらの .dot ファイルは mir_dump ディレクトリに保存され、ファイル名の一部として 解析の NAME(例: maybe_inits)を含みます。各可視化では、 各ブロックの入口と出口における完全なデータフロー状態に加えて、 各文およびターミネータで発生するあらゆる変更が表示されます。以下の例を参照してください。

データフロー解析の graphviz ダイアグラム


  1. ボトム値の主な目的は、初期データフロー状態としての役割です。 各基本ブロックの入口状態は、解析開始前にボトムに初期化されます。

Drop の詳細化

動的 drop

リファレンスによると、次のとおりです。

初期化済みの変数または一時値がスコープを抜けると、そのデストラクタが 実行される、つまり drop されます。代入も、その左辺オペランドが初期化済みであれば、 そのデストラクタを実行します。変数が部分的に 初期化されている場合、初期化済みのフィールドだけが drop されます。

MIR を構築するとき、Drop および DropAndReplace 終端命令は drop が発生し得る場所を表します。しかし、このフェーズでは、これらの 終端命令が存在しても、デストラクタが実行されることは保証されません。これは、 drop の対象が終端命令に到達する前に未初期化になっている可能性があるためです (通常は、そこからムーブされているためです)。一般に、変数が初期化されているかどうかを コンパイル時に知ることはできません。

#![allow(unused)]
fn main() {
let mut y = vec![];

{
    let x = vec![1, 2, 3];
    if std::process::id() % 2 == 0 {
        y = x; // 条件付きで `x` を `y` にムーブする
    }
} // ここで `x` はスコープを抜ける。drop されるべきか?
}

このような場合、変数が初期化されているかどうかを 動的に追跡する必要があります。ルールの詳細は RFC 320: Non-zeroing dynamic drops に示されています。

Drop 義務

RFC から引用します。

ローカル変数が初期化されると、「drop 義務」の集合、つまり drop される必要がある構造的パス(たとえばローカルの a や、 フィールドへのパス b.f.y)の集合が確立されます。

構造体型 T のローカル変数 x に対する drop 義務は、 T の構造を分析することで計算されます。T 自体が Drop を実装している場合、 x が drop 義務になります。TDrop を実装していない場合、 drop 義務の集合は T のフィールドの drop 義務の和集合になります。

構造的パスからムーブされる(したがって未初期化になる)と、そのパスまたはその子孫 (path.fpath.f.g.h など)に対する drop 義務はすべて解放されます。Drop 実装を持つ型では個々の フィールドからのムーブが許可されないため、それらを通じて初期化状態を追跡する必要はありません。

ローカル変数がスコープを抜ける(Drop)とき、または構造的パスが 代入によって上書きされる(DropAndReplace)とき、その変数またはパスに対する drop 義務があるかどうかを確認します。その時点までに義務が 解放されていない限り、関連付けられた Drop 実装が呼び出されます。 enum 型については、「アクティブな」バリアントに対応するフィールドだけを drop する必要があります。このような型の drop 義務を処理する場合、まず 判別子を確認してアクティブなバリアントを決定します。アクティブなもの以外のバリアントに対する drop 義務はすべて無視されます。

これらのルールを説明するために、いくつか興味深い型を示します。

#![allow(unused)]
fn main() {
struct NoDrop(u8); // `Drop` 実装はない。`Drop` 実装を持つフィールドもない。

struct NeedsDrop(Vec<u8>); // `Drop` 実装はないが、`Drop` 実装を持つフィールドがある。

struct ThinVec(*const u8); // カスタム `Drop` 実装。個々のフィールドからはムーブできない。

impl Drop for ThinVec {
    fn drop(&mut self) { /* ... */ }
}

enum MaybeDrop {
    Yes(NeedsDrop),
    No(NoDrop),
}
}

Drop の詳細化

これらのルールの有効なモデルの 1 つは、関数内のどこかで使用される すべての構造的パスに対して真偽値フラグ(「drop フラグ」)を保持することです。このフラグは、 そのパスが初期化されるとセットされ、そのパスからムーブされるとクリアされます。 Drop が発生すると、Drop の対象に関連付けられたすべての義務について フラグを確認し、まだ適用可能なものについては関連付けられた Drop 実装を呼び出します。

このプロセス、つまり不正確な Drop および DropAndReplace 終端命令を持つ新しく構築された MIR を、drop フラグを持つものへ 変換することは、drop の詳細化として知られています。MIR 文によって変数が初期化済み (または未初期化)になると、drop の詳細化はその変数の drop フラグをセット(またはクリア)するコードを挿入します。これは、新しく挿入された drop フラグを確認する 条件分岐で Drop 終端命令をラップします。

drop の詳細化はまた、DropAndReplace 終端命令を、対象の Drop と、 新しく drop された place への書き込みに分割します。これは、上で説明した内容とはやや無関係です。

これが完了すると、MIR 内の Drop 終端命令は、drop される place の型に対する 「drop glue」または「drop shim」の呼び出しに対応します。ある型の drop glue は、その型の Drop 実装(存在する場合)を呼び出し、その後 その型のすべてのフィールドに対して再帰的に drop glue を呼び出します。

rustc における Drop の詳細化

上で説明したアプローチは、必要以上にコストが高くなります。いくつかの最適化を 考えることができます。

  • Drop の対象である(またはその対象を接頭辞として持つ)パスだけが drop フラグを必要とします。
  • いくつかの変数は、drop される時点で初期化済み(または未初期化)であることが 既知です。これらには drop フラグは不要です。
  • パスの集合が共有された接頭辞を通じてのみ drop されたりムーブされたりする場合、それらの パスは単一の drop フラグを共有できます。

これらの一部は rustc に実装されています。

コンパイラでは、drop の詳細化はいくつかのモジュールに分割されています。パス 自体はここで定義されていますが、主なロジックは 別の場所で定義されています。これは、drop shim の構築にも使用されるためです。

drop の詳細化は、新しく構築された MIR 内の各 Drop を 4 種類のいずれかとして 指定します。

  • Static、対象は常に初期化済みです。
  • Dead、対象は常に初期化です。
  • Conditional、対象は全体として初期化済み、または全体として 未初期化のどちらかです。部分的に初期化されていることはありません。
  • Open、対象は部分的に初期化されている可能性があります。

このために、MaybeInitializedPlacesMaybeUninitializedPlaces という 1 組のデータフロー解析を使用します。place が一方に含まれていて他方に含まれていない場合、 対象の初期化状態はコンパイル時に既知です(Dead または Static)。 この場合、drop の詳細化は対象にフラグを追加しません。単に Drop 終端命令を削除(Dead)または保持(Static)します。

Conditional drop については、変数全体の初期化状態が そのフィールドの初期化状態と同じであることがわかっています。したがって、その drop の対象に対する drop フラグを生成すれば、その対象の drop glue を呼び出しても安全です。

Open drop

Open drop は最も複雑です。単一の Drop 終端命令を、対象のフィールドのうち型が drop glue を持つもの (Ty::needs_drop)ごとに、それぞれ異なる複数の終端命令へ分解する必要があるためです。対象自体の drop glue を呼び出すことはできません。なぜなら、それには対象のすべてのフィールドが初期化されている必要があるからです。 カスタム Drop 実装を持つ型の変数では、そのフィールドからムーブできないため、Open drop は許可されないことを思い出してください。

これは、各フィールドを再帰的に DeadStaticConditional、または Open として分類することで実現されます。型が drop glue を持たないフィールドは 自動的に Dead になり、再帰の間に考慮する必要はありません。 種類が Open ではないフィールドに到達した場合は、上で行ったのと同様に処理します。その フィールドも Open である場合、再帰は継続します。 enum の Open ドロップをどのように扱うかは注目に値します。drop elaboration の内部では、 enum の各バリアントはフィールドのように扱われ、それらの「バリアントフィールド」のうち 任意の時点で初期化済みになれるのは 1 つだけである、という不変条件があります。 一般的な場合、どのバリアントがアクティブなものなのかはわからないため、enum のドロップグルー (判別子をチェックするもの)を呼び出すか、精緻化された Open ドロップの一部として 判別子を自分でチェックする必要があります。しかし、特定の場合(たとえば match アーム内)では、 enum のどのバリアントがアクティブかがわかっています。この情報は、非アクティブなバリアントに 対応するすべてのプレースを未初期化としてマークすることにより、MaybeInitializedPlacesMaybeUninitializedPlaces のデータフロー解析にエンコードされます。

クリーンアップパス

TODO: drop elaboration とアンワインドについて説明する。

余談: drop elaboration と const-eval

Rust では、コンパイル時の評価対象となる関数は、const キーワードを使って明示的に マークされていなければなりません。これには Drop トレイトの実装も含まれ、それらは const である場合もあれば、そうでない場合もあります。コンパイル時評価の対象となるコードは const 関数しか呼び出せないため、そのようなコード内での非 const な Drop 実装への呼び出しは 禁止されなければなりません。

Drop impl への呼び出しは、MIR では Drop ターミネーターとしてエンコードされます。しかし、 上で説明したように、新しく構築された MIR に含まれる Drop ターミネーターが、必ずしも Drop::drop の呼び出しになるとは限りません。その時点でドロップ対象が未初期化である可能性があります。 これは、新しく構築された MIR 上で非 const な Drop をチェックすると、偽のエラーが発生する可能性がある ことを意味します。その代わりに、drop elaboration が実行され、Dead ドロップ(対象が未初期化であることが わかっているもの)が削除されるまで待ってから、これらのチェックを実行します。

MIR 借用チェック

借用チェックは Rust の「秘伝のタレ」です。これは、いくつかの性質を 強制する役割を担っています。

  • すべての変数が、使用される前に初期化されていること。
  • 同じ値を 2 回ムーブできないこと。
  • 借用されている間は値をムーブできないこと。
  • 可変に借用されている間は、その参照を通じる場合を除き、プレースにアクセスできないこと。
  • 不変に借用されている間は、プレースを変更できないこと。
  • など

借用チェッカーは MIR 上で動作します。古い実装は HIR 上で動作していました。 MIR 上で借用チェックを行うことには、いくつかの利点があります。

借用チェッカーの主要なフェーズ

借用チェッカーのソースは the rustc_borrowck crate にあります。主なエントリーポイントは mir_borrowck クエリです。

  • まず、MIR の ローカルコピー を作成します。以降の手順では、 計算している新しいリージョンへの参照を含めるように型などを変更するため、 このコピーをインプレースで変更します。
  • 次に、replace_regions_in_mir を呼び出してローカルの MIR を変更します。 特に、この関数は MIR 内のすべての リージョン を新しい 推論変数 に置き換えます。
  • 次に、どのデータがいつムーブされるかを計算する、いくつかの データフロー解析 を実行します。
  • 次に、MIR 全体に対して 2 回目の型チェック を行います。 この型チェックの目的は、異なるリージョン間のすべての制約を決定することです。
  • 次に、リージョン推論 を行います。これは各リージョンの値、 つまり基本的には、収集した制約に従って各ライフタイムが有効でなければならない 制御フローグラフ上のポイントを計算します。
  • この時点で、各ポイントにおける「スコープ内の借用」を計算できます。
  • 最後に、MIR をもう一度走査し、そこで行われるアクションを確認して エラーを報告します。たとえば、*a + 1 のような文を見つけた場合、 変数 a が初期化されていること、および可変に借用されていないことを チェックします。どちらかに該当すると、エラーを報告する必要があります。 このチェックを行うには、これまでのすべての解析結果が必要です。

ムーブと初期化の追跡

借用チェッカーの仕事の一部は、任意の時点でどの変数が 「初期化済み」であるかを追跡することです。これには、 ムーブがどこで発生するかを特定し、それらを追跡することも必要です。

初期化とムーブ

ユーザーの視点からは、初期化――変数に何らかの値を与えること――と、 ムーブ――所有権を別の場所へ移すこと――は、別々のトピックに見えるかもしれません。 実際、借用チェッカーのエラーメッセージでは、それらについて異なる形で述べることがよくあります。 しかし、借用チェッカーの内部では、それらはそれほど別々のものではありません。 大まかに言えば、借用チェッカーはソースコード内の任意の時点で 「初期化済みのプレース」の集合を追跡します。以前は初期化されていなかったローカル変数に代入すると、 その変数がその集合に追加されます。ローカル変数からムーブすると、その変数はその集合から削除されます。

次の例を考えてみましょう。

fn foo() {
    let a: Vec<u32>;

    // a はまだ初期化されていない

    a = vec![22];

    // a はここで初期化されている

    std::mem::drop(a); // a はここでムーブされる

    // a はここではもはや初期化されていない

    let l = a.len(); //~ ERROR
}

ここでは、a が最初は未初期化であることがわかります。 代入されると、a は初期化済みになります。しかし drop(a) が呼び出されると、 それによって a が呼び出しの中へムーブされるため、再び未初期化になります。

サブセクション

読みやすくするため、このセクションはいくつかのサブセクションに分かれています。

  • ムーブパス: どのローカル変数(場合によってはローカル変数の一部)が 初期化済みかを追跡するために使用する ムーブパス の概念。
  • TODO 残りはまだ書かれていません =)

ムーブパス

実際には、初期化をローカル変数の粒度で追跡するだけでは十分ではありません。Rust では、フィールド粒度でのムーブや初期化も可能です。

fn foo() {
    let a: (Vec<u32>, Vec<u32>) = (vec![22], vec![44]);

    // a.0 と a.1 はどちらも初期化されている

    let b = a.0; // a.0 をムーブする

    // a.0 は初期化されていないが、a.1 はまだ初期化されている

    let c = a.0; // エラー
    let d = a.1; // OK
}

これに対処するために、初期化を ムーブパス の粒度で追跡します。MovePath は、ユーザーが初期化したり、ムーブしたりできる何らかの場所を表します。たとえば、ローカル変数 a を表すムーブパスがあり、a.0 を表すムーブパスもあります。ムーブパスは、MIR の Place の概念におおよそ対応しますが、ムーブ解析をより効率的に行えるような方法でインデックス付けされています。

ムーブパスインデックス

MovePath データ構造は存在しますが、それらが直接参照されることはありません。代わりに、すべてのコードは MovePathIndex 型の インデックス を受け渡します。ムーブパスに関する情報を取得する必要がある場合は、このインデックスを MoveDatamove_paths フィールド とともに使用します。たとえば、MovePathIndex mpi を MIR の Place に変換するには、次のように MovePath::place フィールドにアクセスできます。

move_data.move_paths[mpi].place

ムーブパスの構築

MIR 借用チェックで最初に行うことの 1 つは、ムーブパスの集合を構築することです。これは MoveData::gather_moves 関数の一部として行われます。この関数は MoveDataBuilder と呼ばれる MIR ビジターを使用して MIR を走査し、その中の各 Place がどのようにアクセスされているかを調べます。そのような各 Place について、対応する MovePathIndex を構築します。また、その特定のムーブパスがいつ/どこでムーブ/初期化されるかも記録しますが、これについては後のセクションで扱います。

不正なムーブパス

使用される すべての Place に対して実際にムーブパスを作成するわけではありません。特に、ある Place からムーブすることが不正である場合、MovePathIndex は不要です。いくつか例を挙げます。

  • 配列の個々の要素をムーブすることはできないため、たとえば foo: [String; 3] がある場合、 foo[1] に対するムーブパスは存在しません。
  • 借用された参照の内部からムーブすることはできないため、たとえば foo: &String がある場合、 *foo に対するムーブパスは存在しません。

これらのルールは move_path_for 関数によって強制されます。この関数は PlaceMovePathIndex に変換します。先ほど説明したようなエラーケースでは、この関数は Err を返します。その結果、それらの場所が初期化されているかどうかを追跡する手間が不要になります(これによりオーバーヘッドが低減されます)。

プロジェクション

ムーブパス内のプロジェクションは、PlaceElem を使用する代わりに MoveSubPath として格納されます。 外へムーブできないプロジェクションと、スキップ可能なプロジェクションは表現されません。

配列のサブスライスプロジェクション(スライスパターンによって生成されるもの)は特別です。これらは、サブスライス内の各要素に対して 1 つずつ、複数の ConstantIndex サブパスに変換されます。

ムーブパスの検索

Place があり、それを MovePathIndex に変換したい場合は、MoveDatarev_lookup フィールドにある MovePathLookup 構造体を使用して行うことができます。2 つの異なるメソッドがあります。

  • find_local は、ローカル変数を表す mir::Local を受け取ります。これはより簡単なメソッドです。なぜなら、すべてのローカル変数に対して 常に MovePathIndex を作成するからです。
  • find は、任意の Place を受け取ります。このメソッドは少し使いにくいです。というのも、(「不正なムーブパス」セクションで説明したように)すべての Place に対して MovePathIndex があるわけではないからです。そのため、find は、存在するもののうち見つけることができた最も近いパスを示す LookupResult を返します(たとえば foo[1] に対しては、foo のパスだけを返すことがあります)。

相互参照

前述のように、ムーブパスは大きなベクターに格納され、MovePathIndex を介して参照されます。ただし、このベクター内では、それらはツリーとしても構造化されています。したがって、たとえば a.b.c に対する MovePathIndex がある場合、その親ムーブパスである a.b に移動できます。すべての子パスを反復処理することもできます。つまり、a.b から、パス a.b.c を見つけるために反復処理できます(ここで反復処理しているのは、ソース内で 実際に参照されている パスだけであり、参照され得た 可能な すべてのパスではありません)。これらの参照は、たとえば find_in_move_path_or_its_descendants 関数で使用されます。この関数は、あるムーブパス(例: a.b)またはそのムーブパスの任意の子(例: a.b.c)が、指定された述語に一致するかどうかを判定します。

MIR 型チェック

借用チェックの主要なコンポーネントは MIR 型チェック です。 このチェックは MIR を走査し、完全な「型チェック」を行います – 他の言語にも見られるようなものと同じ種類のものです。 この型チェックを行う過程で、プログラムに適用されるリージョン制約も明らかにします。

TODO – さらに詳述する? たぶん? :)

ユーザー型

MIR 型チェックの開始時に、本体内のすべてのリージョンを、制約のない新しいリージョンに置き換えます。 しかし、これにより次のプログラムを受け入れてしまうことになります。

#![allow(unused)]
fn main() {
fn foo<'a>(x: &'a u32) {
    let y: &'static u32 = x;
}
}

y の型のライフタイムを消去すると、それが 'static であるべきだということが分からなくなり、 ユーザーの意図を無視することになります。

これに対処するため、HIR 型チェック中にユーザーが明示的に型に言及したすべての場所を CanonicalUserTypeAnnotations として記憶します。

注目するアノテーションには 2 種類あります。

  • 明示的な型アスクリプション。たとえば let y: &'static u32UserType::Ty(&'static u32) になります。
  • 明示的なジェネリック引数。たとえば x.foo<&'a u32, Vec<String>>UserType::TypeOf(foo_def_id, [&'a u32, Vec<String>]) になります。

HIR 型チェックからのリージョン推論が MIR typeck に影響することは望ましくないため、 ユーザー型は HIR から lowering した直後に保存します。 これは、その型がまだ推論変数を含んでいる可能性があることを意味します。 そのため、正準ユーザー型アノテーションを使用しています。 代わりに、すべての推論変数を存在束縛変数に置き換えます。 したがって、let x: Vec<_> のようなものは exists<T> UserType::Ty(Vec<T>) になります。

let Foo(x): Foo<&'a u32> のようなパターンにはユーザー型 Foo<&'a u32> がありますが、 x の実際の型は &'a u32 のみであるべきです。このために、UserTypeProjection を使用します。

MIR では、ユーザー型をわずかに異なる 2 つの方法で扱います。

明示的な型注釈を持つパターン内の変数に対応する MIR ローカルが与えられた場合、 そのローカルの型が UserTypeProjection の型と等しいことを要求します。 これは LocalDecl に直接保存されます。

また、被検査式の型も制約します。たとえば let _: &'a u32 = x; における x の型です。 ここで T_x はユーザー型のサブタイプであればよいだけなので、その代わりに StatementKind::AscribeUserType を使用します。

MIR 型チェッカーは型および const 推論変数を実際には扱わないため、 ユーザー型を直接使用しないことに注意してください。代わりに、HIR 型チェッカーからの最終的な inferred_type を保存します。その後、MIR typeck 中にそのリージョンを新しい nll 推論変数に置き換え、 実際の UserType と関連付けることで、正しいリージョン制約を再び取得します。

MIR 型チェックの後、すべてのユーザー型アノテーションは不要になるため破棄されます。

ドロップチェック

通常、ローカルが使用されるときには常に、そのローカルの型が整形式であることを要求します。これには、ローカルの where 境界を証明することが含まれ、さらにそのローカルによって使用されるすべてのリージョンがライブであることも要求されます。

唯一の例外は、値がスコープ外に出るときに暗黙的にドロップされる場合です。これは、必ずしも値がライブであることを要求しません。

fn main() {
    let x = vec![];
    {
        let y = String::from("I am temporary");
        x.push(&y);
    }
    // `x` はここでスコープ外に出ます。これは `y` への参照が
    // 無効化された後です。つまり、`x` をドロップしている間、その型は
    // ライブではないリージョンを含んでいるため、整形式ではありません。
}

これは、値をドロップしても死んだリージョンにアクセスしようとしない場合にのみ健全です。これを確認するために、その値の型が drop-live であることを要求します。 その要件は fn dropck_outlives で計算されます。

このセクションの残りでは、リージョンパラメーターがライブであることを要求する型として、次の型定義を使用します。

#![allow(unused)]
fn main() {
struct PrintOnDrop<'a>(&'a str);
impl<'a> Drop for PrintOnDrop<'_> {
    fn drop(&mut self) {
        println!("{}", self.0);
    }
}
}

値がどのようにドロップされるか

本質的には、型 T の値は、その「ドロップグルー」を実行することでドロップされます。ドロップグルーはコンパイラによって生成され、最初に <T as Drop>::drop を呼び出し、その後、再帰的に所有されている値のドロップグルーを再帰的に呼び出します。

  • T に明示的な Drop impl がある場合、<T as Drop>::drop を呼び出します。
  • TDrop を実装しているかどうかに関係なく、T によって 所有されている すべての値へ再帰します。
    • 参照、生ポインター、関数ポインター、関数アイテム、トレイトオブジェクト1、およびスカラーは何も所有しません。
    • タプル、スライス、配列は、その要素を所有されているものとみなします。 長さ 0 の配列については、その要素型の値を一切所有しません。
    • ADT のすべてのフィールド(すべてのバリアントのもの)は所有されているとみなされます。 enum についてはすべてのバリアントを考慮します。ここでの例外は ManuallyDrop<U> であり、これは U を所有しているとはみなされません。 PhantomData<U> も何も所有しません。 クロージャとジェネレーターは、キャプチャした upvar を所有します。

ある型がドロップグルーを持つかどうかは、fn Ty::needs_drop によって返されます。

ローカルを部分的にドロップする

自分自身では Drop を実装していない型については、残りをドロップする前に値の一部を部分的に move することもできます。この場合、まだ move されていない値に対するドロップグルーのみが呼び出されます。例:

fn main() {
    let mut x = (PrintOnDrop("third"), PrintOnDrop("first"));
    drop(x.1);
    println!("second")
}

MIR の構築中には、ローカルの型が drop を必要とする限り、そのローカルはスコープ外に出るときに常にドロップされる可能性があると仮定します。変数に対する正確なドロップグルーの計算は、borrowck のElaborateDrops パスで行われます。つまり、ローカルの一部が以前にドロップされていたとしても、dropck は依然としてこの値がライブであることを要求します。これは、ローカルを完全に move した場合でも同様です。

fn main() {
    let mut x;
    {
        let temp = String::from("I am temporary");
        x = PrintOnDrop(&temp);
        drop(x);
    }
} //~ ERROR `temp` の存続期間が十分に長くありません。

borrowck の前にある程度のドロップ展開を追加できるようにすれば、この例をコンパイルできるようになるはずです。const チェックの前にドロップ展開を移動するための不安定な機能があります: #73255。borrowck の前にいくらかのドロップ展開を行うためのそのような feature gate は存在しませんが、関連する MCP はあります。

dropck_outlives

私たちが実行する「liveness」の計算には、2 つの異なるものがあります。

  • v は、場所 L で、その後に「使用」される可能性がある場合、use-live です。ここでの use は、基本的に drop ではないものすべてです
  • v は、場所 L で、その後にドロップされる可能性がある場合、drop-live です

ものが use-live である場合、その型全体が L で有効でなければなりません。ものが drop-live である場合、dropck によって要求されるすべてのリージョンが L で有効でなければなりません。MIR でドロップされる値は places です。

ある型に対して dropck_outlives によって計算される制約は、その型に対して生成されるドロップグルーとよく対応しています。ドロップグルーとは異なり、dropck_outlives は所有されている値そのものではなく、所有されている値の型を気にします。型 T の値については、次のようになります。

  • T に明示的な Drop がある場合、すべてのジェネリック引数がライブであることを要求します。ただし、それらが #[may_dangle] でマークされている場合は、完全に無視されます
  • T に明示的な Drop があるかどうかに関係なく、T によって 所有されている すべての型へ再帰します
    • 参照、生ポインター、関数ポインター、関数アイテム、トレイトオブジェクト1、およびスカラーは何も所有しません。
    • タプル、スライス、配列は、その要素型を所有されているものとみなします。 配列については、現在その長さが 0 かどうかをチェックしていません
    • ADT のすべてのフィールド(すべてのバリアントのもの)は所有されているとみなされます。 ここでの例外は ManuallyDrop<U> であり、これは U を所有しているとはみなされません。私たちは PhantomData<U>U を所有しているとみなします
    • クロージャとジェネレーターは、キャプチャした upvar を所有します。

太字で示したセクションは、dropck_outlives が、Ty::needs_drop では無視される型を所有されているものとみなすケースです。私たちは、それを含むローカルに対する Ty::needs_droptrue を返した場合にのみ dropck_outlives に依存します。これは、liveness 要件が、ある型がより大きなローカルに含まれているかどうかに応じて変わり得ることを意味します。これは一貫しておらず、修正されるべきです: 配列の例PhantomData の例 があります。2

これらの不整合を修正する 1 つの可能な方法は、MIR の構築をより悲観的にすることです。おそらく Ty::needs_drop をより弱くするか、あるいは dropck_outlives をより精密に変更して、ライブであることを要求されるリージョンを少なくすることです。


  1. トレイトオブジェクトは、vtable によって提供される drop_in_place を直接使用する組み込みの Drop 実装を持つと考えることができます。この Drop 実装は、すべてのジェネリックパラメーターがライブであることを要求します。 ↩2

  2. これは #110288RFC 3417 の中核となる仮定です。

リージョン推論 (NLL)

MIR ベースのリージョンチェックコードは、rustc_mir::borrow_check モジュールにあります。

MIR ベースのリージョン解析は、2 つの主要な関数で構成されています。

  • 最初に呼び出される replace_regions_in_mir には、2 つの役割があります。
    • 1 つ目は、関数のシグネチャ内に現れるリージョンの集合を見つけることです (例: fn foo<'a>(&'a u32) { ... } における 'a)。これらは「ユニバーサル」または「自由」リージョンと呼ばれます。 特に、これらは関数本体において自由に現れるリージョンです。
    • 2 つ目は、関数本体内のすべてのリージョンを新しい推論変数に置き換えることです。 これは、(現時点では)それらのリージョンが字句的リージョン推論の結果であり、 したがってあまり関心の対象ではないためです。意図としては、最終的にはそれらを 「消去されたリージョン」(つまり、情報をまったく持たないもの)にすることです。 なぜなら、字句的リージョン推論はまったく行わなくなるためです。
  • 2 番目に呼び出される compute_regions は、ムーブ解析の結果を引数として受け取ります。 この関数の役割は、replace_regions_in_mir が導入したすべての推論変数の値を計算することです。
    • そのために、まず [MIR 型チェッカー]を実行します。これは基本的には通常の型チェッカーですが、 MIR に特化したものであり、当然ながら完全な Rust よりもはるかに単純です。 ただし、MIR 型チェッカーを実行すると、リージョン変数間にさまざまな制約が作成され、 それらの潜在的な値や相互関係が示されます。
    • その後、RegionInferenceContext を作成し、その solve メソッドを呼び出すことで、制約伝播を実行します。
    • NLL RFC にも、かなり徹底した(そしてできれば読みやすい) 説明が含まれています。

ユニバーサルリージョン

UniversalRegions 型は、何らかの MIR DefId に対応する ユニバーサル リージョンの集合を表します。これは、 すべてのリージョンを新しい推論変数に置き換える際に replace_regions_in_mir で構築されます。UniversalRegions には、 与えられた MIR 内のすべての自由リージョンのインデックスと、 それらの間で成り立つことが 既知 である関係 (例: 暗黙の境界、where 句など)が含まれます。

たとえば、次の関数の MIR が与えられたとします。

#![allow(unused)]
fn main() {
fn foo<'a>(x: &'a u32) {
    // ...
}
}

この場合、'a に対するユニバーサルリージョンと、'static に対するユニバーサルリージョンを作成します。 クロージャの処理にはいくつか複雑な点がある可能性もありますが、ここではひとまず無視します。

TODO: これらのリージョンが どのように 計算されるかについて書く。

リージョン変数

リージョンの値は 集合 と考えることができます。この集合には、 そのリージョンが有効である MIR 内のすべての点と、 このリージョンによってアウトライブされる任意のリージョン (例: 'a: 'b の場合、end('b)'a の集合に含まれる)が含まれます。 この集合のドメインを RegionElement と呼びます。コード内では、 すべてのリージョンの値は the rustc_borrowck::region_infer module で管理されます。 各リージョンについて、その値に含まれる要素を格納する集合を保持します(これを効率的にするために、 各種類の要素にインデックスである RegionElementIndex を与え、 スパースビットセットを使用します)。

リージョン要素の種類は次のとおりです。

  • MIR 制御フローグラフ内の各 location: location は、 基本ブロックとインデックスのペアにすぎません。これは、そのインデックスを持つ文 (または、インデックスが statements.len() と等しい場合は終端)の 入口 の点を識別します。
  • 各ユニバーサルリージョン 'a には、end('a) という要素があります。 これは、呼び出し元(または呼び出し元の呼び出し元など)の制御フローグラフの ある部分に対応します。
  • 同様に、この関数が戻った後のプログラム実行の残りに対応する、 end('static) と表記される要素があります。
  • 各プレースホルダーリージョン !1 には、要素 !1 があります。 これは(直感的には)他の要素からなる未知の集合に対応します。 プレースホルダーの詳細については、プレースホルダーとユニバース のセクションを参照してください。

制約

リージョンの値を推論できるようになる前に、リージョンに関する制約を収集する必要があります。 制約の完全な集合については、制約伝播に関するセクションで説明されていますが、 最も一般的な 2 種類の制約は次のとおりです。

  1. アウトライブ制約。これは、あるリージョンが別のリージョンをアウトライブするという制約です (例: 'a: 'b)。アウトライブ制約は、[MIR 型チェッカー]によって生成されます。
  2. 生存性制約。各リージョンは、それが使用され得る点でライブである必要があります。

推論の概要

では、リージョンの内容はどのように計算するのでしょうか。このプロセスは リージョン推論 と呼ばれます。 大まかな考え方は非常に単純ですが、対処する必要のある細部がいくつかあります。

大まかな考え方は次のとおりです。まず、生存性制約からそのリージョンに含まれていなければならないと わかっている MIR location を、各リージョンの初期値とします。そこから、型チェッカーから計算された すべてのアウトライブ制約を使用して、制約を 伝播 します。各リージョン 'a について、 'a: 'b であれば、end('b) を含む 'b のすべての要素を 'a に追加します。 これはすべて propagate_constraints で行われます。

その後、エラーをチェックします。まず、check_type_tests を呼び出して、 型テストが満たされていることを確認します。これは T: 'a のような制約をチェックします。 次に、ユニバーサルリージョンが「大きすぎ」ないことを確認します。これは check_universal_regions を呼び出すことで行われます。これは、各リージョン 'a について、 'a が要素 end('b) を含む場合、'a: 'b が成り立つことを (例: where 句から)すでに知っていなければならない、ということをチェックします。 これをまだ知らない場合、それはエラーです……まあ、ほぼそうです。クロージャにはいくつか特別な処理があり、 これについては後で説明します。

次の例を考えてみましょう。

fn foo<'a, 'b>(x: &'a usize) -> &'b usize {
    x
}

明らかに、これはコンパイルされるべきではありません。なぜなら、'a'b をアウトライブするかどうかが わからないためです(そうでない場合、戻り値がダングリング参照になり得ます)。

少し戻りましょう。いくつかの自由推論変数を導入する必要があります (これは replace_regions_in_mir で行われます)。この例では実際に生成されるリージョンそのものは 使用していませんが、考え方を伝えるには(おそらく)十分です。

fn foo<'a, 'b>(x: &'a /* '#1 */ usize) -> &'b /* '#3 */ usize {
    x // '#2、位置 L1
}

表記について説明します。'#1'#3'#2 はそれぞれ、引数、戻り値、式 x のユニバーサルリージョンを表します。さらに、式 x の位置を L1 と呼ぶことにします。

これで、生存性制約を使用して次の開始点を得られます。

リージョン内容
’#1
’#2L1
’#3L1

次に、outlives 制約を使用して各リージョンを拡張します。具体的には、'#2: '#3 であることが分かっています …

リージョン内容
’#1L1
’#2L1, end('#3) // '#3 の内容と end('#3) を追加
’#3L1

… そして '#1: '#2 なので …

リージョン内容
’#1L1, end('#2), end('#3) // '#2 の内容と end('#2) を追加
’#2L1, end('#3)
’#3L1

次に、大きすぎるリージョンがなかったことを確認する必要があります(この場合、確認すべき型テストはありません)。'#1 が今や end('#3) を含んでいることに注意してください。しかし、'a: 'b であることを示す where 句や暗黙の境界はありません … これはエラーです!

詳細

RegionInferenceContext 型には、replace_regions_in_mir からのユニバーサルリージョンや、各リージョンについて計算された制約など、推論を行うために必要なすべての情報が含まれています。これは、生存性制約を計算した直後に構築されます。

この構造体のフィールドの一部を以下に示します。

  • constraints: すべての outlives 制約を含みます。
  • universal_regions: replace_regions_in_mir によって返される UniversalRegions を含みます。
  • universal_region_relations: ユニバーサルリージョンについて真であることが分かっている関係を含みます。たとえば、'a: 'b という where 句がある場合、その関係は実装を借用チェックしている間は真であると仮定されます(呼び出し元でチェックされます)。したがって、universal_region_relations には 'a: 'b が含まれます。
  • type_tests: 推論後にチェックしなければならない型に関するいくつかの制約(例: T: 'a)を含みます。

TODO: 他のフィールドについて議論すべきでしょうか?SCC についてはどうでしょうか?

さて、RegionInferenceContext を構築したので、推論を行うことができます。これは、コンテキスト上で solve メソッドを呼び出すことで行われます。ここで propagate_constraints を呼び出し、その後、上で説明したように、結果として得られた型テストとユニバーサルリージョンをチェックします。

制約伝播

リージョン推論の主な作業は制約伝播であり、 これは propagate_constraints 関数で行われます。NLL で使用される制約には 3 種類あり、これらの制約を一度に 1 種類ずつ「重ねる」ことで propagate_constraints がどのように動作するかを説明します(それぞれは互いにかなり独立しています)。

  • 生存性に由来する、生存性制約(R live at E)。
  • サブタイピングに由来する、outlives 制約(R1: R2)。
  • impl Trait に由来する、メンバー制約member R_m of [R_c...])。

この章では、生存性制約と outlives 制約の両方を扱いながら、制約伝播の「核心」を説明します。

記法と高水準の概念

概念的には、リージョン推論は「不動点」計算です。これは、 何らかの制約の集合 {C} を与えられ、各リージョン R を要素の集合 {E} にマッピングする値の集合 Values: R -> {E} を計算します (リージョン要素に関する追加の注記はこちらを参照してください)。

  • 最初は、各リージョンは空集合にマッピングされるため、すべてのリージョン R について Values(R) = {} です。
  • 次に、不動点に到達するまで制約を繰り返し処理します。
    • 各制約 C について:
      • 制約を満たすために必要に応じて Values を更新します

簡単な例として、生存性制約 R live at E がある場合、 その制約を満たすために Values(R) = Values(R) union {E} を適用できます。 同様に、outlives 制約 R1: R2 がある場合は、 Values(R1) = Values(R1) union Values(R2) を適用できます。 (メンバー制約はより複雑で、このセクションで説明します。)

ただし実際には、もう少し賢い方法を取っています。制約をループ内で適用する代わりに、 制約を解析し、それらを適用する正しい順序を見つけ出すことで、 最終結果を得るために各制約を 1 回だけ適用すればよいようにできます。

同様に、実装では Values 集合は scc_values フィールドに格納されますが、 それらはリージョンではなく強連結成分(SCC)によってインデックス付けされます。 SCC は、多くの冗長な格納と計算を避けるための最適化です。 これらについては outlives 制約のセクションで説明します。

生存性制約

生存性制約は、リージョン R を含む型を持つ何らかの変数が、あるポイント P で生存している場合に発生します。 これは単に、R の値がポイント P を含んでいなければならないことを意味します。 生存性制約は MIR 型チェッカーによって計算されます。

生存性制約 R live at E は、EValues(R) のメンバーである場合に満たされます。 したがって、そのような制約を Values に「適用」するには、 Values(R) = Values(R) union {E} を計算するだけで済みます。

生存性の値は型チェックで計算され、作成時に liveness_constraints 引数でリージョン推論へ渡されます。 ただし、これらは R live at E のような個々の制約として表現されるわけではありません。 代わりに、リージョン変数ごとに(疎な)ビットセット(型は LivenessValues)を格納します。 この方法では、各生存性制約に対して 1 ビットだけが必要です。

言及しておく価値のある点が 1 つあります。すべてのライフタイムパラメーターは、常に関数本体全体にわたって生存しているとみなされます。 これは、それらが呼び出し元の実行の一部に対応しており、その実行には明らかにこの関数で費やされる時間が含まれるためです。 呼び出し元は、私たちが戻るのを待っているからです。

Outlives 制約

outlives 制約 'a: 'b は、'a の値が 'b の値の上位集合でなければならないことを示します。 つまり、outlives 制約 R1: R2 は、Values(R1)Values(R2) の上位集合である場合に満たされます。したがって、そのような制約を Values に「適用」するには、Values(R1) = Values(R1) union Values(R2) を計算するだけで済みます。

ここから得られる 1 つの観察は、R1: R2R2: R1 がある場合、 R1 = R2 が真でなければならないということです。同様に、次がある場合:

R1: R2
R2: R3
R3: R4
R4: R1

すると R1 = R2 = R3 = R4 が成り立ちます。後述するように、私たちはこれを利用して処理を大幅に高速化しています。

コードでは、outlives 制約の集合は、作成時に型 OutlivesConstraintSet のパラメーターとしてリージョン推論コンテキストへ渡されます。 この制約集合は、基本的には単に 'a: 'b 制約のリストです。

outlives 制約グラフと SCC

outlives 制約をより効率的に扱うために、それらはグラフの形式に変換されます。 このグラフのノードはリージョン変数('a, 'b)であり、各制約 'a: 'b は 辺 'a -> 'b を導入します。この変換は、推論コンテキストを作成する RegionInferenceContext::new 関数で行われます。

グラフ表現を使用すると、サイクルを探すことで、等しくなければならないリージョンを検出できます。 つまり、次のような制約がある場合

'a: 'b
'b: 'c
'c: 'd
'd: 'a

これは、要素 'a...'d を含むグラフ内のサイクルに対応します。

したがって、リージョン値を伝播する際に最初に行うことの 1 つは、 制約グラフ内の強連結成分(SCC)を計算することです。 結果は constraint_sccs フィールドに格納されます。 その後、constraint_sccs.scc(r) を呼び出すことで、 リージョン r が属している SCC を簡単に見つけることができます。

SCC に基づいて扱うことで、より効率的にできます。単一の SCC の一部であるリージョンの集合 'a...'d がある場合、それらの値を個別に計算または格納する必要はありません。 それらはすべて等しくなければならないため、SCC に対して 1 つの値だけを格納すればよいのです。

リージョン推論コードを見てみると、多くのフィールドが SCC に基づいて定義されていることがわかります。 たとえば、scc_values フィールドは各 SCC の値を格納します。 したがって、特定のリージョン 'a の値を取得するには、まずそのリージョンが属する SCC を見つけ、 次にその SCC の値を見つけます。

SCC を計算するとき、各 SCC のメンバーであるリージョンがどれかを特定するだけでなく、それらの間のエッジも特定します。たとえば、次の outlives 制約の集合を考えてみましょう。

'a: 'b
'b: 'a

'a: 'c

'c: 'd
'd: 'c

ここには 2 つの SCC があります。S0 は 'a'b を含み、S1 は 'c'd を含みます。しかし、これらの SCC は独立していません。'a: 'c があるため、 S0: S1 も成り立つことになります。つまり、S0 の値は S1 の値の スーパーセットでなければなりません。重要な点の 1 つは、この SCC のグラフが常に DAG であることです。つまり、循環を持つことはありません。これは、すべての循環が SCC 自体を形成するために取り除かれているためです。

生存性制約を SCC に適用する

型チェッカーから渡される生存性制約は、リージョンの観点で表現されています。つまり、 Liveness: R -> {E} のようなマップがあります。しかし、最終結果は SCC の観点で表現したいので、単に和集合を取ることで、これらの生存性制約を非常に簡単に統合できます。

for each region R:
  let S be the SCC that contains R
  Values(S) = Values(S) union Liveness(R)

リージョン推論器では、このステップは RegionInferenceContext::new で行われます。

outlives 制約を適用する

SCC の DAG を計算したら、それを使って計算全体を構造化します。2 つの SCC の間にエッジ S1 -> S2 がある場合、 Values(S1) >= Values(S2) が成り立たなければならないことを意味します。したがって、S1 の値を計算するには、まず各後続ノード S2 の値を計算します。その後、それらの値すべての和集合を単純に取ります。準イテレーター風の記法を使うと、次のようになります。

Values(S1) =
  s1.successors()
    .map(|s2| Values(s2))
    .union()

コードでは、この処理は propagate_constraints 関数で始まります。この関数はすべての SCC を反復処理します。各 SCC S1 について、その後続ノードの値を先に計算することで、その値を計算します。SCC は DAG を形成するため、循環について心配する必要はありません。ただし、特定の SCC をすでに処理したかどうかを追跡するための集合を保持しておく必要はあります。各後続ノード S2 について、S2 の値を計算したら、それらの要素を S1 の値に union できます。(ただし、この処理では、高ランクプレースホルダーを適切に扱うよう注意する必要があります)。S1 の値には、生存性制約がすでに含まれていることに注意してください。これは、それらが RegionInferenceContext::new で追加されているためです。

この処理が完了すると、すべての生存性制約と outlives 制約を考慮した S1 の「最小値」が得られます。しかし、処理を完了するには、メンバー制約も考慮しなければなりません。これについては後のセクションで説明します。

ユニバーサルリージョン

「ユニバーサルリージョン」は、コードが「名前付きライフタイム」– たとえばライフタイムパラメーターや 'static – を指すために使用する名前です。この名前は、そのようなライフタイムが「全称量化」されているという事実に由来します(つまり、それらのライフタイムのすべての値に対してコードが正しいことを確認しなければなりません)。リージョン推論中にライフタイムパラメーターがどのように扱われるかについて、少し議論しておく価値があります。次の例を考えてみましょう。

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'b u32 {
  x
}

この例はコンパイルされないことを意図しています。なぜなら、型が &'a u32 である x を返している一方で、シグネチャでは &'b u32 の値を返すことを約束しているからです。しかし、'a'b のようなライフタイムはリージョン推論にどのように統合され、このエラーはどのように検出されることになるのでしょうか。

ユニバーサルリージョンとそれら相互の関係

リージョン推論の初期段階で、最初に行うことの 1 つは UniversalRegions 構造体を構築することです。この構造体は、特定の関数のスコープ内にあるさまざまなユニバーサルリージョンを追跡します。また、それら相互の関係を追跡する UniversalRegionRelations 構造体も作成します。したがって、たとえば where 'a: 'b がある場合、UniversalRegionRelations 構造体は 'a: 'b が成り立つことが分かっている、という情報を追跡します(これは outlives 関数でテストできます)。

すべてはリージョン変数である

NLL リージョン推論の仕組みにおける重要な側面の 1 つは、すべてのライフタイムが番号付きの変数として表現されることです。これは、使用する region_kind::RegionKind のバリアントが ReVar バリアントだけであることを意味します。これらのリージョン変数は、そのインデックスに基づいて 2 つの主要なカテゴリに分けられます。

  • 0..N: ユニバーサルリージョン – ここで議論しているものです。この場合、コードは宣言された関係を満たすこれらの変数の任意の値に関して正しくなければなりません。
  • N..M: 存在リージョン – リージョン推論器が何らかの適切な値を見つけることを課される推論変数です。

実際には、ユニバーサルリージョンは、それらがどこでスコープに導入されたかに基づいてさらに細分化できます(RegionClassification 型を参照してください)。これらの細分化は、ここで議論するトピックにとっては重要ではありませんが、クロージャ制約の伝播を考えるときに重要になるため、そこで議論します。

リージョンの値の要素としてのユニバーサルライフタイム

前述のとおり、各リージョンについて推論する値は集合 {E} です。この集合の要素は制御フローグラフ内の地点である場合もありますが、各ユニバーサルライフタイム 'a に対応する要素 end('a) である場合もあります。あるリージョン R0 の値に end('a) が含まれている場合、これは R0 が呼び出し元における 'a の終端まで延長されなければならないことを意味します。

ユニバーサルリージョンの「値」

リージョン推論中、他のリージョンの値を計算するのと同じ方法で、各ユニバーサルリージョンの値を計算します。この値は、実質的に、そのユニバーサルリージョンの下限 – それが outlive しなければならないもの – を表します。次に、この値を使用してエラーをチェックする方法を説明します。

生存性とユニバーサルリージョン

すべてのユニバーサルリージョンには、関数本体全体を含む初期生存性制約があります。これは、ライフタイムパラメーターが呼び出し元で定義され、この特定の関数を呼び出す関数呼び出し全体を含まなければならないためです。さらに、各ユニバーサルリージョン 'a は、自身(つまり end('a))をその生存性制約に含みます(すなわち、'a は自身の終端まで延長されなければなりません)。コード内では、これらの生存性制約は init_free_and_bound_regions で設定されます。

ユニバーサルリージョンの outlives 制約の伝播

では、このセクションの最初の例を考えてみましょう。

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'b u32 {
  x
}

ここで x を返すには、&'a u32 <: &'b u32 が必要であり、これは outlives 制約 'a: 'b を生じさせます。これをデフォルトの生存性制約と組み合わせると、次のようになります。

'a live at {B, end('a)} // B は「関数本体」を表す
'b live at {B, end('b)}
'a: 'b

したがって、'a: 'b 制約を処理するとき、'a の値に end('b) を追加し、その結果、最終的な値は {B, end('a), end('b)} になります。

エラーの検出

制約の伝播が完了したら、あるユニバーサルリージョン 'a に要素 end('b) が含まれている場合、'a: 'b が関数の境界で宣言されていなければならない、という制約を強制します。宣言されていない場合、今回の例のように、それはエラーです。このチェックは check_universal_regions 関数で行われます。この関数は、すべてのユニバーサルリージョンを単純に反復処理し、それらの最終値を検査して、宣言された UniversalRegionRelations と照合します。

メンバー制約

メンバー制約 'm member of ['c_1..'c_N] は、リージョン 'm が何らかの 選択リージョン 'c_i(ある i について)と等しくなければならないことを表します。 これらの制約をユーザーが表現することはできませんが、impl Trait のライフタイム捕捉ルールにより発生します。 次のような関数を考えてみましょう。

fn make(a: &'a u32, b: &'b u32) -> impl Trait<'a, 'b> { .. }

ここで、真の戻り値の型(しばしば「隠れた型」と呼ばれます)は、ライフタイム 'a または 'b の捕捉のみが許可されます。 このことは、impl Trait 戻り値の型をより明示的な形式に糖衣構文解除すると、もう少し明確に分かります。

type MakeReturn<'x, 'y> = impl Trait<'x, 'y>;
fn make(a: &'a u32, b: &'b u32) -> MakeReturn<'a, 'b> { .. }

ここでの考え方は、隠れた型は impl Trait<'x, 'y> の位置に書くことができた何らかの型でなければならない、というものです。しかし明らかに、そのような型はリージョン 'x または 'y(あるいは 'static!)しか参照できません。スコープ内にある名前はそれだけだからです。 この制限は、最終的に 'a または 'b のみにアクセスするという制約へ変換されます。なぜなら、私たちは MakeReturn<'a, 'b> を返しており、そこでは 'x'y がそれぞれ 'a'b に置き換えられているためです。

詳細な例

メンバー制約をより詳しく説明するために、make の例をもう少し詳細に書き下してみましょう。 まず、何らかのダミー trait があると仮定します。

trait Trait<'a, 'b> { }
impl<T> Trait<'_, '_> for T { }

そして、これが make 関数です(糖衣構文解除後の形式)。

type MakeReturn<'x, 'y> = impl Trait<'x, 'y>;
fn make(a: &'a u32, b: &'b u32) -> MakeReturn<'a, 'b> {
  (a, b)
}

この場合に起こることは、戻り値の型が (&'0 u32, &'1 u32) になり、ここで '0'1 は新しいリージョン変数である、ということです。 次のリージョン制約が得られます。

'0 live at {L}
'1 live at {L}
'a: '0
'b: '1
'0 member of ['a, 'b, 'static]
'1 member of ['a, 'b, 'static]

ここで「生存性集合」{L} は、関数本体のうち '0'1 が生存している部分集合に対応します。基本的には、戻り値のタプルが構築される地点から、それが返される地点までです(実際には、'0'1 はわずかに異なる生存性集合を持つ可能性がありますが、ここで説明している点にとってはあまり重要ではありません)。

'a: '0 および 'b: '1 の制約はサブタイピングから生じます。 (a, b) の値を構築するとき、それには (&'0 u32, &'1 u32) という型が割り当てられます。リージョン変数は、これらの参照のライフタイムをより短くできることを反映しています。 しかし、この値を ab から作成するためには、次が必要です。

(&'a u32, &'b u32) <: (&'0 u32, &'1 u32)

これは、結果として &'a u32 <: &'0 u32 を意味し、したがって 'a: '0 を意味します(同様に、&'b u32 <: &'1 u32、つまり 'b: '1 も意味します)。

メンバー制約を無視した場合、'0 の値は関数本体の何らかの部分集合に推論されることに注意してください(生存性制約からですが、ここではそれを明示的には書いていません)。 それが 'a になることはありません。そうなる必要がないためです。私たちは 'a: '0 という制約を持っていますが、それは単に '0 がどれだけ大きくなれるかに「上限」を設けるだけです。 私たちは可能な限り最小の値を計算するため、'0 を生存性集合と等しいままにしておくことで十分なのです。 ここでメンバー制約が登場します。

選択肢は常にライフタイムパラメータ

現時点では、メンバー制約における「選択」リージョンは常に現在の関数のライフタイムパラメータです。 2026 年 3 月時点では、これは impl Trait の配置から導かれますが、将来的にはそうでなくなる可能性があります。 現在のコードを単純化できるため、私たちはこの事実をある程度利用しています。 特に、'0 member of ['1, 'static] のようなケースを考慮する必要がありません。このケースでは '0'1 の両方の値が推論されており、したがって変化しているためです。 詳細については rust-lang/rust#61773 を参照してください。

メンバー制約の適用

メンバー制約は、他の形式の制約よりも少し複雑です。 これは、メンバー制約に「または」の性質があるためです。つまり、選択しなければならない複数の選択肢を記述します。 たとえば、例の制約 '0 member of ['a, 'b, 'static] では、'0'a'bまたは 'static と等しい可能性があります。 どのように正しいものを選べばよいのでしょうか。 現在行っていることは、最小の選択肢を探すことです。もしそれが見つかれば、'0 をその最小の選択肢と等しくなるように拡大します。 その最小の選択肢を見つけるために、下限と上限という 2 つの要因を考慮します。

下限

下限とは、'0より長く存続しなければならないライフタイム、すなわち '0 がそれより大きくなければならないものです。 実際、メンバー制約を適用する段階では、私たちはすでに '0 の下限を計算済みです。なぜなら、'0 の最小値(少なくとも、メンバー制約以外のすべてを考慮した下限)を計算しているためです。

LB'0 の現在の値とします。 すると、'0 の最終的な値が何であれ、'0: LB が成り立たなければならないことが分かります。 したがって、'choice: LB が成り立たない任意の選択肢 'choice を除外できます。

残念ながら、この例ではこれはあまり役に立ちません。 '0 の下限は単に生存性集合 {L} であり、すべてのライフタイムパラメータがその集合より長く存続することが分かっています。 したがって、ここでは同じ選択肢の集合が残ります。 (ただし他の例、特に異なる分散を持つものでは、下限制約が関連する場合があります。)

上限

上限とは、'0 より長く存続しなければならないライフタイム、すなわち '0 がそれより小さくなければならないものです。 私たちの例では、これは 'a です。なぜなら、'a: '0 という制約があるためです。 より複雑な例では、その連鎖はより間接的な場合があります。

下限と非常によく似た方法で、上限を使ってメンバーを除外できます。 UB が何らかの上限である場合、UB: '0 が成り立たなければならないことが分かります。そのため、UB: 'choice が成り立たない任意の選択肢 'choice を除外できます。

私たちの例では、選択肢集合を ['a, 'b, 'static] から ['a] のみに減らすことができます。 これは、'0 には 'a という上限があり、'a: 'b'a: 'static も成り立つことが分かっていないためです。

(実装において上限をどのように収集するかについてのメモは、下のセクションを参照してください。)

最小の選択肢

下限と上限を適用した後でも、複数の可能性が残ることがあります。 たとえば、反対の分散を持つ型を使った、私たちの例の変種を想像してください。 その場合、'a: '0 ではなく '0: 'a という制約を持つことになります。 したがって、'0 の現在の値は {L, 'a} になります。 これを下限として用いると、'b: 'a が成り立つことは分かっていないため、メンバーの選択肢を ['a, 'static] まで絞り込むことができます(ただし、'a: 'a'static: 'a は成り立ちます)。 上限は存在しないため、それが最終的な選択肢集合になります。

その場合、私たちは最小の選択肢ルールを適用します。基本的には、選択肢の 1 つが他のものより小さい場合、それを使用できます。 この場合は、'a を選ぶことになります('static ではありません)。 この選択は、リージョン伝播の一般的な「流れ」と一貫しています。リージョン伝播は常に、推論対象のリージョンについて最小値を計算することを目指すためです。 ただし、これはやや恣意的です。

実装で上限を収集する

実際には、上限を計算するのは少し不便です。なぜなら、私たちのデータ構造はその逆向きに設定されているからです。 私たちが行うのは、逆SCCグラフを計算することです(これは遅延的に行い、結果をキャッシュします)。つまり、'a: 'b が辺 SCC('b) -> SCC('a) を導くグラフです。 通常のSCCグラフと同様、これはDAGです。 その後、このグラフで SCC('0) から開始して深さ優先探索を行えます。 これにより、'0 より長く生存しなければならないすべてのSCCに到達できます。

ひとつ厄介なのは、「上限」のSCCをたどる時点では、それらの値がまだ完全には計算されていないということです。 しかし、それらの生存性制約はすでに適用済みなので、その値についてある程度の情報はあります。 特に、ライフタイムパラメーターを表す任意のリージョンについて、その値には自身が含まれます(つまり、'a の初期値には 'a が含まれ、'b の値には 'b が含まれます)。 したがって、到達可能なすべてのライフタイムパラメーターを収集できます。これはまさに、私たちが関心を持っているものです。

プレースホルダーとユニバース

私たちは時折、具体的には知ることができない領域について推論しなければならないことがあります。たとえば、次のプログラムを考えてみましょう。

// static 参照を必要とする関数
fn foo(x: &'static u32) { }

fn bar(f: for<'a> fn(&'a u32)) {
       // ^^^^^^^^^^^^^^^^^^^ **任意の**参照を受け取れる関数
    let x = 22;
    f(&x);
}

fn main() {
    bar(foo);
}

このプログラムは型チェックに通るべきではありません。foo は引数に static 参照を必要とし、bar任意の参照を受け取る関数を渡されることを期待しているためです(たとえば、自身のスタック上の何かを使ってその関数を呼び出せるように)。しかし、これを どのように 拒否し、なぜ 拒否するのでしょうか。

サブタイピングとプレースホルダー

main を型チェックするとき、特に bar(foo) の呼び出しを型チェックするとき、最終的に次のようなサブタイピング関係に行き着きます。

fn(&'static u32) <: for<'a> fn(&'a u32)
----------------    -------------------
the type of `foo`   the type `bar` expects

この種のサブタイピングは、スーパータイプで束縛されている変数を取り出し、それらを全称量化された代表で置き換えることで処理します。ここではそれを !1 のように表します。私たちはこれらの領域を「プレースホルダー領域」と呼びます。基本的には、「何らかの未知の領域」を表しています。

この置き換えを行うと、次の関係になります。

fn(&'static u32) <: fn(&'!1 u32)

ここでの重要な考え方は、この未知の領域 '!1 は他のどの領域とも関係していないということです。したがって、サブタイピング関係が '!1 について真であることを証明できれば、それは任意の領域について真であるはずであり、それこそが私たちの望んでいたことです。

では、次に何が起こるかを順に見ていきましょう。2 つの関数がサブタイプであるかどうかを確認するには、それらの引数が望ましい関係を持っているかを確認します(関数の引数は反変なので、ここでは左辺と右辺を入れ替えます)。

&'!1 u32 <: &'static u32

参照に関する基本的なサブタイピング規則によれば、これは '!1: 'static であれば真になります。つまり、「何らかの未知の領域 !1」が 'static より長生きする場合です。さて、これは真である 可能性はあります。結局のところ、'!1'static かもしれません。しかし、それが真であるとは 分かりません。したがって、これは(最終的には)エラーを生成するべきです。

ユニバースとは何か?

前のセクションでは、プレースホルダー領域という考え方を導入し、それを !1 と表しました。私たちはこの数値 1ユニバースインデックス と呼びます。「ユニバース」という考え方は、ある型の中、またはある時点でスコープ内にある名前の集合である、というものです。ユニバースは木として形成され、各子は親にいくつかの新しい名前を追加します。したがって、ルートユニバース は概念的には、ライフタイム 'static や型 i32 のようなグローバルな名前を含みます。コンパイラでは、ジェネリック型パラメータもこのルートユニバースに入れます(この意味では、ルートユニバースは 1 つだけではなく、アイテムごとに 1 つあります)。そこで、この関数 bar を考えてみましょう。

struct Foo { }

fn bar<'a, T>(t: &'a T) {
    ...
}

ここで、ルートユニバースはライフタイム 'static'a で構成されます。実際には、ここではライフタイムに注目していますが、同じ概念を型にも適用できます。その場合、型 FooT が(i32 のような他のグローバル型とともに)ルートユニバースに含まれます。基本的に、ルートユニバースは bar の本体に自由に現れるすべての名前を含みます。

次に、変数 x を追加して bar を少し拡張してみましょう。

fn bar<'a, T>(t: &'a T) {
    let x: for<'b> fn(&'b u32) = ...;
}

ここで、名前 'b はルートユニバースの一部ではありません。代わりに、この for<'b> の中に「入る」とき(たとえば、それをプレースホルダーで置き換えることによって)、ルートの子ユニバースを作成します。これを U1 と呼ぶことにしましょう。

U0 (root universe)
│
└─ U1 (child universe)

この考え方は、この子ユニバース U1 がルートユニバース U0 に新しい名前を追加する、というものです。その名前はユニバース番号 !1 によって識別しています。

次に、もう 1 つの変数 y を追加して bar を少し拡張してみましょう。

fn bar<'a, T>(t: &'a T) {
    let x: for<'b> fn(&'b u32) = ...;
    let y: for<'c> fn(&'c u32) = ...;
}

この 型に入るとき、再び新しいユニバースを作成し、それを U2 と呼びます。その親はルートユニバースになり、U1 はその兄弟になります。

U0 (root universe)
│
├─ U1 (child universe)
│
└─ U2 (child universe)

これは、U2 の中では U0 または U2 のものを名前で参照できますが、U1 のものは参照できないことを意味します。

存在変数にユニバースを与える。 ユニバースという概念を得たので、それを使って型チェッカーなどを拡張し、不正な名前が漏れ出すのを防ぐことができます。その考え方は、各推論(存在)変数に、型であれライフタイムであれ、ユニバースを与えるというものです。その変数の値は、そのユニバースから見える名前だけを参照できます。たとえば、あるライフタイム変数が U0 で作成された場合、その変数に !1!2 の値を割り当てることはできません。なぜなら、それらの名前はユニバース U0 から見えないからです。

単なるカウンタでユニバースを表す。 コンパイラがユニバースの完全な木を追跡していないことを知ると、驚くかもしれません。代わりに、単にカウンタを保持しています。そして、あるユニバースが別のユニバースを見ることができるかどうかを判定するには、単にインデックスがより大きいかどうかを確認します。たとえば、U2 は U0 を見ることができます。なぜなら 2 >= 0 だからです。しかし、U0 は U2 を見ることができません。なぜなら 0 >= 2 は偽だからです。

どうしてこれで済ませられるのでしょうか。これは、U2 が U1 も見ることを許してしまうことを意味しないのでしょうか。答えは、はい、もしその問いが発生することがあれば、そうなります。しかし、私たちの型チェッカーなどの構造上、それが起こる方法はありません。ユニバース U1 で起こっていることが U2 で起こっていることと「通信」するには、共通の共有推論変数 X を持つ必要があります。そして、U1 の中のすべてのものは U1 とその子にだけスコープされているため、その推論変数 X は U0 に存在しなければなりません。そして X は U0 にあるので、U1(または U2)のどのものも名前で参照できません。これは、ある種の汎用的な「論理」の例を使うと最も分かりやすいかもしれません。

exists<X> {
   forall<Y> { ... /* Y は U1 にある ... */ }
   forall<Z> { ... /* Z は U2 にある ... */ }
}

ここで、2 つの forall が相互作用する唯一の方法は X を介することですが、X が宣言された時点では Y も Z もスコープ内にないため、その値はそれらのどちらも参照できません。

ユニバースとプレースホルダー領域要素

しかし、そのエラーはどこから来るのでしょうか。それは次のようにして起こります。領域推論コンテキストを構築しているとき、型推論コンテキストから、存在するプレースホルダー変数の数を知ることができます(InferCtxt には内部カウンタがあります)。それらのそれぞれについて、対応する普遍領域変数 !n と「領域要素」placeholder(n) を作成します。これは「何らかの未知の他の要素の集合」に対応します。!n の値は {placeholder(n)} です。 同時に、各存在変数には ユニバース も与えます(これも InferCtxt から取得します)。このユニバースは、その値にどのプレースホルダー要素が現れてよいかを決定します。たとえば、ユニバース U3 の変数は placeholder(1)placeholder(2)placeholder(3) を参照できますが、placeholder(4) は参照できません。推論変数のユニバースは、その値にどのリージョン要素が現れることができるかを制御するものであり、リージョン要素が現れることになると言っているわけではない点に注意してください。

プレースホルダーと outlives 制約

リージョン推論エンジンでは、outlives 制約は次の形式を持ちます。

V1: V2 @ P

ここで V1V2 はリージョンインデックスであり、したがって何らかのリージョン変数(全称量化または存在量化されている可能性があります)に対応します。ここでの P は制御フローグラフ内の「点」です。このセクションでは重要ではありません。この変数にはユニバースがあるので、それぞれ U(V1) および U(V2) と呼ぶことにしましょう。(実際には、ここで気にするのは U(V1) だけです。)

この制約に遭遇したとき、通常の手順は P から DFS を開始することです。歩いているノードが value(V2) に存在する限り歩き続け、それらのノードを value(V1) に追加します。return の点に到達した場合は、任意の end(X) 要素を追加します。この部分は変更されません。

しかし、その後で、V2 内のプレースホルダー placeholder(x) 要素を反復処理したいとします(それらはそれぞれ U(V2) から見えていなければなりませんが、それが真であると仮定できるはずで、チェックする必要はありません)。value(V1) がそれらの各プレースホルダー要素よりも長く生きることを保証しなければなりません。

これが起こり得る方法は 2 つあります。まず、U(V1) がユニバース x を見ることができる場合(つまり x <= U(V1) の場合)、単に placeholder(x)value(V1) に追加して完了できます。そうでない場合は、近似しなければなりません。placeholder(x) が表す要素の集合が何であるかは分からないかもしれませんが、それに対する何らかの上界 B、つまり placeholder(x) よりも長く生きるリージョン B は計算できるはずです。今のところ、それには単に 'static を使います(すべてよりも長く生きるためです)。将来的には、ここでもっと賢くできる場合があります(実際、他のコンテキストではこれを行うコードがすでにあります)。さらに、'static はルートユニバース U0 にあるため、すべての変数から見えることが分かっています。つまり基本的に、value(V2) が、V1 からは見えない何らかのユニバース xplaceholder(x) を含むことが分かった場合、V1'static に強制します。

「全称リージョン」チェックの拡張

すべての制約が伝播された後、NLL リージョン推論には最後のチェックがあります。そこでは、各全称リージョンについて計算された最終的な値を調べ、それらが「大きくなりすぎて」いないことを確認します。今回の場合は、各プレースホルダーリージョンを調べ、それが outlive することが分かっている placeholder(u) 要素だけを含んでいることを確認します。(後で、fn シグネチャ由来の全称リージョンに対して行っているように、2 つのプレースホルダーリージョン間の関係を把握し、それを考慮に入れられるようになるかもしれません。)

別の言い方をすると、「全称リージョン」チェックは次のような制約をチェックしていると考えることができます。

{placeholder(1)}: V1

ここで {placeholder(1)} は定数集合のようなものであり、V1 は !1 リージョンを表すために作った変数です。

例に戻る

ここまでは順調です。では、最初の例で何が起こるかを見ていきましょう。

fn(&'static u32) <: fn(&'!1 u32) @ P  // この点 P はここでは重要ではありません

リージョン推論エンジンは、次のようなリージョン要素ドメインを作成します。

{ CFG; end('static); placeholder(1) }
  ---  ------------  ------- ユニバース `!1` 由来
  |    'static は常にスコープ内にあります
  CFG 内のすべての点。ここでは特に関連しません

常に 2 つの全称変数を作成します。1 つは 'static を表し、もう 1 つは '!1 を表します。それらを Vs と V1 と呼ぶことにしましょう。これらは次のような初期値を持ちます。

Vs = { CFG; end('static) } // U0 にあるため、他のものを参照できません
V1 = { placeholder(1) }

上記のサブタイピング制約から、次のような outlives 制約が得られます。

'!1: 'static @ P

これを処理するために、V1 の値を拡張して Vs のすべてを含めます。

Vs = { CFG; end('static) }
V1 = { CFG; end('static), placeholder(1) }

この時点で、すべての outlives 関係が満たされているため、制約の伝播は完了します。次に、コードの「全称リージョンをチェックする」部分に進み、全称リージョンが大きくなりすぎていないことをテストします。

この場合、V1 は実際に大きくなりすぎています。end('static) よりも長く生きることは分かっておらず、CFG のどの点よりも長く生きることも分かっていないため、エラーを報告します。

別の例

このサブタイピング関係についてはどうでしょうか。

for<'a> fn(&'a u32, &'a u32)
    <:
for<'b, 'c> fn(&'b u32, &'c u32)

ここでは、以前と同様に、スーパータイプ内の束縛リージョンをプレースホルダーで置き換え、次のようになります。

for<'a> fn(&'a u32, &'a u32)
    <:
fn(&'!1 u32, &'!2 u32)

次に、左辺の変数をユニバース U2 の存在変数でインスタンス化し、次のようになります(?n は存在変数の記法です)。

fn(&'?3 u32, &'?3 u32)
    <:
fn(&'!1 u32, &'!2 u32)

次に、これをさらに分解します。

&'!1 u32 <: &'?3 u32
&'!2 u32 <: &'?3 u32

さらに分解すると、リージョン制約が得られます。

'!1: '?3
'!2: '?3

この場合、'!1'!2 はどちらも変数 '?3 よりも長く生きる必要がありますが、変数 '?3 は他の何かよりも長く生きることを強制されていない点に注意してください。したがって、それは単に空の要素集合として始まり、空の要素集合として終わるため、ここでは型チェックが成功します。

(これは少し驚くはずです。私も最初に気づいたときは驚きました。私たちは、2 つの引数が同じリージョンを持つことを必要とする fn である場合に、2 つの異なるリージョンを持つ引数で呼び出されることを受け入れられる、と言っています。これは直感的には健全でないように思えます。しかし実際には問題ありません。これはずっと前に Rust の issue トラッカーの この issue で説明しようとしたとおりです。理由は、たとえ 2 つの異なるライフタイムを持つ引数で呼び出されたとしても、その 2 つのライフタイムには何らかの共通部分(呼び出し自体)があり、その共通部分を、引数の共通ライフタイムとして使用する 'a の値にできるからです。-nmatsakis)

最後の例

最後にもう 1 つ例を見てみましょう。前の例を拡張して、戻り値の型を持たせます。

for<'a> fn(&'a u32, &'a u32) -> &'a u32
    <:
for<'b, 'c> fn(&'b u32, &'c u32) -> &'b u32

前の例と非常によく似ているように見えますが、このケースではエラーになります。それでよいのです。問題は、2 つの引数のうちどちらかを返すことを約束する fn から、最初の引数を返すことを約束する fn へと変わってしまったことです。これは健全ではありません。どのように展開されるか見てみましょう。

まず、スーパータイプ内の束縛リージョンをプレースホルダーで置き換えます。

for<'a> fn(&'a u32, &'a u32) -> &'a u32
    <:
fn(&'!1 u32, &'!2 u32) -> &'!1 u32

次に、サブタイプを存在変数(U2 内)でインスタンス化します。

```text
fn(&'?3 u32, &'?3 u32) -> &'?3 u32
    <:
fn(&'!1 u32, &'!2 u32) -> &'!1 u32

そしてここで、サブタイプ関係を作成します。

&'!1 u32 <: &'?3 u32 // 引数 1
&'!2 u32 <: &'?3 u32 // 引数 2
&'?3 u32 <: &'!1 u32 // 戻り値の型

そして最後に outlives 関係です。ここで、V1、V2、V3 を、それぞれ !1!2?3 に割り当てる変数とします。

V1: V3
V2: V3
V3: V1

これらの変数は、次の初期値を持ちます。

V1 in U1 = {placeholder(1)}
V2 in U2 = {placeholder(2)}
V3 in U2 = {}

ここで V3: V1 制約があるため、placeholder(1)V3 に追加する必要があります(そして 実際にそれは V3 から可視です)。そのため、次のようになります。

V3 in U2 = {placeholder(1)}

次に V2: V3 という制約があるため、最終的に V2 を拡大して placeholder(1) を含める必要があります(これも可視です)。

V2 in U2 = {placeholder(1), placeholder(2)}

これで制約伝播は完了ですが、outlives 関係を確認すると、V2 にこの新しい要素 placeholder(1) が含まれていることが分かるため、 エラーを報告します。

クロージャ制約の伝播

型テストとユニバーサルリージョンを検査しているとき、クロージャ本体内にいる場合には、まだ証明できない制約に遭遇することがあります!しかし、必要な制約が実際には成り立っている可能性があります(まだそれが分からないだけです)。したがって、クロージャ内にいる場合は、まだ証明できないすべての制約を収集して返すだけにします。後で、そのクロージャを作成した MIR ノードを借用検査するときに、これらの制約が成り立つことも検査できます。その時点で、それらが成り立つことを証明できなければ、エラーを報告します。

これがどのように実装されているか

RegionInferenceContext::solve 内でクロージャを借用検査している間、型 outlives 制約とリージョン outlives 制約をローカルで証明できない場合には、それらを親へ伝播しようと別々に試みます。

リージョン outlives 制約

RegionInferenceContext::check_universal_regions が何らかの outlives 制約 'longer_fr: 'shorter_fr の証明に失敗した場合、fn try_propagate_universal_region_error でそれを伝播しようとします。これらのユニバーサルリージョンはいずれも、クロージャにローカルなリージョンか、外部リージョンのどちらかです。

'longer_fr がローカルなユニバーサルリージョンである場合、'longer_fr によって outlive される最大の外部リージョン 'fr_minus、つまり 'longer_fr: 'fr_minus を探します。そのようなリージョンが複数ある場合は、mutual_immediate_postdominator を選びます。これはすべての GLB の GLB を繰り返し計算することによる不動点です。詳細については TransitiveRelation::postdom_upper_bound を参照してください。

'fr_minus が存在する場合、それが 'shorter_fr のすべての非ローカル上限よりも長く生存することを要求します。少なくとも 1 つの非ローカル上限 'static は常に存在します。

型 outlives 制約

型 outlives 制約は check_type_tests で証明されます。これは outlives グラフの計算後に行われ、そのグラフはこの時点では不変です。

クロージャ内の fn eval_verify_bound によって証明できなかったすべての型テストについて、try_promote_type_test を呼び出します。TypeTest は、verify_bound とともに型 outlives 境界 generic_kind: lower_bound を表します。VerifyBoundlower_bound に対して成り立つ場合、その制約は満たされます。try_promote_type_test verify_bound を気にしません。

これは fn try_promote_type_test_subject を呼び出すことから始まります。この関数は GenericKind を受け取り、クロージャにローカルなものをもはや何も参照しない ClosureOutlivesSubject へ変換しようとします。これは、その型内のすべての自由リージョンを、'static またはその自由リージョンと等しいリージョンパラメータのいずれかに置き換えることで行われます。generic_kind に置き換えられないリージョンが含まれている場合、この操作は失敗します。

次に、lower_bound を呼び出し元のコンテキストに昇格します。下限がプレースホルダーと等しい場合、それを 'static に置き換えます

次に、lower_bound によって outlive される必要があるすべてのユニバーサルリージョン uv、つまり借用検査がリージョン制約を追加したものを確認します。これらのそれぞれについて、uv よりも長く生存することが分かっているすべての非ローカルなユニバーサルリージョンに対して ClosureOutlivesRequirement を出力します。

この時点ではすでにクロージャのリージョングラフを構築しており、それが一貫していることも別途検査しているため、ここでは outlives 制約 uv: lower_bound を仮定することもできます。

したがって、証明できない型 outlives 境界、たとえば T: 'local_infer がある場合、リージョングラフを使用して 'a: local_infer を満たすユニバーサル変数 'a に移動します。'a がローカルな場合は、仮定された outlived 制約を使用して非ローカルなものに移動します。

次に、昇格された型テストのリストを BorrowCheckResults に格納します。 その後、その親を借用検査している間に TypeChecker::prove_closure_bounds でそれらを適用します。

TODO: それが正確にどのように機能するかを説明する :3

リージョンエラーの報告

TODO: これらの解析結果からエラーを生成する方法について議論する必要がある。

2フェーズ借用

2フェーズ借用は、vec.push(vec.len()) のようなネストしたメソッド呼び出しを可能にする、可変借用のより寛容なバージョンです。このような借用は、最初は「予約」フェーズで共有借用として振る舞い、後で完全な可変借用へ「有効化」できます。

特定の暗黙的な可変借用だけが 2フェーズになり得ます。ソースコード内のどの &mutref mut も 2フェーズ借用になることはありません。2フェーズ借用を生成するケースは次のとおりです。

  1. 可変参照レシーバーを持つメソッドを呼び出す際の autoref 借用。
  2. 関数引数内の可変再借用。
  3. オーバーロードされた複合代入演算子における暗黙的な可変借用。

いくつか例を示します。

#![allow(unused)]
fn main() {
// ソースコード内

// ケース 1:
let mut v = Vec::new();
v.push(v.len());
let r = &mut Vec::new();
r.push(r.len());

// ケース 2:
std::mem::replace(r, vec![1, r.len()]);

// ケース 3:
let mut x = std::num::Wrapping(2);
x += x;
}

これらを 2フェーズ借用が分かる程度に展開すると、次のようになります。

// ケース 1:
let mut v = Vec::new();
let temp1 = &two_phase v;
let temp2 = v.len();
Vec::push(temp1, temp2);
let r = &mut Vec::new();
let temp3 = &two_phase *r;
let temp4 = r.len();
Vec::push(temp3, temp4);

// ケース 2:
let temp5 = &two_phase *r;
let temp6 = vec![1, r.len()];
std::mem::replace(temp5, temp6);

// ケース 3:
let mut x = std::num::Wrapping(2);
let temp7 = &two_phase x;
let temp8 = x;
std::ops::AddAssign::add_assign(temp7, temp8);

借用が 2フェーズになり得るかどうかは、型検査後に AutoBorrow 上のフラグで追跡され、その後 MIR 構築中に BorrowKind変換 されます。

各 2フェーズ借用は、一度だけ使用される一時変数に割り当てられます。そのため、次のように定義できます。

  • 一時変数が代入されるポイントを、その 2フェーズ借用の予約ポイントと呼びます。
  • 一時変数が使用されるポイントは、実質的に常に関数呼び出しであり、有効化ポイントと呼びます。

有効化ポイントは GatherBorrows visitor を使用して見つけられます。その後、BorrowData はその借用の予約ポイントと有効化ポイントの両方を保持します。

2フェーズ借用の検査

2フェーズ借用は、次の例外を除き、可変借用であるかのように扱われます。

  1. MIR 内のすべての location で、この location において有効化される 2フェーズ借用があるかどうかを 検査 します。live な 2フェーズ借用がある location で有効化される場合、その 2フェーズ借用と競合する借用がないことを検査します。
  2. 予約ポイントでは、競合する live な可変借用があればエラーにします。また、競合する共有借用があれば lint します。
  3. 予約ポイントと有効化ポイントの間では、2フェーズ借用は共有借用として振る舞います。そのようなポイントにいるかどうかは、MIR グラフの Dominators を使用して(is_active 内で)判定します。
  4. 有効化ポイントの後では、2フェーズ借用は可変借用として振る舞います。

クロージャキャプチャ推論

このセクションでは、rustc がクロージャをどのように扱うかを説明します。Rust のクロージャは、作成元のスタックフレームから、使用する値(または使用する値への参照)を含む構造体へ実質的に「脱糖」されます。rustc の仕事は、クロージャがどの値をどのように使用するかを把握し、特定の変数を共有参照、可変参照、またはムーブのどれでキャプチャするかを決定できるようにすることです。rustc はまた、クロージャがどのクロージャトレイト(FnFnMut、または FnOnce)を実装できるかも把握する必要があります。

いくつかの例から始めましょう。

例 1

まず、次の例のクロージャがどのように脱糖されるかを見てみましょう。

fn closure(f: impl Fn()) {
    f();
}

fn main() {
    let x: i32 = 10;
    closure(|| println!("Hi {}", x));  // クロージャは x を読み取るだけです。
    println!("Value of x after return {}", x);
}

上記が immut.rs というファイルの内容だとします。次のコマンドを使用して immut.rs をコンパイルします。-Z dump-mir=all フラグにより、 rustcMIR を生成し、mir_dump というディレクトリにダンプします。

> rustc +stage1 immut.rs -Z dump-mir=all

このコマンドを実行すると、現在の作業ディレクトリに mir_dump という新しく生成されたディレクトリがあることがわかります。このディレクトリには複数のファイルが含まれています。 ファイル rustc.main.-------.mir_map.0.mir を見ると、ほかの内容に加えて、次の行も含まれていることがわかります。

_4 = &_1;
_3 = [closure@immut.rs:7:13: 7:36] { x: move _4 };

この章の MIR の例では、_1x であることに注意してください。

ここで、1 行目 _4 = &_1; において、mir_dumpx が不変参照として借用されたことを示しています。これは、クロージャが x を読み取るだけであるため、期待どおりです。

例 2

別の例を示します。

fn closure(mut f: impl FnMut()) {
    f();
}

fn main() {
    let mut x: i32 = 10;
    closure(|| {
        x += 10;  // クロージャは x の値を変更します
        println!("Hi {}", x)
    });
    println!("Value of x after return {}", x);
}
_4 = &mut _1;
_3 = [closure@mut.rs:7:13: 10:6] { x: move _4 };

今回は、_4 = &mut _1; という行で、借用が可変借用に変更されていることがわかります。 妥当です!クロージャは x に 10 を加算しています。

例 3

もう 1 つ例を示します。

fn closure(f: impl FnOnce()) {
    f();
}

fn main() {
    let x = vec![21];
    closure(|| {
        drop(x);  // 以後 x を使用できなくします。
    });
    // println!("Value of x after return {:?}", x);
}
_6 = [closure@move.rs:7:13: 9:6] { x: move _1 }; // bb16[3]: move.rs:7:13: 9:6 のスコープ 1

ここでは、x はクロージャに直接ムーブされ、クロージャの後にそれへアクセスすることは許可されません。

コンパイラにおける推論

それでは rustc のコードを詳しく見て、これらすべての推論がコンパイラによってどのように行われるかを確認しましょう。

まず、この後の議論で頻繁に使用する用語を定義することから始めましょう - upvar です。upvar とは、クロージャが定義されている関数にローカルな変数です。したがって、上記の例では、x はクロージャに対する upvar になります。これらは 自由変数 と呼ばれることもあります。これは、クロージャのコンテキストに束縛されていないことを意味します。 compiler/rustc_passes/src/upvars.rs は、この目的のために upvars_mentioned というクエリを定義しています。

遅延呼び出し以外で、クロージャを通常の関数と区別するもう 1 つの点は、upvar を使用できることです。クロージャは周囲のコンテキストからこれらの upvar を借用します。そのため、コンパイラは upvar の借用型を決定する必要があります。コンパイラは不変借用型を割り当てることから始め、使用方法に基づいて必要に応じて制約を弱めます(つまり、immutable から mutable、さらに move へ変更します)。上記の例 1 では、クロージャは変数を出力のために使用するだけで、いかなる方法でも変更しません。そのため、mir_dump では upvar x の借用型が不変であることがわかります。しかし例 2 では、クロージャは x を変更し、何らかの値を加算します。この変更のために、最初は x を不変参照型として割り当てていたコンパイラは、それを可変参照として調整する必要があります。同様に 3 番目の例では、クロージャがベクターをドロップするため、変数 x をクロージャへムーブする必要があります。借用の種類に応じて、クロージャは適切なトレイトを実装する必要があります。不変借用では Fn トレイト、可変借用では FnMut、ムーブセマンティクスでは FnOnce です。

クロージャに関連するコードの大部分は compiler/rustc_hir_typeck/src/upvar.rs ファイルにあり、データ構造は compiler/rustc_middle/src/ty/mod.rs ファイルで宣言されています。

先に進む前に、rustc コードベースを通る制御の流れをどのように調べられるかを説明しましょう。特にクロージャについては、次のように RUSTC_LOG 環境変数を設定し、出力をファイルに収集します。

> RUSTC_LOG=rustc_hir_typeck::upvar rustc +stage1 -Z dump-mir=all \
    <.rs file to compile> 2> <file where the output will be dumped>

これは stage1 コンパイラを使用し、 rustc_hir_typeck::upvar モジュールの debug! ロギングを有効にします。

もう 1 つの選択肢は、lldb または gdb を使用してコードをステップ実行することです。

  1. rust-lldb build/host/stage1/bin/rustc test.rs
  2. lldb で:
    1. b upvar.rs:134 // upvar.rs ファイル内の特定の行にブレークポイントを設定する
    2. r // ブレークポイントに到達するまでプログラムを実行する

upvar.rs から始めましょう。このファイルには euv::ExprUseVisitor と呼ばれるものがあり、クロージャのソースを走査し、 借用、変更、またはムーブされる各 upvar に対してコールバックを呼び出します。

fn main() {
    let mut x = vec![21];
    let _cl = || {
        let y = x[0];  // 1.
        x[0] += 1;  // 2.
    };
}

上記の例では、ビジターは 1 と 2 で示された行に対して 2 回呼び出されます。1 回は共有借用に対して、もう 1 回は可変借用に対してです。また、何が借用されたかも教えてくれます。

コールバックは Delegate トレイトを実装することで定義されます。 InferBorrowKind 型は Delegate を実装し、 各 upvar についてどのキャプチャモードが必要だったかを記録するマップを保持します。キャプチャモードには ByValue(ムーブされた)または ByRef(借用された)があります。ByRef 借用の場合、可能な [BorrowKind] は [compiler/rustc_middle/src/ty/mod.rs][middle_ty] で定義されている ImmBorrowUniqueImmBorrowMutBorrow です。 [BorrowKind]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/enum.BorrowKind.html [middle_ty]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/index.html

Delegate は、いくつかの異なるメソッド(異なるコールバック)を定義します。 変数の move には consume、何らかの種類の borrow (共有または可変)には borrow、何かの assignment が見つかったときには mutate です。

これらのコールバックにはすべて共通の引数 cmt があります。これは Category、 Mutability、Type を表し、 compiler/rustc_hir_typeck/src/expr_use_visitor.rs で定義されています。コード コメントから引用すると、「cmt は、値がどこから発生し、どのように配置されているか、 およびその値が格納されているメモリの可変性を示す、値の完全な分類です」。コールバック (consume、borrow など)に基づいて、関連する adjust_upvar_borrow_kind_for_<something> を呼び出し、 cmt を渡します。borrow 型が調整されると、それをテーブルに格納します。これは基本的に、 各クロージャに対してどの borrow が行われたかを示します。

self.tables
    .borrow_mut()
    .upvar_capture_map
    .extend(delegate.adjust_upvar_captures);

Async クロージャ/“コルーチン・クロージャ”

この機能の一般的な動機を理解するには、RFC 3668 を読んでください。これは非常に技術的で、やや「縦割り」の章です。理想的には、これを分割して関連するすべての章に散りばめるべきですが、async クロージャを全体的に理解する目的のために、ここではすべてを 1 つの章にまとめています。

コルーチン・クロージャ – 技術的な詳細解説

コルーチン・クロージャは async クロージャの一般化であり、コルーチンを返すクロージャ式のための特別な構文です。特に、そのコルーチンはクロージャの upvar からキャプチャすることが許可されています。

現時点で使用可能な唯一のコルーチン・クロージャの種類は async クロージャであり、async クロージャのサポートがこの PR の範囲です。最終的には gen || {} などをサポートする可能性があり、この文書で説明する問題や興味深い点の大半は、一般にすべてのコルーチン・クロージャに当てはまります。

コードがある程度汎用的であることの結果として、この文書ではそれらを「async クロージャ」と呼んだり「コルーチン・クロージャ」と呼んだりする場合があります。async クロージャによって返される future は、一般に「コルーチン」または「子コルーチン」と呼ばれます。

HIR

async クロージャ(および将来的には gen などの他のコルーチン種別)は、HIR では hir::Closure として表現されます。 hir::Closure の closure-kind は ClosureKind::CoroutineClosure(_)1 で、これは async ブロックをラップします。この async ブロックも HIR では hir::Closure として表現されます。 async ブロックの closure-kind は ClosureKind::Closure(CoroutineKind::Desugared(_, CoroutineSource::Closure))2 です。

async fn と同様に、async クロージャの本体を lowering する際には、クロージャのすべての引数を無条件に本体へ move し、それらがキャプチャされるようにする必要があります。これは lower_coroutine_body_with_moved_arguments3 によって処理されます。この関数に関する唯一の注目すべき癖は、最終的に生成される async ブロックが CaptureBy::ByRef4 というキャプチャ種別になることです。後でクロージャの引数はすべて by-value でキャプチャされるように強制します5 が、async ブロック全体async move であるかのように振る舞ってほしくはありません。そうすると async クロージャの自己借用の目的が損なわれてしまうためです。

rustc_middle::ty 表現

実装をできるだけ将来互換に保つ目的で(つまり gen || {}async gen || {} に対応できるように)、このセクションの大部分では async クロージャを「コルーチン・クロージャ」と呼びます。

この PR が導入する主なものは、CoroutineClosure6 という新しい TyKind と、typeck および borrowck の他の関連する enum(UpvarArgs, DefiningTy, AggregateKind)上の対応するバリアントです。

既存の TyKind::Closure を一般化するのではなく、新しい TyKind を導入するのは、型の表現に大きな違いがあるためです。CoroutineClosure の主な違いは、まずコルーチン・クロージャのジェネリクスの「アンパック済み」表現である CoroutineClosureArgsParts を調べることで確認できます。

クロージャとの類似点

クロージャと同様に、parent_argsclosure_kind_tytupled_upvars_ty があります。これらはクロージャにおける対応物と同じものを表します。すなわち、クロージャが定義されている本体から継承されるジェネリクス、クロージャの最大の「呼び出し能力」(つまり、FnOnce のように呼び出すために消費されなければならないのか、それとも by-ref で呼び出せるのか)、そしてクロージャ自身のキャプチャされた upvar です。

シグネチャ

従来のクロージャには、クロージャのシグネチャを表現するために使用する fn_sig_as_fn_ptr_ty があります。これに対して、コルーチン・クロージャのシグネチャは、やや「展開された」形で格納します。これは、コルーチン・クロージャには、それをどの AsyncFn* トレイトで呼び出すかに応じて2 つのシグネチャがあるためです(以下のセクションを参照)。

概念的には、コルーチン・クロージャは、by-ref で呼び出されるか by-move で呼び出されるかに応じて、複数の異なるシグネチャ型を含むものと考えることができます。

これら両方のシグネチャを簡単に再作成するために、signature_parts_ty はこのコルーチン・クロージャによって返されるコルーチンの関連するすべての部分を格納します。この signature parts 型は、一般に fn(tupled_inputs, resume_ty) -> (return_ty, yield_ty) という形になります。ここで resume_tyreturn_tyyield_ty は、コルーチン・クロージャによって返されるコルーチンのそれぞれの型です7

コンパイラは主に CoroutineClosureSignature8 を扱います。これは、上記の fn() ptr 型から関連する型を抽出して作成されるもので、コルーチン・クロージャが最終的に返すコルーチンを構築するために使用できるメソッドを公開しています。

Coroutine 戻り値型を構築するために持ち運ぶ必要があるデータ

シグネチャに格納されたデータに加えて、返す TyKind::Coroutine を構築するには、コルーチンの「witness」も格納する必要があります。

では、返される Coroutine の upvar はどうなるのでしょうか。AsyncFnOnce(つまり call-by-move)の場合、これは単純に、そのコルーチンが返す upvar と同じです。しかし AsyncFnMut/AsyncFn の場合、コルーチン・クロージャから返されるコルーチンは、与えられた「環境」ライフタイム9 を持つコルーチン・クロージャからデータを借用します。これは、AsyncFnMut/AsyncFn 呼び出しシグネチャ上の &self ライフタイム10 と、ByRef の GAT ライフタイム11 に対応します。

実際にコルーチンの戻り値型を取得する

コルーチン・クロージャが返す Coroutine を最も簡単に構築するには、CoroutineClosureSignature 上の to_coroutine_given_kind_and_upvars12 ヘルパーを使用できます。これは CoroutineClosureArgs から取得できます。

その関数への引数の大半は、CoroutineArgs から取り出せるコンポーネントになります。ただし、goal_kind: ClosureKind は例外で、これは渡された ClosureKind に基づいて返すコルーチンの種類を制御します。つまり、ClosureKind::Fn | ClosureKind::FnMut の場合は by-ref コルーチンを準備し、ClosureKind::FnOnce の場合は by-move コルーチンを準備します。

トレイト階層

に実装される、Fn* トレイトの並行した階層を導入します。導入の動機はブログ記事 Async Closures で説明されています。

現在 stable なすべての callable 型(つまり、クロージャ、関数アイテム、関数ポインタ、dyn Fn* トレイトオブジェクト)は、何らかの出力型 Fut について Fn*() -> Fut を実装し、かつ FutFuture<Output = T> を実装する場合、自動的に AsyncFn*() -> T を実装します13

async クロージャは、その本体が許す範囲で AsyncFn* を実装します。つまり、upvar を互換性のある方法で使用する場合です(すなわち、upvar を消費または変更する場合、それが AsyncFn および AsyncFnMut を実装するかどうかに影響する可能性があります…)。

Lending

将来的には、AsyncFn* をより一般的な LendingFn* トレイト群の上に移す可能性があります。しかし、現在のコンパイラで LendingFn を人間工学的に使用する能力を制限している、いくつかの具体的な技術的実装上の詳細があります。これらは次に関係しています。

  • クロージャシグネチャ推論。
  • higher-ranked trait bounds に関する制限。
  • エラーメッセージの不備。

これらの制限に加えて、基盤となるトレイトは async クロージャや async Fn トレイト境界のユーザー体験に影響すべきではないという事実から、今のところ AsyncFn* に行き着きます。最終的にこれらのより一般的なトレイトへ移行できるようにするため、正確な AsyncFn* トレイト定義(関連型を含む)は実装詳細として残されています。

async クロージャはいつ通常の Fn* トレイトを実装するのか?

上では「通常の」callable 型が AsyncFn* を実装できることに触れましたが、その逆の問いとして「async クロージャも Fn* を実装できるのか?」があります。短い答えは「それが妥当なとき」、つまり AsyncFn/AsyncFnMut から返されるはずだったコルーチンが、親の coroutine-closure から「貸し出されている」upvar を実際には持っていない場合です。

詳しい答えについては、下の「follow-up: when do…」セクションを参照してください。完全な答えでは、かなり興味深く、またできれば包括的であることを意図したヒューリスティックについて説明しています。これは、ほとんどの async クロージャが「そのまま動く」ことを保証するために使われています。

2 つの本体の話…

async クロージャが AsyncFn/AsyncFnMut で呼び出されると、クロージャから借用するコルーチンを返します。しかし、AsyncFnOnce 経由で呼び出される場合は、そのクロージャを消費するため、すでに drop されたデータから借用するコルーチンを返すことはできません。

この制限を回避するため、by-ref で呼び出せる coroutine-closure に対して AsyncFnOnce::call_once を呼び出すための、別個の by-move MIR 本体を合成します。

この本体は、coroutine-closure を呼び出して返される「通常の」コルーチンと同一に動作します。ただし、親の coroutine-closure から子コルーチンへキャプチャを move しなければならないため、異なる upvar の集合を持つ点が異なります。

by-move 本体の合成

coroutine-closure から返されるコルーチンの by-move 本体にアクセスしたい場合、coroutine_by_move_body_def_id14 クエリを通じて行えます。

このクエリは、コルーチンの MIR 本体をコピーし、本体の意味論を維持するために追加の deref とフィールド projection15 を挿入することで、新しい MIR 本体を合成します。

新しい def id を合成しているため、このクエリは MIR 本体に関連する他の多数のクエリへ feed する責任もあります。このクエリは、コルーチンの built mir 上で動作するため、mir_promoted クエリ中に ensure() されます16

クロージャシグネチャ推論

async クロージャのクロージャシグネチャ推論アルゴリズムは、「従来の」クロージャの推論アルゴリズムよりも少し複雑です。クロージャと同様に、(渡された期待型に対して)関連する可能性があるすべての clause を反復処理します17

シグネチャを抽出するため、次の 2 つの状況を考慮します。

  • AsyncFnOnce::Output を伴う projection predicate。これは、クロージャの入力と出力型を抽出するために使用します。これは F: AsyncFn*() -> T 境界があった状況に対応します18
  • FnOnce::Output を伴う projection predicate。これは、入力を抽出するために使用します。出力については、関連する Future::Output projection predicate を探すことによって、出力の推定も試みます。これは F: Fn*() -> T, T: Future<Output = U> 境界があった状況に対応します。19
    • Future 境界がない場合は、出力に新しい推論変数を単に使用します。これは、Option::map のようなコンビネータ関数に async クロージャを渡せるケースに対応します。20

後者のケースをサポートしているのは、first-class な AsyncFn* トレイトが利用可能になる前に設計された API を呼び出している場合でも、ユーザーが async || {} 構文を簡単にそのまま差し込めるようにするためです。

kind が推論される前にクロージャを呼び出す

coroutine-closure の「kind」(つまり最大の呼び出しモード: AsyncFnOnce/AsyncFnMut/AsyncFn)の計算は、typeck の最後まで defer します21。しかし、typeck の終わりより前にその coroutine-closure を呼び出せるようにしたいため、それより前に coroutine-closure の戻り値型を決める必要があります。

具体的には、返されるコルーチンの def-id は変わりませんが、upvar22(親コルーチンクロージャから借用されるかムーブされるもの)と coroutine-kind23 は呼び出しモードに依存します。

AsyncFnKindHelper トレイトを導入します。これにより、「このコルーチンクロージャはこの呼び出しモードをサポートするか」24という問いはトレイトゴールを介して遅延でき、「この呼び出しモードにおけるタプル化された upvar は何か」25という問いは関連型を介して遅延できます。この関連型は、upvar 解析中に計算された upvar または “by ref” upvar のいずれかに、コルーチンクロージャの入力型を追加することで計算できます。

なるほど、ではなぜそうするのか?

これは少し回りくどく複雑に見えますし、実際その通りだと認めます。しかし、「何もしない」代替案について考えてみましょう。代わりに、すべての AsyncFn* ゴールを upvar 解析まで曖昧なものとして扱うこともできます。その時点になれば、返すコルーチンの upvar に何を入れるべきかを正確に把握できます。しかし、これは実際にはプログラム内の推論にとって非常に有害です。というのも、次のようなプログラムが妥当でなくなるからです。

let c = async || -> String { .. };
let s = c().await;
// ^^^ `<{c} as AsyncFn>::call()` をコルーチンへ射影できない場合、`.await` の内部にある `IntoFuture::into_future` 呼び出しが停止し、`s` の型は推論変数として制約されないままになります。
s.as_bytes();
// ^^^ つまり、コルーチンクロージャの await された戻り値に対して、どんなメソッドも呼び出せなくなるということです。まったく何も!

そこで代わりに、このエイリアス(この場合は射影: AsyncFnKindHelper::Upvars<'env, ...>)を使って、タプル化された upvar の計算を遅延させ、その場に置ける何かを用意します。一方で、TyKind::Coroutine(これは固定的な型です)を返すことは引き続き可能であり、必要な組み込みトレイト(今回の場合は Future)を正常に確認できます。なぜなら、Future の実装は upvar にまったく依存しないからです。

Upvar 解析

概して、コルーチンクロージャとその子コルーチンに対する upvar 解析は、通常の upvar 解析と同じように進みます。ただし、async クロージャの特殊な性質を考慮するために、いくつか興味深い点があります。

すべての入力をキャプチャさせる

async fn と同様に、すべての入力引数はキャプチャされます。これらの入力はすべて明示的に move でキャプチャするよう強制します26。これにより、async クロージャが返す future コルーチンが、その入力が本体で使用されるかどうかに依存しないようにします。そうでなければ、興味深い semver 上の危険性をもたらすことになります。

by-ref キャプチャの計算

AsyncFn/AsyncFnMut をサポートするコルーチンクロージャについては、コルーチンクロージャのキャプチャとその子コルーチンのキャプチャの関係も計算する必要があります。具体的には、コルーチンクロージャは upvar をそのキャプチャへ move する場合がありますが、コルーチンはその upvar を借用するだけの場合があります。

coroutine_captures_by_ref_ty” は、子コルーチンのすべてのキャプチャを見て、それらを親コルーチンクロージャの対応するキャプチャと比較することで計算します27。この coroutine_captures_by_ref_ty は最終的に for<'env> fn() -> captures... 型として表現され、追加の binder ライフタイムは AsyncFn::async_call または AsyncFnMut::async_call_mut を呼び出す際の “&self” ライフタイムを表します。実際にメソッドを呼び出すときに、後でその binder をインスタンス化します。

親コルーチンクロージャからのすべての by-ref キャプチャが “lending” 借用になるわけではない点に注意してください。詳細については、下の 補足: async クロージャはいつ通常の Fn* トレイトを実装するのか? セクションを参照してください。これは、コルーチンクロージャが Fn* 系のトレイトを実装できるかどうかに密接に影響します。

by-move 本体 + FnOnce の特異性

クロージャの upvar 解析が、コルーチンクロージャの子コルーチンに対して過度に緩い upvar を推論してしまい、結果として borrow-checker エラーにつながる状況がいくつかあります。これは例で示すのが最も分かりやすいです。たとえば、次の場合を考えます。

#![allow(unused)]
fn main() {
fn force_fnonce<T: async FnOnce()>(t: T) -> T { t }

let x = String::new();
let c = force_fnonce(async move || {
    println!("{x}");
});
}

x はコルーチンクロージャへムーブされますが、返されるコルーチンは &x を借用するだけになります。しかし、force_fnonce はコルーチンクロージャを AsyncFnOnce に強制し、これは lending ではありません。そのため、キャプチャを by-move で発生させるよう強制しなければなりません28

同様に、次の場合もあります。

#![allow(unused)]
fn main() {
let x = String::new();
let y = String::new();
let c = async move || {
    drop(y);
    println!("{x}");
};
}

x はコルーチンクロージャへムーブされますが、返されるコルーチンは &x を借用するだけになります。しかし、y もキャプチャして drop しているため、コルーチンクロージャは AsyncFnOnce であることを強制されます。この場合も、x のキャプチャを by-move で発生させるよう強制しなければなりません。特にこの状況を判定するには、前の例とは異なり coroutine-kind の closure-kind がまだ制約されていないため、コルーチンクロージャの本体を解析して、すべての upvar がどのように使われているかを確認し、それらが “consuming” な方法で使われているか、つまり FnOnce を強制するような使われ方をしているかを判定する必要があります29

補足: async クロージャはいつ通常の Fn* トレイトを実装するのか?

まず第一に、すべての async クロージャは FnOnce を実装します。なぜなら、常に少なくとも 1 回は呼び出せるからです。

Fn/FnMut については、詳細な答えは関連する問いに答えることを含みます。つまり、そのコルーチンクロージャは lending か、という問いです。もし lending であれば、非 lending な Fn/FnMut トレイトを実装できません。

コルーチンクロージャがいつその upvar を lend しなければならないかの判定は、should_reborrow_from_env_of_parent_coroutine_closure ヘルパー関数30に実装されています。具体的には、これは 2 か所で発生する必要があります。

  1. 親クロージャが所有するデータを借用しているか?それが該当するかどうかは、親のキャプチャが by-move かどうかを確認することで判定できます。ただし、deref 射影を適用している場合は例外です。これは、by-move でキャプチャした参照を再借用していることを意味します。
#![allow(unused)]
fn main() {
let x = &1i32; // このライフタイムを `'1` と呼ぶことにします。
let c = async move || {
    println!("{:?}", *x);
    // 内側のコルーチンは参照で借用していますが、キャプチャしているのは `*x` だけであり、
    // `x` ではないため、内側のクロージャはそのデータを `'1` の間、再借用できます。
};
}
  1. コルーチンが親のキャプチャから可変に借用している場合、その可変借用は、親または元の upvar に対して持っている借用のいずれよりも長くは生存できません。したがって、子のキャプチャは常に親コルーチンクロージャの環境のライフタイムで借用する必要があります。
#![allow(unused)]
fn main() {
let mut x = 1i32;
let c = async || {
    x = 1;
    // 親は `x` を何らかの `&'1 mut i32` として借用します。
    // しかし、`c()` を呼び出すとき、`AsyncFnMut::async_call_mut` のシグネチャに合わせて
    // 暗黙に autoref します。そのライフタイムを `'call` と呼ぶことにします。
    // `&'call mut &'1 mut i32` を再借用できる最大範囲は `&'call mut i32` であるため、
    // 内側のコルーチンはコルーチンクロージャのライフタイムでキャプチャすべきです。
};
}

これらのいずれかのケースが該当する場合、借用は親コルーチンクロージャの環境のライフタイムでキャプチャすべきです。幸い、この関数が正しくなかったとしても、プログラムが unsound になることはありません。なぜなら、私たちは依然として borrowck を行い、この関数で行われた選択を検証するからです。唯一の副作用は、ユーザーが不要な borrowck エラーを受け取る可能性があることです。

インスタンス解決

コルーチンクロージャのクロージャ種別が FnOnce の場合、その AsyncFnOnce::call_once および FnOnce::call_once の実装はコルーチンクロージャの本体31に解決され、返されるコルーチンの Future::poll は子クロージャの本体に解決されます。

コルーチンクロージャのクロージャ種別が FnMut/Fn の場合、同じことが AsyncFn と、返されるコルーチンの対応する Future 実装にも当てはまります。31 ただし、AsyncFnOnce::call_once/FnOnce::call_once の実装32と、存在する場合は Fn::call/FnMut::call_mut インスタンス33を生成するために MIR shim を使用します。

これは ConstructCoroutineInClosureShim34 によって表現されます。これが Fn::call/FnMut::call_mut のインスタンスである場合、receiver_by_ref bool は true になります。35 これらすべてのインスタンスが返すコルーチンは、この時点までに合成されている by-move 本体に対応します。36

借用チェック

async クロージャの借用チェックはかなり単純であることがわかります。新しい DefiningTy::CoroutineClosure37 バリアントを追加し、コルーチンクロージャのシグネチャを生成する方法を borrowck に教えた後38、borrowck はまったく問題なく進行します。

注意すべき点の 1 つは、by-move コルーチンのために作成する合成本体については借用チェックしないということです。これは、構築方法により(また、それが由来する by-ref コルーチン本体の妥当性により)、妥当でなければならないためです。


  1. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_ast_lowering/src/expr.rs#L1147

  2. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_ast_lowering/src/expr.rs#L1117

  3. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_ast_lowering/src/item.rs#L1096-L1100

  4. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_ast_lowering/src/item.rs#L1276-L1279

  5. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_hir_typeck/src/upvar.rs#L250-L256

  6. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_type_ir/src/ty_kind.rs#L163-L168

  7. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_type_ir/src/ty_kind/closure.rs#L221-L229

  8. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_type_ir/src/ty_kind/closure.rs#L362

  9. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_type_ir/src/ty_kind/closure.rs#L447-L455

  10. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/library/core/src/ops/async_function.rs#L36

  11. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/library/core/src/ops/async_function.rs#L30

  12. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_type_ir/src/ty_kind/closure.rs#L419

  13. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/compiler/rustc_next_trait_solver/src/solve/assembly/structural_traits.rs#L404-L409

  14. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs#L1-L70

  15. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs#L131-L195

  16. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_mir_transform/src/lib.rs#L339-L342

  17. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_hir_typeck/src/closure.rs#L345-L362

  18. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_hir_typeck/src/closure.rs#L486-L487

  19. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_hir_typeck/src/closure.rs#L517-L534

  20. https://github.com/rust-lang/rust/blob/5ca0e9fa9b2f92b463a0a2b0b34315e09c0b7236/compiler/rustc_hir_typeck/src/closure.rs#L575-L590

  21. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_hir_typeck/src/callee.rs#L169-L210 通常のクロージャでは、どの Fn* トレイトで呼び出すかによって戻り値の型が変わることはありません。一方、コルーチンクロージャでは、呼び出しに使われる AsyncFn* トレイトの種類に応じて、最終的に異なるコルーチン型を返すことになります。

  22. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_type_ir/src/ty_kind/closure.rs#L574-L576

  23. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_type_ir/src/ty_kind/closure.rs#L554-L563

  24. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/library/core/src/ops/async_function.rs#L135-L144

  25. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/library/core/src/ops/async_function.rs#L146-L154

  26. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/compiler/rustc_hir_typeck/src/upvar.rs#L250-L259

  27. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/compiler/rustc_hir_typeck/src/upvar.rs#L375-L471

  28. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/compiler/rustc_hir_typeck/src/upvar.rs#L211-L248

  29. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/compiler/rustc_hir_typeck/src/upvar.rs#L532-L539

  30. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/compiler/rustc_hir_typeck/src/upvar.rs#L1818-L1860

  31. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_ty_utils/src/instance.rs#L351 ↩2

  32. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_ty_utils/src/instance.rs#L341-L349

  33. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_ty_utils/src/instance.rs#L312-L326

  34. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_middle/src/ty/instance.rs#L129-L134

  35. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_middle/src/ty/instance.rs#L136-L141

  36. https://github.com/rust-lang/rust/blob/07cbbdd69363da97075650e9be24b78af0bcdd23/compiler/rustc_middle/src/ty/instance.rs#L841

  37. https://github.com/rust-lang/rust/blob/705cfe0e966399e061d64dd3661bfbc57553ed87/compiler/rustc_borrowck/src/universal_regions.rs#L110-L115

  38. https://github.com/rust-lang/rust/blob/7c7bb7dc017545db732f5cffec684bbaeae0a9a0/compiler/rustc_borrowck/src/universal_regions.rs#L743-L790

MIR からバイナリへ

このガイドのこれまでの章には、共通点が 1 つあります。 実行可能な機械語コードをまったく生成していなかったのです! この章で、そのすべてが変わります。

これまで、 コンパイラがテキスト形式の生のソースコードを受け取り、 それを MIR に変換する方法を示してきました。 また、コンパイラがコードに対してさまざまな解析を行い、 型エラーやライフタイムエラーのようなものを検出する方法も示してきました。 ここでついに、MIR を受け取って実行可能な機械語コードを生成します。

注: コンパイラのこの部分は、しばしば バックエンド と呼ばれます。 この用語は少し多義的です。というのも、コンパイラのソースでは、 通常「コード生成バックエンド」(つまり LLVM、Cranelift、または GCC)を指すからです。 通常、この部分で「バックエンド」という言葉を見かけた場合、 「コード生成バックエンド」を指しています。

では、何をする必要があるのでしょうか?

  1. まず、コード生成の対象となるものの集合を収集する必要があります。 特に、 ジェネリックな型に対してどの具象型を代入するかを見つける必要があります。 なぜなら、具象型に対してコードを生成する必要があるからです。 具象型に対してコードを生成すること (つまり、具象型ごとにコードのコピーを出力すること)は 単相化 と呼ばれるため、 すべての具象型を収集する処理は 単相化収集 と呼ばれます。
  2. 次に、収集した各具象型について、実際に MIR をコード生成 IR (通常は LLVM IR)へ下げる必要があります。
  3. 最後に、コード生成バックエンドを呼び出す必要があります。 これは多数の最適化パスを実行し、 実行可能コードを生成し、 実行可能バイナリをリンクします。

コード生成のためのコードは、いくつかの要因により実際には少し複雑です。

  • 複数のコード生成バックエンド(LLVM、Cranelift、GCC)のサポート。 それらの間でできる限り多くのバックエンドコードを共有しようとしているため、 その多くはコード生成の実装に対してジェネリックになっています。 これは、多くの場合、抽象化の層が多数存在することを意味します。
  • パフォーマンスのため、コード生成は別スレッドで非同期に行われます。
  • 実際のコード生成は、サードパーティライブラリ(3 つのバックエンドのいずれか)によって行われます。

一般に、rustc_codegen_ssa クレートにはバックエンドに依存しないコードが含まれ、 rustc_codegen_llvm クレートには LLVM コード生成に固有のコードが含まれます。

非常に高いレベルでは、エントリーポイントは rustc_codegen_ssa::base::codegen_crate です。 この関数は、この章の残りで説明するプロセスを開始します。

MIR 最適化

MIR 最適化とは、codegen の前によりよい MIR を生成するために MIR 上で実行される最適化です。 これは 2 つの理由で重要です。第一に、最終的に生成される実行可能コードがよりよいものになります。 第二に、LLVM が行う作業が少なくなるため、コンパイルが速くなります。MIR はジェネリックである(まだ 単相化されていない)ため、これらの最適化は特に効果的であることに注意してください。 ジェネリック版を最適化できるため、すべての単相化がより低コストになります!

MIR 最適化は借用チェックの後に実行されます。MIR を改善するため、一連の最適化 パスを MIR に対して実行します。一部のパスはすべてのコードで実行する必要があり、 一部のパスは実際には最適化を行わず、何らかのチェックだけを行います。また、一部の パスは release モードでのみ有効になります。

optimized_mir クエリは、指定された DefId の最適化済み MIR を生成するために呼び出されます。このクエリは、借用チェッカーが実行済みであり、 いくつかの検証が行われていることを確認します。その後、MIR を奪取し、 それを最適化して、改善された MIR を返します。

新しい最適化を追加するためのクイックスタート

  1. 最適化したいコードを示す Rust ソースファイルを tests/mir-opt に作成します。 これはシンプルに保つべきなので、その最適化に必要でない場合は println! やその他のフォーマット コードを避けてください。その理由は、println!format! などは大量の MIR を生成し、 その最適化がテストに対して何を行うのかを理解しにくくする可能性があるためです。

  2. ./x test --bless tests/mir-opt/<your-test>.rs を実行して MIR ダンプを生成します。何をダンプするかについての手順は、この README を読んでください。

  3. 現在の作業ディレクトリの状態をコミットします。最適化を実装する前にテスト出力をコミットすべき理由は、 その最適化によって何が変わったのかについて、あなた(およびレビュアー)が前後の差分を確認できるようにするためです。

  4. compiler/rustc_mir_transform/src に新しい最適化を実装します。 これを行う最速かつ最も簡単な方法は、次のとおりです。

    1. 小さな最適化(たとえば remove_storage_markers)を選び、それを新しいファイルにコピーする。
    2. あなたの最適化を run_optimization_passes() 関数内のいずれかのリストに追加する。
    3. その後、コピーした最適化の変更を開始する。
  5. ./x test --bless tests/mir-opt/<your-test>.rs を再実行して、 MIR ダンプを再生成します。差分を見て、それが期待どおりかどうかを確認します。

  6. ./x test tests/ui を実行して、あなたの最適化によって何かが壊れていないかを確認します。

  7. あなたの最適化に問題がある場合は、少し実験し、 手順 5 と 6 を繰り返します。

  8. コミットして PR を開きます。これはいつ行ってもよく、まだ動作していない場合でも、 PR 上でフィードバックを求めることができます。その場合は、「WIP」PR (PR タイトルの先頭に [WIP] を付けるか、作業中であることを別の方法で明記します)を開いてください。

    blessed されたテスト出力も必ずコミットしてください!CI を通すために必要であり、 レビュアーにとっても非常に役立ちます。

途中で質問がある場合は、Zulip の #t-compiler/wg-mir-opt で遠慮なく聞いてください。

最適化パスの定義

実行されるパスの一覧とそれらが実行される順序は、 run_optimization_passes 関数によって定義されています。この関数には、実行するパスの配列が含まれています。 配列内の各パスは、MirPass トレイトを実装する構造体です。 この配列は &dyn MirPass トレイトオブジェクトの配列です。通常、パスは rustc_mir_transform クレートの独自のモジュール内に実装されます。

パスの例をいくつか挙げます。

  • CleanupPostBorrowck: codegen ではなく解析にのみ必要な情報の一部を削除します。
  • ConstProp: 定数伝播を行います。

さらに多くの例については、MirPass rustdoc の「Implementors」セクションを参照できます。

MIR 最適化レベル

MIR 最適化には、さまざまな完成度のレベルがあります。実験的な 最適化は、誤コンパイルを引き起こしたり、コンパイル時間を遅くしたりする可能性があります。 これらのパスは、フィードバックを集め、パスを変更しやすくするために、nightly ビルドには引き続き含まれています。 低速な最適化パスやその他の実験的な最適化パスでの作業を有効にするには、 -Z mir-opt-level デバッグフラグを指定できます。レベルの定義は compiler MCP にあります。MIR パスを開発していて、自分の最適化パスを実行すべきかどうかを問い合わせたい場合は、 tcx.sess.opts.unstable_opts.mir_opt_level を使用して現在のレベルを確認できます。

MIR デバッグ

-Z dump-mir フラグを使用すると、MIR のテキスト表現をダンプできます。 以下の任意のフラグを -Z dump-mir と組み合わせて使用すると、追加の出力形式が有効になります。これには次のものが含まれます。

  • -Z dump-mir-graphviz - MIR を制御フローグラフとして表す .dot ファイルをダンプします
  • -Z dump-mir-dataflow - 制御フローグラフ内の各地点における dataflow state を示す .dot ファイルをダンプします

-Z dump-mir=F は、コンパイルの各ステージで各関数の MIR を表示できる便利なコンパイラオプションです。-Z dump-mirfilter F を受け取り、どの関数とどのパスに関心があるかを制御できます。例:

> rustc -Z dump-mir=foo ...

これは、名前に foo を含む任意の関数について MIR をダンプします。また、各パスの前後の両方で MIR をダンプします。これらのファイルは mir_dump ディレクトリに作成されます。おそらくかなり多くのファイルが作成されるでしょう。

> cat > foo.rs
fn main() {
    println!("Hello, world!");
}
^D
> rustc -Z dump-mir=main foo.rs
> ls mir_dump/* | wc -l
     161

ファイルには rustc.main.000-000.CleanEndRegions.after.mir のような名前が付けられます。これらの名前はいくつかの部分で構成されています。

rustc.main.000-000.CleanEndRegions.after.mir
      ---- --- --- --------------- ----- either before or after
      |    |   |   name of the pass
      |    |   index of dump within the pass (usually 0, but some passes dump intermediate states)
      |    index of the pass
      def-path to the function etc being dumped

より選択的なフィルターを作成することもできます。たとえば、main & CleanEndRegions は、main とパス CleanEndRegions両方 を参照するものを選択します。

> rustc -Z dump-mir='main & CleanEndRegions' foo.rs
> ls mir_dump
rustc.main.000-000.CleanEndRegions.after.mir	rustc.main.000-000.CleanEndRegions.before.mir

フィルターには、複数の & フィルターの集合を組み合わせるための | 部分を含めることもできます。たとえば、main & CleanEndRegions | main & NoLandingPads は、mainCleanEndRegions、または mainNoLandingPadsいずれか を選択します。

> rustc -Z dump-mir='main & CleanEndRegions | main & NoLandingPads' foo.rs
> ls mir_dump
rustc.main-promoted[0].002-000.NoLandingPads.after.mir
rustc.main-promoted[0].002-000.NoLandingPads.before.mir
rustc.main-promoted[0].002-006.NoLandingPads.after.mir
rustc.main-promoted[0].002-006.NoLandingPads.before.mir
rustc.main-promoted[1].002-000.NoLandingPads.after.mir
rustc.main-promoted[1].002-000.NoLandingPads.before.mir
rustc.main-promoted[1].002-006.NoLandingPads.after.mir
rustc.main-promoted[1].002-006.NoLandingPads.before.mir
rustc.main.000-000.CleanEndRegions.after.mir
rustc.main.000-000.CleanEndRegions.before.mir
rustc.main.002-000.NoLandingPads.after.mir
rustc.main.002-000.NoLandingPads.before.mir
rustc.main.002-006.NoLandingPads.after.mir
rustc.main.002-006.NoLandingPads.before.mir

(ここで、main-promoted[0] ファイルは、main 関数内に現れた「昇格された定数」の MIR を参照しています。)

-Z unpretty=mir-cfg フラグを使用すると、クレート全体の graphviz MIR 制御フロー図を作成できます。

制御フロー図

TODO: ほかに何かあるか?

定数評価

定数評価とは、コンパイル時に値を計算するプロセスです。特定のアイテム(定数/static/配列長)については、そのアイテムの MIR が借用検査され、最適化された後にこれが行われます。多くの場合、アイテムを定数評価しようとすると、その MIR の計算が初めてトリガーされます。

代表的な例は次のとおりです。

  • static の初期化子
  • 配列長
    • スタックまたはヒープ領域を確保するために既知である必要があります
  • Enum バリアントの判別子
    • 2 つのバリアントが同じ判別子を持つことを防ぐために既知である必要があります
  • パターン
    • 重複するパターンをチェックするために既知である必要があります

さらに定数評価は、複雑な操作をコンパイル時に事前計算し、その結果だけを格納することで、実行時のワークロードやバイナリサイズを削減するために使用できます。

定数評価のすべての用途は、「型システムに影響する」もの(配列長、Enum バリアントの判別子、const ジェネリックパラメーター)か、実行時に使用される式を事前計算するためだけに行われるもののいずれかに分類できます。

定数評価は、TyCtxtconst_eval_* 関数を呼び出すことで実行できます。これらは const_eval クエリのラッパーです。

  • const_eval_global_id_for_typeck は定数を valtree に評価するため、結果の値をコンパイラがさらに検査できます。
  • const_eval_global_id は定数を、その最終的な値を含む「不透明な blob」に評価します。 これはコード生成バックエンドと CTFE 評価エンジン自体にのみ有用です。
  • eval_static_initializer は static の初期値を具体的に計算します。 Static は特殊です。他のすべての関数は static を正しく表現しないため、 static に対して使用されることを防ぐアサーションがあります。

const_eval_* 関数は、定数が評価される環境(たとえば、その定数が使用される関数)の ParamEnvGlobalId を使用します。GlobalId は、定数または static を参照する Instance、または関数の Instance とその関数の Promoted テーブルへのインデックスから構成されます。

定数評価は、型システムの定数については EvalToValTreeResult を、またはエラーもしくは評価済み定数の表現である valtree または MIR 定数値 のいずれかを持つ EvalToConstValueResult を、それぞれ返します。

インタープリター

インタープリターは、機械語にコンパイルせずに MIR を実行するための仮想マシンです。 通常は tcx.const_eval_* 関数を介して呼び出されます。 インタープリターは、コンパイラ (コンパイル時関数評価、CTFE 用) とツール Miri の間で共有されます。Miri は同じ仮想マシンを使用して、(unsafe な) Rust コード内の未定義動作を検出します。

定数から始める場合:

#![allow(unused)]
fn main() {
const FOO: usize = 1 << 12;
}

rustc は、その定数が使用されるかメタデータに配置されるまで、実際には何も呼び出しません。

次のような使用箇所があるとします:

type Foo = [u8; FOO - 42];

コンパイラは、その型を使用する対象 (ローカル変数、定数、関数引数、…) を作成できるようになる前に、 配列の長さを把握する必要があります。

(この場合は空の) パラメーター環境を取得するには、 let param_env = tcx.param_env(length_def_id); を呼び出せます。 必要な GlobalId は次のとおりです。

let gid = GlobalId {
    promoted: None,
    instance: Instance::mono(length_def_id),
};

tcx.const_eval(param_env.and(gid)) を呼び出すと、配列長式の MIR の作成がトリガーされます。 MIR はおおよそ次のようになります。

Foo::{{constant}}#0: usize = {
    let mut _0: usize;
    let mut _1: (usize, bool);

    bb0: {
        _1 = CheckedSub(const FOO, const 42usize);
        assert(!move (_1.1: bool), "attempt to subtract with overflow") -> bb1;
    }

    bb1: {
        _0 = move (_1.0: usize);
        return;
    }
}

評価の前に、評価結果を格納するための仮想メモリ位置 (この場合は本質的に vec![u8; 4] または vec![u8; 8]) が作成されます。

評価の開始時点では、_0_1Operand::Immediate(Immediate::Scalar(ScalarMaybeUndef::Undef)) です。 これはかなり 込み入った表現です: Operand は、インタープリターのメモリ のどこかに格納されたデータ (Operand::Indirect) か、(最適化として) インラインに格納された即値データのいずれかを表せます。 また Immediate は、単一の (未初期化の可能性がある) スカラー値 (整数または thin pointer) か、 それら 2 つのペアのいずれかです。 この例では、単一のスカラー値は (まだ) 初期化されていません。

_1 の初期化が呼び出されると、FOO 定数の値が必要になり、 ここでは示さない tcx.const_eval_* への別の呼び出しがトリガーされます。 FOO の評価が成功すると、42 がその値 4096 から減算され、その結果が _1Operand::Immediate(Immediate::ScalarPair(Scalar::Raw { data: 4054, .. }, Scalar::Raw { data: 0, .. }) として格納されます。 ペアの最初の部分は計算された値で、 2 番目の部分はオーバーフローが発生した場合に true になる bool です。 Scalar::Raw は、このスカラー値のサイズ (バイト単位) も格納します。ここではそれを示していません。

次の文は、前述の boolean が 0 であることをアサートします。 アサーションが失敗した場合、 そのエラーメッセージがコンパイル時エラーの報告に使用されます。

失敗しないため、Operand::Immediate(Immediate::Scalar(Scalar::Raw { data: 4054, .. })) が、評価前に割り当てられた仮想メモリに格納されます。 _0 は常にその場所を直接参照します。

評価が完了した後、戻り値は op_to_const によって Operand から ConstValue に変換されます。前者の表現は定数評価の実行中に必要なものに合わせたものであり、 ConstValue は定数評価の結果を消費するコンパイラの残りの部分のニーズに合わせた形になっています。 この変換の一部として、スカラー値を持つ型では、 結果の OperandIndirect であっても、通常の ConstValue::Indirect ではなく、即値の ConstValue::Scalar(computed_value) を返します。 これにより、結果の使用がはるかに効率的になり、また usize のような単純なものにアクセスするために 追加のクエリを実行する必要がないため、より便利にもなります。

同じ定数の将来の評価では、実際には インタープリターを呼び出さず、キャッシュ済みの結果を使用するだけです。

データ構造

インタープリターの外部向けデータ構造は、 rustc_middle/src/mir/interpret にあります。 これは主に、エラー enum と ConstValue および Scalar 型です。 ConstValue は、Scalar (単一の Scalar、すなわち整数または thin pointer)、Slice (パターンマッチングに必要なバイトスライスや文字列を表すため)、または Indirect のいずれかです。Indirect はそれ以外のものに使用され、仮想アロケーションを参照します。 これらのアロケーションには、tcx.interpret_interner のメソッドを介してアクセスできます。 Scalar は、何らかの Raw 整数またはポインターです。 詳細については、次のセクション を参照してください。

数値の結果を期待している場合は、eval_usize (u64 として表現できないものではパニックします) または try_eval_usize を使用できます。後者は、可能であれば Scalar を生成する Option<u64> になります。

メモリ

あらゆる種類のポインターをサポートするには、インタープリターにはポインターが指し示せる「仮想メモリ」が必要です。 これは Memory 型で実装されています。 最も単純なモデルでは、すべてのグローバル変数、スタック変数、動的アロケーションが、そのメモリ内の Allocation に対応します。 (実際にすべての MIR スタック変数にアロケーションを使用すると非常に非効率です。そのため、 小さく、かつアドレスが取得されないスタック変数には Operand::Immediate があります。 ただし、これは純粋な最適化です。)

このような Allocation は、基本的には、このアロケーション内の各バイトの値を格納する u8 のシーケンスにすぎません。 (加えて、いくつかの追加データがあります。下記参照。) すべての Allocation には、Memory 内でグローバルに一意な AllocId が割り当てられます。 これにより、 Pointer は、AllocId (アロケーションを示す) と アロケーション内のオフセット (ポインターがアロケーションのどのバイトを指すかを示す) のペアで構成されます。 Pointer が単なる整数アドレスではないのは奇妙に思えるかもしれませんが、 定数評価の間は、アロケーションが最終的に実際のどの整数アドレスに置かれるかを知ることはできない、という点を思い出してください。 そのため、AllocId を記号的なベースアドレスとして使用します。つまり、別個のオフセットが必要になります。 (余談ですが、 実行時のポインターもまた、単なる整数以上のものです。) これらの割り当ては、参照と生ポインターが指す対象を持てるように存在します。 ものが割り当てられるグローバルな線形ヒープは存在せず、それぞれの 割り当て(ローカル変数、static、または(将来の)ヒープ割り当てのいずれであっても)は、 必要なサイズちょうどの小さなメモリを独自に取得します。 したがって、ローカル変数 a の割り当てへの ポインターを持っている場合、当該ポインターを別のローカル変数 b へのポインターに 変更できるような操作は、(どれほど unsafe であっても)存在しません。 a に対するポインター演算は、そのオフセットだけを変更します。AllocId は同じままです。

しかし、これは PointerAllocation に格納したい場合に問題を引き起こします。 それを適切な長さの u8 の列に変換できないためです! AllocId とオフセットを合わせると、ポインターが「見かけ上」持つサイズの 2 倍になります。 これが Allocationrelocation フィールドの目的です。Pointer のバイトオフセットは 一連の u8 として格納される一方、その AllocId は 帯域外に格納されます。 この 2 つは、Pointer がメモリから読み取られるときに再構成されます。 Allocation が必要とするもう 1 つの追加データは、どのバイトが初期化済みかを 追跡するための undef_mask です。

グローバルメモリと特殊な割り当て

Memory は評価中にのみ存在します。定数の最終値が 計算されると破棄されます。 その定数に何らかの ポインターが含まれている場合、それらは「インターン化」され、TyCtxt の一部である グローバルな「const eval memory」に移動されます。 これらの割り当ては残りの計算の間存続し、 最終出力にシリアライズされます(依存クレートがそれらを使用できるようにするためです)。

さらに、関数ポインターにも対応するため、TyCtxt 内のグローバルメモリは 「仮想割り当て」も含むことができます。これらは Allocation の代わりに、 Instance を含みます。 これにより、Pointer は通常のデータまたは 関数のどちらかを指すことができます。これは、関数ポインターから 生ポインターへのキャストを評価できるようにするために必要です。

最後に、グローバルメモリで使用される GlobalAlloc 型には、特定の const または static 項目を指すバリアント Static も含まれています。 これは循環する statics をサポートするために必要です。その場合、 値のバイト列がまだ分からないため Allocation をまだ用意できない static への Pointer が必要になります。

ポインター値とポインター型

インタープリターでよくある混乱の原因の 1 つは、ポインターであることと ポインターを持つことが、完全に独立した性質であるという点です。 「ポインター値」とは、 Pointer を含み、したがってインタープリターの仮想メモリ内のどこかを指す Scalar::Ptr を指します。 これは、単なる具体的な整数である Scalar::Raw とは対照的です。

しかし、*const T&T のようなポインター型または参照の変数は、 ポインターを持っている必要はありません。整数をポインターにキャストまたは transmute することで得られたものかもしれません。 同様に、実際の割り当てへの参照を整数にキャストまたは transmute すると、 整数usize)におけるポインターScalar::Ptr)が得られます。 これは問題です。なぜなら、ポインター値に対して除算のような 整数演算を意味のある形で実行することはできないからです。

解釈

定数評価の主なエントリポイントは tcx.const_eval_* 関数ですが、 rustc_const_eval/src/const_eval には、ConstValueIndirect であるかどうかを問わず)のフィールドにアクセスできる 追加の関数があります。 コンパイルターゲット(現時点では LLVM のみ)へ変換する場合を除き、 Allocation に直接アクセスする必要は決してないはずです。

インタープリターは、評価中の現在の定数のために仮想スタックフレームを作成することから始めます。 このガイドを執筆している時点では、定数ではローカル(名前付き)変数が許可されていないことを除けば、 定数と引数のない関数の間に本質的な違いはありません。

スタックフレームは、 rustc_const_eval/src/interpret/eval_context.rsFrame 型によって定義され、すべてのローカル変数のメモリ (評価開始時は None)を含みます。 各フレームは、ルート定数または後続の const fn 呼び出しのいずれかの 評価を参照します。 別の定数の評価は単に tcx.const_eval_* を呼び出し、それによって 完全に新しく独立したスタックフレームが生成されます。

フレームは単なる Vec<Frame> であり、unsafe コードを通じてひどい小細工を行ったとしても、 Frame のメモリを実際に参照する方法はありません。 参照できる唯一のメモリは Allocation です。

インタープリターはここで、 rustc_const_eval/src/interpret/step.rs にある step メソッドを、エラーを返すか、実行すべき文がなくなるまで呼び出します。 各文はここで、ローカルまたはローカルから参照される仮想メモリを 初期化または変更します。 これには他の定数や statics の評価が必要になる場合があり、 その場合は単に tcx.const_eval_* が再帰的に呼び出されます。

単相化

おそらくご存じのとおり、Rust には非常に表現力豊かな型システムがあり、ジェネリック型を広範にサポートしています。しかし当然ながら、アセンブリはジェネリックではないため、コードを実行できるようにする前に、すべてのジェネリックの具体的な型を把握する必要があります。

言語によって、この問題の扱い方は異なります。たとえば Java などの一部の言語では、実行時まで値の最も正確な型がわからない場合があります。Java の場合、これは問題ありません。なぜなら、ほぼすべての変数はいずれにせよ参照値(つまり、ヒープに割り当てられたオブジェクトへのポインター)だからです。この柔軟性にはパフォーマンス上のコストが伴います。オブジェクトへのすべてのアクセスでポインターをデリファレンスする必要があるためです。

Rust は異なるアプローチを取ります。すべてのジェネリック型を_単相化_します。これは、必要とされる具体的な型ごとに、コンパイラがジェネリック関数のコードの異なるコピーを生成することを意味します。たとえば、コード内で Vec<u64>Vec<String> を使う場合、生成されるバイナリには Vec の生成コードが 2 つ含まれます。1 つは Vec<u64> 用、もう 1 つは Vec<String> 用です。その結果、高速なプログラムになりますが、その代償としてコンパイル時間(これらすべてのコピーを作成するのに時間がかかる可能性があります)とバイナリサイズ(これらすべてのコピーが多くの領域を占める可能性があります)が増加します。

単相化は、Rust コンパイラのバックエンドにおける最初のステップです。

収集

まず、プログラム内のすべてのジェネリックなものについて、どの具体的な型が必要かを把握する必要があります。これは_収集_と呼ばれ、これを行うコードは_単相化コレクター_と呼ばれます。

次の例を見てください。

fn banana() {
   peach::<u64>();
}

fn main() {
    banana();
}

単相化コレクターは、[main, banana, peach::<u64>] というリストを返します。これらは、機械語コードが生成される関数です。コレクターは、statics のようなものもそのリストに追加します。

詳細については、コレクターの rustdocs を参照してください。

単相化コレクターは、MIR lowering と codegen の直前に実行されます。 rustc_codegen_ssa::base::codegen_cratecollect_and_partition_mono_items クエリを呼び出します。このクエリは単相化の収集を行い、その後それらを codegen units に分割します。

Codegen Unit (CGU) のパーティショニング

インクリメンタルビルド時間を改善するため、CGU パーティショナーはソースレベルの各モジュールに対して 2 つの CGU を作成します。一方は「stable」、つまり非ジェネリックコード用で、もう一方はより揮発的なコード、つまり単相化/特殊化されたインスタンス用です。

依存関係について、Crate B が Crate A に依存しているような Crate A と Crate B を考えます。 次の表は、Crate B の 1 つ以上のモジュールで使用される可能性がある Crate A の関数について、さまざまなシナリオを示しています。

Crate A の関数振る舞い
非ジェネリック関数Crate A の関数は Crate B のどの codegen unit にも現れません
非ジェネリックな #[inline] 関数Crate A の関数は Crate B の単一の CGU 内に現れ、post-inlining 段階の後でも存在します
ジェネリック関数インライン化の有無に関係なく、Crate A 由来のすべての単相化(特殊化)された関数は、
Crate B の単一の codegen unit 内に現れます。
その codegen unit は post inlining 段階の後でも存在します。
ジェネリックな #[inline] 関数- 同じ -

パーティショナーの詳細については、モジュールレベルの[ドキュメント]を読んでください。

MIR をコード生成 IR に lowering する

collector から生成すべきシンボルのリストが得られたので、何らかのコード生成 IR を生成する必要があります。この章では、LLVM IR を前提とします。rustc が通常使用するのはそれだからです。実際の monomorphization は、変換を行いながらその場で実行されます。

backend は rustc_codegen_ssa::base::codegen_crate によって開始されることを思い出してください。最終的に、これは rustc_codegen_ssa::mir::codegen_mir に到達し、そこで MIR から LLVM IR への lowering が行われます。

コードは、特定の MIR プリミティブを扱うモジュールに分割されています。

関数が変換される前に、より単純で効率的な LLVM IR を生成しやすくするため、多数の単純でプリミティブな解析 pass が実行されます。そのような解析 pass の例として、どの変数が SSA に類似しているかを特定し、それらの変数については LLVM の mem2reg に頼るのではなく、直接 SSA に変換できるようにするものがあります。この解析は rustc_codegen_ssa::mir::analyze にあります。

通常、単一の MIR basic block は LLVM basic block に対応しますが、ごく少数の例外があります。intrinsic や関数呼び出し、および assert のようなより基本的でない MIR statement は、複数の basic block を生じさせることがあります。これは、コード生成における移植性のない LLVM 固有部分への格好の導入になります。Intrinsic 生成は、間にある抽象化レベルが非常に少ないため比較的理解しやすく、rustc_codegen_llvm::intrinsic にあります。

それ以外のすべては builder interface を使用します。これは、上で説明した rustc_codegen_ssa::mir::* モジュール内で呼び出されるコードです。

TODO: 定数がどのように生成されるかを議論する

コード生成

コード生成(または「codegen」)は、実際に実行可能バイナリを生成する コンパイラの一部です。 通常、rustc はコード生成に LLVM を使用しますが、 CraneliftGCC のサポートもあります。 重要なのは、rustc 自身が codegen を実装しているわけではないということです。 ただし、Rust のソースコードでは、バックエンドの多くの部分の名前に codegen が含まれている点は注目に値します (明確な境界はありません)。

注: コード生成のバグをデバッグする方法のヒントを探している場合は、 デバッグの章のこのセクションを参照してください。

LLVM とは?

LLVM は、「モジュール化され再利用可能なコンパイラおよび ツールチェーン技術の集合」です。特に、LLVM プロジェクトにはプラグイン可能な コンパイラバックエンド(これも「LLVM」と呼ばれます)が含まれており、 clang C コンパイラや私たちの愛する rustc を含む多くのコンパイラプロジェクトで使用されています。

LLVM は LLVM IR 形式の入力を受け取ります。これは基本的には、追加の低レベル型と アノテーションが付与されたアセンブリコードです。これらのアノテーションは、 LLVM IR と出力される機械語コードに対して最適化を行うのに役立ちます。 これらすべての最終結果は、(ようやく)実行可能なもの(たとえば ELF オブジェクト、 EXE、または wasm)になります。

LLVM を使用することには、いくつかの利点があります。

  • コンパイラバックエンド全体を書く必要がありません。これにより、実装と 保守の負担が軽減されます。
  • LLVM プロジェクトが蓄積してきた高度な最適化の大規模なスイートの恩恵を受けられます。
  • LLVM がサポートする任意のプラットフォーム向けに、Rust を自動的にコンパイルできます。 たとえば、LLVM が wasm のサポートを追加した途端、ほら!rustc、 clang、そして他の多くの言語が wasm にコンパイルできるようになりました!(まあ、 追加で行うべき作業はいくらかありましたが、いずれにせよ 90% はそこまで到達していました)。
  • 私たちと他のコンパイラプロジェクトは互いに恩恵を受けます。たとえば、 Spectre と Meltdown のセキュリティ脆弱性が発見されたとき、 パッチを適用する必要があったのは LLVM だけでした。

LLVM の実行、リンク、およびメタデータ生成

すべての関数や static などの LLVM IR が構築されると、LLVM とその最適化パスを 実行し始める段階になります。LLVM IR は「モジュール」にグループ化されます。 マルチコアの利用を助けるため、複数の「モジュール」を同時に codegen できます。 これらの「モジュール」は、私たちが codegen units と呼ぶものです。 これらの unit は、はるか前の単相化収集フェーズ中に確立されています。

LLVM がこれらのモジュールからオブジェクトを生成すると、これらのオブジェクトは、 任意でメタデータオブジェクトとともにリンカに渡され、アーカイブまたは 実行可能ファイルが生成されます。

上で説明した codegen フェーズが必ずしも最適化を実行するわけではありません。 特定の種類の LTO では、最適化は代わりにリンク時に行われる場合があります。 また、一部の最適化がオブジェクトがリンカに渡される前に行われ、 一部がリンク中に行われることも可能です。

これらはすべてコンパイルのごく終盤に行われます。このためのコードは rustc_codegen_ssa::backrustc_codegen_llvm::back にあります。残念ながら、このコード片は LLVM 依存のコードとしてあまりきれいに分離されていません。rustc_codegen_ssa には、 LLVM バックエンド固有のコードがかなり含まれています。

これらのコンポーネントが作業を終えると、要求した出力に対応する多数のファイルが ファイルシステム上に得られます。

LLVM の更新

Rust は複数の LLVM バージョンに対するビルドをサポートしています。

  • 現在の LLVM 開発ブランチのツリー先端は、通常は数日以内にサポートされます。 そのような修正の PR には llvm-main タグが付けられます。
  • 最新のリリース済みメジャーバージョンは常にサポートされます。
  • 直前の 1 つまたは 2 つのメジャーバージョンは、通常、正常にビルドでき、ほとんどのテストに合格することが期待されるという意味でサポートされます。 ただし、誤コンパイルの修正は過去の LLVM バージョンにバックポートされないことが多いため、古いバージョンの LLVM で rustc を使用すると、健全性バグのリスクが高まります。 最新バージョンの LLVM を使用することを強く推奨します。

デフォルトでは、Rust は rust-lang/llvm-project repository にある独自のフォークを使用します。 このフォークはアップストリームプロジェクトの release/$N.x ブランチに基づいており、ここで $N は最新のリリース済みメジャーバージョン、またはリリース候補段階にある現在のメジャーバージョンのいずれかです。 このフォークが main 開発ブランチに基づくことはありません。

私たちの LLVM フォークでは、次のもののみを受け入れます。

  • すでにアップストリームに取り込まれた変更のバックポート。
  • 私たちの CI 環境に影響するビルド問題の回避策。

SGX 有効化のために既得権として残されている 1 つのパッチを例外として、先にアップストリーム化されていない機能的なパッチは受け入れません。

LLVM の更新には 3 つの種類があり、それぞれ手順が異なります。

  • 現在のメジャー LLVM バージョンがサポートされている間のバックポート。
  • 現在のメジャー LLVM バージョンがすでにサポートされていない間のバックポート(または、その変更がアップストリームのバックポート対象ではない場合)。
  • 新しいメジャー LLVM バージョンへの更新。

バックポート(アップストリームでサポートされている場合)

現在のメジャー LLVM バージョンがアップストリームでサポートされている間は、修正を先にアップストリームへバックポートし、その後リリースブランチを Rust フォークへマージする必要があります。

  1. バグ修正がアップストリーム LLVM に入っていることを確認します。
  2. まだ行われていない場合は、アップストリームのリリースブランチへのバックポートをリクエストします。 LLVM のコミットアクセス権がある場合は、backport process に従ってください。 そうでない場合は、バックポートをリクエストする issue を開いてください。 バックポートが承認され、マージされたら続行します。
  3. rustc が現在使用しているブランチを特定します。 src/llvm-project サブモジュールは常に rust-lang/llvm-project repository のブランチに固定されています。
  4. rust-lang/llvm-project リポジトリをフォークします。
  5. 適切なブランチをチェックアウトします(通常は rustc/a.b-yyyy-mm-dd という名前です)。
  6. git remote add upstream https://github.com/llvm/llvm-project.git を使用してアップストリームリポジトリの remote を追加し、git fetch upstream を使用して fetch します。
  7. upstream/release/$N.x ブランチをマージします。
  8. このブランチを自分のフォークに push します。
  9. rust-lang/llvm-project に対して、以前と同じブランチへの Pull Request を送ります。 PR の説明では、修正している Rust および LLVM の issue の一方または両方を必ず参照してください。
  10. PR がマージされるまで待ちます。
  11. バグ修正を含む src/llvm-project サブモジュールを更新する PR を rust-lang/rust に送ります。 通常、これはローカルで git submodule update --remote src/llvm-project を実行することで行えます。
  12. PR がマージされるまで待ちます。

PR の例: #59089

バックポート(アップストリームでサポートされていない場合)

アップストリームの LLVM リリースは、GA リリース後 2〜3 か月間のみサポートされます。 アップストリームでのバックポートが受け入れられなくなったら、変更は私たちのフォークに直接 cherry-pick する必要があります。

  1. バグ修正がアップストリーム LLVM に入っていることを確認します。
  2. rustc が現在使用しているブランチを特定します。 src/llvm-project サブモジュールは常に rust-lang/llvm-project repository のブランチに固定されています。
  3. rust-lang/llvm-project リポジトリをフォークします。
  4. 適切なブランチをチェックアウトします(通常は rustc/a.b-yyyy-mm-dd という名前です)。
  5. git remote add upstream https://github.com/llvm/llvm-project.git を使用してアップストリームリポジトリの remote を追加し、git fetch upstream を使用して fetch します。
  6. git cherry-pick -x を使用して、関連するコミットを cherry-pick します。
  7. このブランチを自分のフォークに push します。
  8. rust-lang/llvm-project に対して、以前と同じブランチへの Pull Request を送ります。 PR の説明では、修正している Rust および LLVM の issue の一方または両方を必ず参照してください。
  9. PR がマージされるまで待ちます。
  10. バグ修正を含む src/llvm-project サブモジュールを更新する PR を rust-lang/rust に送ります。 通常、これはローカルで git submodule update --remote src/llvm-project を実行することで行えます。
  11. PR がマージされるまで待ちます。

PR の例: #59089

新しい LLVM リリースへの更新

バグ修正とは異なり、新しい LLVM リリースへの更新には通常、はるかに多くの作業が必要です。 これは、コミットを後方へ cherry-pick するのが現実的ではないため、完全な更新を行う必要がある場合です。 ここで行うべきことはたくさんあるため、それぞれを詳しく見ていきます。

  1. LLVM が、最新のリリースバージョンがブランチされたことを発表します。 これは llvm/llvm-project repository にブランチとして現れます。 通常は release/$N.x という名前で、 $N はリリースされる LLVM のバージョンです。

  2. rust-lang/llvm-project repository に、 この release/$N.x ブランチから新しいブランチを作成し、 rustc/a.b-yyyy-mm-dd という名前を付けます。 ここで、a.b はブランチ作成時点でツリー内にある LLVM の現在のバージョン番号で、 残りの部分は現在の日付です。

  3. Rust 固有のパッチを llvm-project リポジトリに適用します。 すべての機能とバグ修正はアップストリームにありますが、 アップストリームに送るのが適切でない、ビルド関連の少し奇妙なパッチがしばしば存在します。 これらのパッチは通常、 rustc が現在使用している rust-lang/llvm-project ブランチの最新のパッチです。

  4. rust リポジトリで新しい LLVM をビルドします。 これを行うには、 src/llvm-project リポジトリを自分のブランチと、 作成したリビジョンに更新します。 また、通常は .gitmodules を LLVM サブモジュールの新しい ブランチ名で更新することも推奨されます。 サブモジュールの更新が巻き戻されないように、 src/llvm-project への変更をコミット済みにしておいてください。 実行すべきコマンドには次のものがあります。

    • ./x build src/llvm-project - LLVM が引き続きビルドできることをテストします
    • ./x build - rustc の残りの部分をビルドします

    更新された LLVM バインディングでコンパイルできるように、 llvm-wrapper/*.cpp を更新する必要がある可能性が高いです。 なお、古い LLVM バージョンでもバインディングが引き続きコンパイルできるように、 #ifdef などを使用する必要があります。

    profile = "compiler"./x setup によって設定されるその他のデフォルトは、 LLVM をソースからビルドするのではなく CI からダウンロードすることに注意してください。 変更が使用されていることを確認するために、これを一時的に無効にする必要があります。 これは、bootstrap.toml に次の設定を記述することで行います。

    llvm.download-ci-llvm = false
    
  5. 他のプラットフォームでリグレッションをテストします。 LLVM には非 Tier 1 アーキテクチャ向けのバグが少なくとも 1 つあることが多いため、 これを bors に送る前にもう少しテストしておくとよいでしょう。 リソースが不足している場合は、そのまま PR を bors に送ってもかまいません。 いずれにせよテストされます。

    理想的には、いくつかのプラットフォームで LLVM をビルドしてテストします。

    • Linux
    • macOS
    • Windows

    その後、CI でも実行されるいくつかの Docker コンテナを実行します。

    • ./src/ci/docker/run.sh wasm32
    • ./src/ci/docker/run.sh arm-android
    • ./src/ci/docker/run.sh dist-various-1
    • ./src/ci/docker/run.sh dist-various-2
    • ./src/ci/docker/run.sh armhf-gnu
  6. rust-lang/rust への PR を準備します。 rust-lang/llvm-project のメンテナーと協力して、 そのリポジトリのブランチにコミットを入れてもらい、 その後 rust-lang/rust に PR を送ることができます。 少なくとも src/llvm-project を変更し、 おそらく llvm-wrapper も変更することになります。

    先行事例として、過去の LLVM 更新をいくつか挙げます。

    実際に src/llvm-project を更新する前に、 llvm-wrapper の互換性を PR として取り込むのが最も簡単な場合があることに注意してください。 こうすることで、 LLVM の問題に取り組んでいる間も、 新しい LLVM を試したい他の人が、C++ バインディングを更新するためにあなたが行った作業の恩恵を受けられます。

  7. その後数か月にわたって、 LLVM は release/a.b ブランチに継続的にコミットをプッシュします。 多くの場合、これらのバグ修正も取り込みたくなります。 そのためのマージプロセスは、git merge 自体を使用して LLVM の release/a.b ブランチを手順 2 で作成したブランチにマージすることです。 これは通常、LLVM のリリースブランチが安定していく間に、必要に応じて複数回行われます。

  8. その後、LLVM がバージョン a.b のリリースを発表します。

  9. LLVM の公式リリース後、 rust-lang/llvm-project リポジトリで再び新しいブランチを作成するプロセスに従います。 今回は新しい日付を使用します。 Rust をそのバージョンを使用するように更新する PR がマージされるのは、その後になってからです。

    rust-lang/llvm-project のコミット履歴は、 git rebase が行われることで、よりずっときれいに見えるはずです。 そこでは、素の LLVM のリリースブランチの上に、少数の Rust 固有のコミットだけが積まれます。

注意点と落とし穴

理想的には上記の手順はかなりスムーズですが、進める際には次の注意点を 念頭に置いてください。

  • LLVM のバグは見つけるのが難しいため、遠慮なく助けを求めてください。 ここでは二分探索が間違いなく役に立ちます (はい、LLVM のビルドには非常に時間がかかりますが、それでも二分探索は役に立ちます)。 なお、貢献者に強力なハードウェアへのリモートアクセスを提供する取り組みである Dev Desktops を利用できます。
  • 一般的な質問がある場合は、wg-llvm が助けになります。
  • ブランチの作成は GitHub 上では権限のある操作なので、 多くの場合、書き込み権限を持つ誰かにブランチを作成してもらう必要があります。

LLVM のデバッグ

LLVM のデバッグ

NOTE: コード生成に関する情報を探している場合は、代わりにこの 章を参照してください。

このセクションでは、コード生成におけるコンパイラのバグ(たとえば、 コンパイラがなぜ特定のコードを生成したのか、または LLVM でクラッシュしたのか) をデバッグする方法について説明します。 LLVM は大きなプロジェクトであり、おそらく独自のデバッグ文書が必要ですが、 以下では rustc の文脈で重要ないくつかのヒントを示します。

例を最小化する

一般的なルールとして、コンパイラはコードの解析から大量の情報を生成します。 したがって、有用な最初のステップは通常、最小限の例を見つけることです。 これを行う方法の 1 つは、次のとおりです。

  1. 問題を再現する新しいクレートを作成する(たとえば、問題の原因となっている クレートを依存関係として追加し、そこから使用する)

  2. 外部依存関係を削除してクレートを最小化する。つまり、関連するものをすべて 新しいクレートに移動する

  3. コードを短くして、さらに問題を最小化する(これを支援する creduce のような ツールがあります)

上記の手順 2 と 3 の方法論についての詳しい議論として、Rust プログラムの最小化に特化した pnkfelix による大作ブログ記事があります。

LLVM 内部チェックを有効にする

公式コンパイラ(nightly を含む)では LLVM アサーションが無効になっています。 これは、LLVM のアサーション失敗がコンパイラのクラッシュ(ICE ではなく「本物の」 クラッシュ)やその他の奇妙な挙動として現れる可能性があることを意味します。 これらに遭遇している場合は、LLVM アサーションを有効にしたコンパイラ、 つまり “alt” nightly または bootstrap.toml で llvm.assertions = true を設定して 自分でビルドしたコンパイラを使ってみて、何か分かるか確認するのがよいでしょう。

rustc のビルドプロセスでは、LLVM ツールが build/host/llvm/bin にビルドされます。 これらは直接呼び出すことができます。 これらのツールには次のものが含まれます。

  • llc。ビットコード(.bc ファイル)を実行可能コードにコンパイルします。これは LLVM バックエンドのバグを再現するために使用できます。
  • opt。LLVM 最適化パスを実行するビットコード変換ツールです。
  • bugpoint。大きなテストケースを小さく有用なものに縮小します。
  • その他多数。その一部は以下の本文で参照されています。

デフォルトでは、Rust のビルドシステムは LLVM ソースコードや そのビルド構成設定の変更をチェックしません。 そのため、rustc にリンクされる LLVM をリビルドする必要がある場合は、 まず .llvm-stamp ファイルを削除してください。このファイルは build/host/llvm/ にあるはずです。

デフォルトの rustc コンパイルパイプラインには複数のコード生成ユニットがあり、 これを手動で再現するのは困難で、LLVM が並列に複数回呼び出されることを意味します。 問題がなければ(つまり、それによってバグが消えてしまわないのであれば)、 rustc に -C codegen-units=1 を渡すとデバッグが容易になります。

生の LLVM 入力を入手する

rustc に LLVM IR を生成させるには、--emit=llvm-ir フラグを渡す必要があります。 cargo 経由でビルドしている場合は、 RUSTFLAGS 環境変数を使用します(例: RUSTFLAGS='--emit=llvm-ir')。 これにより、rustc は LLVM IR をターゲットディレクトリに出力します。

cargo llvm-ir [options] path は、path にある特定の関数の LLVM IR を出力します。 (cargo install cargo-asmcargo asmcargo llvm-ir をインストールします)。 --build-type=debug はデバッグビルド用のコードを出力します。 他にも有用なオプションがあります。 また、LLVM IR 内のデバッグ情報は出力を大きく散らかす可能性があります。 RUSTFLAGS="-C debuginfo=0" は非常に有用です。

RUSTFLAGS="-C save-temps" は、コンパイル中の さまざまな段階で LLVM ビットコードを出力します。これは場合によって有用です。 出力される LLVM ビットコードは、rustc--out-dir DIR 引数で設定される コンパイラの出力ディレクトリ内の .bc ファイルになります。

  • rustc 自体を呼び出したときに LLVM バックエンドからアサーション失敗や セグメンテーションフォルトが発生している場合は、これらの .bc ファイルをそれぞれ llc コマンドに渡してみて、同じ失敗が発生するか確認するのがよいでしょう。 (LLVM 開発者は、多くの場合、最小化された再現に Rust クレートを使うものよりも、 .bc ファイルに縮小されたバグを好みます。)

  • LLVM ビットコードの人間が読めるバージョンを取得するには、 ビットコード(.bc)ファイルを llvm-dis を使って .ll ファイルに変換するだけです。 llvm-dis は、ターゲットの rustc ローカルコンパイルに含まれているはずです。

-O が有効かどうかによって、LLVM の最適化なしでも rustc が異なる IR を出力することに 注意してください。そのため、rustc が出力する IR を試したい場合は、 次のようにする必要があります。

$ rustc +local my-file.rs --emit=llvm-ir -O -C no-prepopulate-passes \
    -C codegen-units=1
$ OPT=build/$TRIPLE/llvm/bin/opt
$ $OPT -S -O2 < my-file.ll > my

LLVM パイプライン中の LLVM IR だけを取得したい場合、たとえば、 どの IR が最適化時のアサーション失敗を引き起こすのかを確認したい場合や、 LLVM が特定の最適化をいつ実行するのかを確認したい場合は、 rustc フラグ -C llvm-args=-print-after-all を渡し、必要に応じて -C llvm-args='-filter-print-funcs=EXACT_FUNCTION_NAME を追加できます(例: -C llvm-args='-filter-print-funcs=_ZN11collections3str21_$LT$impl$u20$str$GT$\ 7replace17hbe10ea2e7c809b0bE')。

これは標準エラーに大量の出力を生成するため、それを何らかのファイルにパイプしたくなるでしょう。 また、-filter-print-funcs-C codegen-units=1 のどちらも使用していない場合、複数のコード生成ユニットが並列に実行されるため、 出力が混ざり合い、何も読めなくなります。

  • 前述の方法論に関する 1 つの注意点として、LLVM の -print 系のオプションは、 パスが実行される IR ユニット(たとえば、関数だけ)しか出力せず、 参照される宣言、グローバル、 メタデータなどは含まれません。これは、一般には -print の出力を llc に渡して特定の問題を再現することはできないことを意味します。

  • LLVM 自体の内部では、 SafeStackLegacyPass::runOnFunction の先頭で F.getParent()->dump() を呼び出すと、 モジュール全体がダンプされます。これは 再現のためのより良い基盤を提供するかもしれません。 (ただし、-C save-temps によってダンプされた .bc ファイルから 同じダンプを取得できるはずです。)

特定の関数の IR だけが必要な場合(たとえば、その関数がなぜアサーションを引き起こすのか、 または正しく最適化されないのかを確認したい場合)は、llvm-extract を使用できます。 例:

$ ./build/$TRIPLE/llvm/bin/llvm-extract \
    -func='_ZN11collections3str21_$LT$impl$u20$str$GT$7replace17hbe10ea2e7c809b0bE' \
    -S \
    < unextracted.ll \
    > extracted.ll

LLVM 最適化パスを調査する

最適化パスが原因で誤った動作が発生している場合、非常に便利な LLVM オプションとして -opt-bisect-limit があります。これは、実行する最も大きいパスのインデックス 値を表す整数を受け取ります。 実行対象となったパスのインデックス値は、実行ごとに安定しています。 これを、結果として得られるプログラムに基づいて探索空間の二分探索を自動化するソフトウェアと組み合わせることで、問題のあるパスを素早く特定できます。 -opt-bisect-limit が指定されている場合、すべての実行が 標準エラーに表示され、そのインデックスと、その パスが実行されたかスキップされたかを示す出力も併せて表示されます。制限をインデックス -1 に設定すると(例: RUSTFLAGS="-C llvm-args=-opt-bisect-limit=-1")、すべてのパスと それに対応するインデックス値が表示されます。

最適化パイプラインを試したい場合は、rustc が出力した LLVM IR とともに ./build/host/llvm/bin/ にある opt ツールを使用できます。

LLVM 自体の実装を調査する場合は、その内部デバッグインフラストラクチャを 把握しておく必要があります。 これは LLVM Debug ビルドで提供されており、rustc の LLVM ビルドでは bootstrap.toml の次の設定を変更することで有効にできます。

# LLVM assertions が有効かどうかを示します
llvm.assertions = true

# LLVM ビルドが Release ビルドか Debug ビルドかを示します
llvm.optimize = false

簡単にまとめると次のとおりです。

  • assertions=true を設定すると、粗い粒度のデバッグメッセージングが有効になります。
    • さらに、optimize=false を設定すると、細かい粒度のデバッグメッセージングが有効になります。
  • LLVM における LLVM_DEBUG(dbgs() << msg) は、rustc における debug!(msg) のようなものです。
  • -debug オプションはすべてのメッセージングを有効にします。これは rustc で 環境変数 RUSTC_LOG=debug を設定するようなものです。
  • -debug-only=<pass1>,<pass2> バリアントはより選択的です。これは rustc で 環境変数 RUSTC_LOG=path1,path2 を設定するようなものです。

ヘルプの入手と質問

質問がある場合は、rust-lang Zulip、特に #t-compiler/wg-llvm チャンネルにアクセスしてください。

知って使いこなしたいコンパイラオプション

-C help-Z help コンパイラスイッチは、役立つ可能性のあるさまざまな 興味深いオプションを一覧表示します。 LLVM 開発に関連する、最も一般的なものをいくつか示します(その一部は上記の チュートリアルで使用されています)。

  • --emit llvm-ir オプションは、テキスト形式の LLVM IR を含む <filename>.ll ファイルを出力します
    • --emit llvm-bc オプションは、バイトコード形式(<filename>.bc)で出力します
  • -C llvm-args=<foo> を渡すと、llc や opt のようなツールが受け付ける ほぼすべてのオプションを渡せます。 例: 各 LLVM パスの前に IR を出力するには -C llvm-args=-print-before-all を指定します。
  • -C no-prepopulate-passes は、LLVM パスマネージャーにパスのリストを 事前投入することを避けます。 これにより、最適化後の LLVM IR ではなく、 rustc が生成する LLVM IR を確認できます。
  • -C passes=val オプションでは、実行する追加の LLVM パスをスペース区切りのリストで指定できます
  • -C save-temps オプションは、コンパイル中のすべての一時出力ファイルを保存します
  • -Z print-llvm-passes オプションは、実行される LLVM 最適化パスを出力します
  • -Z time-llvm-passes オプションは、各 LLVM パスの時間を測定します
  • -Z verify-llvm-ir オプションは、LLVM IR の正当性を検証します
  • -Z no-parallel-backend は、別個のコンパイル単位の並列コンパイルを無効にします
  • -Z llvm-time-trace オプションは、Chrome プロファイラー互換の JSON ファイルを出力します。 これには LLVM パスの詳細とタイミングが含まれます。
  • -C llvm-args=-opt-bisect-limit=<index> オプションを使用すると、LLVM 最適化を二分探索できます。

LLVM バグレポートの提出

LLVM バグレポートを提出する場合、問題を実証する何らかの最小の 動作例が必要になるでしょう。 Godbolt compiler explorer はこれに非常に役立ちます。

  1. 問題のあるコードの LLVM IR を入手したら(上記参照)、Godbolt で 最小の動作例を作成できます。 llvm.godbolt.org にアクセスしてください。

  2. プログラミング言語として LLVM-IR を選択します。

  3. llc を使用して、IR をそのまま特定のターゲット向けにコンパイルします。

    • 便利なフラグがいくつかあります。-mattr はターゲット機能を有効にし、-march= はターゲットを選択し、-mcpu= は CPU を選択する、などです。
    • llc -march=help のようなコマンドは利用可能なすべてのアーキテクチャを出力します。これは、 Rust のアーキテクチャ名と LLVM の名前が一致しないことがあるため便利です。
    • どこかで rustc を自分でコンパイルしている場合、ターゲットディレクトリに llcopt などのバイナリがあります。
  4. LLVM-IR を最適化したい場合は、opt を使用して LLVM の 最適化がそれをどのように変換するかを確認できます。

  5. 問題を示す godbolt リンクを入手したら、LLVM のバグを登録するのは非常に簡単です。 その GitHub Issues ページにアクセスするだけです。

LLVM からのバグ修正の移植

バグが LLVM のバグであると特定できたら、それがすでに LLVM で報告され修正されているものの、私たちがまだ その修正を取得していないことがわかる場合があります(あるいは、あなたが LLVM に十分詳しく、自分で修正できる場合もあります)。

その場合、rustc がより簡単に利用できるように、そのバグの修正を 私たち自身の LLVM フォークに直接移植することを選択できる場合があります。 私たちの LLVM フォークは rust-lang/llvm-project で管理されています。 そこで修正を取り込んだら、サブモジュールのコミットを変更する PR も取り込む必要があります。 助けが必要な場合は Zulip で聞いてください。

バックエンド非依存の Codegen

rustc_codegen_ssa は、すべてのバックエンド、すなわち LLVM、CraneliftGCC が実装するための抽象インターフェイスを提供します。

以下は、この抽象インターフェイスを作成したリファクタリングに関する背景情報です。

rustc_codegen_llvm のリファクタリング

Denis Merigoux 著、2018年10月23日

リファクタリング前のコードの状態

MIR から LLVM IR へのコンパイルに関連するすべてのコードは、 rustc_codegen_llvm クレート内に含まれていました。 最も重要な要素の内訳は次のとおりです。

  • back フォルダー(7,800 LOC)は、LLVM を介してさまざまなオブジェクトファイルとアーカイブを作成するメカニズムに加え、並列コード生成のための通信メカニズムを実装しています。
  • debuginfo(3,200 LOC)フォルダーには、デバッグ情報を LLVM に渡すためのすべてのコードが含まれています。
  • llvm(2,200 LOC)フォルダーは、C++ API を使用して LLVM と通信するために必要な FFI を定義しています。
  • mir(4,300 LOC)フォルダーは、MIR から LLVM IR への実際の lowering を実装しています。
  • base.rs(1,300 LOC)ファイルには、いくつかのヘルパー関数に加えて、コード生成を起動し、作業を分配する高レベルのコードが含まれています。
  • builder.rs(1,200 LOC)ファイルには、基本ブロック内で個々の LLVM IR 命令を生成するすべての関数が含まれています。
  • common.rs(450 LOC)には、さまざまなヘルパー関数と、LLVM の静的値を生成するすべての関数が含まれています。
  • type_.rs(300 LOC)は、LLVM IR への型変換の大部分を定義しています。

このリファクタリングの目的は、このクレート内で、LLVM 固有のコードと、他の rustc バックエンドで再利用できるコードを分離することです。 たとえば、 mir フォルダーはほぼ完全にバックエンド固有ですが、 クレートの他の部分に大きく依存しています。 コードの分離は、コードのロジックにも、 その性能にも影響してはなりません。

これらの理由から、分離プロセスでは、結果のコードをコンパイル可能にするために、同時に行う必要がある 2 つの変換が伴います。

  1. 関数シグネチャと構造体定義内のすべての LLVM 固有の型をジェネリクスに置き換える
  2. LLVM FFI を呼び出すすべての関数を、バックエンド非依存コードとバックエンドの間のインターフェイスを定義する一連のトレイト内にカプセル化する

LLVM 固有のコードは rustc_codegen_llvm に残される一方で、すべての新しいトレイトとバックエンド非依存コードは rustc_codegen_ssa(@eddyb による名称提案)に移動されます。

ジェネリックな型と構造体

@irinagpopa は、LLVM では参照 &'ll Value として実装されるジェネリックな Value 型によって、rustc_codegen_llvm の型をパラメータ化し始めました。 この作業は、mir フォルダー内およびその他の場所にあるすべての構造体に加え、LLVM の BasicBlock 型と Type 型にも拡張されました。

LLVM codegen にとって最も重要な 2 つの構造体は CodegenCxBuilder です。 これらは、複数のライフタイムパラメータと Value の型によってパラメータ化されています。

struct CodegenCx<'ll, 'tcx> {
  /* ... */
}

struct Builder<'a, 'll, 'tcx> {
  cx: &'a CodegenCx<'ll, 'tcx>,
  /* ... */
}

CodegenCx は、複数の関数を含むことができる 1 つの codegen-unit をコンパイルするために使用される一方、Builder は 1 つの基本ブロックをコンパイルするために作成されます。

rustc_codegen_llvm のコードは、複数の明示的なライフタイムパラメータを扱う必要があります。これらは次のものに対応します。

  • 'tcx は最も長いライフタイムで、プログラムの情報を含む元の TyCtxt に対応します。
  • 'a は、構造体内の CodegenCx または別のオブジェクトの短命な参照です。
  • 'll は、ValueType などの LLVM オブジェクトへの参照のライフタイムです。

コードにはすでに多くのライフタイムパラメータがありますが、ジェネリック化したことで、操作される LLVM オブジェクトの特殊な性質(それらは extern ポインタです)によってのみ borrow-checker が通過していた状況が明らかになりました。 たとえば、analyse.rsLocalAnalyser に追加のライフタイムパラメータを追加する必要があり、次の定義になりました。

struct LocalAnalyzer<'mir, 'a, 'tcx> {
  /* ... */
}

しかし、最も重要な 2 つの構造体である CodegenCxBuilder は、 バックエンド非依存コードでは定義されていません。 実際、それらの内容はバックエンドに非常に固有であり、バックエンドのコンテキスト用にジェネリックフィールドを介して狭い余地だけを許すよりも、それらの定義をバックエンド実装者に任せるほうが理にかなっています。

トレイトとインターフェイス

CodegenCxBuilder はバックエンドによって定義される必要があるため、 バックエンドのインターフェイスを定義するすべてのトレイトを実装する構造体になります。 これらのトレイトは rustc_codegen_ssa/traits フォルダーで定義され、すべてのバックエンド非依存コードはそれらによってパラメータ化されます。 たとえば、base.rs の関数がどのようにパラメータ化されるかを説明します。

pub fn codegen_instance<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
    cx: &'a Bx::CodegenCx,
    instance: Instance<'tcx>
) {
    /* ... */
}

このシグネチャには、前に説明した 2 つのライフタイムパラメータと、Builder 構造体が満たすインターフェイスに対応する BuilderMethods トレイトを満たすマスター型 Bx があります。 BuilderMethods は関連型 Bx::CodegenCx を定義しており、それ自体が構造体 CodegenCx によって実装される CodegenMethods トレイトを満たします。

トレイト側について、traits/builder.rs にある BuilderMethods の定義の一部を例として示します。

pub trait BuilderMethods<'a, 'tcx>:
    HasCodegen<'tcx>
    + DebugInfoBuilderMethods<'tcx>
    + ArgTypeMethods<'tcx>
    + AbiBuilderMethods<'tcx>
    + IntrinsicCallMethods<'tcx>
    + AsmBuilderMethods<'tcx>
{
    fn new_block<'b>(
        cx: &'a Self::CodegenCx,
        llfn: Self::Function,
        name: &'b str
    ) -> Self;
    /* ... */
    fn cond_br(
        &mut self,
        cond: Self::Value,
        then_llbb: Self::BasicBlock,
        else_llbb: Self::BasicBlock,
    );
    /* ... */
}

最後に、ExtraBackendMethods トレイトを実装するマスター構造体が、base.rscodegen_crate のような高レベルの codegen 駆動関数に使用されます。 LLVM では、それは空の LlvmCodegenBackend です。 ExtraBackendMethods は、rustc_codegen_ssa/src/traits/backend.rs で定義されている CodegenBackend を実装する構造体と同じ構造体によって実装されるべきです。

トレイト化のプロセス中に、一部の関数はローカル構造体のメソッドから CodegenCx または Builder のメソッドに変換され、対応する self パラメータが追加されました。 実際、LLVM は内部的に情報を格納しており、API 経由で呼び出されたときにそれへアクセスできます。 これらのメソッドが呼び出される際に持ち回られる Rust データ構造には、この情報は現れません。 しかし、rustc 用の Rust バックエンドを実装する場合、これらのメソッドは CodegenCx からの情報を必要とするため、追加のパラメータが必要になります(トレイトの LLVM 実装では未使用です)。

リファクタリング後のコードの状態

トレイトは、LLVM の API と非常によく似た API を提供します。 これは最善の解決策ではありません。LLVM には非常に特殊なやり方があるためです。 別のバックエンドを追加する際には、 より高い柔軟性を提供するためにトレイト定義が変更される可能性があります。

しかし、バックエンド非依存のコードと LLVM 固有のコードとの現在の分離により、 古い rustc_codegen_llvm のかなりの部分を再利用できました。 以下は、最も重要な要素についての、バックエンド非依存(BA)と LLVM の 新しい LOC 内訳です。

  • back フォルダー: 3,800(BA)対 4,100(LLVM);
  • mir フォルダー: 4,400(BA)対 0(LLVM);
  • base.rs: 1,100(BA)対 250(LLVM);
  • builder.rs: 1,400(BA)対 0(LLVM);
  • common.rs: 350(BA)対 350(LLVM);

debuginfo フォルダーは分割による影響をほとんど受けず、LLVM 固有のままです。 その高レベル機能だけがトレイト化されています。

新しい traits フォルダーには、トレイト定義だけで 1500 LOC あります。 全体として、 27,000 LOC 規模だった古い rustc_codegen_llvm のコードは、新しい 18,500 LOC 規模の新しい rustc_codegen_llvm と、12,000 LOC 規模の rustc_codegen_ssa に分割されました。 このリファクタリングにより、そうでなければ rustc の複数のバックエンド間で 重複させる必要があったであろう 約 10,000 LOC を再利用できたと言えます。

リファクタリングされた rustc のバックエンドは、 テストスイートにおいても性能ベンチマークにおいても回帰を引き起こしませんでした。これは、 コンパイル時パラメトリシティのみを使用した(トレイトオブジェクトを使用しない) リファクタリングの性質と一致しています。

暗黙的な呼び出し元の位置情報

RFC 2091 で承認されたこの機能により、Option::unwrapResult::expectIndex::index のような関数から開始されたパニック時に、呼び出し元の位置を正確に報告できるようになります。この機能は、関数用の #[track_caller] 属性、caller_location intrinsic、および安定化しやすい core::panic::Location::caller ラッパーを追加します。

動機となる例

このサンプルプログラムを考えてみます。

fn main() {
    let foo: Option<()> = None;
    foo.unwrap(); // これは有用なパニックメッセージを生成するべきです!
}

Rust 1.42 より前は、この unwrap() のようなパニックは core 内の位置を出力していました。

$ rustc +1.41.0 example.rs; example.exe
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value',...core\macros\mod.rs:15:40
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

1.42 以降では、はるかに有用なメッセージが得られます。

$ rustc +1.42.0 example.rs; example.exe
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', example.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

これらのエラーメッセージは、core::panic::Location::caller を利用するための panic! 内部の変更と、呼び出し元情報を伝播する標準ライブラリ内の多数の #[track_caller] アノテーションの組み合わせによって実現されています。

呼び出し元の位置情報を読み取る

以前は、panic!Location を構築するために file!()line!()column!() マクロを使用して、パニックが発生した場所を指していました。これらのマクロにはオーバーライドされた位置を渡すことができなかったため、意図的に panic! を呼び出す関数は独自の位置を提供できず、実際のエラーの発生源が隠されていました。

内部的には、panic!() は現在、どこで展開されたかを調べるために core::panic::Location::caller() を呼び出します。この関数自体にも #[track_caller] がアノテートされており、rustc によって実装された caller_location コンパイラ intrinsic をラップしています。この intrinsic は、const コンテキストでどのように動作するかという観点から説明するのが最も簡単です。

const における呼び出し元の位置情報

const コンテキストで呼び出し元の位置を返すには、主に 2 つのフェーズがあります。適切な位置を見つけるためにスタックをさかのぼることと、返す const 値を割り当てることです。

適切な Location を見つける

const コンテキストでは、intrinsic が呼び出された場所から「スタックをさかのぼり」、その属性を持たないスタック内の最初の関数呼び出しに到達したところで停止します。この走査は InterpCx::find_closest_untracked_caller_location() 内で行われます。

一番下から開始して、InterpCx::stack 内のスタック Frame を上方向に反復し、[FrameInstance][frame-instance] に対して [InstanceKind::requires_caller_location][requires-location] を呼び出します。false` を返すものを見つけた時点で停止し、「最上位」の tracked 関数であった前のフレームの span を返します。

静的な Location を割り当てる

Span が得られたら、Location 用の静的メモリを割り当てる必要があります。これは TyCtxt::const_caller_location() クエリによって実行されます。内部的には、これは InterpCx::alloc_caller_location() を呼び出し、一意の memory kindMemoryKind::CallerLocation)になります。SSA codegen バックエンドはこれらと同じ値のコードを生成できるため、ここでもこのコードを使用します。

Location が静的メモリに割り当てられると、intrinsic はそれへの参照を返します。

#[track_caller] の callee のコードを生成する

tracked 関数とその呼び出し元のために効率的なコードを生成するには、実行時にさかのぼるスタックがなくても、intrinsic の視点から同じ振る舞いを提供する必要があります。そこでアプローチを反転します。スタックを下方向に伸ばす際、intrinsic が呼び出されたときにスタックをさかのぼるのではなく、tracked 関数の呼び出しに追加の引数を渡します。その追加引数は、呼び出し元の位置が照会される場所であればどこでも返すことができます。

追加する引数の型は &'static core::panic::Location<'static> です。執筆時点でポインターは std::mem::size_of::<core::panic::Location>() == 24 の 3 分の 1 のサイズであるため、不要なコピーを避ける目的で参照が選ばれました。

tracked である関数への呼び出しを生成する際には、location 引数として FunctionCx::get_caller_location の値を渡します。

呼び出し元の関数が tracked である場合、get_caller_locationFunctionCx::caller_location 内のローカルを返します。このローカルは現在の呼び出し元の呼び出し元によって設定されています。このような場合、intrinsic は、実際にはその呼び出し元への引数として提供された参照を「返し」ます。

呼び出し元の関数が tracked でない場合、get_caller_location は現在の Span から Location static を割り当て、それへの参照を返します。

スタックを下方向に伸ばす際、複数の FunctionCxcaller_location フィールドを通じて単一の &Location 値を渡すことで、一番下から始まるループと同じ振る舞いをより効率的に実現します。

Codegen の例

この変換は実際にはどのようなものになるのでしょうか。新しい機能を使用するこの例を考えてみます。

#![feature(track_caller)]
use std::panic::Location;

#[track_caller]
fn print_caller() {
    println!("called from {}", Location::caller());
}

fn main() {
    print_caller();
}

ここでは print_caller() は引数を取らないように見えますが、実際には次のようなものへコンパイルされます。

#![feature(panic_internals)]
use std::panic::Location;

fn print_caller(caller: &Location) {
    println!("called from {}", caller);
}

fn main() {
    print_caller(&Location::internal_constructor(file!(), line!(), column!()));
}

動的ディスパッチ

codegen コンテキストでは、この情報をスタックの下方向へ渡すために callee ABI を変更する必要がありますが、この属性は明示的に関数の型を変更しません。ABI の変更は型検査に対して透過的でなければならず、すべての使用において健全なままでなければなりません。

tracked 関数への直接呼び出しでは、callee の完全な codegen フラグが常に分かるため、適切なコードを生成できます。間接呼び出し元はこの情報を持たず、呼び出す関数ポインターの型にもエンコードされていないため、その関数へのポインターを取得するたびに、関数の周囲に ReifyShim を生成します。この shim は間接呼び出しの実際の位置を報告することはできません(代わりに関数の定義位置が報告されます)が、誤コンパイルを防ぎ、完全に安定化された型シグネチャを変更せずにできることとしては、おそらく最善です。

注: tracked 関数へのポインターを取得するときは、常に ReifyShim を出力します。ここでの制約は codegen コンテキストによって課されるものですが、shim の MIR 構築中には、const コンテキスト(shim を無視しても安全)で呼び出されるのか、codegen コンテキスト(shim を無視すると安全でない)で呼び出されるのかは分かりません。仮に分かっていたとしても、const コンテキストと codegen コンテキストの結果は一致しなければなりません。

属性

#[track_caller] 属性は、他の codegen 属性とともにチェックされ、関数が次の条件を満たすことを保証します。

* `"Rust"` ABI を持つ(たとえば `"C"` などではない)
* クロージャではない
* `#[naked]` ではない

使用が有効であれば、[`CodegenFnAttrsFlags::TRACK_CALLER`][attrs-flags] を設定します。このフラグは [`InstanceKind::requires_caller_location`][requires-location] の戻り値に影響し、それはさらに const とコード生成の両方のコンテキストで使用され、正しい伝播を保証します。

### トレイト

トレイトメソッドの実装に適用された場合、この属性は通常の関数に対する場合と同じように動作します。

トレイトメソッドのプロトタイプに適用された場合、この属性はそのメソッドのすべての実装に適用されます。デフォルトのトレイトメソッド実装に適用された場合、この属性はその実装*および*任意のオーバーライドに対して有効になります。

例:

```rust
#![feature(track_caller)]

macro_rules! assert_tracked {
    () => {{
        let location = std::panic::Location::caller();
        assert_eq!(location.file(), file!());
        assert_ne!(location.line(), line!(), "行はこの関数の外側でなければなりません");
        println!("{} で呼び出されました", location);
    }};
}

trait TrackedFourWays {
    /// すべての実装は `#[track_caller]` を継承します。
    #[track_caller]
    fn blanket_tracked();

    /// 実装者は自身にアノテーションを付けることができます。
    fn local_tracked();

    /// この実装は追跡対象になります(オーバーライドも同様です)。
    #[track_caller]
    fn default_tracked() {
        assert_tracked!();
    }

    /// この実装のオーバーライドは追跡対象になります(この実装自体も同様です)。
    #[track_caller]
    fn default_tracked_to_override() {
        assert_tracked!();
    }
}

/// この impl は `default_tracked` にはデフォルト実装を使用し、
/// `default_tracked_to_override` には独自の実装を提供します。
impl TrackedFourWays for () {
    fn blanket_tracked() {
        assert_tracked!();
    }

    #[track_caller]
    fn local_tracked() {
        assert_tracked!();
    }

    fn default_tracked_to_override() {
        assert_tracked!();
    }
}

fn main() {
    <() as TrackedFourWays>::blanket_tracked();
    <() as TrackedFourWays>::default_tracked();
    <() as TrackedFourWays>::default_tracked_to_override();
    <() as TrackedFourWays>::local_tracked();
}

背景/歴史

大まかに言えば、この機能の目標は、安定性の保証を破ることなく、エンドユーザーのソースに変更を要求せず、プラットフォーム固有のデバッグ情報に依存せず、ユーザー定義型が同じエラー報告上の利点を得られなくなることもなく、一般的な Rust のエラーメッセージを改善することです。

これらのパニックの出力を改善することは、少なくとも 2016 年半ば以降、提案の目標となっていました(詳細については、承認済み RFC の non-viable alternatives を参照してください)。RFC 2091 が承認されるまでにはさらに 2 年を要し、この機能の設計に関する rationale の多くは、それ以前のいくつかの提案をめぐる議論を通じて発見されました。

元の RFC の設計は、当時コンパイラ内で大幅なリファクタリングなしに実装できるものに限定されていました。しかし、RFC の承認から実際の実装作業までの 1 年半の間に、revised design が提案され、追跡 issue に書き起こされました。その実装の過程で、関数の MIR における引数の数を変更せずに実装できることも判明しました。これにより後段の処理が簡素化され、トレイトでの使用が可能になりました。

RFC の実装戦略はトレイトを容易にはサポートできなかったため、セマンティクスは当初仕様化されていませんでした。その後、著者とレビュー担当者にとって最も正しいと思われる道筋に従って実装されました。

デバッグ情報

デバッグ情報とは、プログラムの実行中にデバッガーがプログラムの状態を正しく解釈できるように、 コンパイラによって生成される情報の集合です。これには、命令アドレスをソースファイル内のコード行に マッピングする情報や、メモリ内のバイト列を意味のある方法で読み取って表示できるようにするための 型レイアウト情報などが含まれます。

デバッグ情報という用語はやや多義的で、Rust MIR から、エンドユーザーが画面上でデバッガーの出力を 見るまでのすべての層を指すことがあります。簡潔に言うと、最初から最後までのスタックは次のとおりです。

  1. Rustc が MIR を検査し、関連するソース、シンボル、型情報を LLVM に伝達する
  2. LLVM がコンパイル中にこの情報をターゲット固有のデバッグ情報形式へ変換する
  3. デバッガーがデバッグ情報を読み取って解釈し、ソース行をマッピングし、デバッグ対象の メモリ内の変数を正しいレイアウトで特定して読み取れるようにする
  4. 組み込みのデバッガーフォーマットとスタイルが変数に適用される
  5. ユーザー定義スクリプトが実行され、変数に追加のフォーマットとスタイルを適用する
  6. デバッガーフロントエンドが、場合によっては追加の API レイヤー(例: Debug Adapter Protocol を介した VSCode 拡張機能) を通じて、変数をユーザーに表示する

注: 開発ガイドのこのサブセクションは、必要以上に詳細かもしれません。散在している大量の情報を 1 か所に集約し、デバッグスタック全体について可能な限り確かな理解を読者に提供することを目的としています。

ビジュアライザースクリプトの作業だけに関心がある場合は、 debugger-visualizerstesting の情報で十分です。Rust のデバッグノード生成に変更を加える必要がある場合は、 rust-codegen を参照してください。その他のセクションは補足的なものですが、 ビジュアライザーやコード生成が行う必要のある妥協点の一部を理解するうえで重要な場合があります。 また、問題が LLVM やデバッガー自体で解決したほうがよい可能性があるタイミングを知っておくことにも 価値があります。

DWARF

これは *-gnu ターゲット向けの主要なデバッグ情報形式です。通常はバイナリに同梱されますが、 別ファイルとして生成することもできます。DWARF 標準は こちらで入手できます。

注: DWARF デバッグ情報を調べるには、gimli を プログラムから使用できます。GUI を好む場合、著者は DWEX を推奨します。

PDB/CodeView

*-msvc ターゲット向けの主要なデバッグ情報形式です。PDB は Microsoft が作成したプロプライエタリな コンテナ形式であり、残念ながら 複数の意味があります。 Portable PDB は主に .Net アプリケーションで使用されるため、ここで扱うのは通常の PDB ファイルです。 PDB ファイルはコンパイル済みバイナリとは別個のファイルで、.pdb 拡張子を使用します。

PDB ファイルには、DWARF のタグに相当する CodeView オブジェクトが含まれます。CodeView オブジェクトを 消費するデバッガーである CodeView は、もともと 1985 年にリリースされました。当初の目的は C の デバッグであり、後に Visual C++ をサポートするよう拡張されました。現代のアーキテクチャや言語を サポートするために、この形式には現在も小さな変更が加えられていますが、これらの変更の多くは 文書化されていない、または使用例が少ないものです。

CodeView オブジェクトを扱う際には、この背景を念頭に置くことが重要です。その出自ゆえに、これらの オブジェクトの「機能セット」は非常に限定的で、C の中核的な機能を中心としています。現代の DWARF 標準にある多くの利便性や機能は備えていません。CodeView の欠点を補うために、デバッグ情報スタック内には かなりの数の回避策が存在します。

プロプライエタリな性質により、PDB と CodeView に関する情報を見つけるのは非常に困難です。多くの情報源は 作成時期が大きく異なり、不完全または多少矛盾した情報を含んでいます。そのため、このページでは可能な限り 多くの情報源を集約することを目指します。

  • CodeView 1.0 仕様
  • LLVM
  • Microsoft
    • microsoft-pdb - PDB リーダーの C/C++ 実装です。 この実装には完全な PDB または CodeView の仕様は含まれていませんが、他の PDB コンシューマーを 作成するために十分な情報は含まれています。執筆時点(2025 年 11 月)では、 このリポジトリは数年間アーカイブされています。
    • pdb-rs - 他の公開情報に基づく Rust ベースの PDB リーダー兼ライターです。 安定性や仕様準拠は保証されません。また、PDB ファイルをダンプできる pdbtool も含まれています (cargo install pdbtool
    • Debug Interface Access SDK。 PDB 形式を直接文書化しているわけではありませんが、インターフェイス自体から詳細を読み取ることができます。

デバッガー

Rust は 3 つの主要なデバッガー、GDB、LLDB、CDB をサポートしています。それぞれに独自の要件、 制限、癖があります。残念ながら、これにより考慮すべき範囲が広くなっています。

注: CDB は Microsoft が作成したプロプライエタリなデバッガーです。基盤となるエンジンは WinDbg、KD、VSCode 向け Microsoft C/C++ 拡張機能、および Visual Studio Debugger の一部も 支えています。これらのドキュメントでは、一貫性のために CDB と呼びます

GDB と LLDB は Rust の値レイアウトをネイティブにサポートする機能を提供していますが、これは 完全に必須ではありません。Rust は現在、C++ のものと非常によく似たデバッグ情報を出力しており、 Rust サポートのないデバッガーでも、やや劣化した体験で動作できます。詳細は後のセクションに含めますが、 各デバッガーの機能の簡単なリファレンスを次に示します。

デバッガーデバッグ情報形式ネイティブ Rust サポート式のスタイルビジュアライザースクリプト
GDBDWARF完全RustPython
LLDBDWARF と PDB部分的C/C++Python
CDBPDBなしC/C++Natvis

重要: CDB は Windows 上でのみ実行されると想定できます。GDB または LLDB が実行される OS については いかなる想定もできません。

サポート対象外

以下は、将来に影響を与える可能性があるため特に注目すべき、サポート対象外のデバッガーです。

  • Bugstalker は Rust で書かれた x86-64 Linux デバッガーで、 Rust プログラムのデバッグに特化しています。有望ではありますが、まだ開発初期段階です。
  • RAD Debugger は Windows 専用の GUI デバッガーです。 PDB が変換される独自のデバッグ情報形式を持っています。このプロジェクトには、リンク段階で その新しいデバッグ情報形式を生成できるリンカーも含まれています。

Rust Codegen

デバッグ情報生成の最初のフェーズでは、Rust がプログラムの MIR を検査し、それを LLVM に伝達する必要があります。これは主に rustc_codegen_llvm/debuginfo で行われますが、一部の型名処理は rustc_codegen_ssa/debuginfo に存在します。Rust は DIBuilder API を介して LLVM と通信します。これは rustc_llvm に存在する、LLVM の内部機構を薄くラップしたものです。

型情報

型情報は通常、型名、サイズ、アラインメントに加え、関連する場合はフィールド、ジェネリックパラメーター、ストレージ修飾子などで構成されます。この作業の多くは rustc_codegen_llvm/src/debuginfo/metadata で行われます。

念頭に置いておくべき重要な点は、目標が必ずしも「Rust で現れるとおりに型を正確に表現する」ことではなく、デバッグ中にデバッガーがデータを可能な限り正確に再構築できる方法で表現することだという点です。この違いは、この層で発生する中核的な作業を理解するうえで不可欠です。ここで行われる多くの変更は、他に有効な選択肢がない場合に、デバッガーの制限を回避するためのものです。

Rust が生成する DI ノードは、CDB と LLDB の両方のために C/C++ であるかのように「装い」ます。これにより、直感的でなく、慣用的でもないデバッグ情報が生成されることがあります。

ポインターと参照

ワイドポインター/参照/Box は、data_ptrlength の 2 つのフィールドを持つ構造体として扱われます。

すべての非ワイドポインター、参照、Box ポインターはポインターノードとして出力され、mut と非 mut の区別は行われません。これを是正する試みは何度か行われてきましたが、残念ながら単純な解決策はありません。それぞれの形式の reference DI ノードを使用することには落とし穴があります。C++ の参照と Rust の参照の間には、調和できない意味論上の違いがあります。

cppreferenceより:

参照はオブジェクトではありません。参照は必ずしもストレージを占有するとは限りませんが、目的のセマンティクスを実装するために必要な場合、コンパイラーがストレージを割り当てることがあります(たとえば、参照型の非静的データメンバーは通常、メモリアドレスを格納するために必要な分だけクラスのサイズを増加させます)。

参照はオブジェクトではないため、参照の配列、参照へのポインター、参照への参照は存在しません

現在提案されている解決策は、単純にポインターノードを typedef することです。

mut を示すために const 修飾子を使用すると、LLDB の内部最適化により潜在的な問題が生じます。要するに、LLDB はコードをステップ実行する際に、変数の子値(たとえば構造体フィールドや配列要素)をキャッシュしようとします。どの値が安全にキャッシュ可能かを判断するためにヒューリスティックが使用され、const はそのヒューリスティックの一部です。これが Rust の内部可変性構造のようなものとどのように相互作用するかについては、まだ調査されていません。

DWARF と PDB

型情報の大部分はかなり単純ですが、注目すべき問題の 1 つはターゲットのデバッグ情報形式です。各形式には異なるセマンティクスと制限があるため、場合によってはわずかに異なるデバッグ情報が必要になります。これは cpp_like_debuginfo の呼び出しによって制御されます。

命名

Rust は型名を可能な限り正確に伝達しようとしますが、デバッガーやデバッグ情報形式が常にそれを尊重するとは限りません。

MSVC の式パーサーの制限により、PDB デバッグ情報では次の名前変換が行われます。

Rust 名MSVC 名
&str/&mut strref$<str$>/ref_mut$<str$>
&[T]/&mut [T]ref$<slice$<T> >/ref_mut$<slice$<T> >1
[T; N]array$<T, N>
RustEnumenum2$<RustEnum>
(T1, T2)tuple$<T1, T2>
*const Tptr_const$<T>
*mut Tptr_mut$<T>
usizesize_t2
isizeptrdiff_t2
uNunsigned __intN2
iN__intN2
f32float2
f64double2
f128fp1282

ジェネリクス

Rust はジェネリックな情報(ArrayVec<T, N: usize>T)を出力しますが、ジェネリックな情報(ArrayVec<T, N: usize>N)は出力しません。

CodeView にはジェネリクス/C++ テンプレート用のリーフノードがないため、PDB デバッグ情報を生成するときにすべてのジェネリック情報が失われます。デバッガーが型名を介してジェネリック引数を取得できるようにする回避策はありますが、せいぜい脆弱な解決策です。この欠陥を修正するために Microsoft に連絡する、および/または未使用の CodeView ノード型の 1 つを適切な等価物として使用する取り組みが進められています。

型エイリアス

Rust はデバッガーの制限を補うためにいくつかの場合で typedef ノードを出力しますが、現在のところソースコード内の型エイリアス用のノードは出力しません。

列挙型

列挙型の DI ノードは rustc_codegen_llvm/src/debuginfo/metadata/enums で生成されます

DWARF

DWARF には判別共用体用の専用ノード DW_TAG_variant があります。これは、判別子値を含む場合も含まない場合もある DW_TAG_variant_part ノードを参照するコンテナーです。階層は次のようになります。

DW_TAG_structure_type      (top-level type for the coroutine)
  DW_TAG_variant_part      (variant part)
    DW_AT_discr            (reference to discriminant DW_TAG_member)
    DW_TAG_member          (discriminant member)
    DW_TAG_variant         (variant 1)
    DW_TAG_variant         (variant 2)
    DW_TAG_variant         (variant 3)
  DW_TAG_structure_type    (type of variant 1)
  DW_TAG_structure_type    (type of variant 2)
  DW_TAG_structure_type    (type of variant 3)

PDB

PDB には専用ノードがないため、判別共用体の C 相当を生成します。

union enum2$<RUST_ENUM_NAME> {
    enum VariantNames {
        First,
        Second
    };
    struct Variant0 {
        struct First {
            // フィールド
        };
        static const enum2$<RUST_ENUM_NAME>::VariantNames NAME;
        static const unsigned long DISCR_EXACT;
        enum2$<RUST_ENUM_NAME>::Variant0::First value;
    };
    struct Variant1 {
        struct Second {
            // フィールド
        };
        static enum2$<RUST_ENUM_NAME>::VariantNames NAME;
        static unsigned long DISCR_EXACT;
        enum2$<RUST_ENUM_NAME>::Variant1::Second value;
    };
    enum2$<RUST_ENUM_NAME>::Variant0 variant0;
    enum2$<RUST_ENUM_NAME>::Variant1 variant1;
    unsigned long tag;
}

重要な点として、LLDB の制限により、生成される DISCR_* 値は、その値が #[repr(u64)] でない場合でも常に u64 になります。DISCR_* 値と tag は型に関係なく uint64_t 値として読み込まれるため、これは LLDB にとってはほとんど問題になりません。

ソース情報

TODO


  1. MSVC の式パーサーは >> を右シフトとして扱います。型名では、連続する > をスペース(> >)で区切る必要があります。

  2. これらの型名はデバッグ情報ノードの一部として生成されます(その後 Rust 名を持つ typedef ノードでラップされます)が、LLVM-IR ノードが CodeView ノードに変換されると、型名情報は失われます。これは、CodeView にはプリミティブ型用の特別な短縮ノードがあり、それらの短縮ノードには「name」フィールドがないためです。 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

(WIP) LLVM コード生成

Rust が LLVM の DIBuilder 関数を呼び出すと、LLVM は与えられた情報を、形式に依存しない “デバッグレコード” に変換します。これらのレコードは LLVM-IR 内で調査できます。

デバッグレコード内のタグは、常に DWARF タグとして格納される点に注意することが重要です。 ターゲットが PDB デバッグ情報を必要とする場合、コード生成中にデバッグレコードは DWARF タグを対応する CodeView のタグに変換するモジュールを通過します。

デバッガーの内部構造

デバッグ情報をメモリ内表現に変換するのは、デバッガーの役割です。デバッグ情報の解釈とメモリ内表現はどちらも任意です。プログラムの実行中に意味のある情報を再構築できる限り、どのようなものでも構いません。生のデバッグ情報から利用可能な型に至るまでのパイプラインは、非常に複雑になることがあります。

情報が扱いやすい形式になると、デバッガーフロントエンドは次に、データを解釈して表示する方法、ユーザーがそれと対話する方法、そして拡張性のための API を提供しなければなりません。

デバッガーは巨大なシステムであり、ここで完全に扱うことはできません。このセクションでは、Rust のデバッグ体験に直接関係するサブシステムの概要を簡単に説明します。

Microsoft のデバッグエンジンはクローズドソースであるため、ここでは扱いません。

LLDB の内部構造

LLDB のデバッグ情報処理は、主に lldb/src/Plugins で定義されている一連の拡張可能なインターフェイスに依存しています。これらは、サードパーティのコンパイラ開発者が、LLDB によって実行時にロードされる言語サポートを追加できるようにすることを意図したものですが、執筆時点(2025年11月)では公開 API がまだ確定していないため、プラグインは LLDB 自体の内部、または LLDB のスタンドアロンフォーク内に存在しています。

通常、言語サポートはこれらのプラグインのパイプラインとして記述されます: *ASTParser -> TypeSystem -> ExpressionParser/Language

以下は、LLDB のプラグイン API の既存の実装例です。

Rust サポートと TypeSystemClang

デバッグ情報の概要で述べたように、LLDB には部分的な Rust サポートがあります。さらに明確にすると、Rust は C/C++ 向けに構築されたプラグインパイプラインを使用しています(ただし、Rust の enum 型向けのヘルパーもいくつか含まれています)。これは clang コンパイラの型表現に直接依存しています。そのため、LLDB の出力が期待するものと一致しない場合に、どの程度変更できるかに大きな制約が課されます。いくつかの回避策は役立ちますが、最終的には Rust の要求は、C および C++ のコンパイルとデバッグが正しく動作することを保証することに比べると二次的なものです。

LLDB は TypeSystemRust の追加を受け入れる姿勢がありますが、それは非常に大規模な取り組みです。このセクションは、現在 TypeSystemClang とどのように連携しているかを文書化するだけでなく、将来 TypeSystemRust を実装する際の軽い指針としても機能します。

TypeSystem が対象言語のコンパイラと直接連携することが意図されている点は注目に値しますが、それは必須要件ではありません。必要な補助型はすべて、プラグイン実装内で作成できます。

注: LLDB のドキュメントは、ソースコード内のコメントも含めてかなり少ないです。 TypeSystemClang の実装を読んで言語サポートの仕組みを理解しようとするのは、clang コンパイラの内部構造を理解する必要も加わるため、やや困難です。 上に挙げた 2 つの TypeSystemRust 実装を見ることを推奨します。これらはコンパイラの型表現を利用せずに「ゼロから」書かれているためです。これらは、言語サポートを実装するために必要な最小限に比較的近いものです。

DWARF と PDB

LLDB は DWARF と PDB の両方のデバッグ情報を扱えるという点で独自です。ただし、これにはいくらかの複雑さが伴います。さらに事態を複雑にしているのは、PDB サポートが dianative に分かれていることです。dia は Visual Studio とともに配布される msdia140.dll ライブラリに依存しており、native は PDB 形式に関する公開情報を使用してゼロから書かれています。

注: dia は LLDB バージョン 21 まではデフォルトでした。native は LLDB 22 のリリース時点で新しいデフォルトです。dia ベースのプラグインを非推奨にし、完全に削除する計画があります。そのため、以下では native による解析のみを扱います。進捗については、 この Discourse スレッドおよび関連する追跡 issueを参照してください。

native は、LLDB 22 で追加された plugin.symbol-file.pdb.reader 設定、または環境変数 LLDB_USE_NATIVE_PDB_READER=0/1 を使用して切り替えられます。

デバッグノードの解析

最初のステップは、生のデバッグノードを使用可能なものへ処理することです。これは主に DWARFASTParser クラスと PdbAstBuilder クラスで行われます。これらのクラスには、それぞれ SymbolFileDWARFSymbolFileNativePDB から生成された、デシリアライズ済みのデバッグ情報が渡されます。SymbolFile の実装者は、基盤となるデバッグ情報をパーサーに渡す前に、ほとんど変換を行いません。PDB と DWARF のどちらについても、デバッグ情報は LLVM のデバッグ情報ハンドラーを使用して読み取られます。

パーサーは、LLDB の目的にとってより扱いやすい形式へノードを変換します。clang の場合、これらの形式は clang::QualTypeclang::Declclang::DeclContext であり、C および C++ をコンパイルする際に clang が内部的に使用する型です。繰り返しになりますが、コンパイラの型表現を使用することは必須ではありませんが、プラグインシステムはそれが可能であることを前提に構築されています。

注: 上記の型は、TypeSystemClang の具体的な実装詳細が関係しない場合、言語非依存の用語として LangTypeDecl、および DeclContext と呼びます。

LangType は型を表します。これには、型の名前、サイズとアラインメント、分類(例: struct、プリミティブ、ポインター)、修飾子(例: constvolatile)、テンプレート引数、関数の引数型および戻り値型などの情報が含まれます。こちらは、RustType がどのようなものになり得るかの例です。

Decl はあらゆる種類の宣言を表します。それは型、変数、struct の静的フィールド、static または const が初期化される値などである可能性があります。

DeclContext は多かれ少なかれスコープを表します。DeclContext には通常、Decl や他の DeclContext が含まれますが、その関係はそれほど単純ではありません。たとえば、関数は Decl であることも(関数シグネチャは型であるため)、かつ DeclContext であることもできます(関数には変数宣言、ネストされた関数宣言などが含まれるためです)。

変換プロセスはかなり冗長になることがありますが、通常は単純です。ここでの作業の多くは、LangTypeDeclDeclContext を埋めるために必要な正確な情報に依存します。 ノードが変換されると、そのノードへのポインターは型消去され(void*)、CompilerTypeCompilerDecl、または CompilerDeclContext にラップされます。これらのラッパーは、それらを所有する TypeSystem に関連付けます。これらのオブジェクトのメソッドは TypeSystem に委譲され、TypeSystemvoid* を適切な LangType*/Decl*/DeclContext* にキャストし直して内部を操作します。Rust の用語では、この関係はおおよそ次のようになります。

struct CompilerType {
    inner_type: *mut c_void,
    type_system: Arc<dyn TypeSystem>,
}

impl CompilerType {
    pub fn get_byte_size(&self) -> usize {
        self.type_system.get_byte_size(self.lang_type)
    }

}

...

impl TypeSystem for TypeSystemLang {
    pub fn get_byte_size(lang_type: *mut c_void) -> usize {
        let lang_type = lang_type as *mut LangType;

        // LangType の内部を操作して
        // そのサイズを決定する
        ...
    }
}

型システム

TypeSystem インターフェイスには、主に 3 つの目的があります。

  1. ある言語の型に対する「唯一の権威」として機能すること。これにより、その型システムを LLDB の型システムの「プール」に追加できます。実行可能ファイルがロードされると、対象言語が判定され、その言語を処理できると主張する TypeSystem を見つけるためにプールが問い合わせられます。TypeSystem を使用して、背後にある SymbolFile を取得したり、型を検索したり、デバッグ情報に存在しない可能性のある基本型(例: プリミティブ、T の配列、T へのポインター)を合成したりすることもできます。
  2. LangTypeDeclDeclContext オブジェクトのライフタイムを管理すること
  3. それらの型がどのように表示され、どのように操作できるかの「デフォルト」をカスタマイズすること。

最初の 2 つの機能はかなり単純なので、ここでは 3 つ目に焦点を当てます。

TypeSystem インターフェイスの多くの関数は、ビジュアライザースクリプトを扱ったことがあるなら見覚えがあるでしょう。これらの関数は、対応する名前を持つ SBTypeSBValue の関数を支えています。たとえば、TypeSystem::GetFormat は、カスタムフォーマッターが適用されていない場合に、その型のデフォルトフォーマットを返します。

特に注目すべきなのは、GetIndexOfChildWithNameGetNumChildren です。これらの関数の TypeSystem 版は、SBValue 版のように値ではなく、に対して動作します。TypeSystem 関数から返される値は、構造体のどの部分が LLDB の他の部分からそもそも操作可能であるかを決定します。フィールドが省かれると、そのフィールドは LLDB にとって実質的にもはや存在しなくなります。

さらに、これらはオブジェクトを扱わないため、調査または解釈するための基礎となるメモリがありません。基本的に、これはこれらの関数が対応する SyntheticProvider 関数と同じ目的を持たないことを意味します。Vec にいくつの要素があるか、またはそれらの要素がどのアドレスに存在するかを判定する方法はありません。和型の判別子の値を判定することもできません。

理想的には、TypeSystem はデバッグ情報に現れる型を、可能な限り変更を加えずに公開すべきです。LLDB の synthetic とフロントエンドは、型を見やすく整えることができます。ある情報が役に立たない場合は、そもそもそのデバッグ情報を出力しないように Rust コンパイラーを変更すべきです。

式の解析

TypeSystem は通常、式の解析を処理できる対応物を持つように書かれます。そのためには、TypeSystem インターフェイスでいくつか追加の関数を実装する必要があります。式解析コードの大部分は lldb/source/Plugins/ExpressionParser に置くべきです。

パーサーについて特筆すべきことはあまり多くありません。(おそらく簡略化された)Rust 構文を処理できる単純なインタープリターを実装する必要があります。これらは lldb::ValueObject に対して動作し、これは SBValue を支えるオブジェクトです。

Language

Language プラグインは、Python ビジュアライザースクリプトに相当する C++ の機能です。これらは同じ目的、つまり synthetic child の作成と pretty-printing のために、SBValue オブジェクトに対して動作します。LibCxx 型に対する CPlusPlusLanguage の実装は、ビジュアライザーをどのように書くべきかを学ぶための優れた資料です。

これらのプラグインは LLDB の private な内部(基礎となる TypeSystem を含む)にアクセスできるため、Language プラグインとして書かれた synthetic/summary provider は、Python の同等物よりも高品質な出力を提供できます。

デバッグノードの解析、型システム、式の解析はすべて互いに密接に関連していますが、Language プラグインはよりカプセル化されているため、既存の型システムがサポートする任意の言語向けに「スタンドアロン」で書くことができます。参入障壁が低いため、RustLanguage プラグインは LLDB における完全な言語サポートへの良い足がかりになるかもしれません。

ビジュアライザー

作業中

(WIP) GDB の内部構造

GDB の Rust サポートは gdb/rust-lang.hgdb/rust-lang.c にあります。式解析サポートは gdb/rust-exp.hgdb/rust-parse.c にあります。

デバッガービジュアライザー

これらは通常、デバッガーが情報を表示する前の最後のステップですが、結果は IDE のデバッガー API などのデバッグアダプターを通じてパイプされる場合があります。

「Visualizer」という用語は少し誤称です。本当の目的は単に出力を見栄えよくすることではなく、 ユーザーが操作するためのインターフェイスを、可能な限り有用なものとして提供することです。多くの場合、 これは元の型を Rust の表現に可能な限り近い形で再構築することを意味しますが、 常にそうとは限りません。

ビジュアライザーインターフェイスでは、「合成子要素」を生成できます。これは、デバッグ情報には存在しないものの、 言語や型自体に関する不変条件から導出できるフィールドです。簡単な例としては、 Vec<T>*mut u8 ヒープポインター、長さ、容量だけでなく、Vec<T> の要素を操作できるようにすることが挙げられます。

rust-lldbrust-gdb、および rust-windbg.cmd

これらのサポートスクリプトは Rust ツールチェーンとともに配布されます。これらは適切なデバッガーと ツールチェーンのビジュアライザースクリプトを見つけ、デバッグ対象が起動またはアタッチされる前に ビジュアライザースクリプトを読み込むための適切な引数を指定してデバッガーを起動します。

#![debugger_visualizer]

この属性により、Rust ライブラリの作者は、自分の型のためのプリティプリンターを ライブラリ自体に含めることができます。これらのプリティプリンターは一般的な ビジュアライザーと同じ形式ですが、コンパイル済みバイナリに直接埋め込まれます。これらのスクリプトは デバッガーによって自動的に読み込まれ、ユーザーにシームレスな体験を提供します。この属性は現在、 GDB および natvis スクリプトで機能します。

GDB の Python スクリプトは、バイナリの .debug_gdb_scripts セクションに埋め込まれます。詳細情報は こちらで確認できます。Rustc はこれを rustc_codegen_llvm/src/debuginfo/gdb.rs で実現しています。

Natvis ファイルは /NATVIS リンカーオプションを使用して PDB デバッグ情報に埋め込むことができ、 型がどのビジュアライザーを使用するかを解決する際に最も高い優先度を持ちます。属性で指定された ファイルは CrateInfo::natvis_debugger_visualizers に収集され、その後 rustc_codegen_ssa/src/back/linker.rs でリンカー引数として追加されます。

LLDB は現在サポートされていませんが、将来的にサポートを可能にする可能性のある方法がいくつかあります。 公式には、想定されている方法はフォーマッターバイトコードを介するものです。これは GDB と同等の体験を提供しつつ、Python スクリプト全体を埋め込むことに伴う安全性上の懸念を避けるために作成されました。 オペコードは制限されていますが、Python のビジュアライザースクリプトとおおむね同じ方法で SBValueSBType を扱えます。 これを実装するには、何らかの DSL/ミニコンパイラーを書く必要があります。

あるいは、GDB の戦略を完全にコピーすることも可能かもしれません。つまり、バイナリ内に専用セクションを作成し、 そこに Python スクリプトを埋め込む方法です。LLDB はそれを自動的には読み込みませんが、Python API では デバッグ情報の生セクションにアクセスできます。これにより、専用セクションから Python スクリプトを抽出し、Rust のビジュアライザースクリプトの起動時にそれを読み込める可能性があります。

パフォーマンス

ビジュアライザー自体に取り組む前に、これらがパフォーマンスに敏感なシステムの一部であることに注意することが重要です。 少しくだけた言い方になることをお許しください。私は、デバッグにかなりの時間を費やさなければならないと イライラします。デバッガーを待たされるとなると、腹が立ちます。

これらのビジュアライザーで費やされる 1 ミリ秒は、ユーザーが出力を見るまでに 1 ミリ秒余分にかかるということです。 これは、多数または大きなコンテナー型を含む大きなスタックフレームでは特に苦痛になり得ます。 VSCode などのデバッガー GUI はスタックフレーム全体を一度に要求するため、そのフレーム内の 任意の変数を操作できるようになるまでに、数十秒、場合によっては数分の遅延が発生することがあります。

Python コードを最適化するという考えに難色を示す傾向がありますが、実際には大きな影響を与えることがあります。 覚えておいてください。コードを高速に保つのを助けてくれるコンパイラーは存在しません。単純な変換でさえ 自動では行われません。Python を最適化する必要はないと勧める人々の雑音の中から Python のパフォーマンスに関するヒントを見つけるのは難しい場合があるため、これらのスクリプトに関連して 覚えておくべきことをいくつか示します。

  • すべてが割り当てを行います。int でさえそうです
  • 可能な場合はタプルを使用してください。list は実質的に Vec<Box<[Any]>> ですが、タプルは Box<[Any]> に相当します。タプルは間接参照の層が 1 つ少なく、余分な容量を持たず、 伸縮できないため、多くの場合に有利です。追加の利点として、Python はサイズ 20 までの すべてのタプルの基盤となる割り当てをキャッシュし、再利用します。
  • 正規表現は遅いため、単純な文字列操作で済む場合は避けるべきです
  • 文字列はイミュータブルであるため、多くの文字列操作は暗黙的に内容をコピーします。
  • 文字列の大きなリストを連結する場合、通常は "".join(iterable_of_strings) が最速の 方法です。
  • f-string は通常、文字列を括弧で囲むなどの小さく単純な文字列変換を行う最速の方法です。
  • 関数呼び出しという行為はやや遅いです(たとえ関数が完全に空であっても)。その コード区間が非常にホットである場合は、関数を手動でインライン化することを検討してください。
  • ローカル変数アクセスは、グローバル変数や組み込み関数へのアクセスよりも大幅に高速です
  • . 演算子によるメンバー/メソッドアクセスも遅いため、深くネストした値を ローカル変数に再代入してこのコストを避けることを検討してください(例: h = a.b.c.d.e.f.g.h)。
  • 継承されたメソッドやフィールドへのアクセスは、基底クラスのメソッドやフィールドよりも約 2 倍遅いです。 可能な限り継承を避けてください。
  • 可能な限り __slots__ を使用してください。__slots__ は クラスのフィールドが変化しないことを Python に示す方法であり、フィールドアクセスを 目に見えて高速化します。これには、事前にフィールドに名前を付け、__init__ で初期化する必要がありますが、 得られる利点に対しては小さな代償です。
  • match 文や if..elif..else はいかなる形でも最適化されません。条件は順番に、 1 つずつチェックされます。可能であれば、辞書ディスパッチや値のテーブルなどの代替手段を使用してください
  • 可能な場合は遅延計算してください
  • リスト内包表記は通常ループより高速で、ジェネレーター内包表記はリスト内包表記より少し遅いですが、 使用するメモリは少なくなります。内包表記は Rust の iter.map() に相当すると考えることができます。 リスト内包表記は実質的に最後に collect::<Vec<_>> を呼び出しますが、 ジェネレーター内包表記はそうしません。

LLDB - Python プロバイダー

注: LLDB の C++<->Python FFI は、LLDB のコンパイル時に指定されたバージョンの Python を想定しています。LLDB は、このバージョンが一般的な Linux および macOS ディストリビューションの最小バージョンに対応するよう注意していますが、Windows では簡単な解決策はありません。_lldb が存在しないことに関するインポートエラーが発生した場合、Python バージョンの不一致が原因である可能性が高いです。

LLDB はこの問題の解決策を検討しています。更新については、 この議論およびこの github issueを参照してください

注: 2025 年 11 月時点で、 LLDB がサポートする Python の最小バージョンは 3.8 であり、いくつかの外部要因に応じて 3.9 または 3.10 に更新する計画があります。スクリプトは、サポートされる Python の最小バージョンで利用可能な機能のみを使用して書くのが理想です。詳細については、この議論を参照してください。

注: LLDB の Python パッケージへのパスは、CLI コマンド lldb -P で確認できます

LLDB は出力をカスタマイズするための 3 つの仕組みを提供します。

  • フォーマット
  • Synthetic Provider
  • Summary Provider

フォーマット

公式ドキュメントはこちらです。 要するに、 フォーマットを使用すると、プリミティブ型のデフォルトの出力形式を設定できます(たとえば、25u8 を 10 進数の 25、16 進数の 0x19、または 2 進数の 00011001 として出力するなど)。

Rust ではほぼ常に、unsigned charsigned charcharu8、および i8 を (符号なし)10 進数形式にオーバーライドする必要があります。

Synthetic Provider

公式ドキュメントはこちらですが、 一部の情報は曖昧であったり、古くなっていたり、完全に欠落していたりします。

ユーザーが変数と行うほぼすべてのやり取りは、LLDB の SBValue オブジェクトを通じて行われます。これは Python API の両方で使用され、また LLDB の プラグインおよび CLI を通じて内部的にも使用されます。

Synthetic Provider は、特定のインターフェイスを持つように書かれた Python クラスであり、 1 つ以上の Rust 型に関連付けられます。 Synthetic Provider は SBValue オブジェクトをラップし、LLDB は変数を検査するときに このクラスの関数を呼び出します。

ラップされた値は依然として SBValue ですが、たとえば SBValue.GetChildAtIndex を呼び出すと、 内部的には SyntheticProvider.get_child_at_index が呼び出されます。 値に synthetic provider があるかどうかは SBValue.IsSynthetic() で確認でき、どの synthetic であるかは SBValue.GetTypeSynthetic() で確認できます。 基になる非 synthetic 値を操作したい場合は、 SBValue.GetNonSyntheticValue() を呼び出すことができます。

想定されるインターフェイスは次のとおりです。

class SyntheticProvider:
    def __init__(self, valobj: SBValue, _lldb_internal): ...

    # 任意
    def update(self) -> bool: ...

    # 任意
    def has_children(self) -> bool: ...

    # 任意
    def num_children(self, max_children: int) -> int: ...

    def get_child_index(self, name: str) -> int: ...

    def get_child_at_index(self, index: int) -> SBValue: ...

    # 任意
    def get_type_name(self) -> str: ...

    # 任意
    def get_value(self) -> SBValue: ...

以下では、各メソッド、その癖、および一般的な使用方法について説明します。 メソッドが SBValue メソッドをオーバーライドする場合、そのメソッドを示します。

__init__

この関数はオブジェクトごとに 1 回呼び出され、valobj を Python クラス内に保存して、 他の場所からアクセスできるようにしなければなりません。 ここではそれ以外のことはほとんど行うべきではありません。

(任意)update

この関数は、LLDB が変数を操作する前、ただし __init__ の後に呼び出されます。 LLDB は、update がすでに呼び出されたかどうかを追跡します。 すでに呼び出されていて、その変数が変更された可能性がない場合 (たとえば、ステップ実行せずに同じ変数を 2 回目に検査する場合)、update の呼び出しは省かれます。

この関数には 2 つの目的があります。

  • 前回 update が実行されてから変更された可能性のある情報を保存/更新する
  • 子に変更があり、子キャッシュをフラッシュすべきであることを LLDB に通知する。

典型的な操作には、Vec のヒープポインター、長さ、容量、要素型を保存すること、 enum 変数のバリアントを判定すること、または HashMap のどのスロットが使用されているかを確認することが含まれます。

この関数から返される bool はやや複雑です。詳細については、以下の update のキャッシュを参照してください。 迷った場合は、False/None を返してください。

2025 年 11 月時点で、

いずれのビジュアライザーも True を返していませんが、デバッグ情報 テストスイートが改善されるにつれて変わる可能性があります。

update のキャッシュ

LLDB は、子の値を含め、可能な場合には値をキャッシュしようとします。 このキャッシュは実質的に、 子オブジェクトの数と、その子オブジェクトが表す基になるデバッグ対象メモリのアドレスです。 True を返すことで、子の数とそれらの子のアドレスが 前回 update が実行されてから変更されていないことを LLDB に示し、キャッシュ済みの子を再利用できることを意味します。

不適切な状況で True を返すと、デバッガーが誤った情報を出力する結果になります

False を返すと、変更があったことを示し、キャッシュはフラッシュされ、 子は最初から取得されます。 不確かな場合はこちらのほうが安全な選択肢です。

重要なのは親から子への関係のみです。 孫は祖父母ではなく、直接の親の update 関数に依存します。

子キャッシュはメモリへのポインターとして見ることが重要です。 たとえば、スライスの data_ptr 値と length が変更されていない場合、True を返すのが適切です。 スライスがミュータブルで、 その要素が上書きされた場合(たとえば slice[0] = 15)であっても、子キャッシュは ポインターで構成されているため、そのメモリ位置にある新しいデータを反映します。

逆に、data_ptr が変更された場合、それはメモリ内の新しい場所を指していることを意味し、 子ポインターは無効であり、キャッシュをフラッシュしなければなりません。 length が変更された場合は、新しい子の数を反映するためにキャッシュをフラッシュする必要があります。 length が変更されたが data_ptr は 変更されていない場合、古い子を SyntheticProvider 自体に保存しておき(たとえば list[SBValue])、それらを最初から生成する代わりに配ることが可能です。新しい子は SyntheticProvider のリストにまだ存在しない場合にのみ作成します。

さらに明確にするには、この議論を参照してください

注: キャッシュ動作をテストする際は、ステップ実行時に変数を永続化する LLDB のヒューリスティックに依存しないでください。 代わりに、変数を Python オブジェクトに保存し(たとえば v = lldb.frame.var("var_name"))、 前方にステップ実行してから、保存した変数を検査してください。

(任意)has_children

SBValue.MightHaveChildren をオーバーライドします これは、値が子をまったく持つかどうかを、子の数を判定するための潜在的に高コストな計算(例: 連結リスト)を行わずに確認するために LLDB が使用するショートカットです。 多くの場合、これは return True/return False、または return self.valobj.MightHaveChildren() の 1 行になります。

(任意)num_children

SBValue.GetNumChildren をオーバーライドします

型を表示する際に LLDB がアクセスを試みるべき子の総数を返します。 この数は、合成された子の総数と一致している必要はありません

子の数を計算するのにコストがかかる可能性がある場合(例: 連結リスト)は、max_children 引数を返すことができます。 これを考慮する必要がない場合は、関数シグネチャから max_children を省略できます。

さらに、フィールドをユーザーからは引き続きアクセス可能にしたまま、LLDB から意図的に「非表示」にできます。 たとえば、vec![1, 2, 3] では要素だけを表示したい一方で、lencapacity の値は要求に応じてアクセス可能にしておきたい場合があります。 num_children から 3 を返すことで、 LLDB に [1, 2, 3] のみを表示するよう制限しつつ、ユーザーは引き続き v.lenv.capacity に直接アクセスできます。 この実装例については、Example Provider: Vec<T> を参照してください。

get_child_index

SBValue.GetIndexOfChildWithName をオーバーライドします

SBValue.GetChildMemberWithName に影響します

名前を指定すると、その子にアクセスすべきインデックスを返します。 この関数の戻り値は get_child_at_index に直接渡されることが想定されています。 num_children と同様に、ここで返される値は、get_child_at_index と適切に連携している限り、任意の値にすることができます

特別な値の 1 つに $$dereference$$ があります。 この疑似フィールドを考慮すると、LLDB は get_child_at_index から返された SBValue を、LLDB の式パーサーによる参照外しの結果として使用できるようになります (例: *val および val->field)。

get_child_at_index

SBValue.GetChildAtIndex をオーバーライドします

インデックスを指定すると、子の SBValue を返します。 多くの場合、これらは SBValue.CreateValueFromAddress によって生成されますが、それほど一般的ではないものとして SBValue.CreateChildAtOffsetSBValue.CreateValueFromExpressionSBValue.CreateValueFromData もあります。 これらの関数はやや扱いづらいことがあるため、目的の出力を得るには調整が必要になる場合があります。

場合によっては、SBValue.Clone が適切です。 これは既存の子の完全なコピーでありながら、新しい名前を持つ新しい子を作成します。 これは、タプルのようにフィールド名が __0__1、… という形式であるものを、01、… という名前にしたい場合に便利です。

返す前に、結果の子に小さな変更を加えることができます。 これは &str/String の場合に便利で、子を単なる 10 進値としてではなく、 lldb.eFormatBytesWithASCII として表示したい場合があります。

(任意)get_type_name

SBValue.GetDisplayTypeName をオーバーライドします

型の表示名をオーバーライドします。 型名がオーバーライドされた合成 SBValue についても、 元の型名は SBValue.GetTypeName() および SBValue.GetType().GetName() で引き続き取得できます。

これは、一般的な標準ライブラリ型の名前を短縮する場合(例: std::collections::hash::map::HashMap<K, V, std::hash::random::RandomState> -> HashMap<K, V>)や、 MSVC の型名を正規化する場合(例: ref$<str$> -> &str)に役立ちます。

文字列操作は少し難しい場合があり、特に型のジェネリックパラメーターへ簡単にアクセスできない MSVC ではそうです。

(任意)get_value

SBValue.GetValue()SBValue.GetValueAsUnsigned()SBValue.GetValueAsSigned()SBValue.GetValueAsAddress() をオーバーライドします

返される SBValue はプリミティブ型またはポインターであることが想定され、式の中で変数の値として扱われます。

重要: 返される SBValueSyntheticProvider に保存されていなければなりません

2025年11月時点では、

SBValueget_value 内で取得され、どこにも保存されていない場合、 LLDB がその値へアクセスしようとすると Python がセグフォルトするバグがあります。

サマリープロバイダー

サマリープロバイダーは、次の形式の Python 関数です。

def SummaryProvider(valobj: SBValue, _lldb_internal) -> str: ...

ここで、返された文字列はそのままユーザーに渡されます。 返された値が文字列でない場合、それは単純に文字列へ変換されます(例: return None は空文字列ではなく "None" を出力します)。

渡された SBValue が合成プロバイダーを持つ型である場合、valobj.IsSynthetic()True を返し、その合成に対応する関数が使用されます。 これが望ましくない場合は、valobj.GetNonSyntheticValue() によって元の値を取得できます。 これは String のようなケースで役立ちます。この場合、ループ内で GetChildAtIndex を個別に呼び出すよりも、 ヒープポインターにアクセスして、デバッグ対象のメモリからバイト配列全体を直接読み取り、 Python の bytes.decode() を使用する方がはるかに高速です。

インスタンスサマリー

通常の SummaryProvider 関数は不透明な SBValue を受け取ります。 その SBValue は、存在する場合は型の SyntheticProvider を反映しますが、SyntheticProvider インスタンス自体や、 その内部実装の詳細にはアクセスできません。 これは、サマリーを完成させるためにそうした内部詳細の一部が必要な場合に不利です。

2025年11月時点では、合成内で非合成値を合成プロバイダーに通しているだけです

synth = SyntheticProvider(valobj.GetNonSyntheticValue(), _dict))が、これは明らかに最適ではなく、 以下に示す方法を使用する計画があります。

代わりに、Python モジュールの状態を活用してインスタンスサマリーを実現できます。 この手法の先行例は、古い CodeLLDB Rust ビジュアライザースクリプト にあります。

要するに、すべての合成プロバイダーの __init__ 関数は、一意な ID と self への弱参照をグローバル辞書に保存します。 合成プロバイダークラスは get_summary 関数も実装します。 型の SummaryProvider は、この辞書で一意な ID を検索し、 取得したインスタンス上の get_summary を呼び出す関数です。

import weakref

SYNTH_BY_ID = weakref.WeakValueDictionary()

class SyntheticProvider:
    valobj: SBValue

    # slots では __weakref__ へのオプトインが必要
    __slots__ = ("valobj", "__weakref__")

    def __init__(valobj: SBValue, _dict):
        SYNTH_BY_ID[valobj.GetID()] = self
        self.valobj = valobj

    def get_summary(self) -> str:
        ...

def InstanceSummaryProvider(valobj: SBValue, _dict) -> str:
    # InstanceSummaryProvider は `SyntheticProvider` のインスタンスを前提とするため、
    # GetNonSyntheticValue が失敗することは決してありません。非合成型にこのサマリーが
    # 割り当てられることは決してないはずです
    # 合成 vaobj は固有の ID を持つため、GetNonSyntheticValue を使用します
    return SYNTH_BY_ID[valobj.GetNonSyntheticValue().GetID()].get_summary()

たとえば、これを Enum の合成プロバイダーに使用できます。 サマリーは バリアント名にアクセスしたいところですが、synthetic の型名や子の値を介してこれを反映する便利な方法はありません。 インスタンスサマリーを実装することで、 self.variant.GetTypeName() といくつかの文字列操作を介してバリアント名を取得できます。

ビジュアライザースクリプトの作成

重要: GDB や CDB とは異なり、LLDB は DWARF または PDB デバッグ情報のいずれかを持つ実行可能ファイルをデバッグできます。 ビジュアライザーは、可能な限り両方の形式に対応するように記述する必要があります。違いの概要については、次を参照してください: rust-codegen

スクリプトは CLI コマンド command script import <path-to-script>.py によって LLDB に注入されます。 注入されると、 クラスと関数はそれぞれ type synthetic add および type summary add で synthetic/summary プールに追加できます。 サマリーと synthetic は 「カテゴリー」に関連付けることができ、これは通常、プロバイダーが対象とする言語にちなんで名付けられます。 使用するカテゴリーは Rust と呼ばれます。

ヒント: すべての LLDB コマンドには、簡潔な説明、引数の一覧、および例を表示するために help を前置できます(例: help type synthetic add)。

2025 年 11 月現在、

プロバイダーを追加するために、一連の CLI コマンドを ファイル lldb_commands から実行する command source ... を使用しています。 このファイルはやや扱いにくく、まもなく以下で概説する Python API 相当のものに置き換えられる予定です。

__lldb_init_module

これは次の形式の任意の関数です:

def __lldb_init_module(debugger: SBDebugger, _lldb_internal) -> None: ...

この関数は command script import ... の最後、ただし制御が CLI に戻る前に呼び出されます。 これにより、スクリプトは自身の状態を初期化できます。

重要なのは、この関数にはデバッガー自体への参照が渡されることです。 これにより、Rust カテゴリーを作成し、それにプロバイダーを追加できます。 また、スクリプトが検出した LLDB のバージョンに応じて、 使用するプロバイダーを条件付きで変更することもできます。 recognizer 関数の使用を開始すると、recognizer は lldb 19.0 で追加されたため、これは後方互換性に不可欠です。

ビジュアライザーの解決

ビジュアライザーが解決される順序は こちら に記載されています。 簡単に言うと、次のとおりです:

  • 完全一致(正規表現ではない名前、recognizer 関数、またはプロバイダーにすでにマッチ済みの型)がある場合は、 それを使用する
  • オブジェクトがポインター/参照の場合、逆参照された型のフォーマッターを使用しようとする
  • オブジェクトが typedef の場合、基になる型にフォーマッターがあるか確認する
  • 上記のいずれもうまくいかない場合、正規表現の型マッチャーを順に処理する

これらの各ステップ内では、新しいコマンドが古いコマンドを「上書き」できるように、反復は逆順で行われます。 これは、Box<str>Box<T> のようなケースで重要です。前者には特化した synthetic が必要ですが、後者にはより一般化された synthetic が必要だからです。

細かな事項

LLDB の API は非常に強力ですが、いくつかの「落とし穴」や直感的でない挙動があり、その一部を 以下で概説します。 Python 実装は、CLI コマンド lldb -P によって返されるパスの lldb\__init__.py で確認できます。 lldb リポジトリの例 に加えて、参照として使用できる C++ ビジュアライザー もあります (例: Vec<T> に相当する LibCxxVector)。C++ のビジュアライザーは C++ で書かれており、LLDB の内部へアクセスできますが、API と一般的な実践は非常によく似ています。

SBValue

  • ポインター/参照の SBValue は、一部のケースでは実質的に「自動逆参照」され、 指し先オブジェクトの子要素が自身の子要素であるかのように振る舞います。
  • 関数でないフィールドは通常、結局は関数を直接指す property() フィールドです (例: SBValue.type = property(GetType, None))。これらの省略記法経由のアクセスは、 関数を直接呼び出すよりも少し遅いため、避けるべきです。 一部のプロパティは、特殊な性質を持つ特殊なオブジェクトを返します(例: SBValue.member は、 子要素にアクセスするために dict[str, SBValue] のように振る舞うオブジェクトを返します)。 内部的には、これらの特殊なオブジェクトの多くは新しいクラスインスタンスを割り当て、 結局 SBValue 上で関数を呼び出すだけであり、追加の性能低下を招きます (例: SBValue.member は内部的には __getitem__ を実装しているだけで、 これは 1 行の return self.valobj.GetChildMemberWithName(name) です)
  • SBValue.GetID は、デバッグセッションの間、各値に対して一意な int を返します。 合成 SBValue は、その基になる SBValue とは異なる ID を持ちます。 基になる ID は SBValue.GetNonSyntheticValue().GetID() によって取得できます。
  • アドレスを手動で計算する場合、ターゲット固有の挙動 のため、 SBValue.GetValueAsUnsigned よりも SBValue.GetValueAsAddress を優先して使用すべきです。
  • SBValue の文字列表現を取得するのは難しい場合があります。なぜなら GetSummary は サマリープロバイダーを必要とし、GetValue は型がプリミティブで表現可能であることを必要とするからです。 これらの条件のいずれも満たされないほぼすべての場合、その型は StructSummaryProvider に渡せるユーザー定義構造体です。

SBType

  • 「集約型」とは、プリミティブでない構造体/クラス/共用体を意味します
  • 「Template」は「Generic」と同等です
  • 型は SBTarget.FindFirstType(type_name) によって名前で検索できます。 SBTargetSBValue.GetTarget によって取得できます
  • SBType.template_args は、型にジェネリックがない場合、空リストではなく None を返します
  • SBType.GetArrayTypeSBType.GetPointerType のような関数を介して、 型を必要な型に変換することが必要になる場合があります。 これらの関数が失敗することはありません。 これらは基になる LLDB の TypeSystem プラグインに型を要求し、デバッグ情報を完全に迂回します。 その型がデバッグ情報にまったく存在しない場合でも、これらの関数は適切な型を作成できます。
  • SBType.GetCanonicalType は実質的に SBType.GetTypedefedType + SBType.GetUnqualifiedType です。 SBType.GetTypedefedType とは異なり、元の SBType が typedef であるかどうかに関係なく、 常に有効な SBType を返します。
  • SBType.GetStaticFieldWithName は LLDB 18 で追加されました。残念ながら、静的フィールドは それ以外の方法では完全にアクセス不能であるため、後方互換性を常に実現できるとは限りません。

プロバイダーの例: Vec<T>

SyntheticProvider

典型的な前置きから始めます。既知のフィールドがあるため、__slots__ を使用します。 オブジェクト自体に加えて、Vec のヒープポインターは *mut T ではなく *mut u8 であるため、要素の型も格納する必要があります。 Rust は静的型付け言語なので、T の型は決して変わりません。 つまり、初期化時にそれを格納できます。 ただし、ヒープポインター、長さ、容量は変化する可能性があるため、ここではデフォルト初期化しています。

import lldb

class VecSyntheticProvider:
    valobj: SBValue
    data_ptr: SBValue
    len: int
    cap: int
    element_type: SBType

    __slots__ = (
        "valobj",
        "data_ptr",
        "len",
        "cap",
        "element_type",
        "__weakref__",
    )

    def __init__(valobj: SBValue, _dict) -> None:
        self.valobj = valobj
        # 無効な型は `None` よりも優れたデフォルトです
        self.element_type = SBType()

        # DWARF/PDB の違いを考慮するための特別な処理
        if (arg := valobj.GetType().GetTemplateArgumentType(0)):
            self.element_type = arg
        else:
            arg_name = next(get_template_args(valobj.GetTypeName()))
            self.element_type = resolve_msvc_template_arg(arg_name, valobj.GetTarget())

get_template_argsresolve_msvc_template_arg の実装については、以下を参照してください: lldb_providers.py

次に、update 関数です。 ポインターまたは長さが変わったかどうかを確認します。 子の数は len が変わらない限り同じままなので、容量の確認は省略できます。 容量の変更によって再割り当てが発生した場合、data_ptr のアドレスは異なるものになります。

data_ptrlength が変わっていない場合、LLDB のキャッシュを活用して早期に返ることができます。 変わっている場合は、新しい値を格納し、LLDB にキャッシュをフラッシュするよう伝えます。

def update(self):
    ptr = self.valobj.GetChildMemberWithName("data_ptr")
    len = self.valobj.GetChildMemberWithName("length").GetValueAsUnsigned()

    if (
        self.data_ptr.GetValueAsAddress() == ptr.GetValueAsAddress()
        and self.len == len
    ):
        # 子のアドレスオフセットと子の数はまだ有効です
        # そのため、キャッシュされた子を再利用できます
        return True

    self.data_ptr = ptr
    self.len = len

    return False

has_childrennum_children はどちらも単純です:

def has_children(self) -> bool:
    return True

def num_children(self) -> int:
    return self.len

要素にアクセスする際は、インデックス指定を模倣するために [0][1] などの形式の値を想定します。 さらに、デバッグ時に非常に役立つ可能性があるため、ユーザーが長さと容量にも素早くアクセスできるようにしたいです。 これらの値にはそれぞれ u32::MAX - 1u32::MAX - 2 を割り当てます。 これは、それらが要素の値と重複しないことをほぼ確実に保証できるためです。 完全な capacity 名と省略形の両方を扱えることに注意してください。

    def get_child_index(self, name: str) -> int:
        index = name.lstrip("[").rstrip("]")
        if index.isdigit():
            return int(index)
        if name == "len":
            return lldb.UINT32_MAX - 1
        if name == "cap" or name == "capacity":
            return lldb.UINT32_MAX - 2

        return -1

次に、要素、長さ、容量のすべてにアクセスできるように、get_child_at_index を適切に連携させる必要があります。

def get_child_at_index(self, index: int) -> SBValue:
    if index == UINT32_MAX - 1:
        return self.valobj.GetChildMemberWithName("len")
    if index == UINT32_MAX - 2:
        return (
            self.valobj.GetChildMemberWithName("buf")
            .GetChildMemberWithName("inner")
            .GetChildMemberWithName("cap")
            .GetChildAtIndex(0)
            .Clone("capacity")
        )
    addr = self.data_ptr.GetValueAsAddress()
    addr += index * self.element_type.GetByteSize()
    return self.valobj.CreateValueFromAddress(f"[{index}]", addr, self.element_type)

型の表示名については、パス修飾子を取り除くことができます。 Vec という名前のユーザー定義型は完全修飾されるため、曖昧さはないはずです。 また、アロケーターのジェネリックも削除できます。これはほとんど役に立つことがないためです。 self.element_type.GetName() ではなく get_template_args を使用する理由は 3 つあります:

  1. 何らかの理由で要素型の解決に失敗した場合でも、self.valobj の型名によって、 ユーザーは要素の実際の型を知ることができます
  2. 型名は DWARF および PDB ノードの制限を受けないため、名前内のテンプレート型には *const/*mut&/&mut のようなものが反映されます。
  3. 2025 年 11 月時点では、

MSVC の型名を正規化していませんが、いったん正規化するようになれば、いずれにせよ型の 文字列名を扱う必要があります。 また、SBType から文字列への変換よりも、 文字列から文字列への変換をキャッシュする方がはるかに簡単です。

def get_type_name(self) -> str:
    return f"Vec<{next(get_template_args(self.valobj))}>"

Vec を表すのに適したプリミティブ値はないため、単に get_value 関数は省略します。

SummaryProvider

summary provider は、synthetic provider のおかげで非常に単純です。 唯一の実質的な問題は、 GetSummary はオブジェクトの型に SummaryProvider がある場合にのみ値を返すことです。 ない場合は空文字列を返しますが、これは理想的ではありません。 完全な visualizer スクリプトのセットでは、 GetSummary() または GetValue() を持たないすべての型が構造体であることを保証し、その後 汎用の StructSummaryProvider に委譲できます。 このデモでは、その詳細は軽く流します。

def VecSummaryProvider(valobj: SBValue, _lldb_internal) -> str:
    children = []
    for i in range(valobj.GetNumChildren()):
        child = valobj.GetChildAtIndex(i)
        summary = child.GetSummary()
        if summary is None:
            summary = child.GetValue()
            if summary is None:
                summary = "{...}"

        children.append(summary)

    return f"vec![{", ".join(children)}]"

プロバイダーの有効化

この synthetic が lldb_lookup.py にインポートされていると仮定します

CLI コマンドを使用する場合:

type synthetic add -l lldb_lookup.synthetic_lookup -x "^(alloc::([a-z_]+::)+)Vec<.+>$" --category Rust
type summary add -F lldb_lookup.summary_lookup -x "^(alloc::([a-z_]+::)+)Vec<.+>$" --category Rust

__lldb_init_module を使用する場合:

def __lldb_init_module(debugger: SBDebugger, _dict: LLDBOpaque):
    # カテゴリが存在し、有効化されていることを確認する
    rust_cat = debugger.GetCategory("Rust")

    if not rust_cat.IsValid():
        rust_cat = debugger.CreateCategory("Rust")

    rust_cat.SetEnabled(True)

    # Vec プロバイダーを登録する
    vec_regex = r"^(alloc::([a-z_]+::)+)Vec<.+>$"
    sb_name = lldb.SBTypeNameSpecifier(vec_regex, is_regex=True)

    sb_synth = lldb.SBTypeSynthetic.CreateWithClassName("lldb_lookup.VecSyntheticProvider")
    sb_synth.SetOptions(lldb.eTypeOptionCascade)

    sb_summary = lldb.SBTypeSummary.CreateWithFunctionName("lldb_lookup.VecSummaryProvider")
    sb_summary.SetOptions(lldb.eTypeOptionCascade)

    rust_cat.AddTypeSynthetic(sb_name, sb_synth)
    rust_cat.AddSummary(sb_name, sb_summary)

出力

プロバイダーなし:

(lldb) v vec_v
(alloc::vec::Vec<int, alloc::alloc::Global>) vec_v = {
  buf = {
    inner = {
      ptr = {
        pointer = (pointer = "\n")
        _marker = {}
      }
      cap = (__0 = 5)
      alloc = {}
    }
    _marker = {}
  }
  len = 5
}
(lldb) v vec_v[0]
error: <user expression 0>:1:6: subscripted value is not an array or pointer
   1 | vec_v[0]
     | ^

プロバイダーあり(v <var_name> はサマリーを出力し、その後にすべての子のリストを出力します):

(lldb) v vec_v
(Vec<int>) vec_v = vec![10, 20, 30, 40, 50] {
  [0] = 10
  [1] = 20
  [2] = 30
  [3] = 40
  [4] = 50
}
(lldb) v vec_v[0]
(int) vec_v[0] = 10

「隠れた」長さと容量に引き続きアクセスできることも確認できます:

(lldb) v vec_v.len
(unsigned long long) vec_v.len = 5
(lldb) v vec_v.capacity
(unsigned long long) vec_v.capacity = 5
(lldb) v vec_v.cap
(unsigned long long) vec_v.cap = 5

(WIP) GDB - Python プロバイダー

以下は、GDB ドキュメントの関連部分へのリンクです

(WIP) CDB - Natvis

Natvis の公式ドキュメントは、こちらこちらにあります。

(WIP) テスト

デバッグ情報のテストスイートは大規模な書き換えが進行中です。このセクションは、 書き換えの進捗に応じて記入されます。

詳細については、このトラッキング issueを参照してください。

Rust コンパイラにおけるデバッグサポート

このドキュメントでは、Rust コンパイラ(rustc)におけるデバッグツールサポートの状況について説明します。 GDB、LLDB、WinDbg/CDB の概要に加えて、 Rust コードをデバッグするための Rust コンパイラ周辺のインフラストラクチャについても概説します。 Rust コンパイラ自体をデバッグする方法を学びたい場合は、 Debugging the Compiler を参照してください。

この資料は動画 Tom Tromey discusses debugging support in rustc からまとめたものです。

前提知識

デバッガー

Wikipedia によると、

debugger or debugging tool とは、他のプログラム(「ターゲット」プログラム)をテストおよびデバッグするために使用されるコンピュータープログラムです。

ある言語向けにデバッガーをゼロから作成するには、特に さまざまなプラットフォームでデバッガーをサポートする必要がある場合、多くの作業が必要です。 しかし、GDB と LLDB は、ある言語のデバッグをサポートするように拡張できます。 これが Rust が選択した道です。 このドキュメントの主な目的は、Rust コンパイラにおける前述のデバッガーサポートを文書化することです。

DWARF

DWARF 標準の Web サイトによると、

DWARF は、多くのコンパイラやデバッガーがソースレベル デバッグをサポートするために使用するデバッグファイル形式です。C、C++、Fortran など、 多数の手続き型言語の要件に対応しており、他の言語へ拡張できるように設計されています。 DWARF はアーキテクチャ非依存であり、任意のプロセッサーやオペレーティングシステムに適用できます。 Unix、Linux、その他のオペレーティングシステムで広く使用されているほか、 スタンドアロン環境でも使用されています。

DWARF リーダーは、DWARF 形式を取り込み、デバッガー互換の出力を作成するプログラムです。 このプログラムはコンパイラ自体の内部に存在する場合があります。 DWARF は、Debugging Information Entry(DIE)と呼ばれるデータ構造を使用します。これは、関数、 変数などを表すために、情報を「タグ」として格納します。たとえば、DW_TAG_variableDW_TAG_pointer_typeDW_TAG_subprogram などです。 独自のタグや属性を考案することもできます。

CodeView/PDB

PDB(Program Database)は、Microsoft が作成した、デバッグ情報を含むファイル形式です。 PDB は、WinDbg/CDB などのデバッガーやその他のツールによって取り込まれ、デバッグ情報の表示に使用されます。 PDB には、特定のバイナリに関するデバッグ情報を記述する複数のストリームが含まれます。 たとえば、型、シンボル、指定されたバイナリのコンパイルに使用されたソースファイルなどです。 CodeView は、PDB ストリーム内に現れる symbol recordstype records の構造を定義する別の 形式です。

サポートされているデバッガー

GDB

Rust 式パーサー

デバッグ出力を表示できるようにするには、式パーサーが必要です。 この(GDB の)式パーサーは Bison で書かれており、 Rust 式のサブセットのみを解析できます。 GDB パーサーはゼロから書かれており、rustc のパーサーを含む 他のどのパーサーとも関係がありません。

GDB には Rust 風の値と型の出力があります。 出力内で Rust 構文のように見える形で、値や型を表示できます。 また、GDB で ptype として型を表示すると、 それも Rust のソースコードのように見えます。 manual for GDB/Rust のドキュメントを確認してください。

パーサー拡張

式パーサーには、Rust ではできない機能を容易にするための拡張がいくつか含まれています。 いくつかの制限事項は manual for GDB/Rust に記載されています。 これらの拡張をサポートするために、GDB の DWARF リーダーには特別なコードがいくらかあります。

必要となる DWARF リーダーサポートの例をいくつか以下に示します。

  1. 列挙型: 列挙型のサポートに必要です。 Rust コンパイラは列挙型に関する情報を DWARF に書き込み、 GDB は DWARF を読み取って、タグフィールドがどこにあるのか、 あるいはタグフィールドが存在するのか、 あるいはタグスロットが非ゼロ最適化などと共有されているのかを理解します。

  2. トレイトオブジェクトの分解: DWARF 拡張であり、DWARF 内のトレイトオブジェクトの記述が、 対応する vtable のスタブ記述も指し、そのスタブ記述がさらに このトレイトオブジェクトの対象である具体型を指します。 つまり、そのトレイトオブジェクトに対して print *object を実行でき、 GDB はトレイトオブジェクト内のペイロードの正しい型を見つける方法を理解します。

TODO: 重複が生じないように、以下をこのガイドページではなく GDB-Rust ドキュメントで言及すべきかどうかを確認する。 これは以下のコメントに関するものです。

This comment by Tom

gdb の Rust 拡張と制限事項は gdb マニュアルに記載されています: https://sourceware.org/gdb/onlinedocs/gdb/Rust.html – ただし、ここでは gdb の convenience variables と registers が gdb の $ 規約に従うこと、および Rust パーサーが gdb の @ 拡張を実装していることには触れていません。

This question by Aman

@tromey、この部分は重複などが生じないように、この ドキュメントではなく GDB-Rust ドキュメントで言及すべきだと思いますか?

LLDB

Rust 式パーサー

この式パーサーは C++ で書かれています。 これは Recursive Descent parser の一種です。 GDB よりも少し少ない範囲の Rust 言語を実装しています。 LLDB には Rust 風の値と型の出力があります。

開発者向けメモ

  • LLDB にはプラグインアーキテクチャがありますが、言語サポートには機能しません。
  • GDB は一般に Linux 上でよりうまく動作します。

WinDbg/CDB

Microsoft は、Windows Debugger(WinDbg)や Console Debugger(CDB)などの Windows Debugging Tools を提供しており、どちらも Rust で書かれたプログラムのデバッグをサポートしています。 これらのデバッガーは、利用可能な場合、バイナリのデバッグ情報を PDB から解析し、 デバッガー内で提供するための可視化を構築します。

Natvis

WinDbg と CDB はどちらも、Natvis フレームワークを使用して、 デバッガー内で任意の型に対するカスタム可視化を定義および表示することをサポートしています。 Rust コンパイラは、標準ライブラリ内の型のサブセットに対するカスタム可視化を定義する一連の Natvis ファイルを定義しています。 たとえば、stdcorealloc などです。 これらの Natvis ファイルは、 *-pc-windows-msvc ターゲットトリプルによって生成される PDBs に埋め込まれ、 デバッグ時にこれらのカスタム可視化を自動的に有効化します。 このデフォルトは、strip rustc フラグを debuginfo または symbols のいずれかに設定することで上書きできます。

Rust は、標準ライブラリ外のクレートについて Natvis ファイルを埋め込むことを、 #[debugger_visualizer] 属性を使用してサポートしています。 デバッガー可視化機能を埋め込む方法の詳細については、 debugger_visualizer attribute に関するセクションを参照してください。

DWARF と rustc

DWARF は、デバッガーが読み取るデバッグ情報をコンパイラが生成する標準的な方法です。 これは macOS と Linux における the デバッグ形式です。 複数言語に対応した拡張可能な形式であり、 Rust の目的にはおおむね十分です。 したがって、現在の実装では DWARF の概念を再利用しています。 DWARF の一部の概念が Rust と意味論的に一致していない場合でも、これは当てはまります。なぜなら、 一般に、両者の間には何らかのマッピングが可能だからです。

Rust コンパイラが出力し、デバッガーが理解する DWARF 拡張がいくつかありますが、 それらは DWARF 標準には 含まれていません

  • Rust コンパイラは仮想テーブル用の DWARF を出力し、この vtable オブジェクトは実際の型を指す DW_AT_containing_type を持ちます。 これにより、デバッガーはトレイトオブジェクトポインターを分解して、ペイロードを正しく見つけることができます。 以下は、gdb リポジトリのテストケースに含まれる、そのような DIE の例です。

    <1><1a9>: Abbrev Number: 3 (DW_TAG_structure_type)
       <1aa>   DW_AT_containing_type: <0x1b4>
       <1ae>   DW_AT_name        : (indirect string, offset: 0x23d): vtable
       <1b2>   DW_AT_byte_size   : 0
       <1b3>   DW_AT_alignment   : 8
    
  • もう 1 つの拡張は、Rust コンパイラがタグなし判別共用体を出力できることです。 この項目については DWARF feature request を参照してください。

DWARF の現在の制限

  • トレイト - DWARF でトレイトを表現する方法について、通常よりも大きな DWARF への変更が必要です。
  • DWARF には構造体とタプルを区別する方法がありません。 Rust コンパイラは __0 を持つフィールドを出力し、デバッガーはこの制限を回避するためにそのような名前のシーケンスを探します。 たとえば、この場合、デバッガーは x.0 ではなく x.__0 を介してフィールドを参照します。 これはデバッガー内の Rust パーサーによって解決されるため、現在は x.0 を使用できます。

DWARF は、デバッガーがプラットフォーム ABI に関する一部の情報を知っていることに依存しています。 Rust は常にそうしているわけではありません。

開発者向けメモ

このセクションは、開発の特定の側面に関する講演からのものです。

不足しているもの

macOS 上の LLDB デバッグサーバーのコード署名

Wikipedia によると、System Integrity Protection は次のとおりです。

System Integrity Protection(SIP、rootless と呼ばれることもある)は、OS X El Capitan で導入された Apple の macOS オペレーティングシステムのセキュリティ機能です。これは、カーネルによって適用される 多数のメカニズムで構成されています。中心的な機能は、特定の「entitlement」を持たないプロセスによる システム所有のファイルやディレクトリへの変更を防ぐことであり、root ユーザーまたは root 権限(sudo)を持つユーザーとして 実行されている場合でも保護されます。

これは、プロセスが ptrace システムコールを使用することを防ぎます。 プロセスが ptrace を使用したい場合は、コード署名されている必要があります。 それに署名する証明書は、そのマシン上で信頼されている必要があります。

Apple developer documentation for System Integrity Protection を参照してください。

この署名を行うために、Apple に登録して鍵を取得する必要があるかもしれません。 Tom は、Mozilla が署名を許可されている鍵の最大数に達しているため、これを実行できないのではないかと調査しました。 Tom は、Mozilla がさらに鍵を取得できるかどうかを知りません。

あるいは、Tom は、Apple 経由で鍵を取得するために Rust の法人が必要かもしれないと示唆しています。 この問題は技術的な性質のものではありません。 そのような鍵があれば、GDB にも署名して、それを配布できます。

DWARF とトレイト

Rust のトレイトは DWARF にはまったく出力されません。 この影響として、メソッド x.method() の呼び出しはそのままでは機能しません。 その理由は、そのメソッドが型ではなくトレイトによって実装されているためです。 その情報が存在しないため、トレイトメソッドを見つけることができません。

DWARF にはインターフェイス型という概念があります(おそらく Java のために追加されたものです)。 Tom の考えは、このインターフェイス型をトレイトとして使用することでした。

DWARF は具象名のみを扱い、参照型は扱いません。 したがって、ある型に対するトレイトの特定の実装は、これらのインターフェイス(DW_tag_interface 型)の 1 つになります。 また、それが実装されている型は、この型が実装するすべてのインターフェイスを記述することになります。 これには DWARF 拡張が必要です。

GitHub の Issue: https://github.com/rust-lang/rust/issues/33014

デバッグ情報変更の典型的なプロセス(LLVM)

LLVM にはデバッグ情報(DI)ビルダーがあります。 これは、Rust が呼び出す主なものです。 これが、最初に LLVM を変更する必要がある理由です。なぜなら、最初に出力されるのは LLVM であり、DWARF が直接出力されるわけではないからです。 これは、構築して LLVM に引き渡す一種のメタデータです。 Rustc/LLVM の引き渡しでは、 型の表現を構築するために、いくつかの LLVM DI ビルダーメソッドが呼び出されます。

このプロセスの手順は次のとおりです。

  1. LLVM を変更する必要があります。

    LLVM はインターフェイス型をまったく出力しないため、まず LLVM でこれを実装する必要があります。

    これが良いアイデアであることについて、LLVM メンテナーの承認を得ます。

  2. DWARF 拡張を変更します。

  3. デバッガーを更新します。

    DWARF リーダー、式評価器を更新します。

  4. Rust コンパイラを更新します。

    この新しい情報を出力するように変更します。

手続きマクロのステップ実行

非常に根本的な問題は、実際に手続きマクロをどのようにデバッグするのか、ということです。 マクロ展開に対して、どの位置を出力するのでしょうか。 次のようないくつかのケースを考えてみます -

  • マクロの呼び出し位置を出力できます。
  • マクロの定義位置を出力できます。
  • マクロの内容の位置を出力できます。

RFC: https://github.com/rust-lang/rfcs/pull/2117

焦点は、マクロが何をするかを決定できるようにすることです。 これは、マクロがコンパイラに行マーカーをどこに置くべきかを伝えられるようにする 何らかの属性を持たせることで実現できます。 これは、ブレークポイントを設定する場所と、ステップ実行時に何が起こるかに影響します。

デバッグ情報内のソースファイルチェックサム

DWARF と CodeView(PDB)はいずれも、関連付けられたバイナリに寄与した各ソースファイルの暗号学的ハッシュを 埋め込むことをサポートしています。

暗号学的ハッシュは、ソースファイルが実行可能ファイルと一致することをデバッガーが検証するために使用できます。 ソースファイルが一致しない場合、デバッガーはユーザーに警告を提供できます。

また、ハッシュは、特定のソースファイルが実行可能ファイルのコンパイルに使用されて以降変更されていないことを証明するためにも使用できます。 MD5 と SHA1 はどちらも脆弱性が実証されているため、 この用途には SHA256 の使用が推奨されます。

Rust コンパイラは、各ソースファイルのハッシュを SourceMap 内の対応する SourceFile に保存します。 外部クレートへの入力ファイルのハッシュは、rlib メタデータに保存されます。

デフォルトのハッシュアルゴリズムは、ターゲット仕様で設定されます。 これにより、すべてのターゲットがすべてのハッシュアルゴリズムをサポートしているわけではないため、ターゲットが 利用可能な最適なハッシュを指定できます。

ターゲットのハッシュアルゴリズムは、-Z source-file-checksum= コマンドラインオプションで上書きすることもできます。

DWARF 5

DWARF バージョン 5 は、使用中のソースファイルバージョンを検証するために MD5 ハッシュを埋め込むことをサポートしています。 DWARF 5 - セクション 6.2.4.1 opcode DW_LNCT_MD5

LLVM

LLVM IR は、DIFile ノード内の MD5 および SHA1(LLVM 11 以降では SHA256 も)ソースファイルチェックサムをサポートしています。

LLVM DIFile documentation

Microsoft Visual C++ Compiler /ZH オプション

MSVC コンパイラは、/ZH コンパイラオプションを使用して、PDB 内に MD5、SHA1、または SHA256 ハッシュを埋め込むことをサポートしています。

MSVC /ZH documentation

Clang

Clang は常に MD5 チェックサムを埋め込みますが、これはドキュメントには記載されていないようです。

今後の作業

名前マングリングの変更

  • libiberty(gcc ソースツリー)内の新しいデマングラー。
  • LLVM または LLDB 内の新しいデマングラー。

TODO: デマングラーソースの場所を確認する。 #1157

式に Rust コンパイラを再利用する

これは重要な考え方です。というのも、デバッガーは概して型推論を実装しようとしないからです。 実際のソースコードよりも、デバッガーに入力するときのほうが、はるかに明示的である必要があります。 そのため、ソースコードから式をコピーしてデバッガーに貼り付け、そのまま同じ答えが得られると期待することはできませんが、そうできると便利でしょう。 これはコンパイラを使うことで助けられます。

それは確かに実現可能ですが、大きなプロジェクトです。 デバッガーへのブリッジは必ず必要になります。メモリにアクセスできるのはデバッガーだけだからです。 GDB (gcc) と LLDB (clang) のどちらにもこの機能があります。 LLDB は Clang を使ってコードを JIT コンパイルし、GDB も GCC を使って同じことができます。

両方のデバッガーの式評価は、Rust の上位集合であると同時に下位集合でもあるものを実装しています。 それらは式言語だけを実装していますが、 GDB の便宜変数のようないくつかの拡張も追加しています。 したがって、この道を進むのであれば、 このブリッジを作る必要があるだけでなく、 コンパイラが一部の拡張を理解できるようにするための何らかのモードを追加しなければならないかもしれません。

ライブラリとメタデータ

コンパイラが外部クレートへの参照を見つけると、そのクレートに関する何らかの 情報を読み込む必要があります。 この章では、そのプロセスの概要と、 クレートライブラリでサポートされているファイル形式について説明します。

ライブラリ

クレート依存関係は、rlibdylib、または rmeta ファイルから読み込めます。 これらのファイル形式の重要な点は、rustc 固有の メタデータ を含んでいることです。 このメタデータにより、コンパイラは外部クレートに含まれるアイテム、 そのクレートがエクスポートするマクロ、そしてその他多くのことを理解するために十分な 情報を発見できます。

rlib

rlibarchive file であり、tar ファイルに似ています。 このファイル形式は rustc 固有であり、時間とともに変更される可能性があります。 このファイルには次のものが含まれます。

  • オブジェクトコード。これはコード生成の結果です。 これは通常のリンク時に使用されます。 各 codegen unit ごとに個別の .o ファイルがあります。 コード生成ステップは -C linker-plugin-lto CLI オプションでスキップでき、 これは各 .o ファイルに LLVM ビットコードのみが含まれることを意味します。
  • LLVM bitcode。これは LLVM の中間表現をバイナリで表したもので、 .o ファイル内のセクションとして埋め込まれます。 これは Link Time Optimization (LTO) に使用できます。 LTO が不要な場合は、コンパイル時間を改善し、ディスク使用量を削減するために、 -C embed-bitcode=no CLI オプションでこれを削除できます。
  • lib.rmeta という名前のファイル内の rustc metadata
  • シンボルテーブル。これは本質的には、そのシンボルを含む オブジェクトファイルへのオフセットを持つシンボルの一覧です。 これはアーカイブファイルではごく標準的なものです。

dylib

dylib はプラットフォーム固有の共有ライブラリです。 これは .rustc と呼ばれる特別なリンクセクションに rustc metadata を含みます。

rmeta

rmeta ファイルは、クレートの metadata を含むカスタムバイナリ形式です。 このファイルは、すべてのコード生成をスキップすることでプロジェクトの高速な「チェック」 (cargo check で行われるようなもの)を行ったり、ドキュメント作成に十分な情報を収集したり (cargo doc で行われるようなもの)、あるいは pipelining のために使用できます。 このファイルは、--emit=metadata CLI オプションが使用された場合に作成されます。

rmeta ファイルはコンパイル済みのオブジェクトファイルを含まないため、リンクをサポートしません。

メタデータ

メタデータには、非常に幅広いさまざまな要素が含まれています。 このガイドでは、含まれるすべてのフィールドの詳細には立ち入りません。 含まれるさまざまな要素の感覚をつかむために、 CrateRoot の定義に目を通すことをお勧めします。 メタデータのエンコードとデコードに関するものはすべて rustc_metadata パッケージ内にあります。

含まれるもののいくつかのハイライトを次に示します。

  • rustc コンパイラのバージョン。 コンパイラは、他のどのバージョンのファイルも読み込むことを拒否します。
  • Strict Version Hash (SVH)。 これは正しい依存関係が読み込まれるようにするのに役立ちます。
  • Stable Crate Id。 これはクレートを識別するために使用されるハッシュです。
  • ライブラリ内のすべてのソースファイルに関する情報。 これは、依存関係内のソースを指す診断など、さまざまな用途に使用できます。
  • エクスポートされたマクロ、トレイト、型、アイテムに関する情報。 一般に、 パスがクレート依存関係内の何かを参照するときに知る必要があるあらゆるものです。
  • エンコードされた MIR。 これは任意であり、コード生成に必要な場合にのみエンコードされます。 cargo check はパフォーマンス上の理由からこれをスキップします。

Strict Version Hash

Strict Version Hash(SVH、別名「クレートハッシュ」)は、正しいクレート依存関係が 読み込まれるようにするために使用される 64 ビットハッシュです。 1 つのディレクトリに、異なる設定でビルドされた、または異なるソースからビルドされた 同じ依存関係の複数のコピーが含まれることがあります。 クレートローダーは、誤った SVH を持つクレートをすべてスキップします。

SVH は incremental compilation セッションのファイル名にも使用されますが、 その用途は主に歴史的なものです。

このハッシュにはさまざまな要素が含まれます。

  • HIR ノードのハッシュ。
  • すべての上流クレートのハッシュ。
  • すべてのソースファイル名。
  • 特定のコマンドラインフラグ(Stable Crate Id を介した -C metadata など)と、[TRACKED] とマークされたすべての CLI オプションのハッシュ。

ハッシュが実際に計算される場所については、compute_hir_hash を参照してください。

Stable Crate Id

StableCrateId は、同じ名前である可能性のある異なるクレートを識別するために使用される 64 ビットハッシュです。 これは、StableCrateId::new で計算されるクレート名とすべての -C metadata CLI オプションのハッシュです。 これは、シンボル名マングリング、クレートの読み込みなど、 さまざまな場所で使用されます。

デフォルトでは、すべての Rust シンボルはマングルされ、stable crate id が組み込まれます。 これにより、同じクレートの複数のバージョンを一緒に含めることができます。 Cargo は、パッケージバージョン、ソース、ターゲット種別など、さまざまな要因に基づいて -C metadata ハッシュを自動的に生成します(libtest は同じ クレート名を持つことができるため、曖昧さを解消する必要があります)。

クレートの読み込み

クレートの読み込みには、かなりの数の微妙な複雑さが伴うことがあります。 name resolution 中に、外部クレートが(extern crate または パスを介して)参照されると、リゾルバは CStore を使用します。これは、 クレートライブラリを見つけ、それらの metadata を読み込む責任を負います。 依存関係が読み込まれた後、CStore はリゾルバがその処理を行うために必要な情報 (マクロの展開、パスの解決など)を提供します。

各外部クレートを読み込むために、CStoreCrateLocator を使用して、 特定の 1 つのクレートに対応する正しいファイルを実際に見つけます。 locator モジュールには、読み込みの仕組みを詳細に説明した優れたドキュメントがあり、 全体像を把握するために読むことを強くお勧めします。 依存関係の場所は、いくつかの異なる場所に由来する可能性があります。 直接依存関係は通常、--extern フラグで渡され、ローダーはそれらを直接参照できます。 直接依存関係には多くの場合、それ自身の依存関係への参照も含まれており、それらも読み込む必要があります。 これらは通常、-L フラグで渡されたディレクトリをスキャンし、メタデータに一致するクレート名と SVH が含まれるファイルを探すことで見つかります。 ローダーは依存関係を見つけるために sysroot も参照します。

クレートが読み込まれると、それらは CrateMetadata 構造体でラップされたクレートメタデータとともに CStore に保持されます。 解決と展開の後、CStore はコンパイルの残りの処理のために GlobalCtxt に渡されます。

パイプライン化

コンパイル時間を改善するための 1 つのテクニックは、依存関係のメタデータが利用可能になり次第、クレートのビルドを開始することです。 ライブラリの場合、依存関係のコード生成が完了するまで待つ必要はありません。 Cargo は、各依存関係について rlib だけでなく rmeta ファイルも出力するよう rustc に指示することで、この手法を実装しています。 rustc は可能な限り早い段階で、コード生成フェーズに進む前に rmeta ファイルをディスクに保存します。 コンパイラは JSON メッセージを送信して、可能であれば次のクレートのビルドを開始できることをビルドツールに知らせます。

クレート読み込みシステムは十分に賢く、rmeta ファイルを見つけたときに、rlib が存在しない場合(または部分的にしか書き込まれていない場合)にはそれを使用することを認識できます。

このパイプライン化はバイナリでは不可能です。なぜなら、リンクフェーズではすべての依存関係のコード生成が必要になるためです。 将来的には、リンクを別個のコマンドに分割することで、このシナリオをさらに改善できる可能性があります(#64191 を参照)。

プロファイル誘導最適化

rustc はプロファイル誘導最適化(PGO)をサポートしています。 この章では、PGO とは何か、そしてそのサポートが rustc でどのように 実装されているかを説明します。

プロファイル誘導最適化とは?

PGO の基本的な概念は、プログラムの典型的な実行に関するデータ (例: どの分岐を通る可能性が高いか)を収集し、そのデータを使用して インライン化、マシンコードの配置、レジスタ割り当てなどの最適化に 情報を与えることです。

プログラムの実行に関するデータを収集する方法はいくつかあります。 1 つは、プログラムをプロファイラー(perf など)の内部で実行する方法で、 もう 1 つは、インストルメント化されたバイナリ、つまりデータ収集機能が 組み込まれたバイナリを作成し、それを実行する方法です。 後者の方が通常、より正確なデータを提供します。

PGO は rustc でどのように実装されていますか?

rustc の現在の PGO 実装は完全に LLVM に依存しています。 LLVM は実際には PGO の複数の形式をサポートしています。

  • perf のような外部プロファイリングツールを使用してプログラムの実行に関する データを収集する、サンプリングベースの PGO。
  • コードカバレッジ基盤を使用してプロファイリング情報を収集する、 GCOV ベースのプロファイリング。
  • コンパイラーフロントエンド(例: Clang)が、生成する LLVM IR に インストルメンテーション intrinsic を挿入する、フロントエンドベースの インストルメンテーション(ただし、1「注記」を参照)。
  • LLVM が最適化パス中にインストルメンテーション intrinsic を自分で挿入する、 IR レベルのインストルメンテーション。

rustc がサポートしているのは最後のアプローチである IR レベルのインストルメンテーションのみです。主な理由は、 それがほぼ完全に LLVM で実装されており、Rust 側でほとんど メンテナンスを必要としないためです。幸いなことに、これは最も現代的なアプローチでもあり、 最良の結果をもたらします。

つまり、ここで扱っているのはインストルメンテーションベースのアプローチです。すなわち、プロファイリングデータは、 最適化対象のプログラムを特別にインストルメント化したバージョンによって生成されます。 インストルメンテーションベースの PGO には、コンパイル時コンポーネントと 実行時コンポーネントの 2 つのコンポーネントがあり、それらがどのように相互作用するかを理解するには、 全体的なワークフローを理解する必要があります。

全体的なワークフロー

PGO で最適化されたプログラムの生成には、次の 4 つのステップが含まれます。

  1. インストルメンテーションを有効にしてプログラムをコンパイルする(例: rustc -C profile-generate main.rs
  2. インストルメント化されたプログラムを実行する(例: ./main)。これにより default-<id>.profraw ファイルが生成される
  3. LLVM の llvm-profdata ツールを使用して、.profraw ファイルを .profdata ファイルに変換する。
  4. 今度はプロファイリングデータを使用して、プログラムを再度コンパイルする (例: rustc -C profile-use=merged.profdata main.rs

コンパイル時の側面

上記のワークフローのどのステップにいるかに応じて、コンパイル時には 2 つの異なることが 起こり得ます。

インストルメンテーション付きのバイナリを作成する

前述のとおり、プロファイリングインストルメンテーションは LLVM によって追加されます。 rustc は、LLVM PassManager の作成時に適切なフラグを設定することで、 LLVM にそうするよう指示します。

	// `PMBR` は `LLVMPassManagerBuilderRef` です
    unwrap(PMBR)->EnablePGOInstrGen = true;
    // インストルメント化されたバイナリには、`.profraw` ファイルのデフォルト出力パスが
    // ハードコードされています:
    unwrap(PMBR)->PGOInstrGen = PGOGenPath;

rustc はまた、LLVM のプロファイリングランタイムからの一部のシンボルが 適切なエクスポートレベルでマークすることによって削除されないようにする必要があります。

最適化がプロファイリングデータを使用するバイナリをコンパイルする

上記のワークフローの最後のステップでは、プログラムが再度コンパイルされ、 コンパイラーは収集されたプロファイリングデータを使用して最適化の判断を行います。 ここでも rustc は作業の大半を LLVM に任せており、基本的には LLVM PassManagerBuilder に プロファイリングデータがどこにあるかを伝えるだけです。

	unwrap(PMBR)->PGOInstrUse = PGOUsePath;

残りは LLVM が行います(例: 分岐の重みを設定する、関数に coldinlinehint をマークする、など)。

実行時の側面

インストルメンテーションベースのアプローチには常に実行時コンポーネントもあります。つまり、 インストルメント化されたプログラムができたら、そのプログラムを実行して プロファイリングデータを生成する必要があり、このプロファイリングデータを収集して永続化するには、 何らかの基盤が必要です。

LLVM の場合、これらの実行時コンポーネントは compiler-rt に実装され、インストルメント化された バイナリに静的リンクされます。 この rustc バージョンは library/profiler_builtins にあり、 基本的には compiler-rt の C コードを Rust クレートにまとめたものです。

profiler_builtins をビルドするには、rustcbootstrap.tomlprofiler = true を設定する必要があります。

PGO のテスト

PGO ワークフローは複数のコンパイラー呼び出しにまたがるため、テストの大半は run-make tests で行われます(関連するテストは名前に pgo を含みます)。 また、期待されるインストルメンテーションアーティファクトが LLVM IR に現れることを確認する codegen test もあります。

追加情報

Clang のドキュメントには、LLVM における PGO に関する優れた概要が含まれています。


  1. 注記: rustc は現在、実験的なオプション -C instrument-coverage によって、フロントエンドベースのカバレッジ インストルメンテーションをサポートしていますが、これらのカバレッジ結果を PGO に使用することは、 現時点では試みられていません。

LLVM のソースベースのコードカバレッジ

rustc は、コンパイル時に Rust ライブラリとバイナリへ追加の命令とデータをインストルメントするコマンドラインオプション(-C instrument-coverage)により、詳細なソースベースのコードおよびテストカバレッジ解析をサポートします。

カバレッジインストルメンテーションは、コード分岐(MIR ベースの制御フロー解析に基づく)に LLVM 組み込み命令 llvm.instrprof.increment の呼び出しを注入し、LLVM はこれらを、実行時に静的カウンターをインクリメントする命令へ変換します。 LLVM のカバレッジインストルメンテーションには、ソースメタデータをエンコードする Coverage Map も必要です。これは、カウンター ID をファイル内の位置(開始および終了の行と列)へ、直接および間接的にマッピングします。

カバレッジインストルメンテーションの有無にかかわらず、Rust ライブラリはインストルメントされたバイナリへリンクできます。 プログラムが実行され、正常に終了すると、LLVM ライブラリは最終的なカウンター値をファイル(default.profraw、または環境変数 LLVM_PROFILE_FILE で設定されたカスタムファイル)へ書き込みます。

開発者は既存の LLVM カバレッジ解析ツールを使用して .profraw ファイルをデコードし、それに対応する Coverage Map(それらを生成した一致するバイナリから得られるもの)とともに、解析用のさまざまなレポートを生成します。例:

関数 add_quoted_string に対する llvm-cov show のサンプル結果のスクリーンショット

詳細な手順と例は、rustc book に記載されています。

推奨される bootstrap.toml 設定

カバレッジインストルメンテーションコードに取り組む場合、通常は [build]profiler = true を設定して、プロファイラーランタイムを有効にする必要があります。 これにより、コンパイラがインストルメントされたバイナリを生成できるようになり、カバレッジテストスイート全体を実行できるようになります。

コンパイラと LLVM でデバッグアサーションを有効にすることが推奨されますが、必須ではありません。

# "compiler" プロファイルに似ていますが、LLVM のデバッグアサーションも有効にします。
# これらのアサーションは、一部のケースで不正な形式のカバレッジマッピングを検出できます。
profile = "codegen"

# 重要: これは、LLVM プロファイラーランタイムをビルドするようビルドシステムに指示します。
# これがないと、コンパイラはカバレッジインストルメントされたバイナリを生成できず、
# 多くのカバレッジテストがスキップされます。
build.profiler = true

# コンパイラのデバッグアサーションを有効にします。
rust.debug-assertions = true

Rust シンボルマングリング

-C instrument-coverage は、一貫性があり可逆的な名前マングリングを保証するために、(ユーザーが rustc を起動するときに -C symbol-mangling-version=v0 オプションを指定した場合と同様に)Rust シンボルマングリング v0 を自動的に有効にします。 これには 2 つの重要な利点があります。

  1. LLVM カバレッジツールは、ソースコードへの一部の変更を含め、複数回の実行にわたってカバレッジを解析できます。そのため、マングルされた名前はコンパイル間で一貫していなければなりません。
  2. LLVM カバレッジレポートは関数単位でカバレッジを報告でき、複数の型置換バリエーションで呼び出された場合は、ジェネリック関数の一意なインスタンス化ごとのカバレッジカウントを分けて出力することさえできます。

LLVM プロファイラーランタイム

カバレッジデータは、実行可能な Rust プログラムを実行することによってのみ生成されます。 rustc は、カバレッジインストルメントされたバイナリを、カウンター値を .profraw ファイルへ書き込むためのプログラムフック(exit フックなど)を実装する LLVM ランタイムコード(compiler-rt)と静的にリンクします。

rustc ソースツリーでは、library/profiler_builtins が LLVM の compiler-rt コードを Rust ライブラリクレートにバンドルしています。 rustc をビルドする際、profiler_builtinsbootstrap.tomlbuild.profiler = true が設定されている場合にのみ含まれることに注意してください。

-C instrument-coverage でコンパイルする場合、CStore::postprocess()inject_profiler_runtime() を呼び出すことで profiler_builtins を動的に読み込みます。

カバレッジインストルメンテーションのテスト

tests/coverage テストスイートに関する compiletest ドキュメントも参照してください。)

MIR におけるカバレッジインストルメンテーションは、mir-opt テストによって検証されます: tests/mir-opt/coverage/instrument_coverage.rs

LLVM IR におけるカバレッジインストルメンテーションは、coverage-map モードの tests/coverage テストスイートによって検証されます。 これらのテストは、テストプログラムを LLVM IR アセンブリへコンパイルし、その後 src/tools/coverage-dump ツールを使用して、最終的なバイナリに埋め込まれるカバレッジマッピングを抽出し、整形して出力します。

カバレッジインストルメンテーションとカバレッジレポートのエンドツーエンドテストは、coverage-run モードの tests/coverage テストスイート、および tests/coverage-run-rustdoc テストスイートによって実行されます。 これらのテストは、カバレッジインストルメンテーション付きでテストプログラムをコンパイルして実行し、その後 LLVM ツールを使用してカバレッジデータを人間が読めるカバレッジレポートへ変換します。

coverage-run モードのテストには暗黙の //@ needs-profiler-runtime ディレクティブがあるため、プロファイラーランタイムが bootstrap.toml で有効化されていない場合はスキップされます。

最後に、tests/codegen-llvm/instrument-coverage/testprog.rs テストは、-C instrument-coverage を使用して単純な Rust プログラムをコンパイルし、コンパイルされたプログラムの LLVM IR を、カバレッジ対応プログラムに期待される LLVM IR 命令および構造化データと比較します。これには、Coverage Map 関連のメタデータおよびランタイムカウンターをインクリメントする LLVM 組み込み呼び出しに対する各種チェックが含まれます。

coveragecoverage-run-rustdoc、および mir-opt テストの期待結果は、次を実行することで更新できます。

./x test coverage --bless
./x test coverage-run-rustdoc --bless
./x test tests/mir-opt --bless

サニタイザーのサポート

rustc コンパイラは、以下のサニタイザーのサポートを含んでいます。

  • AddressSanitizer は、より高速なメモリエラー検出器です。 ヒープ、スタック、グローバルへの範囲外アクセス、解放後使用、return 後使用、二重解放、不正な解放、メモリリークを検出できます。
  • ControlFlowIntegrity LLVM Control Flow Integrity (CFI) は、フォワードエッジの制御フロー保護を提供します。
  • Hardware-assisted AddressSanitizer は、AddressSanitizer に似ていますが、部分的なハードウェア支援に基づくツールです。
  • KernelControlFlowIntegrity LLVM Kernel Control Flow Integrity (KCFI) は、オペレーティングシステムのカーネル向けにフォワードエッジの制御フロー保護を提供します。
  • LeakSanitizer は、実行時メモリリーク検出器です。
  • MemorySanitizer は、未初期化読み取りの検出器です。
  • ThreadSanitizer は、高速なデータ競合検出器です。

サニタイザーの使い方

サニタイザーを有効にするには、-Z sanitizer=... オプションを付けてコンパイルします。ここで値は addresscfihwaddresskcfileakmemorythread のいずれかです。 サニタイザーの使用方法の詳細については、 The Unstable Book の sanitizer フラグを参照してください。

rustc ではサニタイザーはどのように実装されているか

サニタイザー(CFI を除く)の実装は、ほぼ完全に LLVM に依存しています。 rustc は LLVM のコンパイル時インストルメンテーションパスとランタイムライブラリの統合ポイントです。 実装における最も重要な側面の要点:

  • サニタイザーランタイムライブラリは compiler-rt プロジェクトの一部であり、bootstrap.toml で有効にされている場合、サポート対象ターゲット 上でビルドされます

    build.sanitizers = true
    

    ランタイムはターゲット libdir に配置されます

  • LLVM コード生成中に、インストルメンテーション対象の関数には、適切な LLVM 属性がマークされます: SanitizeAddressSanitizeHWAddressSanitizeMemory、または SanitizeThread。 デフォルトでは、すべての関数がインストルメントされますが、この挙動は #[sanitize(xyz = "on|off|<other>")] で変更できます。

  • インストルメンテーションを実行するかどうかの判断は、関数単位でのみ可能です。 それらの判断が関数間で異なる場合、MIR レベルLLVM レベルの両方でインライン化を抑制する必要があるかもしれません。

  • rustc によって生成された LLVM IR は、各サニタイザーごとに異なる専用の LLVM パスによってインストルメントされます。 インストルメンテーションパスは、最適化パスの後に呼び出されます。

  • 実行可能ファイルを生成する際には、サニタイザー固有のランタイムライブラリがリンクされます。 ライブラリはターゲット libdir 内で検索されます。 まず、検索はオーバーライドされたシステムルートからの相対で行われ、その後、デフォルトのシステムルートからの相対で行われます。 デフォルトのシステムルートへのフォールバックにより、cargo -Z build-std や xargo によって構築された sysroot オーバーライドを使用する場合でも、サニタイザーランタイムが引き続き利用可能であることが保証されます。

サニタイザーのテスト

サニタイザーは、tests/codegen-llvm/sanitize*.rs のコード生成テストと、tests/ui/sanitizer/ ディレクトリ内のエンドツーエンドの機能テストによって検証されます。

サニタイザー機能のテストには、サニタイザーランタイム(bootstrap.tomlbuild.sanitizer = true の場合にビルドされる)と、特定のサニタイザーのサポートを提供するターゲットが必要です。 指定されたターゲットでサニタイザーがサポートされていない場合、サニタイザーのテストは無視されます。 この挙動は、compiletest の needs-sanitizer-* ディレクティブによって制御されます。

新しいターゲットでサニタイザーを有効にする

LLVM によってすでにサポートされている新しいターゲットでサニタイザーを有効にするには:

  1. ターゲット定義内の supported_sanitizers のリストにサニタイザーを含めます。 rustc --target .. -Zsanitizer=.. は、そのサニタイザーをサポート対象として認識するようになるはずです。
  2. ターゲット用のランタイムをビルドし、それを libdir に含めます。
  3. そのターゲットがサニタイザーをサポートするようになったことを compiletest に教えます。 needs-sanitizer-* でマークされたテストが、そのターゲット上で実行されるようになるはずです。
  4. テスト ./x test --force-rerun tests/ui/sanitize/ を実行して検証します。
  5. リリースプロセスの一部としてサニタイザーランタイムをビルドして配布するため、CI 設定で –enable-sanitizers を指定します

追加情報

背景トピック

このセクションでは、このガイドに登場する一般的なコンパイラ用語をいくつか取り上げます。Rust 固有の文脈をいくつか示しつつ、一般的な定義を説明することを試みます。

制御フローグラフとは何ですか?

制御フローグラフ(CFG)は、コンパイラ分野で一般的に使われる用語です。フローチャートを使ったことがあるなら、制御フローグラフの概念はかなり馴染みのあるものに感じられるでしょう。これは、根底にある制御フローを明確に示すプログラムの表現です。

制御フローグラフは、エッジで接続された一連の基本ブロックとして構成されます。基本ブロックの重要な考え方は、それが「一緒に」実行される文の集合であるということです。つまり、ある基本ブロックへ分岐すると、最初の文から開始し、その後に残りすべてを実行します。ブロックの末尾でのみ、複数の場所へ分岐する可能性があります(MIR では、その最後の文をターミネータと呼びます)。

bb0: {
    statement0;
    statement1;
    statement2;
    ...
    terminator;
}

Rust で普段使っている多くの式は、複数の基本ブロックにコンパイルされます。たとえば、if 文を考えてみましょう。

a = 1;
if some_variable {
    b = 1;
} else {
    c = 1;
}
d = 1;

これは MIR では 4 つの基本ブロックにコンパイルされます。テキスト形式では、次のようになります。

BB0: {
    a = 1;
    if some_variable {
        goto BB1;
    } else {
        goto BB2;
    }
}

BB1: {
    b = 1;
    goto BB3;
}

BB2: {
    c = 1;
    goto BB3;
}

BB3: {
    d = 1;
    ...
}

グラフィカルな形式では、次のようになります。

                BB0
       +--------------------+
       | a = 1;             |
       +--------------------+
             /       \
  if some_variable   else
           /           \
     BB1  /             \  BB2
    +-----------+   +-----------+
    | b = 1;    |   | c = 1;    |
    +-----------+   +-----------+
            \          /
             \        /
              \ BB3  /
            +----------+
            | d = 1;   |
            | ...      |
            +----------+

制御フローグラフを使う場合、ループは単にグラフ内のサイクルとして現れ、break キーワードはそのサイクルから出る経路へ変換されます。

データフロー解析とは何ですか?

Anders Møller と Michael I. Schwartzbach による Static Program Analysis は、非常に優れたリソースです!

データフロー解析 は、多くのコンパイラで一般的な静的解析の一種です。これは特定の解析ではなく、一般的な手法を表します。

基本的な考え方は、制御フローグラフ(CFG) をたどりながら、ある値が何であり得るかを追跡できるというものです。走査の終わりには、何らかの主張が真であること、または必ずしも真ではないことを示せるかもしれません(例:「この変数は初期化されていなければならない」)。MIR はすでに CFG であるため、rustc は MIR に対してデータフロー解析を行う傾向があります。

たとえば、次のスニペットで、x が使用される前に初期化されていることを確認したいとします。

fn foo() {
    let mut x;

    if some_cond {
        x = 1;
    }

    dbg!(x);
}

このコードの CFG は次のようになるかもしれません。

 +------+
 | Init | (A)
 +------+
    |   \
    |   if some_cond
  else    \ +-------+
    |      \| x = 1 | (B)
    |       +-------+
    |      /
 +---------+
 | dbg!(x) | (C)
 +---------+

データフロー解析は次のように行えます。まず、x が初期化されていることを把握しているかどうかを示すフラグ init から開始します。CFG をたどるにつれて、このフラグを更新します。最後に、その値を確認できます。

まずブロック (A) では、変数 x は宣言されていますが初期化されていないため、init = false です。ブロック (B) では、値を初期化するため、x が初期化されていることが分かります。したがって、(B) の終わりでは init = true です。

ブロック (C) は、状況が興味深くなる場所です。some_cond が true かどうかに対応して、(A) からのものと (B) からのものという 2 つの入力エッジがあることに注目してください。しかし、それは分かりません!some_cond が常に true である場合もあり得るため、その場合 x は実際には常に初期化されています。また、some_cond が何らかのランダムなもの(たとえば時刻)に依存している場合もあり得るため、x は初期化されていないかもしれません。一般に、静的にはそれを知ることはできません(Rice の定理による)。では、ブロック (C) における init の値はどうあるべきでしょうか?

一般に、データフロー解析では、あるブロックに複数の親がある場合(この例の (C) のように)、そのデータフロー値はすべての親(そしてもちろん、(C) で起こること)に対する何らかの関数になります。どの関数を使うかは、実行している解析によって異なります。

この場合、x が使用前に必ず初期化されていなければならないことを確定的に証明できるようにしたいと考えています。これにより、保守的になり、some_cond は時々 false になるかもしれないと仮定せざるを得ません。したがって、私たちの「マージ関数」は「and」です。つまり、(A) かつ (B) で init = true である場合(または x が (C) で初期化される場合)、(C) で init = true になります。しかし、これは当てはまりません。特に、(A) では init = false であり、x は (C) で初期化されません。したがって、(C) では init = false です。つまり、「x は使用前に初期化されていない可能性がある」というエラーを報告できます。

データフロー解析については、間違いなくさらに多くのことを述べられます。このトピックについては、多くの理論を含む膨大な研究文献があります。ここでは順方向解析についてのみ議論しましたが、逆方向データフロー解析も有用です。たとえば、ブロック (A) から開始して順方向へ進む代わりに、x の使用箇所から開始し、その初期化を見つけるために逆方向へ進むこともできたでしょう。

「全称量化」とは何ですか?「存在量化」はどうでしょうか?

数学では、述語は_全称量化_または_存在量化_されることがあります。

  • 全称 量化:
    • 述語は、すべての可能な入力について真である場合に成立します。
    • 伝統的な表記: ∀x: P(x)。「すべての x について、P(x) が成り立つ」と読みます。
  • 存在 量化:
    • 述語は、真となる入力がいずれか存在する場合に成立します。つまり、単一の入力が存在すればよいということです。
    • 伝統的な表記: ∃x: P(x)。「P(x) が成り立つような x が存在する」と読みます。

Rust では、これらは型チェックやトレイト解決で登場します。たとえば、

fn foo<T>()

この関数は、すべての型 T について関数が well-typed であると主張しています: ∀ T: well_typed(foo)

別の例です。

fn foo<'a>(_: &'a usize)

この関数は、任意のライフタイム 'a(呼び出し元によって決定される)について、well-typed であると主張しています: ∀ 'a: well_typed(foo)

別の例です。

fn foo<F>()
where for<'a> F: Fn(&'a u8)

この関数は、すべてのライフタイム 'a について F: Fn(&'a u8) であるような、すべての型 F について well-typed であると主張しています: ∀ F: ∀ 'a: (F: Fn(&'a u8)) => well_typed(foo)

もう 1 つ例を挙げます。

fn foo(_: dyn Debug)

この関数は、関数が well-typed であるような、Debug を実装する何らかの型 T が存在すると主張しています: ∃ T: (T: Debug) and well_typed(foo)

de Bruijn インデックスとは何ですか?

De Bruijn インデックスは、どの変数がどのバインダーで束縛されているかを、整数のみを使って表現する方法です。もともとはラムダ計算の評価で使うために考案されました(詳細はこの Wikipedia の記事を参照してください)。rustc では、de Bruijn インデックスをジェネリック型を表現するために使用しています。

以下は、de Bruijn インデックスをクロージャにどのように使えるかを示す基本的な例です(ただし、rustc では実際にはこうしていません!):

|x| {
    f(x) // `x` の de Bruijn インデックスは 1。`x` は 1 レベル上で束縛されているため

    |y| {
        g(x, y) // `x` のインデックスは 2。2 レベル上で束縛されているため
                // `y` のインデックスは 1。1 レベル上で束縛されているため
    }
}

共変性と反変性とは?

Rust Nomicon のサブタイピングの章を参照してください。

型チェッカーが分散をどのように扱うかの詳細については、このガイドの variance の章を参照してください。

「自由リージョン」または「自由変数」とは何ですか?「束縛リージョン」はどうですか?

自由と束縛の概念を、プログラム変数の観点から説明しましょう。これは私たちにとって最も馴染みのあるものだからです。

  • クロージャを作成する次の式を考えてください: |a, b| a + b。 ここで、a + b の中の ab は、クロージャが呼び出されたときに渡される引数を参照しています。そこにある ab はクロージャに束縛されており、クロージャシグネチャ |a, b| は名前 abバインダーである、と言います(その内部で ab を参照すると、それが導入する変数を参照するためです)。
  • 次の式を考えてください: a + b。この式では、ab は式の外側で定義されたローカル変数を参照しています。これらの変数は式の中に自由に現れる、つまり自由であり、束縛されていない(結び付けられていない)と言います。

これで説明は終わりです。ある変数が何らかの式/文/その他の中で「自由に現れる」とは、その式/文/その他の外側で定義されたものを参照している場合です。同じことを別の言い方にすると、式の「自由変数」を参照できるということです。これは単に、「自由に現れる」変数の集合です。

では、これはリージョンとどのような関係があるのでしょうか? 同様の概念を型やリージョンにも適用できます。たとえば、型 &'a u32 では、'a は自由に現れます。しかし、型 for<'a> fn(&'a u32) ではそうではありません。

コンパイラに関する参考資料

公式 Discord の memscottmcmLevi の推薦に感謝します。また、さらにいくつかの推薦を含んでいた Graydon Hoare による twitter スレッドへのリンクを投稿してくれた tinaun にも感謝します!

その他の出典: https://gcc.gnu.org/wiki/ListOfCompilerBooks

他にも提案がある場合は、issue または PR をぜひ開いてください。

書籍

コース

Wiki

その他の論文とブログ記事

用語集

用語意味
1-ZSTアラインメントが1の ゼロサイズ型。サイズがゼロで、アラインメント が1の型。
アリーナ、アリーナ割り当てアリーナ は、他のメモリ割り当てが行われる大きなメモリバッファーです。この割り当ての方式は アリーナ割り当て と呼ばれます。詳細は この章 を参照してください。
AFIDTdyn Trait 内の async 関数 の略。AFIT も参照してください。
AFITtrait 内の async 関数 の略。RPITIT へ脱糖されます。
ASTパーサーによって生成される 抽象構文木IR)。表層 / ユーザー構文を非常に忠実に反映します。
APIT引数位置の impl Trait の略。全称的な impl Trait(存在的なものとは対照的)または無名型パラメーターとも呼ばれます(リファレンスを参照)。
ATPIT関連型位置の impl Trait の略。ITIAT とも呼ばれます。
バインダーバインダー とは、変数または型が宣言される場所です。たとえば、fn foo<T>(..) における <T> は型パラメーター T のバインダーであり、for<'a> はライフタイムパラメーター 'a のバインダーであり、|a| … はパラメーター a のバインダーです。詳細は 背景の章 を参照してください。
本体「実行可能コード」を含む関数または定数の定義。
BodyIdクレート内の特定の 本体 を参照する識別子。詳細は HIR の章 を参照してください。
束縛変数束縛変数 とは、式または項(一般的な意味で)の内部で宣言される変数です。たとえば、変数 a はクロージャ式 |a| a * 2 の内部で束縛され、ライフタイム変数 'a は型式 for<'a> fn(&'a str) -> bool の内部で束縛されます。詳細は 背景の章 を参照してください。
codegenコード生成 の略。MIR を LLVM IR に変換するコードです。
codegen ユニットLLVM IR を生成する際、Rust コードを複数の codegen ユニット(CGU と略されることもあります)にグループ化します。これらの各ユニットは LLVM によって互いに独立して処理されるため、並列化が可能になります。また、これらはインクリメンタルな再利用の単位でもあります(詳細)。
完全性型理論の専門用語で、型安全なプログラムはすべて型チェックにも合格することを意味します。健全性と完全性の両方を備えることは非常に難しく、通常は健全性のほうが重要です(「健全性」を参照)。
制御フローグラフ、CFGプログラムの制御フローを表現したもの。詳細は 背景の章 を参照してください。
CTFEコンパイル時関数評価 の略で、コンパイラーがコンパイル時に const fn を評価できる能力です。これはコンパイラーの定数評価システムの一部です(詳細)。
cxcxコンテキスト の略語として使われる傾向があります。tcxinfcx なども参照してください。
ctxtctxtコンテキスト の略語として使用します。例: TyCtxtcx または tcx も参照してください。
DAG有向非巡回グラフ は、コンパイル中にクエリ間の依存関係を追跡するために使用されます(詳細)。
データフロー解析プログラムの制御フロー上の各地点で、どのプロパティが真であるかを把握する静的解析。詳細は 背景の章 を参照してください。
de Bruijn インデックス変数がどのバインダーによって束縛されているかを整数だけで記述する手法です。変数のリネームに対して不変であるという利点があります(詳細)。
DefId定義を識別するインデックス(rustc_middle/src/hir/def_id.rs を参照)。DefPath を一意に識別します。詳細は HIR の章 を参照してください。
判別子enum バリアントまたはジェネレーター状態に関連付けられた基礎となる値で、それが「アクティブ」であることを示します(ただし、その 「バリアントインデックス」 と混同しないでください)。実行時には、アクティブなバリアントの判別子は タグ にエンコードされます。
ダブルポインター追加のメタデータを持つポインター。詳細は ファットポインター を参照してください。
ドロップグルー(内部)データ型のデストラクター(Drop)の呼び出しを処理する、コンパイラーが生成した命令。
DST動的サイズ型 の略で、コンパイラーがメモリ内のサイズを静的に知ることができない型(例: str[u8])です。このような型は Sized を実装せず、スタック上に割り当てることはできません。構造体では最後のフィールドとしてのみ出現できます。ポインター越し(例: &str&[u8])でのみ使用できます。
早期束縛ライフタイム定義サイトで置換されるライフタイム / リージョン。アイテムの Generics 内で束縛され、GenericArgs を使って置換/インスタンス化されます。後期束縛ライフタイム と対比してください(詳細)。
エフェクト現時点では const トレイトと ~const 境界のみを意味します(詳細)。
空型非居住型 を参照してください。
ファットポインター何らかの値のアドレスと、その値を使用するために必要な追加情報を持つ、2ワードの値です。Rust には2種類の ファットポインター、つまりスライスへの参照とトレイトオブジェクトがあります。スライスへの参照は、スライスの開始アドレスと長さを保持します。トレイトオブジェクトは、値のアドレスと、その値に適したトレイト実装へのポインターを保持します。「ファットポインター」は「ワイドポインター」や「ダブルポインター」とも呼ばれます。
自由変数自由変数 とは、式または項(一般的な意味で)の内部で束縛されていない変数です。詳細は 背景の章 を参照してください。
GACジェネリック関連定数。独自のジェネリックパラメーターまたは where 句を持つ関連定数です。機能 generic_const_items(GCI)の一部です。
GATジェネリック関連型。独自のジェネリックパラメーターまたは where 句を持つ関連型です。RFC 1598 で導入されました。
ジェネリクスアイテム上に定義されたジェネリックパラメーターのリストです。ジェネリックパラメーターには、型、ライフタイム、const パラメーターの3種類があります。
HIRAST を lowering / 脱糖して作成される 高水準 IR詳細)。
HirIddef-id と「定義内オフセット」を組み合わせることで、HIR 内の特定のノードを識別します。詳細は HIR の章 を参照してください。
IAC(多くの場合は型レベルの)固有関連定数。固有 impl impl Type { … } 内の関連定数です。機能 min_generic_const_items(mGCA、MGCA)の文脈で言及されることがよくあります。IGAC は固有 GAC です。
IAT固有関連型。固有 impl impl Type { … } 内で定義される関連型です。IGAT は固有 GAT、つまりジェネリックな IAT です。
ICEコンパイラー内部エラー の略で、コンパイラーがクラッシュする場合を指します。
ICHインクリメンタルコンパイルハッシュ の略で、HIR やクレートメタデータなどに対するフィンガープリントとして使用され、変更が行われたかどうかを確認します。これはインクリメンタルコンパイルにおいて、クレートの一部が変更され、再コンパイルすべきかどうかを確認するのに役立ちます。
infcx型推論コンテキスト(InferCtxt)。(rustc_middle::infer を参照)
推論変数、infer var 型、リージョン、const の推論を行うとき、推論変数 とは、推論しようとしているものを表す特別な型/リージョンの一種です。代数学における X のようなものと考えてください。たとえば、プログラム内の変数の型を推論しようとしている場合、その未知の型を表す推論変数を作成します。
インターン化インターン化とは、文字列など特定の頻繁に使用される定数データを格納し、そのデータ自体ではなく識別子(例: Symbol)によって参照することで、メモリ使用量と割り当て回数を削減することを指します。詳細は この章 を参照してください。
インタープリターconst 評価の中核であり、コンパイル時に MIR コードを実行します(詳細)。
イントリンシックイントリンシックは、コンパイラー自体に実装されているものの、(多くの場合は不安定な形で)ユーザーに公開される特殊な関数です。魔法のようで危険なことを行います(std::intrinsics を参照)。
IR中間表現 の略で、コンパイラーにおける一般的な用語です。コンパイル中、コードは生のソース(ASCII テキスト)からさまざまな IR へ変換されます。Rust では、これらは主に HIR、MIR、LLVM IR です。各 IR は、ある種の計算に適しています。たとえば、MIR は borrow checker に適しており、LLVM IR は LLVM が受け入れる形式であるため codegen に適しています。
IRLO、irlointernals.rust-lang.org の略語として使われることがあります。
アイテムstatic、const、use 文、モジュール、構造体など、言語における「定義」の一種です。具体的には、これは Item 型に対応します。
アイテムシグネチャアイテム(例: 構造体、関数)の型シグネチャ / 注釈 / ascription です。本体 内の型とは異なり、ここでは推論を行わないため、型推論の文脈でよく言及されます。
ITIAT関連型内の impl Trait の略。ATPIT とも呼ばれます。
lang item言語自体に組み込まれた概念を表すアイテムです。たとえば、SyncSend のような特殊な組み込みトレイト、Add のような演算を表すトレイト、またはコンパイラーによって呼び出される関数などです(詳細)。
後期束縛ライフタイム呼び出しサイトで置換されるライフタイム / リージョン。HRTB 内で束縛され、liberate_late_bound_regions など、コンパイラー内の特定の関数によって置換されます。早期束縛ライフタイム と対比してください(詳細)。
ローカルクレート現在コンパイルされているクレートです。これは、ローカルクレートの依存関係を指す「上流クレート」と対比されます。
loweringより高水準の IR をより低水準の IR に変換する行為です。例: AST lowering(AST から HIR)や HIR ty lowering(HIR から middle ty IR)。
LTA遅延型エイリアスmiddle ty IR 内でエイリアスとして「適切に」表現される型エイリアスです。(即時)型エイリアスでは、HIR ty lowering 中に参照サイトが基礎となるエイリアス先の型(インスタンス化後の型エイリアスの RHS)へ展開されるのに対し、これは参照サイトが AliasTylowering されます。
LTOリンク時最適化 の略で、最終バイナリがリンクされる直前に行われる LLVM が提供する一連の最適化です。たとえば、最終プログラムで一度も使用されない関数を削除するような最適化が含まれます。ThinLTO は LTO の一種で、もう少しスケーラブルかつ効率的であることを目指しますが、一部の最適化を犠牲にする可能性があります。Rust リポジトリの issue で「FatLTO」について読むこともあるかもしれませんが、これは non-Thin LTO に付けられた愛称です。LLVM のドキュメント: こちらこちら
LLVM(実際には頭字語ではありません :P)オープンソースのコンパイラーバックエンドです。LLVM IR を受け取り、ネイティブバイナリを出力します。さまざまな言語(例: Rust)は、LLVM IR を出力するコンパイラーフロントエンドを実装し、LLVM がサポートするすべてのプラットフォームへコンパイルするために LLVM を使用できます。
メモ化将来それらを繰り返す必要を避けるために、(純粋関数呼び出しのような)純粋な計算の結果を格納するプロセスです。これは通常、実行速度とメモリ使用量の間のトレードオフです。
middle ty IR型チェッカーとトレイトソルバーによって使用される、モジュール rustc_middle::ty で定義された型の集合です。これらは本質的に IR を形成します。
MIR型チェック後に borrowck と codegen で使用するために作成される 中水準 IR詳細)。
Miri(unsafe な)Rust コード内の未定義動作を検出するためのツールです(詳細)。
単相化型や関数のジェネリックな実装を取り、それらを具体的な型でインスタンス化するプロセスです。たとえば、コード内には Vec<T> があるかもしれませんが、最終的な実行可能ファイルには、プログラム内で使用される具体的な型ごとに Vec コードのコピー(例: Vec<usize> 用のコピー、Vec<MyStruct> 用のコピーなど)が含まれます。
正規化より標準的な形式へ変換することを指す一般的な用語ですが、rustc の場合は通常、関連型の正規化 を指します。
newtype何らかの別の型を包むラッパー(例: struct Foo(T)T に対する「newtype」です)。これは Rust でインデックスにより強い型を与えるためによく使用されます。
ニッチレイアウト最適化に 使用可能な 型の無効なビットパターンです。一部の型は特定のビットパターンを持つことができません。たとえば、NonZero* 整数や参照 &T は 0 のビット列では表現できません。これは、コンパイラーが無効な「ニッチ値」を利用してレイアウト最適化を行えることを意味します。この応用例の1つが Option 風 enum における判別子の省略 であり、別のフィールドを必要とせずに、型のニッチを enum「タグ」 として使用できるようにします。
NLL非字句的ライフタイム の略で、Rust の借用システムを制御フローグラフに基づくものにする拡張です。
node-id または NodeIdAST または HIR 内の特定のノードを識別するインデックスです。段階的に廃止され、HirId に置き換えられつつあります。詳細は HIR の章 を参照してください。
オブリゲーショントレイトシステムによって証明されなければならないものです(詳細)。
プレースホルダー注: skolemization は placeholder によって非推奨になっています。「for-all」型(例: for<'a> fn(&'a u32))周辺のサブタイピングを扱う方法であり、高ランクのトレイト境界(例: for<'a> T: Trait<'a>)を解決する方法でもあります。詳細は placeholder と universe の章 を参照してください。
pointNLL 解析で、MIR 内の特定の場所を指すために使用されます。通常は制御フローグラフ内のノードを指すために使用されます。
射影「相対パス」を指す一般的な用語です。たとえば、x.f は「フィールド射影」であり、T::Item「関連型射影」 です。
昇格された定数関数から抽出され、static スコープへ持ち上げられた定数です。詳細は このセクション を参照してください。
プロバイダークエリを実行する関数です(詳細)。
量化された数学や論理では、存在量化と全称量化は「それが真となる型 T は存在するか?」や「これはすべての型 T について真か?」のような問いを尋ねるために使用されます。詳細は 背景の章 を参照してください。
クエリコンパイル中の部分計算です。クエリ結果は、現在のセッション内またはインクリメンタルコンパイルのためにディスクへキャッシュできます(詳細)。
リカバリーリカバリーとは、パース中に無効な構文(例: カンマの欠落)を処理し、AST のパースを継続することを指します。これにより、ユーザーに疑似的なエラー(例: 構造体定義にエラーが含まれているときに「フィールドが見つからない」エラーを表示すること)を表示することを避けられます。
リージョン文献や borrow checker でよく使われる「ライフタイム」の別名です。
rib名前リゾルバー内のデータ構造で、名前に対する単一のスコープを追跡します(詳細)。
RPIT戻り値位置の impl Trait の略。TAITATPIT のような、全称的なものではなく存在的な impl Trait です(リファレンスを参照)。
RPITITtrait 内の戻り値位置の impl Trait の略。RPIT とは異なり、これはジェネリック関連型(GAT)へ脱糖されます。RFC 3425 で導入されました(詳細)。
rustbuild 👎Rust で書かれた bootstrap の部分を指す 非推奨 の用語
被照合式被照合式とは、match 式や同様のパターンマッチ構文で照合対象となる式です。たとえば、match x { A => 1, B => 2 } では、式 x が被照合式です。
sessコンパイラーの セッション。コンパイル全体で使用されるグローバルデータを格納します。
サイドテーブルAST と HIR は一度作成されると不変であるため、特定のノードの id によってインデックス付けされたハッシュテーブルの形で、それらに関する追加情報を保持することがよくあります。
シジルキーワードに似ていますが、完全に非英数字トークンで構成されるものです。たとえば、& は参照を表すシジルです。
健全性型理論の専門用語です。大まかには、型システムが健全であれば、型チェックに合格するプログラムは型安全です。つまり、(safe Rust では)誤った型の変数へ値を強制的に入れることは決してできません(「完全性」を参照)。
スパンユーザーのソースコード内の位置で、主にエラー報告に使用されます。これは、ファイル名/行番号/列のタプルを強化したようなものです。開始/終了点を保持し、さらにマクロ展開やコンパイラーによる脱糖も追跡します。しかも、すべてが数バイトに詰め込まれています(実際には、テーブルへのインデックスです)。詳細は Span データ型を参照してください。
subst 👎型や定数式などの内部にあるジェネリックパラメーターを、substs を供給することで具体的なジェネリック引数へ 置換する 行為です。現在のコンパイラーでは インスタンス化 と呼ばれています。
substs 👎与えられたジェネリックアイテムに対する 置換(例: HashMap<i32, u32> における i32u32)です。現在のコンパイラーでは ジェネリック引数 のリストと呼ばれています(ただし、厳密に言えばこれら2つの概念は異なることに注意してください。文献を参照してください)。
sysroot実行時にコンパイラーによって読み込まれるビルド成果物のディレクトリです(詳細)。
タグenum/ジェネレーターの「タグ」は、アクティブなバリアント/状態の 判別子 をエンコードします。タグは「直接的」(単にフィールド内に判別子を格納する)な場合もあれば、「ニッチ」 を使用する場合もあります。
TAIT型エイリアス impl Trait の略。RFC 2515 で導入されました。
tcxコンパイラーの主要なデータ構造である「型付けコンテキスト」(TyCtxt)の標準的な変数名です(詳細)。
'tcxTyCtxt によって使用される割り当てアリーナのライフタイムです。コンパイルセッション中にインターン化されるほとんどのデータは、このライフタイムを使用します。ただし、HIR データは例外で、'hir ライフタイムを使用します(詳細)。
トークンパースの最小単位です。トークンは字句解析後に生成されます(詳細)。
TLSスレッドローカルストレージ。各スレッドが(すべてのスレッドで変数を共有するのではなく)独自のコピーを持つように変数を定義できます。これは LLVM といくつかの相互作用があります。すべてのプラットフォームが TLS をサポートしているわけではありません。
トレイト参照、trait ref トレイトの名前と、適切なジェネリック引数のリストです(詳細)。
trans 👎translation の略で、MIR を LLVM IR に変換するコードです。codegen に改名されました
Ty型の内部表現です(詳細)。
TyCtxtコード内で tcx と呼ばれることが多いデータ構造で、セッションデータとクエリシステムへのアクセスを提供します。
UFCS 👎universal function call syntax の略で、メソッドを呼び出すための曖昧さのない構文です。この用語はもう使われていません! 完全修飾パス / 構文 を推奨します(詳細リファレンスを参照)。
非居住型値を持たない 型です。これは、正確に1つの値を持つ ZST とは同じではありません。非居住型の例は enum Foo {} で、これはバリアントを持たないため、決して作成できません。そのような値は操作対象として存在しないため、コンパイラーは非居住型を扱うコードをデッドコードとして扱うことができます。!(never 型)は非居住型です。非居住型は 空型 とも呼ばれます。
upvarクロージャの外側からクロージャによって捕捉された変数です。
変性ジェネリックパラメーターの変更がサブタイピングにどのように影響するかを決定します。たとえば、TU のサブタイプである場合、Vec<T>Vec<U> のサブタイプです。これは Vec がそのジェネリックパラメーターに対して 共変 であるためです。より一般的な説明については 背景の章 を参照してください。型チェックが変性をどのように処理するかの説明については、変性の章 を参照してください。
バリアントインデックスenum 内で、バリアントに0から始まるインデックスを割り当てることでバリアントを識別します。これは完全に内部的なものであり、ユーザーが上書きできる 「判別子」(例: enum Bool { True = 42, False = 0 })と混同しないでください。
整形式性、wfness、wf意味論的には、意味のある結果へ評価される式です。型システムにおいては、型システムの規則に従う型関連の構成要素です。
ワイドポインター追加のメタデータを持つポインター。詳細は ファットポインター を参照してください。
ZSTゼロサイズ型。値のサイズが0バイトである型です。2^0 = 1 であるため、このような型は正確に1つの値を持つことができます。たとえば、()(ユニット)は ZST です。struct Foo; も ZST です。コンパイラーは ZST の周辺でいくつかの優れた最適化を行えます。
関連項目も参照してください https://doc.rust-lang.org/reference/glossary.html#glossary

コード索引

rustc には重要なデータ構造が数多くあります。 これは、コンパイラの主要なデータ構造の一部について、 どこで詳しく学べるかを示すためのものです。

項目種類簡潔な説明宣言
BodyId構造体HIR ノード識別子の 4 種類の型のうちの 1 つHIR における識別子compiler/rustc_hir/src/hir.rs
Compiler構造体コンパイラセッションを表し、コンパイルを駆動するために使用できます。Rustc ドライバーとインターフェイスcompiler/rustc_interface/src/interface.rs
ast::Crate構造体パースされたクレートの構文レベルの表現パーサーcompiler/rustc_ast/src/ast.rs
hir::Crate構造体クレートの AST をより抽象化し、コンパイラにとって扱いやすくした形式HIRcompiler/rustc_middle/src/hir/mod.rs
DefId構造体HIR ノード識別子の 4 種類の型のうちの 1 つHIR における識別子compiler/rustc_hir/src/def_id.rs
Diag構造体エラーや lint など、コンパイラ診断のための構造体診断の出力compiler/rustc_errors/src/diagnostic.rs
DocContext構造体rustdoc がクレートを巡回してそのドキュメントを収集する際に使用する状態コンテナーRustdocsrc/librustdoc/core.rs
HirId構造体HIR ノード識別子の 4 種類の型のうちの 1 つHIR における識別子compiler/rustc_hir_id/src/lib.rs
Lexer構造体これはパース中に使用されるレキサーです。コンパイル対象の生のソースコードから文字を消費し、パーサーの残りの部分で使用する一連のトークンを生成しますパーサーcompiler/rustc_parse/src/lexer/mod.rs
NodeId構造体HIR ノード識別子の 4 種類の型のうちの 1 つ。段階的に廃止されていますHIR における識別子compiler/rustc_ast/src/ast.rs
ParamEnv構造体ジェネリックパラメーターまたは Self に関する情報で、関連項目やジェネリック項目を扱う際に有用ですパラメーター環境compiler/rustc_middle/src/ty/mod.rs
ParseSess構造体この構造体はパースセッションに関する情報を含みますパーサーcompiler/rustc_session/src/parse/parse.rs
Rib構造体名前の単一スコープを表します名前解決compiler/rustc_resolve/src/lib.rs
Session構造体コンパイルセッションに関連付けられたデータパーサー, Rustc ドライバーとインターフェイスcompiler/rustc_session/src/session.rs
SourceFile構造体SourceMap の一部です。単一のソースファイルについて、AST ノードをそのソースコードにマップします。以前は FileMap と呼ばれていましたパーサーcompiler/rustc_span/src/lib.rs
SourceMap構造体AST ノードをそのソースコードにマップします。SourceFile 群で構成されます。以前は CodeMap と呼ばれていましたパーサーcompiler/rustc_span/src/source_map.rs
Span構造体ユーザーのソースコード内の位置で、主にエラー報告に使用されます診断の出力compiler/rustc_span/src/span_encoding.rs
rustc_ast::token_stream::TokenStream構造体TokenTree 群として構成された、トークンの抽象的な列パーサー, マクロ展開compiler/rustc_ast/src/tokenstream.rs
TraitDef構造体この構造体は、型情報を含むトレイトの定義を含みますty モジュールcompiler/rustc_middle/src/ty/trait_def.rs
TraitRef構造体トレイトとその入力型の組み合わせ(例: P0: Trait<P1...Pn>トレイト解決: ゴールと節compiler/rustc_middle/src/ty/sty.rs
Ty<'tcx>構造体これは型チェックに使用される型の内部表現です型チェックcompiler/rustc_middle/src/ty/mod.rs
TyCtxt<'tcx>構造体「型付けコンテキスト」です。これはコンパイラの中心的なデータ構造です。あらゆる種類のクエリを実行するために使用するコンテキストですty モジュールcompiler/rustc_middle/src/ty/context.rs

コンパイラ講義シリーズ

これらは、さまざまな専門家がコンパイラの各部分について解説する動画です:

全般

Rust Analyzer

型システム

クロージャ

Chalk

Polonius

Miri

非同期

コード生成

Rust 参考文献

これは Rust に関連する資料の読書リストです。 Rust の設計に、これまでのある時点で影響を与えた先行研究、および Rust に関する出版物を含みます。

型システム

並行性

その他

Rust に関する 論文

Rust におけるユーモア

ユーモアのセンスがないプロジェクトなんてあるでしょうか? そして率直に言って、これらのいくつかは 啓発的ではないでしょうか?