syn::parse::ParseBuffer::peek の謎に迫る
この記事は Rust 2 Advent Calendar 2020 の 17 日目の記事です。
syn
crate に含まれる関数の一つ ParseBuffer::peek
では少し変な仕組みが使われています。
今まで何故コンパイルエラーが出ないのか不思議に思っていましたが、その仕組みが解明できたので本記事で解説したいと思います。
ParseBuffer::peek
の謎に迫る前に
さて、謎について解説する前に簡単に syn
crate について説明しておきます。
syn
crate について既に完全に理解している方は "ちょっとまって!? 引数に型名を指定??" にお進みください。
Procedural Macro で使用する 3 つの crate
syn
は Rust のソースコード変換・自動生成の仕組みである Procedural Macro の作成時に必ず使用する 3 つの crate のうちの一つです。[1]
3 つの crate はそれぞれ、次のような役割を持っています。
crate | 役割 |
---|---|
proc-macro2 |
Procedural Macro 以外の普通の Rust のプログラムからも Rust のソースコードを扱えるようにする |
syn |
Rust のソースコードを構文木に変換する |
quote |
Rust のソースコードを生成するテンプレートエンジン |
proc-macro2
Procedural Macro では Rust のソースコードは proc_macro::TokenStream
という型で表されます。
しかし、この型は Rust コンパイラから呼び出されることが前提の特殊な形態のプログラム内でのみ使用可能で、普通の Rust のプログラムからは使用できません。
main
から始まるプログラムや #[test]
が付けられたテストは Procedural Macro ではない普通の Rust のプログラムとなるので、Rust のソースコードを取り扱うプログラムはテストやデバッグ実行ができないという事になってしまいます。
しかし、それでは不便・・・という事で登場するのがこの proc-macro2
です。proc-macro2::TokenStream
は proc_macro::TokenStream
と同じ機能を持ち、普通のプログラム、 Procedural Macro のどちらからも使用できる型となっています。2 つの TokenStream
は相互に変換できるので、proc-macro2
の方を使用する事で Rust のソースコードを取り扱うプログラムのテストやデバッグ実行が可能になります。
本記事中のサンプルコードは fn main() { }
の中に書いて実行できるコードにしていますが、これができるのも proc-macro2
のおかげです。
syn
この crate では構文木を表す型が定義され TokenStream
を構文木に変換する事ができます。
独自の構文を定義することもできます。
本記事の主役です。
quote
TokenStream
を生成するためのテンプレートエンジンです。
Procedural Macro ではマクロが出力する TokenStream
を生成するために使用されます。
本記事では syn
に構文解析させるソースコードの TokenStream
を生成するために使用します。
syn
と ParseBuffer
Procedural Macro における syn
crate の役割について簡単に解説しました。
次は syn
crate における ParseBuffer
の役割を見ていきます。
ParseBuffer
は 簡単に言うと TokenStream
に対するイテレータのようなもので、独自構文の構文木を作る際に使用します。
std::iter::Iterator
と異なり、構文解析に便利なメソッドを多数備えています。
さて、独自構文を扱う ParseBuffer
について説明する前に、独自でない既存の構文の扱い方について見ていきましょう。
構文解析を行う
TokenStream
を構文木に変換するには 関数 syn::parse2
を使用します。
早速試してみましょう。(完成したコードは step1.rs
です。)
まずは cargo.toml
に先ほどの 3 つの crate を追加します。
[dependencies]
proc-macro2 = "1.0.24"
syn = "1.0.54"
quote = "1.0.7"
次に、main 関数の中にコード書いていきます。
TokenStream
を作るには quote::quote
を使用します。
次のコードにより、変数 tokens
にはソースコード (1 + 2) * 3
を表す TokenStream
が入ります。
let tokens = quote::quote! {
(1 + 2) * 3
};
TokenStream
を解析して構文木を作成するには syn::parse2
を使用します。
次のコードにより、変数 expr
には構文木が入ります。
let expr: syn::Result<syn::Expr> = syn::parse2(tokens);
(1 + 2) * 3
が本当に式として解析できたか確認してみましょう。
if expr.is_ok() {
println!("これは式です。");
} else {
println!("これは式ではない。");
}
これは式です。
式、関数定義、構造体定義など、様々な構文を解析する事ができますが、今回解析する (1 + 2) * 3
は式なので syn::parse2
の戻り値には式を表す syn::Expr
を使用しました。
TokenStream
からの変換が可能な構文木には syn::parse::Parse
が実装されています。 syn::Expr
の部分を syn::parse::Parse
を実装する別の型に置き換え、他の構文を解析してみるのも良いでしょう。
独自構文を作る
独自構文を作る、それは言い換えれば syn::parse::Parse
を実装する、という事になります。
今回は
file = "abc"
value = 1 + 2 + 3
のような 名前 = 式
の独自構文を実装してみましょう。
(完成したコードを先に見たい方は step2.rs
をどうぞ。)
まず、既存の構文を組み合わせて、独自構文の構文木を表す型を作ります。
struct NameAndExpr {
name: syn::Ident,
eq_token: syn::Token![=],
expr: syn::Expr,
}
syn::Ident
は 変数名や関数名などの識別子を表します。上の例では file
や value
の部分です。
syn::Token![=]
は =
を表します。syn::token::Eq
と同じですが、syn::Token!
を使用した方がわかりやすいため、こちらを使用する事が推奨されています。
syn::Expr
は式です。前の例でも出てきましたね。
この構造体に対して syn::parse::Parse
を実装し、syn::parse2
で構文木を構築できるようにしてみます。
impl syn::parse::Parse for NameAndExpr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
todo!()
}
}
syn::parse::ParseStream
が出てきましたね。実はこの ParseStream<'a>
は &'a ParseBuffer<'a>
の別名なのです。ParseBuffer
は 最初に紹介した変な関数 peek
が定義された型であり TokenStream
に対するイテレータのようなものでもあります。
ParseBuffer::parse
を使用すると、このイテレータの先頭から構文木を構築し、イテレータを進める事ができます。先頭から順に3つの構文木を構築し、1つにまとめましょう。
impl syn::parse::Parse for NameAndExpr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let name = input.parse()?;
let eq_token = input.parse()?;
let expr = input.parse()?;
Ok(Self {
name,
eq_token,
expr,
})
}
}
これで独自構文が完成しました!
試しに使ってみましょう。
fn main() {
let tokens = quote::quote! { file = "abc" };
let name_and_expr: syn::Result<NameAndExpr> = syn::parse2(tokens);
if let Ok(name_and_expr) = name_and_expr {
println!("名前は {} です", name_and_expr.name);
} else {
println!("解析失敗");
}
}
名前は file です
ちゃんと構文木ができましたね。
ParseBuffer::peek
を使う
前の例では {名前} = {式}
の構文を作りました。
次はこの構文の {名前} =
の部分を書略可能にしてみます。
(完成したコードは step3.rs
です。)
{名前} =
ありの場合と無しの場合があるので、解析結果は 2 つの variant を持つ次のような enum にすることにします。
enum OptionNameAndExpr {
NameAndExpr {
name: syn::Ident,
eq_token: syn::Token![=],
expr: syn::Expr,
},
ExprOnly {
expr: syn::Expr,
},
}
次にこの型に対して syn::parse::Parse
を実装するわけですが、{名前} =
が省略されているかどうかを判断するにはどうしたら良いのでしょうか?
最初の部分が識別子で、次の部分が =
なら省略されていないと判断できそうですよね?
そんな判断を行うのにピッタリな関数があります。それが ParseBuffer::peek
です。
ParseBuffer::peek
の引数に型名を指定すると、ParseBuffer
の先頭が指定した内容と一致するかどうかを調べることができます。先頭から 2 番目を調べる peek2
もあります。これらの関数を使用して処理を分岐させ、最初が識別子と =
なら {名前} =
の省略無し、そうでないなら、省略ありとして解析しましょう。
impl syn::parse::Parse for OptionNameAndExpr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(syn::Ident) && input.peek2(syn::Token![=]) {
let name = input.parse()?;
let eq_token = input.parse()?;
let expr = input.parse()?;
Ok(Self::NameAndExpr {
name,
eq_token,
expr,
})
} else {
let expr = input.parse()?;
Ok(Self::ExprOnly { expr })
}
}
}
できました!
ちょっとまって!? 引数に型名を指定??
先ほど ParseBuffer::peek
の引数に型名を指定しました。
input.peek(syn::Ident)
ちょっと待ってください、引数に型名???
input.peek::<syn::Ident>()
のようにジェネリック引数に型名を指定するならわかりますが、普通の引数に型名を指定するなんて、こんな構文は他に見たことがありません。これはいったい何なのでしょうか?
ParseBuffer::peek
のドキュメントにもこのように使うように書かれており、使い方が間違っているわけではなさそうです。
pub fn peek<T: Peek>(&self, token: T) -> bool
Looks at the next token in the parse stream to determine whether it matches the requested type of token.Does not advance the position of the parse stream.
Syntax
Note that this method does not use turbofish syntax. Pass the peek type inside of parentheses.
- input.peek(Token![struct])
- input.peek(Token![==])
- input.peek(Ident) (does not accept keywords)
- input.peek(Ident::peek_any)
- input.peek(Lifetime)
- input.peek(token::Brace)
実は Ident
は次のようなフィールドの無い構造体で、syn::Ident
はコンストラクタの呼び出しになっていて、Ident
型の値を渡しているのでしょうか?
struct Ident; // 実はこんな定義?
いやいや、Ident
には変数名などを格納できるわけですし、そんなわけありませんね。実際、Ident
の定義は次のようになっています。
pub struct Ident {
inner: imp::Ident,
_marker: Marker,
}
分からない時は基本に戻りましょう。peek
のドキュメント を改めて見てみます。
pub fn peek<T: Peek>(&self, token: T) -> bool
どうやら、引数には Peek
を実装する値を指定するようですね。
では Peek
のドキュメントを見てみましょう。
pub trait Peek: Sealed { }
impl<F: Copy + FnOnce(TokenMarker) -> T, T: Token> Peek for F
type Token = T
Peek
にはメソッドが無く、TokenMarker
を引数に取る関数が Peek
を実装しているようです。Peek
を実装する型はこれだけです。
関数???型名がなぜ関数になるのでしょうか?
型名が関数になるといえばタプル構造体を思い出します。
タプル構造体の場合、型名はコンストラクタを呼び出す関数として使えます。
struct MyTuple(u8, u16);
fn fn_arg_func(f: impl Fn(u8, u16) -> MyTuple) {
todo!();
}
fn_arg_func(MyTuple);
同じように、タプルでない構造体も関数をして使えるのかも・・・とも思いましたが、使えませんでした。
struct MyStruct {
x: u8,
y: u16,
}
fn fn_arg_func(f: impl Fn(u8, u16) -> MyStruct) {
todo!();
}
fn_arg_func(MyStruct); // コンパイルエラー!!!
そもそも、使えたとしても TokenMarker
という全く関係ない型の引数を取るわけがありませんね。
Ident
の定義にあった
謎の答えは 我々はこの謎を解明するため、アマゾンの奥地・・・ではなく Ident
の定義へと向かいました。
その結果、謎の答えを見つけたのです。
pub fn Ident(marker: lookahead::TokenMarker) -> Ident {
match marker {}
}
Ident
の定義です。型ではありません。どう見ても関数です。
Rust では型と同名の関数を定義できるのです。[2]
// コンパイルエラーは発生しない
struct ThisIsType {
value: u8,
}
fn ThisIsType() {}
騙されました。
この仕組みを使って peek
では型を渡していると見せかけて、関数を渡していたわけですね。
そして peek
では Peek
を介して渡した関数の戻り値の型(T::Token
)を取得し、戻り値の型の関連関数を呼び出していました。
pub fn peek<T: Peek>(&self, token: T) -> bool {
let _ = token;
T::Token::peek(self.cursor())
}
おわりに
わかれば簡単な仕組みですね。次からは型名っぽい見た目には惑わされません!
-
本記事では Procedural Macro の作り方については説明していません。Procedural Macro の作り方ついては Rust 3 Advent Calendar 2020 12 日目 の記事、magurotuna さんの Rust の procedural macro を操って黒魔術師になろう〜proc-macro-workshop の紹介 が参考になります。 ↩︎
-
ただし、タプル構造体の場合は同名の関数が自動的に作成されるため、自分で同名の関数を定義することはできません。 ↩︎
Discussion