Rust手続きマクロ エラーハンドリング手法


こちらの記事は [Rustマクロ冬期講習アドベントカレンダー](https://qiita.com/advent-calendar/2024/rust-macro) 22日目の記事です！

> アドカレまとめ記事はこちら！: [Rustマクロ作成チートシート！](https://qiita.com/namn1125/items/d601d2cbbf2cc7b5d3dc)

今回は今までの回でなあなあにしてきた、手続きマクロのエラーハンドリングに関する話をまとめたいと思います！

:::note
**本記事で伝えたいこと**

- **手続きマクロの出力はあくまでも [`proc_macro::TokenStream`](https://doc.rust-lang.org/beta/proc_macro/struct.TokenStream.html)**
- **通常のエラーハンドリングとの違いは [`proc_macro::Span`](https://doc.rust-lang.org/proc_macro/struct.Span.html) を扱うことである**
- マクロ定義部について定石な書き方がある。関数を切り分けることで丁寧にハンドリングしやすくする
  - 入力のパースは [`parse_macro_input`](https://docs.rs/syn/latest/syn/macro.parse_macro_input.html) 一択
  - パース後の入力を利用して切り分けた関数の引数とする、切り分けた関数の返り値型を [`syn::Result`](https://docs.rs/syn/latest/syn/type.Result.html) にしておく
  - 切り分けた関数の返り値 `syn::Result` を、 [`unwrap_or_else`](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_else) と [`syn::Error::into_compile_error`](https://docs.rs/syn/latest/syn/struct.Error.html#method.into_compile_error) を活用することでエラーの場合でも `TokenStream` に変換できるようにする

まさに [`syn::Error`](https://docs.rs/syn/latest/syn/struct.Error.html) にて紹介されている書き方が定石です！

```rust:Rust
#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
    // 入力パース
    let input = parse_macro_input!(input as DeriveInput);

    // 出力
    // fn my_derive(DeriveInput) -> syn::Result<proc_macro2::TokenStream>
    expand::my_derive(input)
        .unwrap_or_else(syn::Error::into_compile_error) // proc_macro2::TokenStream へ変換
        .into() // proc_macro::TokenStream へ変換
}
```
:::

本記事では、 [`compile_error!` マクロ](https://doc.rust-lang.org/std/macro.compile_error.html)、 [`syn::Error`](https://docs.rs/syn/latest/syn/struct.Error.html) に備わっている機能、 [`Span`](https://doc.rust-lang.org/proc_macro/struct.Span.html) 及び `syn::Error` の生成方法についての説明を通し、上記コードが行っていることを解説していきます！

:::note warn
記事中の表記が煩雑になることを避けるため [`proc_macro::TokenStream`](https://doc.rust-lang.org/proc_macro/struct.TokenStream.html) および [`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html) を共に `TokenStream` と呼んでいます。両者の違いは、 `2` がついていない方は手続きマクロ以外では使用できない(マクロをテストしたい場合テスト環境下では使えない)点と、故に三種の神器(syn, quote, proc_macro2)では `2` の方が使われているという点です。

`proc_macro2::TokenStream` は `.into()` でシンプルに `proc_macro::TokenStream` に変換できるため存在としては同一視して問題ありません。一応こまめにどちらであるか明記していますが忘れている箇所があるかもしれません。余力があれば `2` の方なのか無印の方なのか意識して読んでみてください。
:::

# 手続きマクロエラーの行く末「 [`compile_error!` マクロ](https://doc.rust-lang.org/std/macro.compile_error.html) 」

[Rustマクロの事前知識①「入出力はトークン木」](https://qiita.com/namn1125/items/3288db9acdf2012f78b0) で解説した通り、Rustの手続きマクロは [`proc_macro::TokenStream`](https://doc.rust-lang.org/beta/proc_macro/struct.TokenStream.html) を入力として受け取り `proc_macro::TokenStream` を出力します。

「じゃあマクロが失敗したらどうするのさ...？」それを知りたくて本記事にたどり着いたという人もいるでしょう。2つ手段があります。

- パニックさせる
- [`compile_error!` マクロ](https://doc.rust-lang.org/std/macro.compile_error.html) を `TokenStream` として出力する

パニックでも実は特に問題はないのですが、マクロがパニックした場合マクロ全体にエラーの赤線が引かれてしまいます。ユーザー目線だと「マクロ実装の方に問題があったみたいだ。信頼できないマクロだなぁ」となってしまいます。

そこで、後者の手法を取ります。後者の方法はパニックせずマクロが仕事を完遂させるための書き方です。ただマクロをエラーで終わらせるのではなく、「(ユーザーの入力では) **コンパイルしようとするとエラーになりますよ**」ということを伝えるべく、 `compile_error!` を `TokenStream` として出力します！

![syn_result2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/8da1ba3b-a61a-34a7-d848-87d75d4824c0.png)

`compile_error!` マクロがあると、即座にその箇所がコンパイルエラーとなります。ややこしいのですが、この `compile_error!` マクロは手続きマクロの処理途中で実行させるのではなく、手続きマクロの出力結果となる `TokenStream` それ自体を構成するのに使います！

筆者にとって `compile_error!` マクロの使い道は今まで謎だったのですが、手続きマクロの出力に使えたのですね[^compileerror]...上手いことできてるなぁと思いました

[^compileerror]: ところで専用のキーワードでも良さそうな感じはしますが、この機能のためだけに設けるのもヘン、ということで、ビルトインマクロに落ち着いているんだと思います。こういうマクロ他にもありそう...トレイトにしろマクロにしろ、Rustはヘンテコな言語機能をRustの基本的な文法要素で提供するのが上手だなぁと改めて感じました。

# [`syn::Error`](https://docs.rs/syn/latest/syn/struct.Error.html) の [`into_compile_error`](https://docs.rs/syn/latest/syn/struct.Error.html#method.into_compile_error)

何かしら想定外の結果になった際、自分で `compile_error!` を吐きだす処理を書くのは技術的には可能ですが面倒です。

[`syn::Error`](https://docs.rs/syn/latest/syn/struct.Error.html) が持つ [`into_compile_error`](https://docs.rs/syn/latest/syn/struct.Error.html#method.into_compile_error) メソッドまたは [`to_compile_error`](https://docs.rs/syn/latest/syn/struct.Error.html#method.to_compile_error) メソッドを呼べば、 `compile_error!` への変換を代わりに行ってくれるようになります！

よって、内側で呼び出す関数の返り値型を [`syn::Result`](https://docs.rs/syn/latest/syn/type.Result.html) `<proc_macro2::TokenStream>` = `Result<proc_macro2::TokenStream, syn::Error>` にしておけば、 [`unwrap_or_else`](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_else) メソッドを呼び出し、コールバックに `into_compile_error` を指定することで最終的な返り値型を `TokenStream` にできます！

```rust:src/lib.rs
#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    inner::my_macro_inner(input.into())
        .unwrap_or_else( // Ok(TokenStream) ならそのまま出力
            // Err(syn::Error) なら compile_error! を TokenStream として吐きだしてもらう
            syn::Error::into_compile_error
        )
        .into()
}

mod inner {
    pub fn my_macro_inner(
        input: proc_macro2::TokenStream,
    ) -> syn::Result<proc_macro2::TokenStream> {
        // ...
    }
}
```

# `syn::Error` の作り方と [`Span`](https://doc.rust-lang.org/proc_macro/struct.Span.html)

エラー発生時はパニックしなくても `syn::Error` さえ返せれば `compile_error!` を結果として返せることがわかりました。

では `syn::Error` をどうやって生成するか？なのですが、エラーメッセージに加え、構造体 [`proc_macro2::Span`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.Span.html) が必要になります。

```rust:Rust
syn::Error::new(Span構造体, エラーメッセージ)
```

`Span` 構造体は「ソースコード中の位置情報」と「マクロ展開における情報( `call_site` か `mixed_site` か)」から成ります。詳細は今回は省きますが、 **ソースコード中の位置情報** はマクロを使用するユーザーにコンパイルエラーを伝えるにあたり活用されるので、適切な設定が求められます。

`Span` 構造体の取得は基本的には難しくなく、大体は入力の構文要素を利用することで得られます。少し実験してみましょう！トークン列を受け取り、 `error` という識別子が出てきたらその箇所をエラーにしてみます。

```rust:src/lib.rs
mod inner {
    pub fn my_macro_inner(
        input: proc_macro2::TokenStream,
    ) -> syn::Result<proc_macro2::TokenStream> {
        use proc_macro2::TokenTree::Ident;

        for tt in input.clone() {
            if let Ident(ident) = tt {
                if ident == "error" {
                    // panic!("`error` ident found (panic)");
                    return Err(syn::Error::new_spanned(ident, "`error` ident found"));
                }
            }
        }

        Ok(input)
    }
}

#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    inner::my_macro_inner(input.into())
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}
```

パニックの場合はマクロ全体に赤線が引かれてしまいます。

![error_ident_found_panic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/5fc30e2b-2206-0340-5c38-1a9e8bc27ea3.png)

一方で、 `syn::Error::new_spanned(ident, "error ident found")` と書くことによりエラー原因となる箇所を `Error` に渡すようにした場合は、該当箇所のみに赤線が引かれます！

![error_ident_found.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/96c361fd-81ec-80f1-e5b7-612644e02aec.png)

`syn::Error` は、 `new` メソッドの他、ソースコード中で示した `new_spanned` メソッドを使うことで生成できます。どちらを使用しても構わないのですが使い心地が若干異なります。

- `new` メソッド: 第一引数は `Span` 本体
- `new_spanned` メソッド: 第一引数は [`quote::ToTokens`](https://docs.rs/quote/1.0.38/quote/trait.ToTokens.html) トレイトを実装した型

`ToTokens` トレイトを実装した型というのは、大体は入力の `TokenStream` をパースする過程で登場します。 `ToTokens` を実装していると [`syn::spanned::Spanned` トレイトも自動実装される](https://docs.rs/syn/latest/syn/spanned/trait.Spanned.html#:~:text=This%20trait%20is%20automatically%20implemented%20for%20all%20types) ことより、これらの型はSpan情報を保持していることがわかります。なお、マクロ展開のための情報としては、マクロの入力(ユーザーのソースコード)から取られた範囲の場合 `call_site` が設定されています。

`new` メソッドを使用する場合で、こちらに設定する `Span` については、由来となる構文要素がある場合は由来元に対し `.span()` メソッドを呼んでみると得られるかもしれません。[`syn::spanned::Spanned`](https://docs.rs/syn/latest/syn/spanned/trait.Spanned.html) トレイトが実装されていれば `Span` を得ることができ、エラーに活用できます。マクロ側で作った `Span` でエラーにしたかったり、複雑な条件より導き出される `Span` でエラーにしたい時は、 `new` を。特に問題がなければシンプルな `new_spanned` を使うと良さそうです。

```rust:Rust
pub fn my_macro_inner(
    input: proc_macro2::TokenStream,
) -> syn::Result<proc_macro2::TokenStream> {
    use proc_macro2::TokenTree::Ident;

    let mut guido_mista_counter = 0;

    for tt in input.clone() {
        if let Ident(ident) = tt {
            if ident == "error" {
                // return Err(syn::Error::new_spanned(ident, "`error` ident found"));

                /* ユーザー定義側のSpanを使う例 */
                let span = ident.span();
                return Err(syn::Error::new(span, "`error` ident found"));
            }
        }

        guido_mista_counter += 1;
    }

    if guido_mista_counter == 4 {

        /* マクロ側の都合でErrorを作る場合 */
        return Err(syn::Error::new(
            Span::mixed_site(),
            "Unlucky number 4 is detected",
        ));
    }

    Ok(input)
}
```

:::note
**最終的にはどうやってSpanの情報をコンパイラに伝えてるの？**

とにもかくにも `syn::Error` に `Span` 情報を渡せればいい感じの場所に赤線を引いてくれることはわかりました。しかし一体このSpanをどうやってコンパイラに伝えているのでしょうか？

[`into_compile_error` の実装](https://docs.rs/syn/latest/src/syn/error.rs.html#275-324) を見るとなんとなく答えが載っており、方法はシンプルで `set_span` メソッドを使い `compile_error!` マクロに対し与えられたSpan情報を設定しているようです。
:::

# 入力パースのマクロ [`parse_macro_input`](https://docs.rs/syn/latest/syn/macro.parse_macro_input.html)

手続きマクロは「入力のパース」 → 「出力のレンダリング」の順で行われます。マクロ処理は当然副作用を持つべきではなく参照透過的であるべきなので、入力が正しければ出力でエラーになることはあまりないでしょう(実際は入力パースが中途半端になる影響でしばしばありますが...)。

ということで、入力パースの成功失敗は関心事です。自前で用意した入力構造体が [`syn::parse::Parse` トレイト](https://docs.rs/syn/latest/syn/parse/trait.Parse.html) を実装していれば、 [`syn::parse`](https://docs.rs/syn/latest/syn/fn.parse.html) 関数で `TokenStream` から入力構造体へ変換することができます。

```rust:Trait Parse
pub trait Parse: Sized {
    // Required method
    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self>;
}
```

ここまでの話を踏まえれば、 `Parse::parse` の返り値型が `syn::Result` なのは納得できる話でしょう。 `#[proc_macro]` を付与された手続きマクロエントリにて `into_compile_error` / `to_compile_error` を活用することで、適切に入力パースエラーを処理できます！

```rust:Rust
use syn::parse::Parse;

struct MyInput;

impl Parse for MyInput {
    // ...省略...
}

#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input: MyInput = match syn::parse(input) {
        Ok(parsed) => parsed,
        Err(err) => return err.to_compile_error().into(),
    };

    todo!()
}
```

いちいち `match` 式を書くのは...ちょっと面倒ですよね？このボイラープレートに展開してくれるのが [`parse_macro_input!`](https://docs.rs/syn/latest/syn/macro.parse_macro_input.html) マクロです！

```rust:Rust
#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = syn::parse_macro_input!(input as MyInput);

    todo!()
}

// ↓ cargo expand

#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = match ::syn::parse::<MyInput>(input) {
        ::syn::__private::Ok(data) => data,
        ::syn::__private::Err(err) => {
            return ::syn::__private::TokenStream::from(err.to_compile_error());
        }
    };
    ::core::panicking::panic("not yet implemented")
}
```

すなわち、入力パースに関して丁寧なエラーハンドリングを短い記述で行えるのが `parse_macro_input!` マクロということです。というわけでこのマクロの使用が定石になっています。

# 定石的なエラーハンドリング まとめ

本記事のまとめとして、「与えられた回数分、ステートメントを繰り返す。ただし、回数が4回の時はエラーにする」という手続きマクロを書いてみます！

以下作成のポイントです。

- 入力のパースは [`parse_macro_input!`](https://docs.rs/syn/latest/syn/macro.parse_macro_input.html) で行う
  - そのために、入力用構造体に [`syn::parse::Parse` トレイト](https://docs.rs/syn/latest/syn/parse/trait.Parse.html) を実装する
- 実際に出力を生成する関数は切り分けて、 [`syn::Result`](https://docs.rs/syn/latest/syn/type.Result.html) `<` [`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html) `>` を返り値型とする
  - [`syn::Error`](https://docs.rs/syn/latest/syn/struct.Error.html) を作成するために、 [`proc_macro2::Span`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.Span.html) を何かしらの方法で得る。 `Span` があるなら `new` 、元となる構文要素があるなら `new_spanned` を使用する
- `syn::Result<proc_macro2::TokenStream>` は、 [`unwrap_or_else`](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_else) と [`into_compile_error`](https://docs.rs/syn/latest/syn/struct.Error.html#method.into_compile_error) を利用して `TokenStream` に変換する

```rust:src/lib.rs
use quote::{quote, ToTokens};
use syn::parse::Parse;

#[proc_macro]
pub fn repeat_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // 入力部
    // 入力パースに失敗したら `compile_error!` を出力してくれる
    let input = syn::parse_macro_input!(input as Repeat);

    // 出力部
    repeat_macro_inner(input)
        .unwrap_or_else(
            // Errorの時、 `compile_error!` に変換してもらう
            syn::Error::into_compile_error
        )
        .into()
}

struct Repeat {
    stmt: syn::Stmt,
    count: syn::LitInt,
}

// repeat_macro!(回数; ステートメント); をパース
impl Parse for Repeat {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let count: syn::LitInt = input.parse()?;
        let _ = input.parse::<syn::Token![;]>()?;
        let stmt = input.parse()?;

        Ok(Repeat { stmt, count })
    }
}

fn repeat_macro_inner(Repeat { count, stmt }: Repeat) -> syn::Result<proc_macro2::TokenStream> {
    let span = count.span(); // Error生成用にSpanを確保しておく
    let count = count.base10_parse::<usize>()?;

    if count == 4 {
        return Err(syn::Error::new(
            span, // 繰り返し回数部分にだけエラーの赤線が引かれる
            "The `4` is unlucky number for Mr. Guido Mista. Please choose another number.",
        ));
    }

    let expanded = (0..count).map(|_| stmt.to_token_stream());

    Ok(quote! {
        #(#expanded)*
    })
}
```

# まとめ・所感

筆者的に色々試したりドキュメントを読んだりした上でたどり着いた手続きマクロのエラーハンドリング手法及び定石な書き方を紹介しました！

「これが定石！こういう風に書くべき！」と言われると反発したくなるものです。もちろん、別に本記事で書いた通りに書く必要はないと思います。本記事が色々な書き方をコネコネする際の参考になれば幸いです。

ここまで読んでいただきありがとうございました！