🏞

syn::parse::ParseBuffer::peek の謎に迫る

2020/12/17に公開

この記事は 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::TokenStreamproc_macro::TokenStream と同じ機能を持ち、普通のプログラム、 Procedural Macro のどちらからも使用できる型となっています。2 つの TokenStream は相互に変換できるので、proc-macro2 の方を使用する事で Rust のソースコードを取り扱うプログラムのテストやデバッグ実行が可能になります。

本記事中のサンプルコードは fn main() { } の中に書いて実行できるコードにしていますが、これができるのも proc-macro2 のおかげです。

syn

この crate では構文木を表す型が定義され TokenStream を構文木に変換する事ができます。
独自の構文を定義することもできます。

本記事の主役です。

quote

TokenStream を生成するためのテンプレートエンジンです。
Procedural Macro ではマクロが出力する TokenStream を生成するために使用されます。

本記事では syn に構文解析させるソースコードの TokenStream を生成するために使用します。

synParseBuffer

Procedural Macro における syn crate の役割について簡単に解説しました。
次は syn crate における ParseBuffer の役割を見ていきます。

ParseBuffer は 簡単に言うと TokenStream に対するイテレータのようなもので、独自構文の構文木を作る際に使用します。
std::iter::Iterator と異なり、構文解析に便利なメソッドを多数備えています。

さて、独自構文を扱う ParseBuffer について説明する前に、独自でない既存の構文の扱い方について見ていきましょう。

構文解析を行う

TokenStream を構文木に変換するには 関数 syn::parse2 を使用します。
早速試してみましょう。(完成したコードは step1.rs です。)

まずは cargo.toml に先ほどの 3 つの crate を追加します。

cargo.toml
[dependencies]
proc-macro2 = "1.0.24"
syn = "1.0.54"
quote = "1.0.7"

次に、main 関数の中にコード書いていきます。

TokenStream を作るには quote::quote を使用します。
次のコードにより、変数 tokens にはソースコード (1 + 2) * 3 を表す TokenStream が入ります。

step1.rs
let tokens = quote::quote! {
    (1 + 2) * 3
};

TokenStream を解析して構文木を作成するには syn::parse2 を使用します。
次のコードにより、変数 expr には構文木が入ります。

step1.rs
let expr: syn::Result<syn::Expr> = syn::parse2(tokens);

(1 + 2) * 3 が本当に式として解析できたか確認してみましょう。

step1.rs
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 をどうぞ。)

まず、既存の構文を組み合わせて、独自構文の構文木を表す型を作ります。

step2.rs
struct NameAndExpr {
    name: syn::Ident,
    eq_token: syn::Token![=],
    expr: syn::Expr,
}

syn::Ident は 変数名や関数名などの識別子を表します。上の例では filevalue の部分です。
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つにまとめましょう。

step2.rs
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,
        })
    }
}

これで独自構文が完成しました!
試しに使ってみましょう。

step2.rs
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 にすることにします。

step3.rs
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 もあります。これらの関数を使用して処理を分岐させ、最初が識別子と = なら {名前} = の省略無し、そうでないなら、省略ありとして解析しましょう。

step3.rs
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 の定義は次のようになっています。

proc_macro2/src/lib.rs
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 の定義へと向かいました。
その結果、謎の答えを見つけたのです。

syn/src/ident.rs
pub fn Ident(marker: lookahead::TokenMarker) -> Ident {
    match marker {}
}

Ident の定義です。型ではありません。どう見ても関数です。

Rust では型と同名の関数を定義できるのです。[2]

// コンパイルエラーは発生しない
struct ThisIsType {
    value: u8,
}
fn ThisIsType() {}

騙されました。
この仕組みを使って peek では型を渡していると見せかけて、関数を渡していたわけですね。

そして peek では Peek を介して渡した関数の戻り値の型(T::Token)を取得し、戻り値の型の関連関数を呼び出していました。

syn/src/parse.rs
pub fn peek<T: Peek>(&self, token: T) -> bool {
    let _ = token;
    T::Token::peek(self.cursor())
}

おわりに

わかれば簡単な仕組みですね。次からは型名っぽい見た目には惑わされません!

脚注
  1. 本記事では Procedural Macro の作り方については説明していません。Procedural Macro の作り方ついては Rust 3 Advent Calendar 2020 12 日目 の記事、magurotuna さんの Rust の procedural macro を操って黒魔術師になろう〜proc-macro-workshop の紹介 が参考になります。 ↩︎

  2. ただし、タプル構造体の場合は同名の関数が自動的に作成されるため、自分で同名の関数を定義することはできません。 ↩︎

Discussion