Day 074 — 2026-10-14 | マクロ① macro_rules! ⑲ 衛生性の 3 種類を同じ 7 ケースで比べる:macro_rules!(mixed site)と proc-macro の Span::mixed_site() は 7 ケース中 7 件で結果も位置も一致、Span::call_site() は 7 件すべて名前が通り、捕獲の事故(K7)では利用者の n = 1 が黙ってマクロの n = 100 に置き換わる(出力 200・警告 0 件)。def_site は stable で E0658

2026-10-14 🟣 上級者 / Phase 3 概念理解 マクロ①(宣言的マクロ macro_rules!)
フェーズ: 上級者 (Phase 3) | 問題タイプ: 概念理解 | テーマ: マクロ①(宣言的マクロ macro_rules!)

📚 背景知識(読んでから問題へ)

Day 073 で、ガードに ev == Ev::Fail(g08)や self == Door::Locked(g09)と書くと、展開後のコードには ev も self も書いてあるのに E0425・E0424 になることを確かめました。rustc の help は "not accessible due to macro hygiene" と言います。一方、ctx c: Ctx; で利用者が名付けた c はガードから見えました。

これが衛生性(hygiene)です。マクロが作った名前と利用者が書いた名前を、字面が同じでも「別の名前」として扱う仕組みです。Rust には 3 種類あります。

種類どこで使われるか一次資料の定義(2026-10-14 取得)
mixed sitemacro_rules! の既定、proc-macro の Span::mixed_site()(1.45.0 で安定)Reference「Macros by example › Hygiene」: "loop labels, block labels, and local variables are looked up at the macro definition site while other symbols are looked up at the macro invocation site"
call siteproc-macro の既定(Span::call_site()、1.29.0 で安定。quote! の既定もこれ)proc_macro docs: "Identifiers created with this span will be resolved as if they were written directly at the macro call location"
def siteproc-macro の Span::def_site()proc_macro docs: "A span that resolves at the macro definition site." — nightly 専用(feature proc_macro_def_site、tracking issue #54724)

Reference の Procedural macros の節は、proc-macro を一言で "Procedural macros are unhygienic" と書いています。これは既定の call_site の話で、mixed_site を自分で選べば macro_rules! と同じ規則になります。**今日はこの「同じ規則になる」を実測で確かめ、来週から始まるマクロ②(proc-macro)で fsm! を作り直すときに、どの名前にどの span を付けるかを決めます。**

Rust がなぜ衛生性を持つのかは、所有権と同じ発想で説明できます。C のプリプロセッサは文字列置換なので、マクロ内の tmp が利用者の tmp を黙って奪っても、コンパイラには区別する手段がありません。Rust は名前に「どの展開から来たか」を持たせ、黙って起きる取り違えを、名前解決のエラーに変える。実行時の事故をコンパイル時に移すという点で、借用チェッカーと同じ側の設計です。

📝 問題

環境: rustc 1.99.0(stable)と rustc 1.85.1、Apple M1・8GB。マクロは利用者と別のクレートに置きます(hygmr = macro_rules!、hygpm = 依存 0 の proc-macro)。proc-macro 側は、マクロが自分で書くトークンだけ span を call_site または mixed_site に付け替え、利用者のトークンは span をそのまま運びます。

比べる 7 ケース(+対照 1)

#マクロが展開するもの利用者が書くもの名前が通れば出る値
K1{ let ev = 7u32; <利用者> }ev + 18
K2'outer: loop { <利用者>; break 'outer 0u32; }break 'outer 1u321
K3{ fn helper() -> u32 { 9 } <利用者> }helper()9
K4x + 1(マクロ本体が呼び出し側のローカルを名指し)呼び出し前に let x = 41u32;42
K5caller_fn() + 1(マクロ本体が呼び出し側の関数を名指し)fn caller_fn() -> u32 { 41 }42
K6impl W { pub fn get(self) -> u32 { <利用者> } }self.0(W(5).get() で呼ぶ)5
K7{ let n = 100u32; n + <利用者> }呼び出し前に let n = 1u32;、渡すのは n101 か 200
K0{ let $name = 7u32; <利用者> }(名前を利用者が渡す。macro_rules! のみ)mr_named!(ev, ev + 1)8

