Day 072 — 2026-10-12 | マクロ① macro_rules! ⑰ const 評価の境界と「位置を運べるもの」:for・format!・iter().max() は E0015、while と panic!("{}", &str) は通る——E0080 は行ごとの const _ にしても fsm! { を指したまま。max 4 を _ _ _ _ に変えて macro_rules! のマッチで数えると、5 個目のアクション(13:128)を指し、Day 071 の例外表が空になる

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

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

Day 069 で fsm! に max 4 を足し、「1 行のアクションが 4 個を超えたらコンパイルを止める」検査を const 評価で書きました。

const _: () = {
    let rows = $State::ROW_ACTIONS;
    let mut i = 0;
    while i < rows.len() {
        if rows[i].1 > $cap { panic!("{}", rows[i].0); }
        i += 1;
    }
};

Day 071 で、この検査だけが表の行を指さないことが分かりました(E10。最初の --> が spanfsm::fsm! { の 5:1)。ui_position.rs の例外表には、理由「const 評価の panic!。macro_rules! から行の位置を渡せない」と書いて 1 件だけ残してあります。

今日はこの理由を言葉ではなく仕組みで説明できるようにします。論点は 2 つです。

  1. const 評価で書けること・書けないこと。 Rust Reference は const 評価を "computing the result of expressions during compilation" と定義し、"Only a subset of all expressions can be evaluated at compile-time" と続けます(一次資料)。while は書けたのに、なぜ for は書けないのか。panic! に何を渡せるのか
  2. エラーの位置(span)はどこから来るのか。 const 評価が見ているのは「値」です。マクロのマッチが見ているのは「トークン」です。どちらが利用者の表の行の位置を持っているのか

Rust がコンパイル時の検査を重んじるのは、誤りを実行前に止めるためです(所有権・型・#[deny] と同じ発想)。ただ、止めた場所を正しく指せない検査は、表を書く人にとって「どこかがおかしい」としか言いません。今日は、検査を書く場所(const 評価か、マクロのマッチか)の選び方を、実測で決めます。

📝 問題

環境: rustc 1.96.0(stable)と rustc 1.85.1(+1.85)、Apple M1・8GB。マクロの本体は Day 071 の spanfsm(src/lib.rs 215 行、tests/ui 10 ケース+ok_table、tests/ui_position.rs)をそのまま使います。

Q1: const 評価で書けるか(予想してから確かめる)

次の定数を前提に、各行を const _: … = …; の初期化式に書いたとき、(a) 通る(評価が意図どおりに失敗する E0080 も「書ける」に数える)(b) E0015 などで書けない のどちらかを予想してください。2 行目の行は 5 個のアクションを持つので、検査が書ければ評価が失敗します。

const ROWS: &[(&str, usize)] = &[
    ("(Locked, Unlock) if c.failures >= 3", 2),
    ("(Locked, Unlock) if c.failures + 1 >= 3", 5),
];
const CAP: usize = 4;
#初期化式の中身
c1while で回し、超えたら panic!("too many")
c2for r in ROWS { if r.1 > CAP { panic!(..) } }
c3for i in 0..ROWS.len() { .. }
c4ROWS.iter().map(|r| r.1).max()
c5panic!("{}", ROWS[i].0)(&str を 1 個)
c6panic!("{} has {} actions", ROWS[i].0, ROWS[i].1)(usize も渡す)
c7panic!(concat!("fsm!: ...", stringify!(4), ""))`
c8assert!(ROWS[i].1 <= CAP, "{}", ROWS[i].0)
c9let s = format!("{}", ROWS[0].0);
c10ROWS[0].0.eq("x")(&str の比較)
c11Some(ROWS[0].1).unwrap()
c12CAP - ROWS[1].1(4 − 5)
c13[(); CAP][ROWS[1].1](長さ 4 の配列の 5 番目)
c14自作の const fn n(r: &(&str, usize)) -> usize を呼ぶ
c15変数を捕まえないクロージャ |r| r.1 を作って呼ぶ
c16ROWS[0].0.as_bytes() の長さと先頭バイトを比べる

1.85.1 でも結果が同じかも予想してください。

Q2: E0080 はなぜ表の行を指さないのか

Day 071 の E10(3 行目にアクション 5 個)の出力は次のとおりでした。

error[E0080]: evaluation panicked: (Locked, Unlock) if c.failures + 1 >= 3 --> tests/ui/e10_over_max.rs:5:1

**メッセージには行の字面が出ているのに、位置は 5 行目(spanfsm::fsm! {)です。** 字面は届いて、位置は届かない理由を、「const 評価が受け取るもの」と「span がどのトークンから来るか」の 2 語を使って 3 文以内で説明してください。

Q3: 検査を書く場所を変えたら、どこを指すか

max の検査だけを次の 5 通りに書き換えます(他の部分は Day 071 のまま)。それぞれ、E10 の最初の --> が何行何列を指すか(または、どんなエラーになるか)を予想してください。表の 3 行目は 13 行目、5 個目のアクション SoundAlarm は 13 行目の 128 列目、max 4 の 4 は 9 行目の 20 列目です。

案書き方
V1行ごとに const _: () = assert!(<その行の数> <= $cap, concat!("…", stringify!(<行>))); を生成する
V3bmax $cap:literal で受けた $cap を補助マクロ __within! に渡し、(4; …) => … のように**リテラル 4 で一致**させて _ _ _ _ に変える
V3V3b と同じだが max $cap:tt で受ける。_ を 1 個ずつ消しながらアクションを 1 個ずつ読み、_ が尽きたのにアクションが残ると、一致する規則が無い
V4V3 で _ が尽きた場合に一致する規則を足し、compile_error!(concat!("fsm!: ", stringify!($a), " exceeds max")) を出す
V5V3 で _ が尽きた場合に、どの規則にも一致しない補助マクロ __fsm_row_has_more_actions_than_max!{$a} を呼ぶ(エラー文にマクロ名を出す狙い)

Q4: どれを採るか

Q1〜Q3 の結果から、fsm! の max の検査をどの形にするかを、採らなかった案と見直し条件つきで 5 行以内に書いてください。max 9 や max 4usize と書いた利用者がどうなるかも考えること。

💡 ヒント

ヒント1(方向性)
  • for は糖衣構文です。for x in it { .. } は IntoIterator::into_iter(it) と、繰り返しの Iterator::next(&mut iter) に展開されます。どちらもトレイトのメソッド呼び出しです
  • const 評価器(Miri と同じ仕組みの解釈器)が実行するのは、マクロ展開も型検査も終わった後の中間表現です。そこにあるのは「値」と「その命令がどのソースの場所から来たか」です
ヒント2(アプローチ)
  • Reference の const 評価の節は、定数式として loop と while を列挙し、for は列挙していません。"Calls of const functions and const methods" も列挙されています
  • 別クレートで定義された macro_rules! の本体にあるトークンの span は、利用者のクレートから見るとマクロ呼び出し全体に写されます(Day 071 Part 2 の実測: Day 069 の形の E1 が 5:1 になった)
  • Reference の macros-by-example の「転送」の節(Day 070 で取得): 一度 $x:literal などで受けた断片を別のマクロに渡すと、受け側は中身のトークンでは一致させられない。例外は ident・lifetime・tt
ヒント3(コード骨格)
#[doc(hidden)]
#[macro_export]
macro_rules! __within {
    (4; $($acts:tt)*) => { $crate::__within!{@[_ _ _ _] $($acts)*} };
    // … 0..=8 まで
    (@[$($s:tt)*]) => {};                                   // アクションを読み終えた
    (@[_ $($s:tt)*] $a:ident $(, $($rest:tt)*)?) => { $crate::__within!{@[$($s)*] $($($rest)*)?} };
    (@[_ $($s:tt)*] $a:ident ( $($arg:tt)* ) $(, $($rest:tt)*)?) => { /* 同上 */ };
    // `_` が尽きて、まだアクションがある → どの規則にも一致しない
}

項目の位置で呼ぶ補助マクロの再帰は { } で囲むこと(( ) だと "macros that expand to items must be delimited with braces or followed by a semicolon"、実測)。

✅ 模範解答

Q1 の答え(実測、rustc 1.96.0 と 1.85.1、--edition 2021 --crate-type lib)

#中身1.96.01.85.1
c1while + panic!("too many")書ける(E0080 evaluation panicked: too many)書ける(E0080 の文言は "evaluation of constant value failed")
c2for r in ROWSE0015 cannot use for loop on &[(&str, usize)] in constants(2 件)E0015
c3for i in 0..lenE0015E0015
c4iter().map().max()E0015(2 件)+ "core::slice::<impl [T]>::iter is not yet stable as a const fn"E0015
c5panic!("{}", &str)書ける(メッセージに行の字面)書ける
c6panic!("{} … {}", &str, usize)E0015 cannot call non-const formatting macro in constantsE0015
c7panic!(concat!(…))書ける書ける
c8assert!(cond, "{}", &str)書ける(E0080 evaluation panicked: (Locked, Unlock) if c.failures + 1 >= 3)書ける
c9format!E0015(2 件)+ E0493 destructor of String cannot be evaluated at compile-time同じ
c10str の eqE0658 cannot call conditionally-const method <str as PartialEq>::eq + "PartialEq is not yet stable as a const trait"(issue #143874 を案内)E0015(版で番号が違う)
c11Option::unwrap書ける(警告のみ)書ける
c124 - 5書ける(E0080 attempt to compute 4_usize - 5_usize, which would overflow)書ける
c13[(); 4][5]書ける(E0080 index out of bounds: the length is 4 but the index is 5)書ける
c14自作 const fn書ける書ける
c15クロージャを呼ぶE0015 cannot call non-const closure in constants(note: closures need an RFC before allowed to be called in constants)E0015
c16as_bytes() + バイト比較書ける書ける

16 個中 9 個が書け、7 個が書けません。 両版で「書ける/書けない」の判定は 16 個とも同じで、変わったのは c10 のエラー番号(E0015 → E0658)と E0080 の文言だけでした。

読み方の規則は 1 つにまとまります。**const 評価の中で呼べるのは const fn だけで、トレイトのメソッドは(安定版では)呼べない。**

  • for(c2・c3)は IntoIterator・Iterator のメソッド呼び出しに展開されるので書けない。while(c1)は条件式と分岐だけなので書ける。Reference の定数式の列挙も "loop and while expressions" で、for はありません
  • iter().map().max()(c4)・str の ==(c10)・クロージャの呼び出し(c15)も、トレイトのメソッド(Iterator::map、PartialEq::eq、Fn::call)です。c10 の 1.96 のメッセージが「const trait がまだ安定していない」と言っているのは、この壁を取り除く作業(const trait)が進行中だという意味です。c15 は Reference の列挙に「変数を捕まえないクロージャ式」がありますが、それは作れるという意味で、呼べるわけではありません(実測で E0015)
  • panic! の書式(c5〜c8)は「{} に &str を 1 個」までです。usize を混ぜると(c6)書式化の仕組みが動くので書けません。format!(c9)は String を確保して解放するので、確保(E0015)と解放(E0493)の 2 つの壁に当たります
  • c12・c13 のように、算術のオーバーフローと配列の範囲外も E0080 になるので、panic! を書かずに検査を書くこともできます([(); CAP][n] は昔から使われる形)。ただ、メッセージは「4 − 5 があふれた」で、どの行かは言いません

Q2 の答え

const 評価が受け取るのは、マクロ展開と型検査が終わった後の「値」と「各命令の span」で、行の字面は stringify! で &str の値にしたから届いた。
panic!・assert! のトークンは spanfsm の macro_rules! の本体に書かれたものなので、その span はマクロ定義から来ている。
別クレートのマクロ定義の span は、利用者側では呼び出し全体(5:1 の spanfsm::fsm! { … })に写されるため、値に字面を詰めても位置は表の行にならない。

要点は 「値は字面を運べるが、位置は運べない」 です。位置を持っているのは利用者が書いたトークンで、トークンを扱えるのはマクロのマッチ(とその先の型検査・名前解決)です。Day 070 で E1〜E9 が表の行を指したのは、利用者のトークンをそのまま式・識別子として置いたからでした。

Q3 の答え(実測、rustc 1.96.0、UI ファイルは別クレート)

案E10 の結果最初の -->
V0(Day 071 のまま)E0080 evaluation panicked: (Locked, Unlock) if c.failures + 1 >= 35:1
V1 行ごとの const _E0080 evaluation panicked: fsm!: more actions than max in row: (Locked, Unlock) if c.failures + 1 >= 35:1 のまま
V3b $cap:literal を転送ライブラリ自体がコンパイルできない: no rules expected literal metavariable(src/lib.rs:39:30)。ok_table も単体テストも止まる—
**V3 $cap:tt + _ の列****no rules expected SoundAlarm**(note: while trying to match ])13:128(5 個目のアクション)
V4 compile_error! で文言を足すfsm!: SoundAlarm exceeds max5:1
V5 名前付きの行き止まりマクロno rules expected identifier SoundAlarm(note: while trying to match end of macro)5:1
error: no rules expected `SoundAlarm` --> tests/ui/e10_over_max.rs:13:128 | 13 | ...+ 1, false), SoundAlarm, NotifyGuard, SoundAlarm]; | ^^^^^^^^^^ no rules expected this token in macro call | note: while trying to match `]` --> src/lib.rs | | (@[$($s:tt)*]) => {}; | ^

読み取れること:

  1. V1: 行ごとに分けても、検査が const 評価である限り位置は変わらない。 assert! のトークンはマクロ定義のもので、行ごとに生成しても span の出どころは同じです。変わったのはメッセージ(concat! で説明を足せた)だけ。Day 071 の「発展」の仮説(行ごとの const _ で下線が行に付くか)は否定されました
  2. V3: 「数える」を const 評価からマクロのマッチに移すと、位置は利用者のトークンになる。 一致しなかった最初のトークンが 5 個目の SoundAlarm で、それは利用者が書いたトークンだからです。max 3 に変えると 4 個目の NotifyGuard(13:115)を指しました(実測)
  3. **V3b: $cap:literal で一度受けると、4 というリテラルで一致させられない。** Reference の転送の規則どおりで、literal は不透明な断片になります。tt で受ければ透過します。エラーは利用者の誤りではなくライブラリの定義の誤りとして出るので、正しい表も通らなくなります
  4. V4・V5: 文言を良くしようとすると、位置を失った。 compile_error! のトークンはマクロ定義のもの(V4)。V5 は $a:ident を別のマクロに渡し直したところで、位置が呼び出し全体に戻りました。V5 の理由は確かめていません(ident は転送で透過するので、トークン自体の span は残ると予想していた。実測はそうならなかった)。今日の範囲では「位置」と「独自の文言」は両立しませんでした

Q4 の答えの例

採るのは V6 = V3 + const 評価への退避: max 0..=8 は `_` の列で数えて、超えた最初のアクションを指す。9 以上や 4usize は const 評価(V1)に回し、位置はマクロ呼び出し全体のまま。
根拠: E10 が 5:1 → 13:128 になり、Day 071 の例外表が空になった(ui_position 0 violations)。V3 だけだと max 9・max 4usize が「no rules expected `9`」(9:20)で書けなくなる(Day 071 の形は通っていた)ので退避の規則を足した。
却下: V1(位置が変わらない)/V3b(ライブラリが壊れる)/V4・V5(文言は良いが位置を失う)/const 評価だけ(Day 071 のまま、例外が残る)。
見直し条件: 行のアクションが 9 個以上の表が実際に現れたら、表を 0..=16 に広げるか proc-macro(マクロ②)で span を指定する。rustc がマクロ定義の span の扱いを変えたら V1 を測り直す。

V6 の実装(src/lib.rs の差分、215 → 235 行)

fsm! 側の変更は 2 か所です。

-        action $Action:ident $( max $cap:literal )? { $( $A:ident $( ( $($T:ty),* ) )? ),* $(,)? }
+        action $Action:ident $( max $cap:tt )? { $( $A:ident $( ( $($T:ty),* ) )? ),* $(,)? }
 …
-        $(
-            const _: () = {
-                let rows = $State::ROW_ACTIONS;
-                let mut i = 0;
-                while i < rows.len() {
-                    if rows[i].1 > $cap {
-                        panic!("{}", rows[i].0);
-                    }
-                    i += 1;
-                }
-            };
-        )?
+        $crate::__per_row! { cap [$($cap)?]; $( row [ ($s, $e) $(if $g)? ] [ $($($acts)*)? ] )* }

$cap は「0 回か 1 回」、行は「0 回以上」の別々の繰り返しなので、fsm! の中で両方を 1 つの繰り返しに入れられません。__per_row! に cap [..] として 1 回だけ渡し、そこで行ごとに展開します。

追加する補助マクロ(fsm! の前に置く):

#[doc(hidden)]
#[macro_export]
macro_rules! __per_row {
    (cap []; $($rest:tt)*) => {};
    (cap [$cap:tt]; $( row [$($r:tt)*] [$($acts:tt)*] )*) => {
        $( $crate::__within!($cap; $($acts)*); )*
    };
}
/// max N を N 個の `_` に置き換え、アクション 1 個ごとに 1 個消す。
/// 空になってもアクションが残ると、残った最初のトークン(利用者の表の上)で一致する規則が無くなる
#[doc(hidden)]
#[macro_export]
macro_rules! __within {
    (0; $($acts:tt)*) => { $crate::__within!{@[] $($acts)*} };
    (1; $($acts:tt)*) => { $crate::__within!{@[_] $($acts)*} };
    (2; $($acts:tt)*) => { $crate::__within!{@[_ _] $($acts)*} };
    (3; $($acts:tt)*) => { $crate::__within!{@[_ _ _] $($acts)*} };
    (4; $($acts:tt)*) => { $crate::__within!{@[_ _ _ _] $($acts)*} };
    (5; $($acts:tt)*) => { $crate::__within!{@[_ _ _ _ _] $($acts)*} };
    (6; $($acts:tt)*) => { $crate::__within!{@[_ _ _ _ _ _] $($acts)*} };
    (7; $($acts:tt)*) => { $crate::__within!{@[_ _ _ _ _ _ _] $($acts)*} };
    (8; $($acts:tt)*) => { $crate::__within!{@[_ _ _ _ _ _ _ _] $($acts)*} };
    // 0..=8 以外(9 以上・4usize など)は const 評価に戻す。位置はマクロ呼び出し全体になる
    ($cap:literal; $($acts:tt)*) => {
        const _: () = assert!(0usize + $crate::__count!($($acts)*) <= $cap, "fsm!: a row has more actions than `max`");
    };
    (@[$($s:tt)*]) => {};
    (@[_ $($s:tt)*] $a:ident $(, $($rest:tt)*)?) => { $crate::__within!{@[$($s)*] $($($rest)*)?} };
    (@[_ $($s:tt)*] $a:ident ( $($arg:tt)* ) $(, $($rest:tt)*)?) => { $crate::__within!{@[$($s)*] $($($rest)*)?} };
}

tests/ui_position.rs の例外表は空にします。

/// 表の行を指さなくてよい例外。理由を必ず書く(Day 072 で E10 を外して空になった)
const EXCEPTIONS: &[(&str, u32, &str)] = &[];

実行と結果(実測、2026-10-12)

$ cargo test --lib                          # 単体テスト(door・motor の表、720 通り)
test result: ok. 4 passed; 0 failed
$ cargo test --test ui                      # 旧スナップショットのまま: e10 だけ mismatch、他 9 件+ok_table は一致
test tests/ui/e10_over_max.rs [should fail to compile] ... mismatch
$ TRYBUILD=overwrite cargo test --test ui   # e10 のスナップショットを更新(中身を目で確認: 13:128)
test result: ok. 1 passed
$ cargo test --test ui_position -- --nocapture
checked 10 snapshots, 0 violations          # 例外表が空で通る
$ cargo clippy --all-targets                # 警告 0 件

max の書き方の境界(V6、examples で確認):

書き方V0(Day 071)V3(_ の列だけ)V6
max 4、5 個E0080 5:113:12813:128
max 3、4 個E0080 5:113:11513:115
max 9、4 個通る**no rules expected 9(9:20)**通る
max 4usize、4 個通るno rules expected 4usize(9:20)通る
max 9、12 個E0080 5:1—E0080 5:1(退避の規則。位置は 0..=8 の外では改善しない)
max N(識別子)no rules expected N(9:20)9:209:20

rustc 1.85.1(trybuild 抜きの同じ src/lib.rs、edition 2021 のクレート)でも、E10 は 13:128、max 9 は通り、12 個の行は E0080 "evaluation of constant value failed" 5:1 でした(実測)。

コンパイル時間: 300 行 × 4 アクションの表を cargo check(例のファイルを touch して 5 回、2 巡)で測ると、V0 は 246〜462 ms、V6 は 270〜387 ms で、範囲が重なり差は測れませんでした。測定中の load average は 21〜22(並行作業あり)。V6 は 1 行あたり最大 9 段の再帰が増えますが、recursion_limit(既定 128)には遠く、300 行でも止まりませんでした。

🪜 Step-by-Step 解説

Step 1: 「書けない」の理由を 1 つの規則にまとめる

Q1 の 7 個の「書けない」は、ばらばらに見えて 1 つです。**const 評価の中では、const fn でない関数・メソッドを呼べない**(E0015 の note: "calls in constants are limited to constant functions, tuple structs and tuple variants")。for・iter()・==(str)・クロージャの呼び出し・format! は、どれも裏で const fn でないトレイトのメソッドを呼んでいます。だから ROW_ACTIONS の最大値も、Day 069 では while と添字で書くしかありませんでした(MAX_ACTIONS の実装がそうなっている理由)。

Step 2: 「書けるが位置を指せない」を分けて考える

c1・c5・c8・c12・c13 はすべて「書ける」側で、E0080 で止まります。ここで気づくべきは、書けることと、利用者に正しい場所を示せることは別の性質だという点です。const 評価器が持っているのは値と、命令ごとの span だけです。命令はマクロ定義が作ったので、span もマクロ定義のものです。

Step 3: 位置を持っているのはトークン

利用者が書いた SoundAlarm というトークンは、表の 13 行 128 列の span を持っています。マクロのマッチはこのトークンを 1 個ずつ見て、一致しなかったトークンの位置でエラーを出します。Day 070 の E7・E8(no rules expected)が表の行を指したのも同じ理由です。だから、数える処理を値の世界(const 評価)からトークンの世界(マッチ)に移すと、位置が手に入ります。

Step 4: macro_rules! は算術ができないので、数を「並び」に変える

macro_rules! は 4 から 1 を引けません。そこで 4 を _ _ _ _ という長さ 4 の並びに変え、アクションを 1 個読むたびに _ を 1 個消します。並びが空になった時点でアクションが残っていれば、一致する規則がありません。代わりに、4 を _ _ _ _ に変える規則を 0..=8 の 9 個だけ手で書く必要があります(有限の表)。

Step 5: 転送で不透明になる断片に気をつける

V3b で、$cap:literal で受けた 4 を補助マクロの (4; …) に一致させようとして、ライブラリ自体が壊れました。literal・expr などで一度受けた断片は、次のマクロからは中身の見えない 1 個の塊です。tt で受けると透過します。Day 070 で $a:expr で受け直したのと逆向きの使い方で、「中身で一致させたいなら tt、型として扱わせたいなら expr/ty」と整理できます。

Step 6: 有限の表の外は、前の仕組みに退避する

V3 だけでは max 9 が書けなくなります。これは Day 071 までの利用者を壊す変更です。($cap:literal; …) を最後に置き、表に無い数は const 評価(V1 の形)に回すことで、改善できる範囲は改善し、それ以外は以前と同じにしました(V6)。

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

Rust のコンパイル時計算には、性質の違う 2 つの道があります。

道扱うもの得意なこと苦手なこと
const 評価(const・const fn)型のついた値算術・比較・ループ(while)。型の世界の数(size_of、const generics)と組み合わせられるトレイトのメソッド、確保、書式化。誤りの位置は評価した式の span
マクロのマッチ(macro_rules!)span つきのトークン形の検査。誤りの位置が利用者のトークン算術、型の情報。数は「並び」に変えるしかない

Rust の設計は、const 評価を「実行時と同じ意味の計算を、コンパイル時にもする」方向に広げてきました(const trait の作業中の表示が c10 の E0658 に出ている)。それでも const 評価はマクロ展開の後に走るので、トークンの位置は原理的に持っていません。検査の目的が「利用者が書いた表のどこが悪いかを示す」なら、トークンが残っている段階(マッチ)で検査するのが筋です。目的が「型や大きさが条件を満たすか」なら(Day 069 の size_of の検査)、const 評価の段階でしか書けません。

迷った他案と採らなかった理由

案中身採らなかった理由
V0 const 評価の while(Day 071 のまま)表全体を 1 つの const _ で検査位置が 5:1。例外表に 1 件残る
V1 行ごとの const _行ごとの assert! + concat! の説明位置は 5:1 のまま(実測)。V6 の退避先としてだけ使う
V3b $cap:literal を転送—転送で不透明になり、ライブラリが壊れる(実測)
V3 _ の列だけmax 0..=8 だけ受けるmax 9・max 4usize が書けなくなる(Day 071 まで通っていた)
V4 compile_error!独自の文言位置が 5:1 に戻る(実測)
V5 名前付きの行き止まりマクロ名を文言に出す位置が 5:1 に戻る(実測)、理由は未解明
V6 = V3 + V1 への退避0..=8 はトークンで、他は const 評価で採用。 例外表が空、max 9 も通る
proc-macro で Span を指定するsyn で表を読み、5 個目のアクションの span で compile_error! を出す位置も文言も自由になるが、依存(syn・quote)と別クレートが要る。マクロ② の題材
generic_const_exprs で型の世界の数にする[(); MAX - N] を型に置くnightly のみ(feature gate)。安定版で使えない。今日は実行していない

🚨 コンパイルエラーが出た場合

エラー出た原因対処
E0015 cannot use for loop on … in constantsconst 評価の中の forwhile と添字に書き直す(c1)
E0015 cannot call non-const formatting macro in constantspanic! に &str 以外(usize など)を渡した&str 1 個に絞るか、concat!/stringify! で文字列リテラルにする(c5・c7)
E0493 destructor of String cannot be evaluated at compile-timeconst の中で String を作った文字列はリテラルか &'static str で持つ
E0658 cannot call conditionally-const method <str as PartialEq>::eqstr を == で比べた(1.96。1.85 では E0015)as_bytes() で長さとバイトを比べる(c16)
no rules expected literal metavariable$x:literal で受けた断片を、別のマクロでリテラルの字面に一致させようとした受ける側を tt にする(V3b → V3)
macros that expand to items must be delimited with braces or followed by a semicolon項目の位置で補助マクロを name!( … ) で再帰呼び出ししたname!{ … } で呼ぶ
no rules expected 9(max 9)_ の列の表が 0..=8 しかない退避の規則 ($cap:literal; …) を最後に足す(V6)

🌐 他言語との比較

観点Rust の const 評価(今日実行)Rust の macro_rules!(今日実行)C++ の constexpr/static_assertZig の comptime
ループwhile・loop は可、for は不可(E0015)再帰で書くfor も可(C++14 以降)for・while とも可
書式つきのエラー文{} に &str 1 個までstringify!・concat! でリテラルを作るだけstatic_assert の文は文字列リテラル(C++26 で式も可)@compileError に std.fmt.comptimePrint で作った文字列を渡せる
誤りの位置評価した式の span(マクロ定義なら呼び出し全体)一致しなかった利用者のトークンstatic_assert の行@compileError を呼んだ場所
確保不可(E0493・E0015)該当しないC++20 で一時的な確保が可comptime のアロケータがある

Rust の 2 列は今日実行した結果です。C++・Zig の列は仕様の記憶に基づく要旨で、今日は実行していません。比べて分かるのは、Rust の const 評価は他の 2 言語より書ける範囲が狭いことと、そのぶん macro_rules! の「トークンで一致させる」が別の道具として残っていることです。

🏭 実務での使いどころ

  • DSL マクロの件数・形の上限: 「1 行あたり N 個まで」「引数は 3 個まで」のような上限は、N が小さく固定なら V6 のように並びで数えると、利用者に該当のトークンを示せます。SmallVec<[T; N]> の容量に合わせる Day 069 の max がその実例です
  • 型の大きさの検査: const _: () = assert!(size_of::<T>() <= 48); のような検査は、トークンに無い情報なので const 評価でしか書けません。位置が呼び出し全体になるのは受け入れ、メッセージに説明を入れます
  • ライブラリのマクロの引数の受け方: 補助マクロに渡して中身で分岐させたいなら tt で受ける。literal や expr で受けると、次のマクロからは中身が見えません(V3b の事故)

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
「const の中では for も書ける」while が書けるのでfor はトレイトのメソッド呼び出しに展開されるので E0015(c2・c3、両版で実測)
「panic! にはいつもの書式を渡せる」実行時の panic! と同じ見た目const では {} に &str 1 個まで(c6 は E0015)
「行ごとに const _ を作れば、エラーはその行を指す」生成した場所が行ごとに分かれるのでspan はマクロ定義のトークンのもの。5:1 のまま(V1、実測)
「メッセージに行の字面が出るなら、位置も出せる」両方とも「どの行か」の情報に見える字面は値として運べるが、位置は運べない(Q2)
「$x:literal で受けたものは、次のマクロで 4 に一致する」中身は 4 に見える転送すると不透明な断片になる。tt で受ける(V3b、実測)
「文言を良くすれば、利用者に親切」説明が長いほど分かりやすい今日の範囲では compile_error! や別マクロへの転送で位置を失った(V4・V5)。位置と文言の両方が要るなら proc-macro

🔗 一次資料(参照日: 2026-10-12)

  • Rust Reference "Constant evaluation"(https://doc.rust-lang.org/reference/const_eval.html): "Only a subset of all expressions can be evaluated at compile-time."/定数式として "loop and while expressions"、"Calls of const functions and const methods"、"Closure expressions which don't capture variables from the environment" を列挙し、for は列挙していない。const context は配列の長さ・定数と static の初期化子・const generic の引数・const ブロック — Q1 の c1〜c3・c15 の根拠
  • Rust Reference "Macros By Example" の転送の節(Day 070 で取得した内容を再利用。今日は取得し直していない): 転送された断片は中身のトークンで一致させられず、例外は ident・lifetime・tt — V3b の根拠
  • rustc の issue #143874(c10 の 1.96 のエラーが案内したもの。中身は開いていない)
  • エラー文はすべて rustc 1.96.0 と 1.85.1 で今日実行した出力

🚧 今日扱わなかった範囲(次回以降)

扱わなかったこと理由・次の予定
V5 で位置が呼び出し全体に戻った理由ident の転送は透過するはずなので予想と違った。rustc の span の扱い(macro_rules の NoMatch の報告箇所)のソースは未読
generic_const_exprs(nightly)での上限の型への持ち上げnightly ツールチェーンを入れていない。安定版で使えないので fsm! には採らない前提
max 9 以上を 0..=16 などに広げたときのコンパイル時間表は 0..=8 の 9 規則だけ測った。300 行 × 4 アクションで V0 と差が測れなかった(load average 21〜22、10 回のみ)。静かな Mac mini での取り直しは Day 066・068・070・071 の宿題と一緒に
ガード(if $g:expr)の誤りの位置Day 071 からの持ち越し。今日は max の検査だけを動かした
proc-macro で span を指定する版マクロ②(次のテーマ)の題材。位置と文言を両立できるかはそこで確かめる
C++・Zig の比較列の実行記憶ベース
const trait(issue #143874)の進み具合エラー文が案内したことのみ。安定化の時期は確かめていない

3 問の自己点検

  1. 同じ指示を AI に出せば数時間で出るか — 「const では for が書けない」「while を使う」という一般論はすぐ出る。ただし、16 個の候補を両版で実行した判定表(9 書ける/7 書けない、c10 だけ番号が変わる)、行ごとの const _ でも 5:1 のまま(Day 071 の仮説の否定)、$cap:literal の転送でライブラリが壊れること、compile_error!・名前付き行き止まりで位置を失うこと、_ の列で 13:128 を指し例外表が空になること、は今日ビルドして得た結果
  2. AI に指示するだけでは出ない部分 — E10 が表の行を指さない理由を「値は字面を運べるが、位置は運べない」と特定し、検査を const 評価からマクロのマッチに移すことで位置を取り戻したこと。そのとき max 9 が壊れることに気づいて退避の規則を足し、Day 071 の利用者を壊さない形(V6)にしたこと
  3. 世界の実物と比べて何割か — Reference の定数式の列挙のうち、今日試したのはループ・関数呼び出し・クロージャ・panic!・算術・添字の 6 系統。static・const fn の中の可変参照・浮動小数点・unsafe の生ポインタ・const generics との組み合わせは扱っていない。static_assertions クレートなど実在の const 検査ライブラリの書き方とは照合していない

➡️ 次のステップ

  • 発展: V5 で位置が戻った理由を確かめる($a:ident を $a:tt で渡す、補助マクロを __within! 自身の別規則にする、の 2 通りを測る)。位置を保ったまま文言を足せる形が macro_rules! の範囲にあるかを決着させる
  • 次回予告: マクロ① の残り期間(〜10/24)。今日の概念理解の次は設計か実装。候補は (1) 入れ子の状態(Alarm の中に鳴動中/消音中)を自作 fsm! と statig の superstate で比べる設計、(2) ガード(if $g:expr)に bool 以外を書いたときの位置を UI テストに足す実装

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

Q1 の予想(16 個のうち書けると思った数)と実際(9 個)

Q3 の予想(V1・V3・V4 が何行目を指すか)と実際

自分の回答(Q2 の 3 文、Q4 の判断)

気づき・メモ