更新履歴

  1. : Qiita から移植

この記事は Qiita から移植してきたものです。 元 URL: https://qiita.com/nsfisis/items/9a429432258bbcd6c565


前置き

Rust において、プリミティブ型の名前は予約語でない。したがって、次のコードは合法である。

#![allow(non_camel_case_types)]
#![allow(dead_code)]

struct bool;
struct char;
struct i8;
struct i16;
struct i32;
struct i64;
struct i128;
struct isize;
struct u8;
struct u16;
struct u32;
struct u64;
struct u128;
struct usize;
struct f32;
struct f64;
struct str;

では、普段単に bool と書いたとき、この bool は一体どこから来ているのか。rustc のソースを追ってみた。

前提知識: 一般的なコンパイラの構造、用語。rustc そのものの知識は不要 (というよりも筆者自身がよく知らない)

調査

調査に使用したソース (調査時点での最新 master)

どのようにして調べるか。rustc の構造には詳しくないため、すぐに当たりをつけるのは難しい。

大雑把な構造としては、compiler フォルダ以下に rustc_* という名前のクレートが数十個入っている。これがどうやら rustc コマンドの実装部のようだ。

rustc はセルフホストされている (= rustc 自身が Rust で書かれている) ので、boolchar などで適当に検索をかけてもノイズが多すぎて話にならない。 しかし、お誂え向きなことに i128/u128 というコンパイラ自身が使うことがなさそうな型が存在するのでこれを使って git grep してみる。

$ git grep "\bi128\b" | wc      # i128
     165    1069   15790

$ git grep "\bu128\b" | wc      # u128
     293    2127   26667

$ git grep "\bbool\b" | wc      # cf. bool の結果
    3563   23577  294659

165 程度であれば探すことができそうだ。今回は、クレート名を見ておおよその当たりをつけた。

$ git grep "\bi128\b"
...
rustc_resolve/src/lib.rs:        table.insert(sym::i128, Int(IntTy::I128));
...

rustc_resolve というのはいかにも名前解決を担いそうなクレート名である。該当箇所を見てみる。

/// Interns the names of the primitive types.
///
/// All other types are defined somewhere and possibly imported, but the primitive ones need
/// special handling, since they have no place of origin.
struct PrimitiveTypeTable {
    primitive_types: FxHashMap<Symbol, PrimTy>,
}

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 の使用箇所を追う。追うと言っても使われている箇所は次の一箇所しかない。なお説明に不要な箇所は大きく削っている。

    /// This resolves the identifier `ident` in the namespace `ns` in the current lexical scope.
    /// (略)
    fn resolve_ident_in_lexical_scope(
        &mut self,
        mut ident: Ident,
        ns: Namespace,
        // (略)
    ) -> Option<LexicalScopeBinding<'a>> {
        // (略)

        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() で作られた変数) に含まれている識別子 (booli32 など) かどうか判定し、そうであればそれに紐づけられたプリミティブ型を返している。

なお、ns は「名前空間」を示す変数である。Rust における名前空間はC言語におけるそれとほとんど同じで、今探している名前が関数名/変数名なのか型なのかマクロなのかを区別している。この if は、プリミティブ型に解決されるのは型を探しているときだけだ、と言っている。

重要なのは、これが resolve_ident_in_lexical_scope() の最後に書かれている点である。つまり、最初に挙げたプリミティブ型の識別子は、「名前解決の最終段階で」、「他に同名の型が見つかっていなければ」プリミティブ型として解決される。

動作がわかったところで、例として次のコードを考える。

#![allow(non_camel_case_types)]

struct bool;

fn main() {
    let _: bool = bool;
}

ここで main()boolstruct bool として解決される。なぜなら、プリミティブ型の判定をする前に bool という名前の別の型が見つかるからだ。

まとめ

Rust のプリミティブ型は予約語ではない。名前解決の最終段階で特別扱いされ、他に同名の型が見つかっていなければ対応するプリミティブ型に解決される。