フェーズ: 上級者 (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 つです。
- 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!に何を渡せるのか - エラーの位置(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;
| # | 初期化式の中身 |
|---|---|
| c1 | while で回し、超えたら panic!("too many") |
| c2 | for r in ROWS { if r.1 > CAP { panic!(..) } } |
| c3 | for i in 0..ROWS.len() { .. } |
| c4 | ROWS.iter().map(|r| r.1).max() |
| c5 | panic!("{}", ROWS[i].0)(&str を 1 個) |
| c6 | panic!("{} has {} actions", ROWS[i].0, ROWS[i].1)(usize も渡す) |
| c7 | panic!(concat!("fsm!: ...", stringify!(4), ""))` |
| c8 | assert!(ROWS[i].1 <= CAP, "{}", ROWS[i].0) |
| c9 | let s = format!("{}", ROWS[0].0); |
| c10 | ROWS[0].0.eq("x")(&str の比較) |
| c11 | Some(ROWS[0].1).unwrap() |
| c12 | CAP - ROWS[1].1(4 − 5) |
| c13 | [(); CAP][ROWS[1].1](長さ 4 の配列の 5 番目) |
| c14 | 自作の const fn n(r: &(&str, usize)) -> usize を呼ぶ |
| c15 | 変数を捕まえないクロージャ |r| r.1 を作って呼ぶ |
| c16 | ROWS[0].0.as_bytes() の長さと先頭バイトを比べる |
1.85.1 でも結果が同じかも予想してください。
Q2: E0080 はなぜ表の行を指さないのか
Day 071 の E10(3 行目にアクション 5 個)の出力は次のとおりでした。
**メッセージには行の字面が出ているのに、位置は 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!(<行>))); を生成する |
| V3b | max $cap:literal で受けた $cap を補助マクロ __within! に渡し、(4; …) => … のように**リテラル 4 で一致**させて _ _ _ _ に変える |
| V3 | V3b と同じだが max $cap:tt で受ける。_ を 1 個ずつ消しながらアクションを 1 個ずつ読み、_ が尽きたのにアクションが残ると、一致する規則が無い |
| V4 | V3 で _ が尽きた場合に一致する規則を足し、compile_error!(concat!("fsm!: ", stringify!($a), " exceeds max")) を出す |
| V5 | V3 で _ が尽きた場合に、どの規則にも一致しない補助マクロ __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.0 | 1.85.1 |
|---|---|---|---|
| c1 | while + panic!("too many") | 書ける(E0080 evaluation panicked: too many) | 書ける(E0080 の文言は "evaluation of constant value failed") |
| c2 | for r in ROWS | E0015 cannot use for loop on &[(&str, usize)] in constants(2 件) | E0015 |
| c3 | for i in 0..len | E0015 | E0015 |
| c4 | iter().map().max() | E0015(2 件)+ "core::slice::<impl [T]>::iter is not yet stable as a const fn" | E0015 |
| c5 | panic!("{}", &str) | 書ける(メッセージに行の字面) | 書ける |
| c6 | panic!("{} … {}", &str, usize) | E0015 cannot call non-const formatting macro in constants | E0015 |
| c7 | panic!(concat!(…)) | 書ける | 書ける |
| c8 | assert!(cond, "{}", &str) | 書ける(E0080 evaluation panicked: (Locked, Unlock) if c.failures + 1 >= 3) | 書ける |
| c9 | format! | E0015(2 件)+ E0493 destructor of String cannot be evaluated at compile-time | 同じ |
| c10 | str の eq | E0658 cannot call conditionally-const method <str as PartialEq>::eq + "PartialEq is not yet stable as a const trait"(issue #143874 を案内) | E0015(版で番号が違う) |
| c11 | Option::unwrap | 書ける(警告のみ) | 書ける |
| c12 | 4 - 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 |
| c16 | as_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 >= 3 | 5:1 |
V1 行ごとの const _ | E0080 evaluation panicked: fsm!: more actions than max in row: (Locked, Unlock) if c.failures + 1 >= 3 | 5: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 max | 5:1 |
| V5 名前付きの行き止まりマクロ | no rules expected identifier SoundAlarm(note: while trying to match end of macro) | 5:1 |
読み取れること:
- V1: 行ごとに分けても、検査が const 評価である限り位置は変わらない。
assert!のトークンはマクロ定義のもので、行ごとに生成しても span の出どころは同じです。変わったのはメッセージ(concat!で説明を足せた)だけ。Day 071 の「発展」の仮説(行ごとのconst _で下線が行に付くか)は否定されました - V3: 「数える」を const 評価からマクロのマッチに移すと、位置は利用者のトークンになる。 一致しなかった最初のトークンが 5 個目の
SoundAlarmで、それは利用者が書いたトークンだからです。max 3に変えると 4 個目のNotifyGuard(13:115)を指しました(実測) - **V3b:
$cap:literalで一度受けると、4というリテラルで一致させられない。** Reference の転送の規則どおりで、literalは不透明な断片になります。ttで受ければ透過します。エラーは利用者の誤りではなくライブラリの定義の誤りとして出るので、正しい表も通らなくなります - 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:1 | 13:128 | 13:128 |
max 3、4 個 | E0080 5:1 | 13:115 | 13: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:20 | 9: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 constants | const 評価の中の for | while と添字に書き直す(c1) |
| E0015 cannot call non-const formatting macro in constants | panic! に &str 以外(usize など)を渡した | &str 1 個に絞るか、concat!/stringify! で文字列リテラルにする(c5・c7) |
E0493 destructor of String cannot be evaluated at compile-time | const の中で String を作った | 文字列はリテラルか &'static str で持つ |
E0658 cannot call conditionally-const method <str as PartialEq>::eq | str を == で比べた(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_assert | Zig の 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 問の自己点検
- 同じ指示を AI に出せば数時間で出るか — 「const では
forが書けない」「whileを使う」という一般論はすぐ出る。ただし、16 個の候補を両版で実行した判定表(9 書ける/7 書けない、c10 だけ番号が変わる)、行ごとのconst _でも 5:1 のまま(Day 071 の仮説の否定)、$cap:literalの転送でライブラリが壊れること、compile_error!・名前付き行き止まりで位置を失うこと、_の列で 13:128 を指し例外表が空になること、は今日ビルドして得た結果 - AI に指示するだけでは出ない部分 — E10 が表の行を指さない理由を「値は字面を運べるが、位置は運べない」と特定し、検査を const 評価からマクロのマッチに移すことで位置を取り戻したこと。そのとき
max 9が壊れることに気づいて退避の規則を足し、Day 071 の利用者を壊さない形(V6)にしたこと - 世界の実物と比べて何割か — 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 テストに足す実装