Rust のプリミティブ型はどこからやって来るか
Rust のプリミティブ型は予約語ではなく普通の識別子である。どのようにこれが名前解決されるのかを調べた。
rust
2021-10-02
Qiita から移植
この記事は Qiita から移植してきたものです。 元 URL:
https://qiita.com/nsfisis/items/9a429432258bbcd6c565
前置き
Rust
において、プリミティブ型の名前は予約語でない。したがって、次のコードは合法である。
では、普段単に bool と書いたとき、この bool
は一体どこから来ているのか。rustc のソースを追ってみた。
前提知識: 一般的なコンパイラの構造、用語。rustc そのものの知識は不要
(というよりも筆者自身がよく知らない)
調査
調査に使用したソース (調査時点での最新 master)
https://github.com/rust-lang/rust/tree/511ed9f2356af365ad8affe046b3dd33f7ac3c98
どのようにして調べるか。rustc
の構造には詳しくないため、すぐに当たりをつけるのは難しい。
大雑把な構造としては、compiler フォルダ以下に rustc_*
という名前のクレートが数十個入っている。これがどうやら rustc
コマンドの実装部のようだ。
rustc はセルフホストされている (= rustc 自身が Rust で書かれている)
ので、bool や char
などで適当に検索をかけてもノイズが多すぎて話にならない。
しかし、お誂え向きなことに i128/u128
というコンパイラ自身が使うことがなさそうな型が存在するのでこれを使って
git grep してみる。
165
程度であれば探すことができそうだ。今回は、クレート名を見ておおよその当たりをつけた。
rustc_resolve
というのはいかにも名前解決を担いそうなクレート名である。該当箇所を見てみる。
,
}
impl PrimitiveTypeTable {
fn new() -> PrimitiveTypeTable {
let mut table = FxHashMap::default();
table.insert(sym::bool, Bool);
table.insert(sym::char, Char);
table.insert(sym::f32, Float(FloatTy::F32));
table.insert(sym::f64, Float(FloatTy::F64));
table.insert(sym::isize, Int(IntTy::Isize));
table.insert(sym::i8, Int(IntTy::I8));
table.insert(sym::i16, Int(IntTy::I16));
table.insert(sym::i32, Int(IntTy::I32));
table.insert(sym::i64, Int(IntTy::I64));
table.insert(sym::i128, Int(IntTy::I128));
table.insert(sym::str, Str);
table.insert(sym::usize, Uint(UintTy::Usize));
table.insert(sym::u8, Uint(UintTy::U8));
table.insert(sym::u16, Uint(UintTy::U16));
table.insert(sym::u32, Uint(UintTy::U32));
table.insert(sym::u64, Uint(UintTy::U64));
table.insert(sym::u128, Uint(UintTy::U128));
Self { primitive_types: table }
}
}
]]>
これは初めに列挙したプリミティブ型の一覧と一致している。doc comment
にも、
All other types are defined somewhere and possibly imported, but the
primitive ones need special handling, since they have no place of
origin.
とある。次はこの struct
の使用箇所を追う。追うと言っても使われている箇所は次の一箇所しかない。なお説明に不要な箇所は大きく削っている。
Option> {
// (略)
if ns == TypeNS {
if let Some(prim_ty) = self.primitive_type_table.primitive_types.get(&ident.name) {
let binding =
(Res::PrimTy(*prim_ty), ty::Visibility::Public, DUMMY_SP, ExpnId::root())
.to_name_binding(self.arenas);
return Some(LexicalScopeBinding::Item(binding));
}
}
None
}
]]>
関数名や doc comment が示している通り、この関数は識別子 (identifier,
ident) を現在のレキシカルスコープ内で解決 (resolve) する。
if ns == TypeNS のブロック内では、primitive_type_table (上記の
PrimitiveTypeTable::new() で作られた変数) に含まれている識別子
(bool、i32 など)
かどうか判定し、そうであればそれに紐づけられたプリミティブ型を返している。
なお、ns は「名前空間」を示す変数である。Rust
における名前空間はC言語におけるそれとほとんど同じで、今探している名前が関数名/変数名なのか型なのかマクロなのかを区別している。この
if
は、プリミティブ型に解決されるのは型を探しているときだけだ、と言っている。
重要なのは、これが resolve_ident_in_lexical_scope()
の最後に書かれている点である。つまり、最初に挙げたプリミティブ型の識別子は、「名前解決の最終段階で」、「他に同名の型が見つかっていなければ」プリミティブ型として解決される。
動作がわかったところで、例として次のコードを考える。
ここで main() の bool は struct bool
として解決される。なぜなら、プリミティブ型の判定をする前に bool
という名前の別の型が見つかるからだ。
まとめ
Rust
のプリミティブ型は予約語ではない。名前解決の最終段階で特別扱いされ、他に同名の型が見つかっていなければ対応するプリミティブ型に解決される。