Rustマクロの衛生性はどのように実現されているか(1/2) 識別子に関する衛生性
概要: Rustマクロは2つの意味で衛生的である。その衛生性の説明とともに、それを実現するためのコンパイラの仕組みを説明する。
Rustマクロの2つの衛生性
Rustマクロ (ja) は次の2つの意味で衛生的(hygienic; 健全ともいう)である。
- マクロ内で導入される変数名と、マクロ呼び出し側の変数名が衝突しない。(Lispマクロの意味での衛生性)
- 構文の優先順位の違いによる非直感的な挙動が発生しない。
この記事では、識別子に関する衛生性を説明する。(構文の優先度に関する衛生性については次記事を参照)
識別子に関する衛生性とは
次のようなプログラムが直感的な動作をするのが、識別子に関する衛生性である。
macro_rules! copy_swap { ($x:expr, $y:expr) => {{ let t = $x; $x = $y; $y = t; }}; } fn main() { let (mut r, mut s, mut t, mut u) = (30, 40, 50, 60); copy_swap!(r, s); println!("{}, {}", r, s); copy_swap!(t, u); println!("{}, {}", t, u); }
ここで、 copy_swap!(t, u)
を単純に展開するようなマクロ展開器の場合、以下のようなコードが生成されてしまう。
{ let t = t; t = u; u = t; }
このコードを実行するとt=u=60になってしまうが、Rustではそうはならず、あたかもマクロ呼び出しの実引数がレキシカルスコープを持っているかのように振る舞う。
構文文脈によるローカル変数名の衛生性の実現
衛生性を実現する基本アイデアは、syntax::ast::Ident
の定義を見るとわかる。
/// An identifier contains a Name (index into the interner /// table) and a SyntaxContext to track renaming and /// macro expansion per Flatt et al., "Macros That Work Together" #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct Ident { pub name: Symbol, pub ctxt: SyntaxContext }
なお、ここに出てくる Symbol
はinternされた文字列である。
つまり、この SyntaxContext
というもので、同名の識別子を区別するというのが基本アイデアである。
SyntaxContextの実体
結論から言うと、 SyntaxContext
は、 Vec<Mark>
をinternしたものと考えてよい。ただし、 Mark
はマクロ呼び出しを識別するための整数である。
syntax::ext::hygiene::SyntaxContext
は整数だが、これはinternパターンで特定の syntax::ext::hygiene::SyntaxContextData
に紐付けられている。
#[derive(Copy, Clone)] pub struct SyntaxContextData { pub outer_mark: Mark, pub prev_ctxt: SyntaxContext, }
またここに出てくる syntax::ext::hygiene::Mark
も整数である。これはgensymパターンで、マクロ展開ごとに異なる値が割り振られるようになっている。
SyntaxContext
は次の2つの方法で生成される。
syntax::ext::hygiene::SyntaxContext::empty
… 空の文脈を返す。実体はsyntax::ext::hygiene::SyntaxContextData::new
で生成されている。outer_mark
は0番(crate全体をあらわす特別なマーク)で、prev_ctxt
はこれ自身を参照している。syntax::ext::hygiene::SyntaxContext::apply_mark
… markを1つ追加した文脈を生成する。実体はこの関数内で生成されている。outer_mark
とprev_ctxt
は関数の引数self
,mark
に一致する。
生成済みの構文文脈全体は根つき木をなすが、木そのものには特に興味はない。
文脈の追加と削除
文脈の追加と削除はsyntax::ext::hygiene::SyntaxContext::apply_mark
で行う。 ctx.apply_mark(mark)
は以下を返す:
ctx
がmark
で終わるなら、それを削除した文脈を返す。- それ以外の場合は、
ctx
にmark
を追加した文脈を返す。
apply_mark
を全ての識別子に適用する処理が syntax::ext::expand::Marker
に実装されている。これは主に、次の2箇所で使われている。
syntax::ext::expand
396行目: マクロ展開前のトークンツリー列にapply_mark
を適用している。syntax::ext::expand
483行目: マクロ展開後のASTにapply_mark
を再び適用している。
ローカル変数の衛生性の実現例
以下のようなプログラムを考える。
macro_rules! foo { ($x:ident) => { let mut x = 0; $x = x; x = 2; } } fn main() { let mut x = 1; foo!(x); println!("{}", x); }
これは0を出力する。
以下、これがどのように展開されるかを、擬似的なRustコードを用いて説明する。
まず、 macro_rules!
はマクロ展開であるから、これにmarkが割り当てられる。これを A
とおく。すると、 macro_rules!
内の解析前に、識別子には以下のように文脈が付与される。
macro_rules! foo { ($x:ident) => { let mut x@A = 0; $x = x@A; x@A = 2; } }
macro_rules!
はASTに展開されず、単に内部で構文拡張DBに記録されるだけなので、展開後の後処理で上の文脈が削除されるわけではない。
これを踏まえてmain内のマクロ呼び出しが展開される。まずこの foo!
の呼び出しにmarkが割り当てられる。
fn main() { let mut x = 1; /* Mark B */ foo!(x); println!("{}", x); }
これに基づいて、まず foo!
の呼び出し中に出現する識別子に B
が付与される。
fn main() { let mut x = 1; @B[foo!(x@B);] println!("{}", x); }
この状態で foo!
が展開される。
fn main() { let mut x = 1; @B[ let mut x@A = 0; x@B = x@A; x@A = 2; ] println!("{}", x); }
展開後の列に対する後処理として B
が追加または削除される。
fn main() { let mut x = 1; let mut x@A@B = 0; x = x@A@B; x@A@B = 2; println!("{}", x); }
これにより識別子の衝突が回避される。
非ローカルなアイテムの衛生性を実現する $crate
置換
ここまで紹介した構文文脈は、ローカル変数の衛生性の実現に利用される。非ローカルなアイテムについては、マクロ定義で $crate
を用いることで解決する。これは、 $crate
が書かれた時点のcrateの最上位モジュールを指す。
$crate
は、ドル記号の構文解析時に特別扱いされ、 $crate
という特殊な名前をもった識別子として定義される。 (syntax::parse::parser
2558行目)
この $crate
は名前解決時に発見され、この識別子が定義された当時のmarkからcrate名が復元される。(rustc_resolve
2426行目
まとめ
Rustマクロの2つの衛生性のうち、識別子の衝突を防ぐ衛生性は、識別子に文脈を付与することによって巧妙に実現されている。