K1 のケースファイル(他も同じ形で 4〜5 行):

fn main() {
    let v: u32 = hygmr::mr_local!(ev + 1);   // hygpm::cs_local! / hygpm::ms_local! に差し替える
    println!("{v}");
}

Q1: 3 × 7 の表を予想する

macro_rules!(mr)・proc-macro call_site(cs)・proc-macro mixed_site(ms)の 3 方式 × K1〜K7 の 21 マスそれぞれについて、通る(値)/エラー(番号) を予想してください。def site は 4 列目として「一次資料の定義から予想するだけ」でかまいません(stable で使えるかも予想してください)。

Q2: self はどの仲間か

Reference の mixed-site の定義は「ローカル変数・ラベル・$crate は定義側、それ以外は呼び出し側」で、self について書いていません。K6 の結果から、self は「ローカル変数」「それ以外(アイテム)」のどちらとして扱われているかを答え、なぜそう扱うのが自然かを fn get(self) の self が何なのかから説明してください。

Q3: K7 の 200 は何が悪いのか

call_site の K7 は「エラーにならず 200 が出る」と仮定して(Q1 で確かめます)、

  1. 利用者から見て、なぜこれが E0425 より悪い結果なのかを 2 文で
  2. 利用者が n を println! などで別の場所でも使っていたら、コンパイラの警告は何件出るか予想を
  3. Reference が勧める回避策(__internal_foo のような名前)と mixed_site を、**利用者が実際に __n という名前を使っていた場合**で比べてください

Q4: マクロ② の fsm! で、どの名前にどの span を付けるか

proc-macro 版の fsm! が生成する名前は次の 5 つです。それぞれに call_site / mixed_site / 利用者のトークンの span をそのまま使う のどれを付けるかを、採らなかった案と、前提が変わったら判断がどう変わるかつきで決めてください。

名前生成される場所利用者のガード・アクションから見えるべきか
onimpl Door { pub fn on(...) }呼び出し側から door.on(ev, &ctx) で呼ぶ
evon の引数見えなくてよい(Day 073 の g08)
selfon の受け手見えなくてよい(g09)
con の引数見えるべき(利用者が ctx c: Ctx; で名付けた)
__count・__emit 相当の内部ヘルパーモジュールの中の関数・定数見えなくてよい

💡 ヒント

ヒント1(方向性)
  • mixed site の規則は「ローカル(変数・ラベル)は定義側、アイテム(関数・型・モジュール)は呼び出し側」の 2 分割です。K1〜K7 を、まずこの 2 つに仕分けてください
  • 「定義側で解決」とは「マクロを定義したクレートの、そのマクロの中にあるものしか見えない」という意味です。K4 の x は定義側(別クレート hygmr)にありません
ヒント2(アプローチ)
  • proc-macro では、トークンを "let ev = 7u32;".parse::<TokenStream>() で作ると span は call_site になります。TokenTree::set_span で再帰的に付け替えると mixed_site にできます(Group は中身ごと作り直す)
  • quote! は「quote! の中に書いたトークンを Span::call_site() にする」と docs に書いてあります。つまり何もしなければ proc-macro は非衛生です
  • K6 の self は、fn get(self) の引数です
ヒント3(コード骨格)
fn respan(ts: TokenStream, sp: Span) -> TokenStream {
    ts.into_iter().map(|tt| match tt {
        TokenTree::Group(g) => {
            let mut ng = Group::new(g.delimiter(), respan(g.stream(), sp));
            ng.set_span(sp);
            TokenTree::Group(ng)
        }
        mut other => { other.set_span(sp); other }
    }).collect()
}
// 利用者のトークン user は respan しない。マクロ自身のトークンだけ付け替えて連結する
fn local(user: TokenStream, sp: Span) -> TokenStream {
    brace(cat(vec![mk("let ev = 7u32;", sp), user]), sp)
}

✅ 模範解答

Q1 の答え(実測、rustc 1.99.0 と 1.85.1 で 21 マス+K0 すべて同じ結果)

