Rustのモジュールを詳細に理解する(7) 名前解決とマクロ
概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。
- モジュール入門
- 名前解決の役割と用語
- モジュールグラフの構造
- インポート解決
- 可視性
- パス解決
- 名前解決とマクロ
本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。
注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。
衛生性とは
衛生性 (hygiene) とは、マクロ内外の変数をマクロ展開前の構造にもとづいてレキシカルスコープで解決できるようにする仕組みです。
macro_rules! foo { ($e:expr) => { let mut x = 42; $e; eprintln!("x in foo = {}", x); // 42 } } fn main() { let mut x = 53; foo!(x = 64); eprintln!("x in main = {}", x); // 64 }
上のコードでは、 main
が導入した束縛 x
と foo
が導入した束縛 x
は区別されます。
マクロ展開を追跡するために、コンパイラはマクロ展開ひとつひとつに Mark
という番号を割り振ります。たとえば上の例では foo!
が1回、 eprintln!
が2回展開されているので少なくとも3つの Mark
が割り振られることになります。そして、衛生性を追跡するために、マクロ展開境界をまたいだ識別子には SyntaxContext
という情報が割り当てられます。これは実質的には Mark
の列 (より正確には Mark
と透明度の対の列) で、今ある位置に至るまでにまたいだマクロ展開境界の一覧が表現されます。RustのASTの識別子やキーワードには、この SyntaxContext
が付随しています。
上の例では、 foo!(x = 64)
を展開するときに foo
内の識別子にだけマークが付与されて以下のようになります。
macro_rules! foo { ($e:expr) => { let mut x = 42; $e; eprintln!("x in foo = {}", x); // 42 } } fn main() { let mut x = 53; // macro[1] { // of foo!() let mut x /* [1] */ = 42; x = 64; eprintln!("x in foo = {}", x /* [1] */); // 42 // } eprintln!("x in main = {}", x); // 64 }
正確には let
などのキーワードにもマークが付与されていますが、これはパースの際に無視されます。
レガシーマクロとモダンマクロ
macro_rules!
によるマクロはグローバル定義に対する衛生性を持ちません。
mod m1 { const X: i32 = 42; #[macro_export] macro_rules! foo { () => { X } } } const X: i32 = 53; fn main() { dbg!(foo!()); // 53 }
一方、RFC1584の定める宣言マクロ2.0ではグローバル定義に対する衛生性も導入されます。
#![feature(decl_macro)] mod m1 { const X: i32 = 42; pub macro foo() { X } } const X: i32 = 53; fn main() { dbg!(m1::foo!()); // 42 }
これらを区別するために、Rustのマクロ衛生性には透明度という概念があります。
Transparent
(透明): このマークを衛生性のために使わない。#[rustc_transparent_macro]
のついた宣言マクロ2.0や、手続きマクロの衛生性オプトアウトで使われる。
SemiTransparent
(半透明): ローカル束縛を探すときは考慮するが、グローバル定義を探すときは考慮しない。macro_rules!
で使われる。
Opaque
(不透明): このマークを衛生性のために使う。- 宣言マクロ2.0や手続きマクロのデフォルト挙動。
少し前の段落で SyntaxContext
は Mark
の列だと書きましたが、 (Mark, Transparency)
の列であるとみなすほうが正確です。そして SyntaxContext
には以下の2つのメソッドが備わっています。
modern
: Opaqueなマークだけを取り出したSyntaxContext
を返す。modern_and_legacy
: OpaqueまたはSemiTransparentなマークだけを取り出したSyntaxContext
を返す。
名前解決時は、識別子を比較する前に modern
/modern_and_legacy
のいずれかを呼んで正規化をしています。基本的には modern
が使われます。modern_and_legacy
が呼ばれる場面として重要なのが「値名前空間でレキシカルスコープ解決をするとき」です。これはまさに、旧来の macro_rules!
で衛生性が有効だった場面に他なりません。
さて、modern形式のマクロ(手続きマクロ、宣言マクロ2.0)では、マクロ内の識別子がマクロ呼び出しの外側 (caller-site) のスコープで解決されることはありません。 (Markがずれるため)
かわりに、通常のスコープ内探索が終わったあと、マクロ定義の外側 (def-site) のスコープで探索を再開する仕組みになっています。(前の記事でも少し言及していました) これにより、「マクロ展開前の構造に基づいてレキシカルスコープで解決する」という衛生性の目標をより広く達成できるようになっています。
インポート解決とマクロ
Rustのマクロは元々、構文解析の後・名前解決の前に展開されていました。その名残りもあって、 #[macro_export]
のない macro_rules!
は今でも後方定義を許容しません。
foo!(); //~ERROR cannot find macro macro_rules! foo { () => { fn f() {} } }
現在は、マクロ解決は部分的に名前解決に統合されています。そのおかげで外部マクロを use
したり、::
で参照したりできています。
use log::debug; fn main() { debug!("foo"); log::info!("bar"); }
これはつまり、インポート解決をしなければマクロが展開できない場合があるということです。一方で、マクロは任意のアイテムに展開できるので、マクロの展開により新たなインポート解決が必要になる可能性もあります。
インポート解決とマクロ: コンパイラアーキテクチャ
Rustコンパイラは syntax
と rustc
という2大クレートと、その周辺クレートから成り立っています。 rustc
は syntax
に依存し、 rustc_*
は rustc
に依存し、 rustc_driver
が rustc_*
に依存するという形でコードベースが分割されています。
ここで、マクロの展開は syntax
の責務であるのに対し、インポート解決は rustc
の責務です。この部分を繋ぐために、 syntax
側にDIのようなインターフェースが用意されています。
pub trait Resolver { fn next_node_id(&mut self) -> ast::NodeId; fn get_module_scope(&mut self, id: ast::NodeId) -> Mark; fn resolve_dollar_crates(&mut self, fragment: &AstFragment); fn visit_ast_fragment_with_placeholders(&mut self, mark: Mark, fragment: &AstFragment, derives: &[Mark]); fn add_builtin(&mut self, ident: ast::Ident, ext: Lrc<SyntaxExtension>); fn resolve_imports(&mut self); fn resolve_macro_invocation(&mut self, invoc: &Invocation, invoc_id: Mark, force: bool) -> Result<Option<Lrc<SyntaxExtension>>, Determinacy>; fn resolve_macro_path(&mut self, path: &ast::Path, kind: MacroKind, invoc_id: Mark, derives_in_scope: Vec<ast::Path>, force: bool) -> Result<Lrc<SyntaxExtension>, Determinacy>; fn check_unused_macros(&self); }
syntax
はこのような「パスを渡すとマクロ定義への解決を試みてくれるオラクル」を受け取り、これを使いながらマクロを展開していきます。
依存性の実際の注入は rustc_driver
で行われています。名前解決器をあらかじめ生成した上で、
- 名前解決器にビルトインマクロを登録する。
- 名前解決器を使って、マクロを展開する。
- 名前解決器側だけでできる処理 (インポート解決のファイナライズ + パス解決) をする。
という3段階の処理をしています。
インポート解決とマクロ: シャドウイングの処理
未展開のマクロは名前解決にとってはいくつか困った問題があります。
- 未展開マクロは任意の名前を追加で定義しうる。
- 未展開マクロは
extern crate
を生成しうる。 - 未展開マクロは
#[macro_export] macro_rules!{}
を生成しうる。
未展開マクロは任意の名前を追加で定義しうる
このため、未展開マクロの残っているモジュールでは、ある識別子が存在しないことを結論づけることはできないという追加規則が必要です。
ただし、シャドウイングに関してはRFC1560に追加の規則が定義されています。
Caveat: an explicit name which is defined by the expansion of a macro does not shadow implicit names.
これにより、globインポートのシャドウイングを検討するときにマクロを気にする必要はなくなっています。
未展開マクロは extern crate
を生成しうる
extern crate
は名前解決に対するグローバルな影響があります。クレートルートで extern crate serde as serde2;
とすることにより serde2::Serialize
というパスが解決可能になってしまうからです。
そのため、ExternPreludeの解決時はクレートルートに未解決のマクロがある場合、ExternPreludeの非存在を結論づけないという追加規則が必要です。
未展開マクロは #[macro_export] macro_rules!{}
を生成しうる
#[macro_export] macro_rules!{}
を使うと、任意の位置からクレートルートに名前を挿入できてしまいます。
このせいで名前解決が進まないのは本望ではないので、マクロ内定義マクロのパスによる参照を禁止するという方向で対策されています。これにより、マクロ解決時に未展開マクロを気にする必要はほぼなくなっています。
まとめ
Rustはマクロに衛生性を導入しており、そのせいで識別子に対する処理が複雑になっています。さらに歴史的な事情もあって衛生性には3段階のレベルがあるので、このことをきちんと理解しつつ名前解決の挙動を追うのは大変です。また、インポート解決とマクロ展開が相互作用するのも、この部分を複雑にしている一因となっています。
とはいえ、こういった細かい配慮によって全体としてはそれなりに一貫した体感のモジュールシステムが提供されており、一般的なRustプログラマーはここまで複雑な問題を気にする必要がないようになっていると思います。
ここまで7日間かけて名前解決についてまとめましたが、2年前に同じ部分を調べたときに比べて全容がより複雑になっていると感じます。その一方で、当時からあった細かい仕様や実装についてもより深く理解できるようになったので、この記事をまとめた甲斐があると思います。
この記事を通しで読む人がどれくらいいるかはわかりませんが、きちんと読めば(すぐには役立たなくても)Rustでのプログラミングを底から支える力になると思います。また、何か細かい仕様で困った人にとっての辞書としても使える記事に仕上がったと思います。
関連RFC
Rust2018以前の仕様に関連したRFC
- RFC0063 enforce module directory structure more strictly ...
foo.rs
からfoo/bar.rs
への参照を禁止 (Rust2018でリバート) - RFC0116 No Module Shadowing ... 同一モジュール内でuseが他のアイテムをシャドウできる規則の廃止
- RFC0136 Ban private items in public APIs ... 非公開な型をもつ公開APIの禁止
- RFC0169 Replace
use id = path;
withuse path as id;
... 別名インポートの構文が現在のものになった - RFC0234 Add enum variants to the type namespace ... 構造体と異なり、列挙型のバリアントは全ての種類で型・値の両方の名前空間を専有する
- RFC0385 Module system cleanup ... 2014年のモジュール改革。
extern crate
の動作をuse
と一様にする。 - RFC0390 Enum namespacing ... 列挙型のバリアントを
variant
ではなくEnum::Variant
で参照するようにする。 - RFC0453 Macro reform ... 宣言マクロの整理。
#[macro_use]
,#[macro_export]
,#[macro_reexport]
と$crate
が導入された。 - RFC0459 Disallow type/lifetime parameter shadowing ... 型パラメーターとライフタイムパラメーターのシャドーイングの禁止
- RFC0501 Consistent no-prelude attribute ...
#[no_prelude]
は当該mod自身にしか影響しない - RFC0532 Self in use ... RFC0168の改訂。
{}
によるインポートでself
を許可する。 - RFC0572 Feature gate unused attributes ... 未知の属性の禁止
- RFC0735 Allow inherent impls anywhere ... RFC0155のリバート。固有implを当該の型と同じモジュールに置く必要がなくなった
- RFC0736 Privacy-respecting Functional Record Update ... 構造体アップデート構文で引き継がれるフィールドの可視性もチェックされる
- RFC0940 Disallow hyphens in Rust crate names ...
extern crate "tokio-threadpool";
のような記法を廃止し、自動的にアンダースコアに変換されるようにする - RFC1184 Stabilize the
#![no_std]
attribute ...#![no_std]
の安定化 - RFC1191 Add a HIR to the compiler ... ASTの次段階としてのHIR(高レベル中間表現)の導入。AST→HIRの途中で名前解決を行うと言及されている
- RFC1219 use_group_as ...
use {}
とuse as
の併用を可能にする - RFC1260 Allow a re-export for
main
...use foo::main;
のような形でmain関数を定義してもよい - RFC1422 pub(restricted) ... 公開と非公開の2択ではなく、
pub(in)
構文で中間の可視性(特定のモジュールにのみ公開)を選択できるようにする - RFC1560 Changes to name resolution ... 2016年のモジュール改革。名前解決に関する細かい仕様を改めることで、将来の機能追加 (RFC1422やRFC1561) に耐えられるようにする
- RFC1566 proc macros ... 手続きマクロの拡充。手続きマクロを
extern crate
したときの(当時の)挙動が指定されている。RFC1681のほうが前にマージされていることに注意。 - RFC1681 Procedural macros 1.1 ... 手続きマクロの拡充。RFC1566のほうが後にマージされていることに注意。
Rust2018に関連した(または2018年頃に実装された)RFC
- RFC1561 Macro naming and modularization ... RFC1584の一部で、マクロを名前解決に統合することを提案している
- RFC2103 tool attributes ... ツールのためのモジュール化された属性。これにより
rustfmt
やclippy
などのextern preludeモジュールが導入されることになった。 - RFC2126 Path Clarity ... Rust2018のモジュール改革を提案したRFC
- RFC2128 Nested groups in imports ...
use {}
のネストを可能にする - RFC2166 impl-only-use ...
use FooExt as _;
のような無名インポートを可能にする。
実装途上のRFC
- RFC1584 Macros 2.0 ... 宣言マクロの刷新を提案したRFC。RFC1561の親RFC
- RFC1977 public/private dependencies ... 依存関係にprivate/publicの別をつける
- RFC2145 Type privacy and private-in-public lints ... RFC0136 (public-in-private) の規則を整理し、lintへ降格する
- RFC2338 type alias enum variants ... 列挙型の型エイリアスからもバリアント名を参照できるようにする
- RFC2526 Support underscores as constant names ...
const _: i32 = 42;
のような無名定数の定義を可能にする