syn::visit, syn::visit_mut, syn::foldの概要
これらの機能を使うと、一言でいえば、「対象構文を再帰的に見て(トラバーサルして)構文について集計等する・構文を加工する」ことができます!
3つのモジュール・トレイトは似ていますが、シグネチャの違いにより得手不得手があります。
-
syn::visit の
Visit: 構文(ノード)の 不変 参照を得る- 構文に対しては唯一ただの閲覧用
-
syn::visit_mut の
VisitMut: 構文(ノード)の 可変 参照を得る -
syn::fold の
Fold: 構文(ノード)の 実体 (所有権)を得て、新しい構文を返す
visit_mut と fold は書きやすい方を選択するのでよさそうです。
こんにチュア!本記事は hooqアドベントカレンダー 12日目の記事です!
hooqはメソッドを ? 演算子にフックする属性マクロです。
属性マクロを書いていると、マクロにより対象構文を再帰的に見て(トラバーサルして)構文を加工する操作はかなり頻出なのではないかと思います。hooq属性マクロもそのアイデンティティ(全 ? へのフック)からわかる通りもちろんこの操作をしています。
画像: 関数を見て、その中にある文を見て、さらにその中にある文やリテラルを見て...というトラバーサルな操作
以降利便性のため、この「対象構文を再帰的に見て(トラバーサルして)マクロにより構文を加工する」操作を本記事独自に「構文走査」あるいは「構文走査加工」(visit_mut・foldで特に加工する場合)と呼称することにします。
構文走査は頻出で需要があるためか、synクレートからは構文走査のための機能として、3つのトレイトが提供されています: Visit, VisitMut, Foldです。
本記事では、これらのトレイト(以降構文走査トレイトと呼称)を利用した属性マクロ例を示し、それぞれの書き味をまとめたいと思います。
記事の流れ:
-
Visitの利用例 -
VisitMutを利用した簡易版hooqマクロ作成ハンズオン - 上記簡易版hooqマクロの
Foldバージョンの記載
Qiitaの目次機能も合わせてご参照ください ![]()
Visitで解説!構文走査トレイト共通構造・使い方
構文走査トレイトすべてについて、以下の共通の構造をしていて、共通の使い方をします。異なるのは登場するメソッド・関数の参照・所有権周りのシグネチャのみです!
Visit を例に説明しますが VisitMut / Fold も同じなので置き換えて読んでください。
トレイトのメソッドと、それに対応する関数がある
Visit トレイトに存在するメソッドは、そのメソッドと同じ関数がそのまま syn::visit に表出しています。
pub trait Visit<'ast> {
// ...省略...
// デフォルト実装付きで要求メソッド定義
fn visit_stmt(&mut self, i: &'ast crate::Stmt) {
visit_stmt(self, i);
}
// ...省略...
}
// ...省略...
pub fn visit_stmt<'ast, V>(v: &mut V, node: &'ast crate::Stmt)
where
V: Visit<'ast> + ?Sized,
{
match node {
crate::Stmt::Local(_binding_0) => {
v.visit_local(_binding_0);
}
crate::Stmt::Item(_binding_0) => {
v.visit_item(_binding_0);
}
crate::Stmt::Expr(_binding_0, _binding_1) => {
v.visit_expr(_binding_0);
skip!(_binding_1);
}
crate::Stmt::Macro(_binding_0) => {
v.visit_stmt_macro(_binding_0);
}
}
}
// ...省略...
構文走査における集計対象・加工対象構文種 xxx に対して、メソッド syn::visit::Visit::visit_xxx と関数 syn::visit::visit_xxx が定義されているわけです。今回は文( stmt )を走査する visit_stmt の部分を抜き出してみました。
Visit トレイトを実装する型(構造体や列挙体)はユーザーで用意します。もちろんどんな型名でも良いのですが便宜上 Visitor としておきましょう。
Visitor では 走査対象種 xxx のメソッドだけ 上書き定義します!他のメソッド Visit::visit_yyy については、内部で visit_yyy 関数を呼ぶように書かれていて、適切に他対象も走査されます。走査の必要がないならばそのままにしておきましょう。
struct Visitor {
stmts_count: usize,
}
impl<'ast> Visit<'ast> for Visitor {
fn visit_stmt(&mut self, i: &'ast syn::Stmt) {
// 構文中に出てくる「文」(statement) の数をカウントする処理をフック
self.stmts_count += 1;
// デフォルトで呼び出している通り
// 従来の走査を行うため関数のvisit_stmtを呼ぶ
syn::visit::visit_stmt(self, i);
}
}
Visitor は集計者なので内部可変である必要があり、自身は可変参照です。今回は構文中に登場する文の回数をカウントしてみましょう! self.stmts_count += 1 が走査で行ってほしい処理になります。イメージとしては、この処理を本来の走査にフック1している感じです。
ここで作った Visitor の全体の使用例は次のような感じになります。
use syn::visit::Visit;
#[derive(Debug)]
struct Visitor {
stmts_count: usize,
}
impl<'ast> Visit<'ast> for Visitor {
fn visit_stmt(&mut self, i: &'ast syn::Stmt) {
self.stmts_count += 1;
syn::visit::visit_stmt(self, i);
}
}
fn main() {
let item: syn::Item = syn::parse_quote! {
mod hoge {
fn fuga() {
let mut bar = { // 1
let a = 10; // 2
let b = 20; // 3
a + b // 4
};
for i in 0..bar { // 5
println!("{i}"); // 6
}
}
}
};
let mut visitor = Visitor {
stmts_count: 0,
};
visitor.visit_item(&item);
println!("{visitor:?}");
}
Visitor { stmts_count: 6 }
Rust Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=dbdbe2e4886eb8275d78a1f6ca1aa2d4
visitor.visit_item → ... visit_item 等、別な構文要素走査 → visitor.visit_stmt でカウント更新。visit_stmt が呼ばれる → ... → さらに内側の visitor.visit_stmt が呼ばれカウント更新。visit_stmt が呼ばれる → ... という感じで走査され、構文中に登場する全文の数がカウントされます。
ここまででsyn::visit::Visitの利用例を示しました。Rust Playground を使って例示したことで暗に示していたのですが、実は加工を伴わない構文走査は手続きマクロ自体ではあんまり需要を感じられず、どちらかというとRustの構文を受け取ってそれを解析するようなプログラムでの需要の方がありそうです。
実際過去に自分が Visit を使った時も、マクロではなくRust構文解析の文脈でした: https://github.com/anotherhollow1125/coloring_rust/blob/v1.0.1/coloring_common/src/lib.rs#L42-L77
参考: 【手続きマクロだけではない】synクレートを活用して便利ツールを作った話【Rust】
構文走査トレイトはやはり VititMut か Fold が求めるものでしょう。
syn::visit_mut::VisitMutで簡易版hooqを作る
use syn::visit_mut::VisitMut;
struct Visitor;
impl VisitMut for Visitor {
// VisitMutは、 `i: &'ast syn::ExprTry` ではなく `i: &mut syn::ExprTry` である点のみがVisitと異なる
fn visit_expr_try_mut(&mut self, i: &mut syn::ExprTry) {
syn::visit_mut::visit_expr_try_mut(self, i);
}
}
属性マクロの実例として紹介するとちょうど良さそうなのは残りの構文走査加工クレート2つです!色々注意点もあるのでハンズオン的に解説します。
VisitMut は Visit の可変参照版です。トレイト実装先の構造体(self)は Visit の時から可変参照(&mut self)だったことからわかる通り、 VisitMut で新たに可変参照になっているのは構文(ノード)の方です。
つまり VisitMut を使うことで 構文に手を加える ことができます。というわけで簡易版hooqを作ってみましょう!
-
?を見つけたら.inspect_err(|| eprintln!("at line {}", 行番号))を式と?の間に挿入する
この機能だけ持った属性マクロ insert_inspect を作ってみます。
まずはいつも通り cargo new します!そして三種の神器 proc-macro2, quote と syn を入れます。
cargo new insert_inspect --lib
cd insert_inspect
cargo add proc-macro2 quote
cargo add syn --features full --features visit-mut
syn::visit_mut::visit_mut を使うために --features visit-mut を付けるのを忘れないでください!
Cargo.toml に [lib] proc-macro = true を入れ、マクロクレートにします。
[package]
name = "insert_inspect"
version = "0.1.0"
edition = "2024"
+[lib]
+proc-macro = true
[dependencies]
proc-macro2 = "1.0.103"
quote = "1.0.42"
syn = { version = "2.0.111", features = ["full", "visit-mut"] }
例によって proc_macro と proc_macro2 を分離するため、 lib.rs には定型の骨組みを置きます。
use proc_macro::TokenStream;
use syn::parse_macro_input;
mod impls;
#[proc_macro_attribute]
pub fn insert_inspect(_attr: TokenStream, item: TokenStream) -> TokenStream {
let item: syn::Item = parse_macro_input!(item);
impls::insert_inspect(item)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
impls.rs の方に本体を置きます。一旦 todo!() で。
use proc_macro2::TokenStream;
pub fn insert_inspect(_item: syn::Item) -> syn::Result<TokenStream> {
todo!()
}
ここまでで属性マクロの骨組みは揃ったので、いよいよ VisitMut を使用して ? にフックを設ける処理を実現したいと思います!
use proc_macro2::TokenStream;
use syn::visit_mut::VisitMut;
struct Visitor;
impl VisitMut for Visitor {
fn visit_expr_try_mut(&mut self, i: &mut syn::ExprTry) {
/* ここにフックする */
// ここでデフォルトの関数を呼ぶ
syn::visit_mut::visit_expr_try_mut(self, i);
}
}
pub fn insert_inspect(_item: syn::Item) -> syn::Result<TokenStream> {
todo!()
}
今回特に走査中に集めたい情報はないので、 Visitor はユニット構造体にしています。
i: &mut syn::ExprTry を編集したいです。 ExprTry は次の定義です。
pub struct ExprTry {
pub attrs: Vec<Attribute>,
pub expr: Box<Expr>,
pub question_token: Question,
}
attrs は次回の話として...注目すべきは expr フィールドと question_token フィールドです。名前の通り expr には ? の前にある式が、 question_token にはそのまま ? が入っています。ということは、 expr の中身を expr.inspect_err(|_| eprintln!("at line {}", 行番号)) に差し替えてあげるとよさそうです。 Expr それ自体ではなく Box であることに気を付け、 Expr を楽に作るために syn::parse_quote マクロを利用します。
i.expr が i それ自体を含めてしまうようにすると、 ? も含まれてフックが無限ループになりマクロが爆発します!筆者もやらかしました... ![]()
i.expr を一旦別変数に退避してからそれを用いて構築しましょう。
use syn::visit_mut::VisitMut;
struct Visitor;
impl VisitMut for Visitor {
fn visit_expr_try_mut(&mut self, i: &mut syn::ExprTry) {
let expr = i.expr.clone(); // `expr ?` の `expr` の部分
i.expr = Box::new(syn::parse_quote! {
#expr.inspect_err(|_| eprintln!("at line {}", 0))
});
// ここでデフォルトの関数を呼ぶ
syn::visit_mut::visit_expr_try_mut(self, i);
}
}
今行番号は一旦 0 にしました。正確な行番号を入れましょう。Spannedトレイトを利用して一旦proc_macro2::Spanを引き出し、.unwrap() して2正確な行数を返してくれる我らのproc_macro::Spanを召喚します。あとは .line() メソッドを呼べば行数が得られます!つまり let line = i.span().unwrap().line(); で行番号が得られます。
line!() マクロを使わない理由はこちら: 【Rust】属性マクロとline!マクロの相性が悪かった話
use syn::spanned::Spanned; // <- 追加
// ...省略...
impl VisitMut for Visitor {
fn visit_expr_try_mut(&mut self, i: &mut syn::ExprTry) {
let line = i.span().unwrap().line();
let expr = i.expr.clone(); // `expr ?` の `expr` の部分
i.expr = Box::new(syn::parse_quote! {
#expr.inspect_err(|_| eprintln!("at line {}", #line))
});
// ここでデフォルトの関数を呼ぶ
syn::visit_mut::visit_expr_try_mut(self, i);
}
}
最後に、 Visitor を使うように insert_inspect を書き換えればマクロは完成です!
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{spanned::Spanned, visit_mut::VisitMut};
struct Visitor;
impl VisitMut for Visitor {
fn visit_expr_try_mut(&mut self, i: &mut syn::ExprTry) {
let line = i.span().unwrap().line();
let expr = i.expr.clone();
i.expr = Box::new(syn::parse_quote! {
#expr.inspect_err(|_| eprintln!("at line {}", #line))
});
syn::visit_mut::visit_expr_try_mut(self, i);
}
}
pub fn insert_inspect(mut item: syn::Item) -> syn::Result<TokenStream> {
let mut visitor = Visitor;
visitor.visit_item_mut(&mut item);
Ok(item.into_token_stream())
}
src/main.rs にて直接試せるので試してみましょう。
use insert_inspect::insert_inspect;
#[insert_inspect]
fn hoge() -> Result<(), String> {
let _: () = Err("an error occurred".to_string())?;
Ok(())
}
#[insert_inspect]
fn main() -> Result<(), String> {
hoge()?;
Ok(())
}
$ cargo run -q
at line 5
at line 12
Error: "an error occurred"
cargo-expand というツールを使い展開してみると、 inspect_err がフックされているのがわかります!
$ cargo expand --bin insert_inspect
Checking insert_inspect v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/adv11/insert_inspect)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use insert_inspect::insert_inspect;
fn hoge() -> Result<(), String> {
let _: () = Err("an error occurred".to_string())
.inspect_err(|_| {
::std::io::_eprint(format_args!("at line {0}\n", 5usize));
})?;
Ok(())
}
fn main() -> Result<(), String> {
hoge()
.inspect_err(|_| {
::std::io::_eprint(format_args!("at line {0}\n", 12usize));
})?;
Ok(())
}
syn::fold::Foldで簡易版hooqを作る
use syn::{ExprTry, fold::Fold};
struct Folder;
impl Fold for Folder {
// Foldは、実体を直接受け取り、新たな実体を返す形式
fn fold_expr_try(&mut self, i: ExprTry) -> ExprTry {
syn::fold::fold_expr_try(self, i)
}
}
Visit/VisitMut が参照系統のトレイトなら、 Fold は所有権を奪う系のトレイトです。 違いとしてはそれぐらい で、身分が異なるので書きやすい処理や書き心地はだいぶ変わるのですが、書き方さえ気を付ければ VisitMut と同じことが可能です。
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{ExprTry, fold::Fold, parse_quote, spanned::Spanned};
struct Folder;
impl Fold for Folder {
fn fold_expr_try(&mut self, i: ExprTry) -> ExprTry {
let line = i.span().unwrap().line();
// 所有権を持っているので分解できる
let ExprTry {
attrs,
expr,
question_token,
} = i;
// 再帰的に子ノードを処理
let expr = self.fold_expr(*expr);
let expr = Box::new(parse_quote! {
#expr.inspect_err(|_| eprintln!("at line {}", #line))
});
// 新しいノードを構築して返す
ExprTry {
attrs,
expr,
question_token,
}
}
}
pub fn insert_inspect_fold(mut item: syn::Item) -> syn::Result<TokenStream> {
let mut folder = Folder;
item = folder.fold_item(item);
Ok(item.into_token_stream())
}
マクロの利用結果は VisitMut 版と同一なので割愛します。
ありきたりなことを言いますが、ケースバイケースあるいは好みでどちらを使うかを決めるのが良さそうです...自分自身を大きく変更したい時などは Fold の方が書き心地が良いのではないかと思います。
まとめ
...こんなに説明してきましたが、実は結局hooqマクロのフックを行う構文走査加工では構文走査トレイトは使っておらず、 match 式でゴリ押していたりします!
該当箇所にした自分のコメントはちょっと的外れなのですが3、今思えば「全構文にしっかりフックするため、つまり網羅性確認のためにmatch式を使った」の一言でまとめられますね。もちろん一部見る必要がない構文要素等もあってそれは match 式の下の方にまとめています。これらの構文要素について無視を行っていることを明示できるという点で、紹介したトレイトより match 式の方が優れているだろうと判断したわけです4。
とはいえhooqクレート全体でみるともちろん構文走査のために使っている場所もあり、やはり便利なためRustマクロ作者は頭の片隅にとどめておくと幸せになれそうなトレイト群です。というわけで構文走査トレイトたちを紹介しました。
ここまで読んでいただきありがとうございました!