#macro_rules!(mr)proc-macro mixed_site(ms)proc-macro call_site(cs)def_site(未実測・定義からの予想)
K1 ローカル変数(マクロ→利用者)E0425 ev(2:35=利用者の ev)E0425(2:35)通る → 8E0425
K2 ラベルE0426 'outer(2:41)E0426(2:41)通る → 1(unreachable statement の警告 1 件)E0426
K3 アイテム(マクロ→利用者)通る → 9通る → 9通る → 9通らない(定義側にしか無い名前として扱う)
K4 ローカル変数(利用者→マクロ)E0425 x(3:18=マクロ呼び出し)E0425(3:18)通る → 42E0425
K5 アイテム(利用者→マクロ)通る → 42通る → 42通る → 42通らない(caller_fn は定義側に無い)
K6 selfE0424(2:20=利用者の self)E0424(2:20)通る → 5E0424
K7 捕獲の事故通る → 101通る → 101通る → 200(警告は「n 未使用」1 件)101
K0 名前を利用者が渡す通る → 8———
  • mr と ms は 7 ケースすべてで、結果・エラー番号・行:列まで一致しました。Span::mixed_site() の docs の "A span that represents macro_rules hygiene" は、少なくともこの 7 ケースでは字義どおりです
  • cs は 7 ケースすべてで名前が通ります。衛生性による拒否が 1 件も無い、という意味で Reference の "unhygienic" と一致します
  • def_site は stable で Span::def_site() を呼ぶだけで error[E0658]: use of unstable library feature proc_macro_def_site になりました(実測)。nightly が手元に無いため 4 列目は docs の定義("resolves at the macro definition site")からの予想で、測っていません
error[E0425]: cannot find value `ev` in this scope --> cases/mr_k1_local.rs:2:35 | 2 | let v: u32 = hygmr::mr_local!(ev + 1); | ^^ not found in this scope | help: an identifier with the same name is defined here, but is not accessible due to macro hygiene --> cases/mr_k1_local.rs:2:18 | 2 | let v: u32 = hygmr::mr_local!(ev + 1); | ^^^^^^^^^^^^^^^^^^^^^^^^
error[E0425]: cannot find value `x` in this scope --> cases/mr_k4_caller_local.rs:3:18 | 3 | let v: u32 = hygmr::mr_caller_local!(); | ^^^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope | help: an identifier with the same name exists, but is not accessible due to macro hygiene --> cases/mr_k4_caller_local.rs:2:9 | 2 | let x = 41u32; | ^
⚠ 対照: call_site の K7 は何も出ない。 let n = 1u32; let v = cs_capture!(n); println!("{v} {n}"); は警告 0 件でビルドされ、200 1 を出力する。

