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::apply_markで行う。 ctx.apply_mark(mark) は以下を返す:

  • ctxmark で終わるなら、それを削除した文脈を返す。
  • それ以外の場合は、 ctxmark を追加した文脈を返す。

apply_mark を全ての識別子に適用する処理が syntax::ext::expand::Marker に実装されている。これは主に、次の2箇所で使われている。

ローカル変数の衛生性の実現例

以下のようなプログラムを考える。

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つの衛生性のうち、識別子の衝突を防ぐ衛生性は、識別子に文脈を付与することによって巧妙に実現されている。