Rustの基本型のメソッドはどこで定義されているか

概要: Rustの基本型そのものはコンパイラで特別に定義されている。では型に関連づけられたメソッドはどこにあるのか。

固有メソッドのありか

固有メソッドは core の各所で定義されている。例えば i32 の固有実装は core::numに定義されている

#[lang = "i32"]
impl i32 {
    ...
}

ここで、 #[lang = "i32"] に注意する必要がある。基本型の固有実装にはそれぞれlang item markerが割り当てられている。実際の固有メソッド解決はこのlang item marker経由で行われているようである。

なお、数値型以外の基本型の固有実装は以下の場所に定義されている。

トレイト実装のありか

トレイト実装は固有実装のような特別扱いを受けない。固有実装と同じように基本型ごとにまとめられたモジュールで定義されるか、各トレイトの近くで定義されることが多いようである。

演算子のありか

トレイト実装のなかでも、いくつかの演算子の実装は特別な扱いを受ける。というのも、標準ライブラリ内での基本型の演算子の実装はおおむね以下のような形になっているのである。

impl core::ops::Add<i32> for i32 {
    type Output = i32;
    fn add(self, other: i32) -> i32 {
        self + other
    }
}

これは一見すると循環論法であるが、そうではない。実は話が逆で、ユーザー定義型の場合は +std::ops::Add の糖衣構文だが、基本型については + が組込みで、それを用いて std::ops::Add の実装が与えられているというわけである。

実は、Rust 1.17.0 では、上のような実装をそのまま用いて型検査を行う。つまりこの時点では実際に self + other<i32 as core::ops::Add<i32>>::add(self, other) と認識されている。

もちろん、そのままコード生成に進むと無限ループが生成されてしまうので、型検査のあとに、メソッド解決の情報を削除することでワークアラウンドしている。ここで削除されるのは、スカラーに対する1項演算か、スカラー同士の2項演算である。ここでいうスカラーとは、 bool, char, 整数、浮動小数点数、関数ポインタ、生ポインタである

演算子はどう変換されるか

続いて、スカラーに対する演算子は、HIRからHAIRへの変換時に組込みの演算子に変換される

さらにHAIRはMIRに変換されるが、スカラーに対する基本演算はどれも右辺値を生成するため、最終的には as_rvalue のビルダーで処理される

コードを見るとわかるように、スカラーに対する基本演算にオーバーフローチェックが入るのはこのタイミングである。オーバーフローチェックが生成されるかどうかは次の基準で決定されている

  • コンパイラオプションでオーバーフローチェックを有効にするよう指定された。 または
  • 定数が要求されている文脈である。 または
  • #[rustc_inherit_overflow_checks] が指定された。

ただしコンパイラオプションは以下のように解釈される

  • -C overflow-checks があれば、そのyesまたはnoが採用される。
  • 上が指定されておらず、 -Z force-overflow-checks が指定されていれば、そのyesまたはnoが採用される。
  • 上のいずれも指定されていない場合は、 -C debug-assertions の値が採用される。

まとめ

Rustの基本型のうち、固有実装はlang item経由で発見される。トレイト実装は一般的な仕組みを使っているが、演算子だけは例外で、HIR→MIRへの変換時に組込みの演算に変換される。