エラーの位置の規則(実測):

  • K1・K2・K6 は利用者が書いたトークン(ev・'outer・self)を指す。見つからない名前を書いたのは利用者だから
  • K4 はマクロ呼び出し全体(3:18)を指す。見つからない x を書いたのはマクロ本体だから。help は "an identifier with the same name exists, but is not accessible due to macro hygiene" と言い、利用者の let x(2:9)を副ラベルで示す
  • K1 の help は向きが逆で "an identifier with the same name is defined here, but is not accessible due to macro hygiene" と、マクロ呼び出し(2:18)を副ラベルで示す

Q2 の答え

self はローカル変数として扱われています(K6: mr・ms とも E0424、cs だけ通る)。fn get(self) の self は、fn get(w: W) の w と同じ関数の引数=ローカル変数で、アイテムではありません。ローカル変数を定義側で解決する規則に入るのが筋で、マクロが作った self を利用者のトークンが拾えないのは K1 の ev と同じ理由です。Reference の列挙("loop labels, block labels, and local variables")は self を名指ししていませんが、実測では self もこの仲間でした。Day 073 の g09(ガードの self)が E0424 になったのも同じ理由です。

Q3 の答え

  1. E0425 はコンパイルが止まり、位置と help が出るので直す人は困りません。K7 の 200 はコンパイルが通り、値だけが違うので、テストでその値を検査していなければ本番まで気づけません。衛生性の役割は、後者を前者に変えることです
  2. 0 件(実測)。let n = 1u32; let v = cs_capture!(n); println!("{v} {n}"); は警告なしでビルドされ、200 1 を出力しました。利用者の n は 1 なのに、マクロに渡した n は 100 として読まれています。K7 で出た「n 未使用」の警告は、たまたま利用者が n を他で使っていなかったから出ただけです
  3. __n のような名前はぶつかる確率を下げるだけで、利用者が __n を使っていれば K7 と同じ事故が起きます(call_site のまま n を __n に変えたのと同じ)。mixed_site は名前の字面に関係なく別の名前として扱うので、利用者が何を使っていても事故になりません。ただし mixed_site で守られるのはローカル変数とラベルだけで、アイテム(K3・K5)は守られません。マクロが生成する関数名・型名の衝突には、依然として __ 接頭辞やモジュールで包む工夫が要ります

実物の照合(ローカルのレジストリ、serde_derive 1.0.229 のソースを grep、実測): mixed_site() の使用は 0 件、call_site() は 5 ファイル中で使用、__ で始まる識別子は 41 種類(多い順に __value 54 回・__deserializer 38 回・__serializer 33 回・__serde_state 31 回)。ser.rs 106 行は remote 派生のとき Ident::new("__self", Span::call_site()) を作っています。Reference の勧める「__ 接頭辞」で衝突を避け、mixed_site は使っていない、という実例です。理由(MSRV が 1.45 未満だった時期の名残か、方針か)はリポジトリを読んでおらず未確認です。

Q4 の答え(ADR-074: proc-macro 版 fsm! の span の割り当て)

名前決定採らなかった案と理由前提が変わったら
oncall_sitemixed_site でも同じ(アイテムは呼び出し側で解決、K3・K5)。明示のために call_site利用者にメソッド名を選ばせる要望が出たら、fn $name と同じく利用者のトークンにする
evmixed_sitecall_site は K1・K7 の通り、ガードから見える上に利用者の ev を奪う。__ev は Q3-3 の通り確率を下げるだけ「ガードでイベントを見たい」要望が出たら、ctx c と同じく event ev: Ev で利用者に名付けさせる(K0 の形)。call_site には戻さない
selfmixed_sitecall_site だとガードに self が書けてしまい、状態を表から読む設計(状態は行の左辺で決める)が崩れる状態をガードで読みたい要望は、表の左辺((Locked, Fail))で表せるので要らない
c利用者のトークンの span をそのまま使うcall_site で作り直すと、利用者がタイプミスしたときの位置が呼び出し全体に移る恐れ(Day 070: 利用者のトークンを置けば表の行を指す)変えない
内部ヘルパーcall_site+__fsm_ 接頭辞+const _: () = { ... }; で包むmixed_site はアイテムを守らない(K3)。モジュールで包むだけでは利用者の use super::* と衝突しうるdef_site が stable になったら(#54724)def_site に移す。ただしその時点で MSRV を上げる判断が要る

macro_rules! 版(今の fsm!)はこれを何も書かずに満たしています。ev・self は mixed site で隠れ、c は $c:ident で利用者の span を持ちます。proc-macro に移ると、この既定が call_site(非衛生)に変わるので、明示しない限り Day 073 の g08・g09 がコンパイルの通るコードになる——これが今日の結論です。

再現用コード(依存 0、rustc だけで 21+1 ケースを生成・判定)

作業ディレクトリに次の 3 ファイルを置き、./gen.sh(stable)または ./gen.sh +1.85 で実行します。

pm.rs(proc-macro、101 行):

//! hygpm: 依存 0 の proc-macro。マクロが「自分で書く」トークンの span だけを
//! call_site / mixed_site に切り替え、利用者のトークンは span をそのまま運ぶ。
extern crate proc_macro;
use proc_macro::{Delimiter, Group, Span, TokenStream, TokenTree};

/// 文字列からトークンを作り、すべての span を `sp` に付け替える(再帰)
fn mk(src: &str, sp: Span) -> TokenStream {
    respan(src.parse().expect("parse"), sp)
}
fn respan(ts: TokenStream, sp: Span) -> TokenStream {
    ts.into_iter()
        .map(|tt| match tt {
            TokenTree::Group(g) => {
                let mut ng = Group::new(g.delimiter(), respan(g.stream(), sp));
                ng.set_span(sp);
                TokenTree::Group(ng)
            }
            mut other => {
                other.set_span(sp);
                other
            }
        })
        .collect()
}
/// 入力を最初の `,` で 2 つに割る(intro_self 用)
fn split_comma(input: TokenStream) -> (TokenStream, TokenStream) {
    let mut a = Vec::new();
    let mut it = input.into_iter();
    for tt in it.by_ref() {
        if let TokenTree::Punct(p) = &tt {
            if p.as_char() == ',' {
                break;
            }
        }
        a.push(tt);
    }
    (a.into_iter().collect(), it.collect())
}
fn brace(ts: TokenStream, sp: Span) -> TokenStream {
    let mut g = Group::new(Delimiter::Brace, ts);
    g.set_span(sp);
    TokenTree::Group(g).into()
}
fn cat(parts: Vec<TokenStream>) -> TokenStream {
    parts.into_iter().collect()
}

// K1: マクロが let ev を作り、利用者の式の中で ev を使う
fn local(user: TokenStream, sp: Span) -> TokenStream {
    brace(cat(vec![mk("let ev = 7u32;", sp), user]), sp)
}
// K2: マクロがラベル 'outer を作り、利用者が break 'outer する
fn label(user: TokenStream, sp: Span) -> TokenStream {
    let body = brace(cat(vec![user, mk("; break 'outer 0u32;", sp)]), sp);
    cat(vec![mk("'outer: loop", sp), body])
}
// K3: マクロが fn helper を作り、利用者が helper() を呼ぶ
fn item(user: TokenStream, sp: Span) -> TokenStream {
    brace(cat(vec![mk("fn helper() -> u32 { 9 }", sp), user]), sp)
}
// K4: マクロ本体が呼び出し側のローカル x を名指しする(利用者からは何も渡さない)
fn caller_local(sp: Span) -> TokenStream {
    mk("x + 1", sp)
}
// K5: マクロ本体が呼び出し側の関数 caller_fn を名指しする
fn caller_item(sp: Span) -> TokenStream {
    mk("caller_fn() + 1", sp)
}
// K6: マクロが fn get(self) を作り、利用者の式の中で self を使う
fn intro_self(input: TokenStream, sp: Span) -> TokenStream {
    let (ty, user) = split_comma(input);
    let f = cat(vec![mk("pub fn get(self) -> u32", sp), brace(user, sp)]);
    cat(vec![mk("impl", sp), ty, brace(f, sp)])
}
// K7: 捕獲の事故。マクロが let n = 100 を作り、利用者は「自分の n」を渡す
fn capture(user: TokenStream, sp: Span) -> TokenStream {
    brace(cat(vec![mk("let n = 100u32; n +", sp), user]), sp)
}

macro_rules! pair {
    ($cs:ident, $ms:ident, $f:ident) => {
        #[proc_macro]
        pub fn $cs(i: TokenStream) -> TokenStream { $f(i, Span::call_site()) }
        #[proc_macro]
        pub fn $ms(i: TokenStream) -> TokenStream { $f(i, Span::mixed_site()) }
    };
}
pair!(cs_local, ms_local, local);
pair!(cs_label, ms_label, label);
pair!(cs_item, ms_item, item);
pair!(cs_self, ms_self, intro_self);
pair!(cs_capture, ms_capture, capture);

#[proc_macro]
pub fn cs_caller_local(_: TokenStream) -> TokenStream { caller_local(Span::call_site()) }
#[proc_macro]
pub fn ms_caller_local(_: TokenStream) -> TokenStream { caller_local(Span::mixed_site()) }
#[proc_macro]
pub fn cs_caller_item(_: TokenStream) -> TokenStream { caller_item(Span::call_site()) }
#[proc_macro]
pub fn ms_caller_item(_: TokenStream) -> TokenStream { caller_item(Span::mixed_site()) }

mr.rs(macro_rules!、18 行):

//! hygmr: 同じ 7 種を macro_rules! で書く(mixed site 衛生性)
#[macro_export]
macro_rules! mr_local { ($($u:tt)*) => {{ let ev = 7u32; $($u)* }}; }
#[macro_export]
macro_rules! mr_label { ($($u:tt)*) => { 'outer: loop { $($u)*; break 'outer 0u32; } }; }
#[macro_export]
macro_rules! mr_item { ($($u:tt)*) => {{ fn helper() -> u32 { 9 } $($u)* }}; }
#[macro_export]
macro_rules! mr_caller_local { () => { x + 1 }; }
#[macro_export]
macro_rules! mr_caller_item { () => { caller_fn() + 1 }; }
#[macro_export]
macro_rules! mr_self { ($T:ty, $($u:tt)*) => { impl $T { pub fn get(self) -> u32 { $($u)* } } }; }
#[macro_export]
macro_rules! mr_capture { ($($u:tt)*) => {{ let n = 100u32; n + $($u)* }}; }
/// 対照: 名前を利用者に付けさせる(fsm! の `ctx c: Ctx;` と同じ形)
#[macro_export]
macro_rules! mr_named { ($name:ident, $($u:tt)*) => {{ let $name = 7u32; $($u)* }}; }

gen.sh(26 行):

#!/bin/bash
# 3 方式 × 7 種 = 21 ケースを生成して、コンパイル結果と実行結果を表にする
# 使い方: ./gen.sh [toolchain]   例: ./gen.sh +1.85
TC=${1:-}
cd "$(dirname "$0")"
rm -rf cases out; mkdir -p cases out
rustc $TC --edition 2021 --crate-type proc-macro --crate-name hygpm pm.rs -o out/libhygpm.dylib
rustc $TC --edition 2021 --crate-type rlib --crate-name hygmr mr.rs -o out/libhygmr.rlib
for m in mr cs ms; do
  if [ $m = mr ]; then P="hygmr::mr"; else P="hygpm::$m"; fi
  printf 'fn main() {\n    let v: u32 = %s_local!(ev + 1);\n    println!("{v}");\n}\n' "$P" > cases/${m}_k1_local.rs
  printf 'fn main() {\n    let v: u32 = %s_label!(break '"'"'outer 1u32);\n    println!("{v}");\n}\n' "$P" > cases/${m}_k2_label.rs
  printf 'fn main() {\n    let v: u32 = %s_item!(helper());\n    println!("{v}");\n}\n' "$P" > cases/${m}_k3_item.rs
  printf 'fn main() {\n    let x = 41u32;\n    let v: u32 = %s_caller_local!();\n    println!("{v}");\n}\n' "$P" > cases/${m}_k4_caller_local.rs
  printf 'fn caller_fn() -> u32 { 41 }\nfn main() {\n    let v: u32 = %s_caller_item!();\n    println!("{v}");\n}\n' "$P" > cases/${m}_k5_caller_item.rs
  printf 'pub struct W(pub u32);\n%s_self!(W, self.0);\nfn main() {\n    println!("{}", W(5).get());\n}\n' "$P" > cases/${m}_k6_self.rs
  printf 'fn main() {\n    let n = 1u32;\n    let v: u32 = %s_capture!(n);\n    println!("{v}");\n}\n' "$P" > cases/${m}_k7_capture.rs
done
printf 'fn main() {\n    let v: u32 = hygmr::mr_named!(ev, ev + 1);\n    println!("{v}");\n}\n' > cases/mr_k0_named.rs
for f in cases/*.rs; do
  b=$(basename $f .rs)
  err=$(rustc $TC --edition 2021 --extern hygpm=out/libhygpm.dylib --extern hygmr=out/libhygmr.rlib -L out \
        --error-format=short $f -o out/$b 2>&1 | grep -E '^[^ ]+:[0-9]+:[0-9]+: (error|warning)' | sed "s#cases/##" | head -3 | tr '\n' ' ')
  if [ -x out/$b ]; then r="OK -> $(out/$b)"; else r="NG"; fi
  printf '%-22s | %-12s | %s\n' "$b" "$r" "$err"
done

実行結果(rustc 1.99.0、抜粋。1.85.1 も OK/NG と値は 22 件すべて同じ):

cs_k1_local            | OK -> 8      |
cs_k6_self             | OK -> 5      |
cs_k7_capture          | OK -> 200    | cs_k7_capture.rs:2:9: warning: unused variable: `n` ...
mr_k0_named            | OK -> 8      |
mr_k1_local            | NG           | mr_k1_local.rs:2:35: error[E0425]: cannot find value `ev` in this scope ...
mr_k4_caller_local     | NG           | mr_k4_caller_local.rs:3:18: error[E0425]: cannot find value `x` in this scope ...
mr_k6_self             | NG           | mr_k6_self.rs:2:20: error[E0424]: cannot find value `self` in this scope ...
mr_k7_capture          | OK -> 101    |
ms_k1_local            | NG           | ms_k1_local.rs:2:35: error[E0425]: cannot find value `ev` in this scope ...
ms_k7_capture          | OK -> 101    |

./gen.sh 1 回(22 ケースのコンパイルと実行)は 3.22〜3.62 秒(3 回、load average 0.95→17.41 に上がる中で測定。参考値)。

🪜 Step-by-Step 解説

Step 1: 名前を「ローカル」と「アイテム」に仕分ける

mixed site の規則は 2 分割です。K1(let ev)・K2('outer)・K4(let x)・K6(self)はローカル、K3(fn helper)・K5(fn caller_fn)はアイテム。表の mr 列は、ローカルの 4 件がすべてエラー、アイテムの 2 件がすべて通る、ときれいに割れています。K7 はローカルですが、利用者の n とマクロの n が別の名前として両方解決できるので、エラーではなく正しい値(101)になります。

Step 2: proc-macro で span を付け替える

proc-macro が返すトークンは、1 個ずつ span を持ちます。名前解決はその span の「衛生性の文脈」で行われるので、同じ展開の中でトークンごとに規則を変えられます。pm.rs の local() は、マクロが書く let ev = 7u32; だけを sp に付け替え、利用者のトークン user は触りません。cs_local! と ms_local! の違いは sp だけで、それが K1 の 8 と E0425 を分けます。

Group({ } や ( ))は set_span で括弧の span しか変わらないので、中身を再帰で付け替えて作り直す必要があります。これを忘れると、括弧だけ mixed_site で中身は call_site という混ざった展開になります。

Step 3: エラーの位置は「見つからない名前を書いたのは誰か」で決まる

K1 の ev は利用者のトークン(2:35)、K4 の x はマクロのトークン(呼び出し全体 3:18)。Day 070〜073 で追ってきた「位置」の規則と同じで、主役のトークンの spanが位置になります。mixed_site の docs にある "The span location is taken from the call-site" は、マクロのトークンの位置がマクロ呼び出しになることを言っており、K4 の 3:18 と一致します。

Step 4: 利用者に名付けさせる(K0)

mr_named!(ev, ev + 1) は 8 を返します。let $name = 7u32; の $name は利用者のトークンなので、利用者の文脈の名前になり、利用者の ev + 1 から見えます。fsm! の ctx c: Ctx; はこの形です。「見せたい名前は利用者に書かせる、隠したい名前はマクロが書く」——衛生性の下での DSL の作り方は、これだけで説明できます。

🧭 設計思想・なぜこう書くのか

  • 既定が違う理由: macro_rules! は利用者が気軽に書くので、安全な mixed site が既定です。proc-macro は derive のように「利用者の型に impl を足す」用途が中心で、生成した impl や関数が呼び出し側から見えないと役に立ちません。だから既定は call site で、隠したいものだけ作者が選んで隠します。安全側の既定と、表現力側の既定を、道具ごとに分けているのが Rust の判断です
  • なぜアイテムは守らないのか: アイテムまで定義側で解決すると、macro_rules! の中の Vec・Option・利用者の型名がすべて見えなくなり、$crate:: か完全パスでしか書けなくなります。def site がまさにその規則で、だから tracking issue #54724 が開いたまま安定していません(安定しない理由の議論は tracking issue を読んでおらず未確認)
  • エラーに変えることの価値: K7 の 200 は型も借用も正しく、Rust の他のどの検査にも引っかかりません。名前を取り違えたことを止められるのは衛生性だけです

🌐 他言語との比較

言語・仕組み既定の衛生性K7(捕獲の事故)にあたるもの根拠
Rust macro_rules!mixed site(ローカル・ラベルのみ衛生的)起きない(101)実測
Rust proc-macro(quote! 既定)call site(非衛生)起きる(200、警告 0)実測
Rust proc-macro Span::mixed_site()macro_rules! と同じ起きない(101)実測
C/C++ プリプロセッサ無し(字句置換)起きる。慣習は do { int _tmp_ ... } while (0) と名前の工夫記憶ベース
Scheme syntax-rules完全に衛生的(アイテム相当も)起きない記憶ベース
Common Lisp defmacro無し起きる。gensym で一意の名前を作るのが作法記憶ベース
Julia マクロ既定で衛生的、esc() で利用者の文脈に出す起きない(esc すると起きる)記憶ベース
TypeScriptマクロ無し(型レベルの計算のみ)——

Rust の proc-macro は Common Lisp に近く(既定は非衛生、作者が手で守る)、macro_rules! は Scheme と C の中間(ローカルだけ守る)です。

🏭 実務での使いどころ

  • derive マクロのローカル変数: serialize の中で作る一時変数(__serializer など)は、利用者のフィールド名とぶつかると K7 と同じ事故になります。serde_derive は __ 接頭辞で避けています(41 種類、実測)。新しく書くなら、ローカル変数は mixed_site で作る方がぶつかりません
  • 属性マクロで関数本体を包む(#[instrument] のような計測・ログ): 本体の前後に足す let __start = Instant::now(); は、利用者の本体から見えてはいけません。mixed_site の典型的な使いどころです
  • DSL の「見せる名前」: ルールエンジン・ルーター・状態機械で、条件式に渡したい名前(リクエスト・文脈)は fsm! の ctx c: Ctx; のように利用者に名付けさせます。暗黙の req や ctx を call site で生やす設計は、利用者の同名変数を黙って隠します

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
「Rust のマクロは衛生的」macro_rules! だけを見ているproc-macro の既定(call_site・quote!)は非衛生。K1〜K7 すべて通る
「衛生性があれば名前の衝突は起きない」規則の 2 分割を知らないmixed site が守るのはローカル変数とラベル(と self)だけ。関数・型(K3・K5)は守らない
「__ 接頭辞を付ければ安全」衝突の確率が下がるのを安全と取り違える利用者が __n を使えば同じ事故。ローカル変数なら mixed_site の方が確実
「self はキーワードだから衛生性と無関係」Reference の列挙に self が無い実測では self もローカル変数として定義側で解決される(E0424)
「マクロ本体から呼び出し側の x を読める」C マクロの感覚mixed site では E0425(K4)。読ませたいなら $x:ident で受ける
「Group::set_span で中身まで変わる」API 名から推測括弧の span だけ。中身は再帰で付け替える

🛑 扱わなかった範囲(止め方)

  • def site は未実測: stable で E0658 になることのみ確認。nightly を入れていないため、Q1 の 4 列目は docs からの予想です
  • **Span::resolved_at / located_at**(衛生性と位置を別々に選ぶ API)は試していません。Q4 の「c は利用者の span を使う」の代わりに「位置は利用者・衛生性は mixed」にする選択肢があり、マクロ② で測る候補です
  • **proc-macro に $crate が無い問題**(生成コードから自分のランタイムクレートをどう名指しするか。proc-macro-crate クレートなど)は扱っていません
  • edition による違い: すべて edition 2021 で測定。2015/2018 での差は未確認
  • serde_derive が mixed_site を使わない理由はソースの grep だけで、issue・CHANGELOG は読んでいません。thiserror・tokio-macros などの照合も未実施(ローカルのレジストリに無い)
  • 他言語比較の C・Scheme・Lisp・Julia 列は記憶ベースで、実行していません
  • Day 073 の宿題(ハーネスの WARN 注釈、if-let ガードの専用行規則、ui_test クレート)は今日は進めていません

➡️ 次のステップ

  • 発展: Span::resolved_at で「位置は利用者のトークン、名前解決は mixed site」のトークンを作り、K1 のエラー位置と K7 の値がどう変わるかを測る
  • 次回予告: マクロ① の残り期間(〜10/24)。今日の概念理解の次は設計か実装。候補は (1) 入れ子の状態(Alarm の中に鳴動中/消音中)を自作 fsm! と statig の superstate で比べる設計、(2) fsm! の UI テスト(Day 073 のハーネス)に今日の K1・K6・K7 相当のケース(ガードの ev・self、ctx と同名の利用者変数)を //~ ERROR 注釈つきで足す実装

🎯 自己評価(解いた後に記入)

Q1 で外したマス(21 マス中)

自分の回答(Q2 の self の扱い、Q3 の 3 問、Q4 の割り当て)

気づき・メモ