フェーズ: 上級者 (Phase 3) | 問題タイプ: 実装 | テーマ: マクロ①(宣言的マクロ macro_rules!)
📚 背景知識(読んでから問題へ)
Day 070 で、fsm! の行のアクションを [ $($acts:tt)* ] で受け、__count! で形を検査し、__emit! で $a:expr として取り込み直す形にしました。7 種の誤りがすべて表の行を指すことを、手で 1 種ずつ入れ替えて確かめています。
手で確かめた結果は、次に誰かがマクロを触った瞬間に守られなくなります。「__emit! を消して emit($Action::$a ...) に戻したほうが短い」という善意の PR が来ても、cargo test は通ります。エラーの位置はコンパイルが失敗したときにしか現れないので、普通の単体テストでは守れません。
Rust の生態系でこれを止める道具が trybuild です(dtolnay 作。crates.io で累計 71,767,595 ダウンロード、逆依存 2,970 クレート、2026-10-11 に crates.io API から取得)。tests/ui/*.rs を 1 ファイルずつ別のクレートとしてコンパイルし、出たエラー文を *.stderr と比べます。serde・thiserror・tokio などの proc-macro の利用者体験は、この形で守られています(採用例は一般に知られている範囲の話で、今日は各リポジトリを開いていません)。
ただし trybuild が見ているのは「前回と同じ文か」だけです。今日は、それで足りるのかを実測で確かめます。
📝 問題
環境: rustc 1.96.0(stable)と rustc 1.85.1(+1.85)、trybuild = "=1.0.122"、edition 2024。マクロの本体は Day 070 の模範解答の src/lib.rs をそのまま使います(215 行。fsm!・__count!・__emit!・door・motor・単体テスト 4 件)。
Part 1: UI テストを書く
扉の表の 3 行目(警報の行)を壊したファイルを tests/ui/ に 10 個作り、壊していない表 ok_table.rs と合わせて trybuild で検査してください。E1〜E8 は Day 070 と同じ、E9・E10 は今日足すものです。
| # | 3 行目に書くもの | 誤りの種類 |
|---|---|---|
| E1 | Audit(c.failures + 1) | 引数が足りない |
| E2 | Audit(c.failures + 1, 0) | 引数の型が違う |
| E3 | NotifyGuard(1) | unit variant に引数 |
| E4 | Audit | 引数が要る variant に引数なし |
| E5 | Audti(c.failures + 1, false) | 綴り誤り |
| E6 | Audit(c.failures + 1, false, 9) | 引数が多い |
| E7 | if c.is_admin { SoundAlarm } else { NotifyGuard } | variant 以外の式 |
| E8 | Act::SoundAlarm | 完全修飾 |
| E9 | 遷移先を => Alrm | 存在しない状態($to:ident) |
| E10 | アクションを 5 個(max 4 超え) | 件数の上限 |
予想してから実行: E9 と E10 は、それぞれ何行目を指すでしょうか(表の 3 行目は、UI ファイルの 13 行目です)。
Part 2: 退行を検出できるか
src/lib.rs の 3 か所を Day 069 の形($a:ident $( ( $($arg:expr),* ) )? で受けて emit($Action::$a ...) を直接生成)に戻し、Part 1 で作ったスナップショットのまま cargo test --test ui を実行します。
- 何件が
mismatchになるか - そのうち、エラーの位置が本当に表の行から外れた(本物の退行)のは何件か
Part 3: TRYBUILD=overwrite の穴を塞ぐ
trybuild の README は、スナップショットの更新に TRYBUILD=overwrite を勧め、その後 git diff を見るよう書いています。Day 069 の形に戻した状態で TRYBUILD=overwrite cargo test --test ui を実行すると、どうなるでしょうか。
そのうえで、スナップショット(*.stderr)を読み、「最初の --> が UI ファイルの 13 行目を指すか」「help: がマクロ定義を書き換える提案をしていないか」を検査するテスト tests/ui_position.rs を書いてください。表の行を指せないと分かっている誤りは、理由つきの例外表に入れること。
Part 4: rustc の版を変える
rustc 1.85.1 で同じ UI テストを実行し、(a) trybuild のスナップショット照合、(b) Part 3 の位置の検査が、それぞれ何件で落ちるかを測ってください。trybuild 1.0.122 が 1.85 で使えない場合は、使える版を選び直すこと。
Part 5: CI でどちらを門にするか
Part 2〜4 の数字をもとに、CI で「落ちたらマージさせない」門にするのはどちらか(両方か)を、却下案と見直し条件つきで 5 行以内に書いてください。
💡 ヒント
ヒント1(方向性)
- trybuild は UI ファイルを別のクレートとしてコンパイルします。Day 070 は同じクレートの中で試したので、エラーは
src/lib.rs:56(マクロ定義)を指しました。別のクレートから使ったとき、依存先のマクロ定義の行は表示されるでしょうか - 「スナップショットが一致しない」と「エラーの位置が悪くなった」は、同じことではありません。文言・空白・
help:の書き方が変わっても不一致になります
ヒント2(アプローチ)
- 10 個の UI ファイルは、壊す 1 行以外は同じです。ひな形(
tests/_base.txt)からsedで生成すれば、行番号が全ファイルで 13 にそろい、Part 3 の検査が単純になります - trybuild はスナップショットの中で、入力ファイルの行番号だけを残し、それ以外のファイル(依存先の
src/lib.rs)の行番号を消します(src/normalize.rsのコメント)。だから--> tests/ui/<name>.rs:L:CのLを読めば位置が分かります - 1.85 で trybuild 1.0.122 が使えないときは、
rust-version = "1.85"を書いてCARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo +1.85 generate-lockfileで、1.85 に対応する版を選ばせます
ヒント3(コード骨格)
// tests/ui.rs
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/ok_table.rs");
t.compile_fail("tests/ui/e*.rs");
}
// tests/ui_position.rs の中心
fn first_primary(stderr: &str) -> Option<(String, u32)> {
let line = stderr.lines().find(|l| l.trim_start().starts_with("--> "))?;
let loc = line.trim_start().strip_prefix("--> ")?;
let mut parts = loc.rsplitn(3, ':'); // 列, 行, ファイル の順に取れる
// ...
}
✅ 模範解答
ファイル構成
spanfsm/
├── Cargo.toml
├── src/lib.rs # Day 070 の模範解答そのまま(215 行)
└── tests/
├── _base.txt # UI ファイルのひな形(@ROW3@ を差し替える)
├── gen_ui.sh # ひな形から 11 ファイルを生成
├── ui.rs # trybuild(スナップショット照合)
├── ui_position.rs # スナップショットに対する位置の方針の検査
└── ui/
├── ok_table.rs
├── e1_missing_arg.rs … e10_over_max.rs
└── e1_missing_arg.stderr … e10_over_max.stderr # TRYBUILD=overwrite で生成し、目で確認してから commit
Cargo.toml:
[package]
name = "spanfsm"
version = "0.1.0"
edition = "2024"
[dependencies]
[dev-dependencies]
trybuild = "=1.0.122" # rust-version = "1.88"。1.85 では 1.0.119 を使う(Part 4)
tests/_base.txt(13 行目が壊す行):
// @CASE@
pub struct Ctx { pub failures: u32, pub is_admin: bool }
impl Ctx { pub fn code_ok(&self) -> bool { false } }
spanfsm::fsm! {
context c: Ctx;
state Door { Locked, Closed, Open, Alarm }
event Ev { Unlock, Lock, Push, Pull, Reset }
action Act max 4 { RecordFailure, ResetFailures, SoundAlarm, NotifyGuard, Audit(u32, bool) }
(Locked, Unlock) if c.failures >= 3 => Alarm / [SoundAlarm, Audit(c.failures, false)];
(Locked, Unlock) if c.code_ok() => Closed / [ResetFailures, Audit(0, true)];
@ROW3@
(Locked, Unlock) => Locked / [RecordFailure, Audit(c.failures + 1, false)];
(Closed, Lock) => Locked;
(Closed, Push) => Open;
(Open, Pull) => Closed;
(Alarm, Reset) if c.is_admin => Locked / [ResetFailures,];
}
fn main() {}
tests/gen_ui.sh(sh tests/gen_ui.sh で 11 ファイルを作り直す):
#!/bin/sh
set -eu
cd "$(dirname "$0")"
P='(Locked, Unlock) if c.failures + 1 >= 3 => '
gen() { sed -e "s|@CASE@|$1|" -e "s|@ROW3@|$P$2|" _base.txt > "ui/$1.rs"; }
gen ok_table 'Alarm / [RecordFailure, Audit(c.failures + 1, false), SoundAlarm, NotifyGuard];'
gen e1_missing_arg 'Alarm / [RecordFailure, Audit(c.failures + 1), SoundAlarm, NotifyGuard];'
gen e2_wrong_type 'Alarm / [RecordFailure, Audit(c.failures + 1, 0), SoundAlarm, NotifyGuard];'
gen e3_arg_to_unit 'Alarm / [RecordFailure, Audit(c.failures + 1, false), SoundAlarm, NotifyGuard(1)];'
gen e4_no_args 'Alarm / [RecordFailure, Audit, SoundAlarm, NotifyGuard];'
gen e5_typo 'Alarm / [RecordFailure, Audti(c.failures + 1, false), SoundAlarm, NotifyGuard];'
gen e6_extra_arg 'Alarm / [RecordFailure, Audit(c.failures + 1, false, 9), SoundAlarm, NotifyGuard];'
gen e7_if_expr 'Alarm / [RecordFailure, if c.is_admin { SoundAlarm } else { NotifyGuard }];'
gen e8_qualified 'Alarm / [RecordFailure, Act::SoundAlarm];'
gen e9_bad_target 'Alrm / [RecordFailure, Audit(c.failures + 1, false), SoundAlarm, NotifyGuard];'
gen e10_over_max 'Alarm / [RecordFailure, Audit(c.failures + 1, false), SoundAlarm, NotifyGuard, SoundAlarm];'
tests/ui.rs:
//! エラーの位置の退行テスト。tests/ui/e*.rs はコンパイルに失敗し、
//! その stderr が同名の .stderr と一致しなければならない(trybuild)。
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/ok_table.rs");
t.compile_fail("tests/ui/e*.rs");
}
tests/ui_position.rs:
//! スナップショット(tests/ui/*.stderr)に対する「位置の方針」の検査。
//! trybuild は「前回と同じ文か」しか見ない。`TRYBUILD=overwrite` で退行を
//! 受け入れてしまっても、ここで「最初の `-->` が表の行を指すか」を止める。
use std::fs;
use std::path::Path;
/// 壊した行(_base.txt の @ROW3@)の行番号
const ROW: u32 = 13;
/// 表の行を指さなくてよい例外。理由を必ず書く
const EXCEPTIONS: &[(&str, u32, &str)] = &[(
"e10_over_max",
5,
"E0080 は const 評価の panic!。macro_rules! から行の位置を渡せない(Day 069・070 の宿題)",
)];
/// stderr の最初の `--> tests/ui/<name>.rs:L:C` から (ファイル名, L) を取り出す
fn first_primary(stderr: &str) -> Option<(String, u32)> {
let line = stderr.lines().find(|l| l.trim_start().starts_with("--> "))?;
let loc = line.trim_start().strip_prefix("--> ")?;
let mut parts = loc.rsplitn(3, ':');
let _col = parts.next()?;
let row: u32 = parts.next()?.parse().ok()?;
let file = parts.next()?.to_string();
Some((file, row))
}
#[test]
fn every_error_points_at_the_table_row() {
let dir = Path::new("tests/ui");
let mut checked = 0;
let mut bad = Vec::new();
let mut names: Vec<_> = fs::read_dir(dir)
.unwrap()
.map(|e| e.unwrap().path())
.filter(|p| p.extension().is_some_and(|x| x == "stderr"))
.collect();
names.sort();
for p in names {
let name = p.file_stem().unwrap().to_str().unwrap().to_string();
let text = fs::read_to_string(&p).unwrap();
let want = EXCEPTIONS
.iter()
.find(|(n, _, _)| *n == name)
.map_or(ROW, |(_, l, _)| *l);
match first_primary(&text) {
Some((file, row)) if file == format!("tests/ui/{name}.rs") && row == want => {}
got => bad.push(format!("{name}: want line {want}, got {got:?}")),
}
// help: の提案がマクロ定義(依存クレートの src/lib.rs)を書き換えていないこと
if text.contains("help:") && text.contains("--> src/lib.rs") {
bad.push(format!("{name}: help: がマクロ定義を指している"));
}
checked += 1;
}
println!("checked {checked} snapshots, {} violations", bad.len());
assert!(checked >= 10, "スナップショットが {checked} 件しかない");
assert!(bad.is_empty(), "位置の方針に反する:\n{}", bad.join("\n"));
}
#[test]
fn parser_reads_trybuild_paths() {
let s = "error[E0061]: x\n --> tests/ui/e1_missing_arg.rs:13:73\n |\n";
assert_eq!(first_primary(s), Some(("tests/ui/e1_missing_arg.rs".into(), 13)));
assert_eq!(first_primary("no arrow"), None);
}
実行と結果(実測、rustc 1.96.0、Apple M1・8GB、2026-10-11)
$ sh tests/gen_ui.sh # UI ファイル 11 個を生成
$ TRYBUILD=overwrite cargo test --test ui # 初回だけ: スナップショット 10 個を書き出す(中身を目で確認する)
$ cargo test # 単体 4 + ui 1 + ui_position 2
test result: ok. 4 passed; 0 failed; ... (src/lib.rs の単体テスト)
test result: ok. 1 passed; 0 failed; ... finished in 2.63s (trybuild 11 ケース)
test result: ok. 2 passed; 0 failed; ... finished in 0.00s (ui_position: checked 10 snapshots, 0 violations)
$ cargo clippy --all-targets # 警告 0 件(grep -c "^warning" で 0)
trybuild の 11 ケースの所要時間は 2.08〜3.40 秒(4 回、実測)。測定直前の load average は 16.58 で、ばらつきは大きいです。trybuild の作業ディレクトリ(target/tests/trybuild)を消した後も 3.08 秒でした(依存のビルド結果は target/ に残っているため)。
Part 1 の答え(実測)
| # | エラー | 最初の --> |
|---|---|---|
| E1 | E0061 | 13:73(表の行) |
| E2 | E0308 | 13:95 |
| E3 | E0618 | 13:115 |
| E4 | E0308 | 13:73 |
| E5 | E0425 | 13:73 |
| E6 | E0061 | 13:73 |
| E7 | no rules expected c | 13:76 |
| E8 | no rules expected :: | 13:76 |
| E9 | E0425 cannot find value Alrm | 13:48(表の行)。help: が Alarm を提案 |
| E10 | E0080 evaluation panicked | 5:1(spanfsm::fsm! { の行)。下線はマクロ呼び出し全体 |
E9 が表の行を指すのは、$to:ident で受けた識別子を Ok($to) にそのまま置いているからです。Day 070 の結論(「利用者が書いた識別子そのものが誤りの場所ならば表の行」)と合います。help: は表の行で Alrm → Alarm に + を 1 文字足す提案になっています。
E10 は表の行を指しません。メッセージの中には行の字面 (Locked, Unlock) if c.failures + 1 >= 3 が出ています(Day 069 で panic!("{}", rows[i].0) にした効果)が、下線はマクロ呼び出し全体です。これが Part 3 の例外表に入る 1 件です。
Part 2 の答え(実測)
Day 069 の形に戻す差分は 3 行です:
- $( / [ $($acts:tt)* ] )? ; )*
+ $( / [ $( $a:ident $( ( $($arg:expr),* ) )? ),* $(,)? ] )? ; )*
- 0usize $( + $crate::__count!($($acts)*) )?
+ 0usize $( $( + { let _ = stringify!($a); 1usize } )* )?
- $( $crate::__emit!(emit, $Action; $($acts)*); )?
+ $( $( emit($Action::$a $( ( $($arg),* ) )?); )* )?
- 10 件中 8 件が
mismatch(E1〜E8。E9・E10 は一致) - そのうち本物の退行は 4 件(E1・E3・E4・E6)。最初の
-->が 13 行目から 5:1(spanfsm::fsm! {)に移りました
残りの 4 件(E2・E5・E7・E8)は、位置は 13 行目のままで文言だけが変わったものです。
| # | Day 069 の形での違い | 位置 |
|---|---|---|
| E2 | 下線の描き方が「マクロ呼び出し全体の枠+13 行目の ^」に変わる | 13:95 のまま |
| E5 | E0425 → E0599(no variant ... named Audti found for enum Act) | 13:73 のまま |
| E7・E8 | note: while trying to match ']' が --> src/lib.rs(行番号なし)を指すようになる | 13:76 のまま |
別のクレートから使うと、「マクロ定義を指す」はマクロ呼び出し全体を指すに変わります。 Day 070 は同じクレートの中で試したので src/lib.rs:56 が出ました。trybuild の UI ファイルは別クレートなので、依存先のマクロ定義の行は出ず、spanfsm::fsm! { から } までの 15 行に枠が付きます。表を書く人から見れば、どちらも「どの行が悪いか分からない」点は同じです。
E1 の Day 069 の形での出力:
Part 3 の答え(実測)
TRYBUILD=overwrite は退行をそのまま書き込み、テストは通ります(test result: ok. 1 passed、実測)。README は "be sure the compiler's output is what you had in mind" と、確認を人の git diff に任せています(一次資料)。8 ファイルの差分のうち 4 件だけが本物の退行、という状態で人が見落とさない保証はありません。
ui_position.rs を、Day 069 の形で上書きされたスナップショットに対して実行した結果(実測):
checked 10 snapshots, 5 violations
e1_missing_arg: want line 13, got Some(("tests/ui/e1_missing_arg.rs", 5))
e3_arg_to_unit: want line 13, got Some(("tests/ui/e3_arg_to_unit.rs", 5))
e4_no_args: want line 13, got Some(("tests/ui/e4_no_args.rs", 5))
e4_no_args: help: がマクロ定義を指している
e6_extra_arg: want line 13, got Some(("tests/ui/e6_extra_arg.rs", 5))
本物の退行 4 件と、E4 の help: がマクロ定義を書き換える提案(emit($Action::$a(/* u32 */, /* bool */) ...))の 1 件だけを止め、文言だけ変わった 4 件は通しました。Day 070 の形のスナップショットでは 0 violations です。
Part 4 の答え(実測)
trybuild 1.0.122 は rust-version = "1.88" なので 1.85.1 では requires rustc 1.88 で止まりました。CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback で選び直すと trybuild 1.0.119 になりました。
| 検査 | rustc 1.85.1 での結果 |
|---|---|
| (a) trybuild のスナップショット照合(1.96 で作ったもの) | 10 件中 8 件が mismatch(一致は E7・E8 のみ) |
(b) 1.85 の出力で上書きしたスナップショットに ui_position | 0 violations(10 件すべて同じ位置) |
8 件の不一致の中身は、行番号の欄の桁そろえ( 9 | → 9 |)、help: の差分の描き方(+++ → ~~~)、E0080 の文言(evaluation panicked: ... → evaluation of constant value failed)でした。位置は 1 件も変わっていません。 マクロは何も変えていないのに、スナップショットだけを見ると 8 件の「失敗」に見えます。
Part 5 の答えの例
門にするのは ui_position(位置の方針)。trybuild の照合は「文言が変わった」の通知に留め、rustc を上げる PR でだけ overwrite して差分を読む。
根拠: 1.85→1.96 で照合は 8/10 が不一致・位置は 0/10 の変化。Day 069 への退行では照合 8 件中 本物は 4 件、位置の検査は 4 件+help 1 件だけを止めた。
却下: trybuild 照合だけを門にする(rustc 更新のたびに 8 件の誤報、overwrite で退行が通る)/位置の検査だけにする(E5 の E0425→E0599 のような文言の悪化に気づけない)。
見直し条件: rustc を固定(rust-toolchain.toml)して版が動かないなら照合も門にしてよい。E10 を表の行に向けられたら例外表から消す。
🪜 Step-by-Step 解説
Step 1: UI ファイルをひな形から作る
10 個の誤りは「3 行目だけが違う」ファイルです。手で 10 個書くと、どこかで 1 行ずれて行番号がそろわなくなります。ひな形 _base.txt の @ROW3@ を sed で差し替えれば、全ファイルで壊した行が 13 行目になり、Part 3 の検査は ROW = 13 の 1 定数で済みます。ok_table.rs も同じひな形から作るので、「壊していない表はコンパイルが通る」ことも同じ条件で確かめられます。
Step 2: スナップショットは目で確認してから commit する
TRYBUILD=overwrite は「今の出力を正解にする」操作です。最初の 1 回は、10 個の .stderr を開いて、最初の --> が 13 行目か、help: が表の行を直す提案かを見ました(E10 だけが 5 行目)。この目視を、Step 4 でテストに置き換えます。
Step 3: 退行を入れて、テストが何を言うかを見る
テストは「通る」ことより「落ちるべきときに落ちる」ことを確かめないと信用できません。Day 069 の形に 3 行戻して実行すると 8 件が落ちました。ここで 8 件全部を「退行」と読むと間違えます。1 件ずつ最初の --> を見ると、位置が動いたのは 4 件でした。スナップショットの不一致は「何かが変わった」までしか言わないことが分かります。
Step 4: 方針をテストにする
「エラーは表の行を指す」は、このマクロの仕様です。仕様をスナップショット(ある日の出力)だけに持たせると、overwrite のたびに仕様が上書きされます。そこで仕様だけを抜き出して ui_position.rs にしました。
- 読むのはスナップショットのファイルなので、コンパイルは走らず 0.00 秒
- trybuild が入力ファイル以外の行番号を消す(
src/normalize.rs)ので、--> src/lib.rsが出ていればマクロ定義を指している、と判定できる - 表の行を指せない E10 は、理由を書いた例外表に入れる。黙って除外しない
Step 5: rustc の版を変えて、2 つの検査の性質を測る
1.85 で 8 件が不一致になり、位置は 0 件変わりませんでした。スナップショットはrustc の表示の変化とマクロの変化を区別できません。位置の検査は区別できます。この差が Part 5 の判断の根拠です。
🧭 設計思想・なぜこう書くのか
Rust は「コンパイルが通らないこと」を設計の道具にする言語です。所有権・型・#[deny(...)]・Day 069 の const _ による件数の検査は、どれも誤った使い方をコンパイル時に止める仕組みでした。その仕組みを作る側にとって、どの入力で、どの位置で、どう止まるかはライブラリの公開仕様の一部です。だから Rust の生態系には、コンパイルの失敗そのものをテストする trybuild(や、rustc 本体の tests/ui を回す compiletest)が育ちました。
一方で rustc のエラー文は、Rust のリリースごとに良くなるよう書き換えられています(今日 1.85→1.96 で 10 件中 8 件の文面が変わったのが実例)。エラー文を丸ごと固定するテストは、コンパイラの改善を「失敗」と報告する性質を持ちます。今日の形は、変わりやすい文面(スナップショット)と変わってはいけない仕様(位置)を別のテストに分けたものです。
迷った他案と採らなかった理由
| 案 | 中身 | 採らなかった理由 |
|---|---|---|
| (A) 今日の形: trybuild + スナップショットに対する位置の検査 | 2 つのテスト | 採用。 退行 4 件+help: 1 件を止め、文言の変化 4 件と版の違い 8 件は止めない(実測) |
| (B) trybuild だけ | スナップショット照合 | 版を変えると 8/10 誤報、overwrite で退行が通る(実測) |
(C) 手製のハーネス(cargo build --message-format=json で主 span を読む) | trybuild を使わない | 位置は JSON から正確に取れるが、UI ファイルごとのクレート生成・依存の解決・並列化を自前で持つことになる。trybuild がローカルに無い前提(Day 070 の宿題)で考えた代替で、今日はネットワークから取得できたので不要になった |
| (D) 位置の検査を trybuild の出力ではなく JSON の span で行う | (A) の検査部分だけ (C) にする | 1 回余分に全ケースをコンパイルする。今日の 10 件では、trybuild が残す --> の行番号で足りた |
| (E) 文言を正規表現でゆるく照合 | *.stderr をパターン化 | trybuild にその機能は無い。自作すると「何をゆるめたか」が新しい仕様になる |
| (F) テストしない(Day 070 の手作業のまま) | — | 次の変更で守られなくなる。Part 2 の 3 行の差分は「短くした」に見えて普通の単体テストは全部通る(4 passed、実測) |
🚨 コンパイルエラーが出た場合
今日は「わざと失敗させる」ことが目的なので、出るべきエラーは Part 1 の表です。それ以外で出たもの:
| エラー | 出た原因 | 対処 |
|---|---|---|
rustc 1.85.1 is not supported by the following package: trybuild@1.0.122 requires rustc 1.88 | trybuild の rust-version が 1.88 | rust-version = "1.85" を書き、CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo +1.85 generate-lockfile で 1.0.119 を選ばせる |
trybuild の mismatch | スナップショットと今の出力が違う | 差分を読み、位置が動いたか文言だけかを分ける。位置の検査(ui_position)が通っていれば文言だけ |
ui_position の want line 13, got ... 5 | エラーがマクロ呼び出し全体を指すようになった | マクロの本体で呼び出し式を組み立てていないかを見る(Day 070 の規則: 呼び出し全体を $a:expr で受ける) |
help: がマクロ定義を指している | help: の提案が依存先の src/lib.rs を書き換える | 同上。利用者がその提案に従うとマクロを壊す |
🌐 他言語との比較
| 観点 | Rust(trybuild、今日実行) | Rust 本体(compiletest) | C++(static_assert・concepts) | TypeScript(@ts-expect-error) |
|---|---|---|---|---|
| 「コンパイルが失敗すること」のテスト | tests/ui/*.rs を別クレートでビルドし .stderr と比較 | //~ ERROR 注釈で行とエラー番号を照合、.stderr も比較 | 失敗のテストは CMake の WILL_FAIL などビルド系で行う | 次の行に型エラーが無いと、それ自体がエラーになる |
| 位置の照合 | スナップショットに含まれる(今日はそれを読んで方針を検査) | 注釈が行を指すので位置が第一級 | 標準の仕組みは無い | 注釈の直後の行に限られる |
| 文言の変化への強さ | 弱い(1.85→1.96 で 8/10 不一致) | 本体の変更と一緒に更新される前提 | — | 文言は見ないので強い |
trybuild の列は今日実行した結果です。compiletest・C++・TypeScript の列は仕様の記憶に基づく要旨で、今日は実行していません。compiletest の //~ ERROR 注釈は、今日の ui_position と同じ発想(行とエラー番号は仕様、文言は付随物)を rustc 自身が採っている例として挙げました。
🏭 実務での使いどころ
- DSL マクロ・derive マクロの公開前: 利用者が書き間違えそうな入力を UI ファイルにし、エラーの位置を固定する。今日のように「壊し方の表」から生成すれば、表がそのまま仕様書になります
- rustc を上げる PR: スナップショットの差分が大量に出ます。位置の検査が通っていれば「文言の更新」として overwrite してよい、と判断できます。今日の 1.85→1.96 では 8 件がこれに当たりました
- マクロのリファクタリング: Part 2 の 3 行の差分は単体テストを全部通しました。エラーの位置を守るテストが無いと、レビューでも気づきにくい変更です
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 「trybuild が通っていれば、エラーの位置は守られている」 | スナップショットに位置も含まれている | TRYBUILD=overwrite で退行がそのまま正解になる(Part 3、実測)。位置は別に検査する |
| 「スナップショットの不一致 = 退行」 | 落ちたテストは悪いと読む | Day 069 の形で 8 件中 4 件は文言だけ、1.85 では 8 件全部が文言だけ(実測) |
「マクロ定義を指すエラーは、利用者には src/lib.rs:56 と見える」 | Day 070 は同じクレートで試した | 別クレートの利用者には fsm! { から } までの枠に見える(Part 2、実測)。テストは利用者と同じ別クレートの形で行う |
| 「UI ファイルは手で 1 個ずつ書く」 | trybuild の例が手書き | ひな形から生成すれば行番号がそろい、方針の検査が 1 定数で済む |
| 「E0080 も表の行を指すはず」 | E1〜E9 が全部表の行を指したので | const 評価の panic! はマクロ呼び出し全体を指す(E10、実測)。例外表に理由つきで残す |
🔗 一次資料(参照日: 2026-10-11)
- trybuild 1.0.122 の README(ローカルの crates.io レジストリのソース、Workflow の節):
*.stderrの更新にTRYBUILD=overwriteを使い、その後 "be sure the compiler's output is what you had in mind" とgit diffの確認を求めている — Part 3 の「確認が人任せ」の根拠 - trybuild 1.0.122
src/normalize.rsの 243〜245 行のコメント: 入力ファイルの中だけ行番号を残し、他のソースファイルの行番号は消す —ui_positionが-->の行番号と--> src/lib.rsの有無で判定できる根拠。E7・E8 の Day 069 版スナップショットで--> src/lib.rsに行番号が無いことも実物で確認 - trybuild 1.0.122
Cargo.toml:rust-version = "1.88"— Part 4 で 1.85 では使えなかった理由 - crates.io API(
/api/v1/crates/trybuild、/reverse_dependencies): 累計 71,767,595 ダウンロード、直近 16,819,191、逆依存 2,970 — 背景知識の採用規模の根拠 - エラー文はすべて rustc 1.96.0 と 1.85.1 で今日実行した出力
🚧 今日扱わなかった範囲(次回以降)
| 扱わなかったこと | 理由・次の予定 |
|---|---|
| E10(E0080)を表の行に向けること | 例外表に入れて止めた。行ごとに const _ を生成する形は未試行。proc-macro(マクロ②)の題材 |
ガード(if $g:expr)の誤りの位置 | 今日足したのは $to(E9)と件数(E10)。ガードに bool 以外の式を書いたときの位置は未確認 |
| 手製ハーネス(案 C・D)の実装と、trybuild との時間比較 | trybuild がネットワークから取れたため。オフラインの機械で必要になったら実装する |
compiletest の //~ ERROR 方式の実行 | rustc 本体の仕組みで、外部クレートから使う手順を確かめていない |
| CI(GitHub Actions など)への組み込み | 今日はローカルの cargo test まで。rustc の版を固定する rust-toolchain.toml も未作成 |
| 時間の静かな機械での取り直し | load average 16.58 の M1 8GB で 4 回のみ。Mac mini での取り直しは Day 066・068・070 の宿題と一緒に |
| C++・TypeScript・compiletest の比較列の実行 | 記憶ベース |
3 問の自己点検
- 同じ指示を AI に出せば数時間で出るか — trybuild の使い方の一般論はすぐ出る。ただし、Day 069 の形への 3 行の退行で照合が 8 件落ちて本物は 4 件、overwrite で退行が通る、1.85→1.96 で 8/10 が文言だけ不一致・位置は 0/10、別クレートでは「マクロ定義を指す」が
fsm! {の枠になる、E10 だけが表の行を指さない、は今日ビルドして得た数字 - AI に指示するだけでは出ない部分 — 「スナップショットが落ちる」を「位置が動いた」と「文言が変わった」に分け、変わってはいけない方だけを別のテストに抜き出したこと。それを退行の注入と rustc の版の変更という 2 つの実験で確かめたこと
- 世界の実物と比べて何割か — trybuild の機能のうち使ったのは
pass・compile_fail・TRYBUILD=overwriteの 3 つ。wipディレクトリの運用、TestCasesの features 指定、normalize.rsの正規化の全規則(今日読んだのは行番号の消去の 1 か所)は扱っていない。rustc 本体の compiletest とは実行して比べていない
➡️ 次のステップ
- 発展: E10 を表の行に向ける。行ごとに
const _: () = assert!(N <= max)を生成すると、下線が行のトークンに付くかを試し、付いたら例外表から消す(例外表が空になることがゴール) - 次回予告: マクロ① の残り期間(〜10/24)。今日の実装の次は概念理解か設計。候補は (1) 入れ子の状態(
Alarmの中に鳴動中/消音中)を自作fsm!と statig の superstate で比べる設計、(2) const 評価でできること・できないこと(E0015、for不可、generic_const_exprs)をROW_ACTIONSと今日の E10 を題材に整理する概念理解