フェーズ: 上級者 (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 site | macro_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 site | proc-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 site | proc-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 + 1 | 8 |
| K2 | 'outer: loop { <利用者>; break 'outer 0u32; } | break 'outer 1u32 | 1 |
| K3 | { fn helper() -> u32 { 9 } <利用者> } | helper() | 9 |
| K4 | x + 1(マクロ本体が呼び出し側のローカルを名指し) | 呼び出し前に let x = 41u32; | 42 |
| K5 | caller_fn() + 1(マクロ本体が呼び出し側の関数を名指し) | fn caller_fn() -> u32 { 41 } | 42 |
| K6 | impl W { pub fn get(self) -> u32 { <利用者> } } | self.0(W(5).get() で呼ぶ) | 5 |
| K7 | { let n = 100u32; n + <利用者> } | 呼び出し前に let n = 1u32;、渡すのは n | 101 か 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 で確かめます)、
- 利用者から見て、なぜこれが E0425 より悪い結果なのかを 2 文で
- 利用者が
nをprintln!などで別の場所でも使っていたら、コンパイラの警告は何件出るか予想を - Reference が勧める回避策(
__internal_fooのような名前)とmixed_siteを、**利用者が実際に__nという名前を使っていた場合**で比べてください
Q4: マクロ② の fsm! で、どの名前にどの span を付けるか
proc-macro 版の fsm! が生成する名前は次の 5 つです。それぞれに call_site / mixed_site / 利用者のトークンの span をそのまま使う のどれを付けるかを、採らなかった案と、前提が変わったら判断がどう変わるかつきで決めてください。
| 名前 | 生成される場所 | 利用者のガード・アクションから見えるべきか |
|---|---|---|
on | impl Door { pub fn on(...) } | 呼び出し側から door.on(ev, &ctx) で呼ぶ |
ev | on の引数 | 見えなくてよい(Day 073 の g08) |
self | on の受け手 | 見えなくてよい(g09) |
c | on の引数 | 見えるべき(利用者が 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) | 通る → 8 | E0425 |
| 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) | 通る → 42 | E0425 |
| K5 アイテム(利用者→マクロ) | 通る → 42 | 通る → 42 | 通る → 42 | 通らない(caller_fn は定義側に無い) |
K6 self | E0424(2:20=利用者の self) | E0424(2:20) | 通る → 5 | E0424 |
| K7 捕獲の事故 | 通る → 101 | 通る → 101 | 通る → 200(警告は「n 未使用」1 件) | 101 |
| K0 名前を利用者が渡す | 通る → 8 | — | — | — |
- mr と ms は 7 ケースすべてで、結果・エラー番号・行:列まで一致しました。
Span::mixed_site()の docs の "A span that representsmacro_ruleshygiene" は、少なくともこの 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")からの予想で、測っていません
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 の答え
- E0425 はコンパイルが止まり、位置と help が出るので直す人は困りません。K7 の 200 はコンパイルが通り、値だけが違うので、テストでその値を検査していなければ本番まで気づけません。衛生性の役割は、後者を前者に変えることです
- 0 件(実測)。
let n = 1u32; let v = cs_capture!(n); println!("{v} {n}");は警告なしでビルドされ、200 1を出力しました。利用者のnは 1 なのに、マクロに渡したnは 100 として読まれています。K7 で出た「n未使用」の警告は、たまたま利用者がnを他で使っていなかったから出ただけです __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 の割り当て)
| 名前 | 決定 | 採らなかった案と理由 | 前提が変わったら |
|---|---|---|---|
on | call_site | mixed_site でも同じ(アイテムは呼び出し側で解決、K3・K5)。明示のために call_site | 利用者にメソッド名を選ばせる要望が出たら、fn $name と同じく利用者のトークンにする |
ev | mixed_site | call_site は K1・K7 の通り、ガードから見える上に利用者の ev を奪う。__ev は Q3-3 の通り確率を下げるだけ | 「ガードでイベントを見たい」要望が出たら、ctx c と同じく event ev: Ev で利用者に名付けさせる(K0 の形)。call_site には戻さない |
self | mixed_site | call_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注釈つきで足す実装