こんにチュア!本記事は hooqアドベントカレンダー 10日目の記事です!
hooqはメソッドを ? 演算子にフックする属性マクロです。
hooqクレートでは、与えられたトークン列を整形し行番号を付与した文字列へと変換するsummary!マクロを提供しています。
fn main() {
let s = hooq::summary! {
{
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5];
v.sort();
v
}
};
println!("{s}");
}
3> {
4| let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5];
5| v.sort();
6| v
7| }
|
※ 3行目のインデントが減っていますが、諸事情につきわざとこうしています。
summary! マクロを内部的に利用することで、hooqマクロは見やすいエラーロギングを提供しています!
use hooq::hooq;
#[hooq]
fn hoge() -> Result<(), String> {
Err("an error occurred".to_string())
}
#[hooq]
fn main() -> Result<(), String> {
hoge()?;
Ok(())
}
[src/main.rs:5:5] "an error occurred"
5> Err("an e..rred".to_string())
|
[src/main.rs:10:11] "an error occurred"
10> hoge()?
|
Error: "an error occurred"
今回の記事では、summary! マクロをどのように作成したかのメイキング記事として、 summary! の簡素な属性マクロバージョン stringify_with_line_num のソースコードを元に実装ポイントを紹介したいと思います。
属性マクロ自体の作り方については拙著「Rust 属性風マクロを軽くハンズオン」をご覧いただけると幸いです。
行数表示クワイン...なの?
適切なタイトルがすぐ出なかったのでそれっぽいものを付けました。全然クワインと呼ぶようなものではないです。関数が「行番号が付与された自分自身」を標準出力するようなプログラムを書くので、類似性はあると思います。
実装ポイント解説
プロジェクト全体はGitHubの方を見てください。本節では実装本体と、マクロを実際に使用したコードを記載します。
use proc_macro2::{Delimiter, TokenStream, TokenTree};
use syn::{Ident, spanned::Spanned};
pub fn stringify_with_line_num(name: Ident, input: TokenStream) -> syn::Result<TokenStream> {
let end_line = input.span().unwrap().end().line();
let padding_num = end_line.to_string().chars().count().max(4); // `xxxx| ` の `xxxx` 部分の幅
let mut current_line = input.span().unwrap().line();
let mut res = format!("{current_line: >padding_num$}| "); // 最初の行数表示
let mut current_col = 1;
// Spanそれ自体を取りまわすと
// Groupのdelimiter周りで意図通りにいかない
// そのため柔軟に情報を指定可能な構造体を利用
#[derive(Debug)]
struct TokenInfo {
str: String,
line: usize,
start_col: usize,
end_col: usize,
}
// トークンツリーがグループの場合に容易に呼び出せるよう、
// 文字列への結果の挿入はクロージャとして切り出す
let mut add_tt = |token: TokenInfo| {
// dbg!(&token); // extra-traits featureを有効にしておくとデバッグ可能
// 行が変わったら改行と行番号を追加
if current_line < token.line {
for i in (current_line + 1)..=token.line {
// パディングしてくれる書き方
res.push_str(&format!("\n{i: >padding_num$}| "));
}
// 行・列を更新
current_line = token.line;
current_col = 1;
}
// 先頭のインデント等が適切になるよう間隙に空白を追加
res.push_str(
" ".repeat(token.start_col.saturating_sub(current_col))
.as_str(),
);
res.push_str(&token.str);
// 列を更新
current_col = token.end_col;
};
fn rec(input: TokenStream, add_tt: &mut impl FnMut(TokenInfo)) {
for tt in input {
match tt {
TokenTree::Ident(_) | TokenTree::Punct(_) | TokenTree::Literal(_) => {
let span = tt.span().unwrap();
let token = TokenInfo {
str: tt.to_string(),
line: span.start().line(),
start_col: span.start().column(),
end_col: span.end().column(),
};
add_tt(token);
}
TokenTree::Group(group) => {
// グループの場合
// - かっこの始め
// - 再帰処理
// - かっこの終わり
// の順で追加処理を行う
let span = group.span().unwrap();
let open_token = TokenInfo {
str: delim_start(group.delimiter()).to_string(),
line: span.start().line(),
start_col: span.start().column(),
end_col: span.start().column() + 1,
};
add_tt(open_token);
rec(group.stream(), add_tt);
let close_token = TokenInfo {
str: delim_end(group.delimiter()).to_string(),
line: span.end().line(),
start_col: span.end().column() - 1,
end_col: span.end().column(),
};
add_tt(close_token);
}
}
}
}
rec(input.clone(), &mut add_tt);
Ok(quote::quote! {
const #name: &str = #res;
#input
})
}
/// かっこの始め
fn delim_start(d: Delimiter) -> &'static str {
match d {
Delimiter::Parenthesis => "(",
Delimiter::Brace => "{",
Delimiter::Bracket => "[",
Delimiter::None => "",
}
}
/// かっこの終わり
fn delim_end(d: Delimiter) -> &'static str {
match d {
Delimiter::Parenthesis => ")",
Delimiter::Brace => "}",
Delimiter::Bracket => "]",
Delimiter::None => "",
}
}
次のように利用できます!
use stringify_with_line_num::stringify_with_line_num;
#[stringify_with_line_num(MY_SOURCE)]
fn self_introduction() {
let _ = {
println!("nested");
let _ = {
println!("deeply nested");
42
};
42
};
println!("self_introduction:\n{}", MY_SOURCE);
}
fn main() {
self_introduction();
}
実行結果は以下のようになります。
$ cargo run -q
nested
deeply nested
self_introduction:
4| fn self_introduction() {
5| let _ = {
6| println!("nested");
7| let _ = {
8| println!("deeply nested");
9| 42
10| };
11| 42
12| };
13|
14| println!("self_introduction:\n{}", MY_SOURCE);
15| }
実装ポイント1: 行列情報の取得はproc_macro::Spanより取る
Span情報(トークンの位置情報)は proc_macro2::Spanではなく 2 じゃないほうの普通の Span を利用しましょう!
というのも、【Rust】属性マクロとline!マクロの相性が悪かった話で解説したように行列情報を得るAPI proc_macro::Span::line はRust 1.88で安定化したのですが、安定化したのが最近のためなのか、 proc_macro2::Spanの方はいまだに不正確な位置を指すことがあります。 .unwrap() を呼んで、 proc-macro2 から proc-macro の Span とすることで最新の正確なSpanが得られます!
実装ポイント2: 列数の見方は行列インデックスに似てる...?
例えば fn hoge() {} のident hoge に注目した場合、列数は次のように考えます。
fn hoge() {}
^^ ^^^^^^
123456789
span.start().column() は対象トークンを含んだ最初の文字のある列を指します。一方で span.end().column() は自身の次の列を指します。 0..2 が 0, 1 を指すのに似ていますね!
-
hoge-
.start().column(): 4 -
.end().column(): 8
-
-
()-
.start().column(): 8 -
.end().column(): 10
-
-
{}-
.start().column(): 11 -
.end().column(): 13
-
正確な列数が知りたくなった際は syn の extra-traits featureを有効にした上で dbg! マクロ等でデバッグすると捗ります。
トークン間(now と current)に適切な数のスペースを設置するため、 now_span.end().column() と current_span.start().column() の差の分スペースを繰り返して挿入しています。
res.push_str(
" ".repeat(token.start_col.saturating_sub(current_col))
.as_str(),
);
実装ポイント3: グループの中身とデリミタも表示されるよう、再帰にする
ここが一番頭を使う部分かなと思います。トークンごとにストリームで繰り返し処理したい場合、トークンツリーの要素としてグループ(括弧で包まれた表現)が流れてきた際は、さらにその中身のストリーム( group.stream() )を見なければなりません。そのため、再帰関数にする必要があります。
前後の括弧はそれ単体では Span を持ったトークンになってくれないので、 TokenInfo に自分で疑似的に情報をまとめて、追加処理に渡す必要があります。
fn rec(input: TokenStream, add_tt: &mut impl FnMut(TokenInfo)) {
for tt in input {
match tt {
TokenTree::Ident(_) | TokenTree::Punct(_) | TokenTree::Literal(_) => {
add_tt(/* 略 */);
}
TokenTree::Group(group) => {
let span = group.span().unwrap();
let open_token = TokenInfo {
str: delim_start(group.delimiter()).to_string(),
line: span.start().line(),
start_col: span.start().column(),
end_col: span.start().column() + 1,
};
add_tt(open_token); // `(`, `[`, `{` などの前括弧
// グループが持つストリームも再帰で結果に追加
rec(group.stream(), add_tt);
let close_token = TokenInfo {
str: delim_end(group.delimiter()).to_string(),
line: span.end().line(),
start_col: span.end().column() - 1,
end_col: span.end().column(),
};
add_tt(close_token); // `)`, `]`, `}` などの後括弧
}
}
}
}
まとめ・所感
駆け足なメイキング記事でした!アドカレの疲れが出始めています... ![]()
再帰で書かなければならないなどの細かい注意点は色々ありますが、Rust 1.88で安定化してくれたproc_macro::Span::lineやproc_macro::Span::columnの情報をもとに行数を出し、改行数、空白数を算出して適宜加えているだけだということが伝わったなら幸いです。
皆さんも気軽にトークンの行列番号情報を利用しましょう!