2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【手続きマクロだけではない】synクレートを活用して便利ツールを作った話【Rust】

Last updated at Posted at 2024-12-25

こちらの記事は Rustマクロ冬期講習アドベントカレンダー 24日目の記事です!

アドカレまとめ記事はこちら!: Rustマクロ作成チートシート!

14日目の記事、Rustマクロ フラグメント指定子(ident, expr, item, stmt...)なんもわからん!となったので判別器作った #Rust - Qiita にて、Rustコードを与えるとフラグメント指定子ごとに色分けしてくれる 判別器 を作成した話をしました。

この記事では話していませんでしたが、実はこのツール、三種の神器の一つの syn を利用したものです。というわけで、その話をしたいと思います!

Rustフラグメント指定子判別器.png

本記事で伝えたいこと

syn クレートは手続きマクロ以外でもカジュアルに使えます! Rustの構文解析をしたいならば次の機能が便利かもしれません。

  • syn::parse_str
    • 対象構文要素( syn::parse::Parse トレイトを実装した型)への文字列のパースを試みることができます
  • proc_macro2::Span
    • 構文要素のソースコード中での位置情報が格納されています。ただし条件つきなので要ドキュメント確認
  • syn::visit::Visit
    • 構文要素を再帰的に探索するために使用できます
    • 他にも syn::fold モジュールも再帰的な処理に使えるかもです

例えばマクロのテストをする際に使えそうですね :sunglasses:

カラクリ・処理順序

本節ではこの判別器の仕組みを軽く解説します。

対象ソースコード:

構文要素を判定する機能は syn::parse_str を基軸に作成し、WebAssemblyを利用して呼び出すようにしました。(ちなみにフロントエンドは色々検討したのですが結局React + muiに落ち着きました... :sweat_smile: )

処理順序になります。

(1) 様々な構文要素で parse_str を試す

  • マッチしたものについてリストアップ。サイトでは左側の "Whole Matcher" 欄にてマッチした要素の情報を表示

(2) マッチした構文要素について、再帰的に構文要素を調べる

  • 再帰処理には syn::visit::Visit を利用
  • ある構文要素 S がヒットした際、 <S as Spanned>::span を調べ、 byte_range メソッドでソースコード位置( Range<usize> )を取得。ハッシュマップ HashMap<構文要素列挙体, Vec<Range<usize>>> に格納

(3) 然るべき加工(CLIならASCIIエスケープシーケンスによる着色。WebAssemblyならマークアップ)を施して表示

オプション等の処理を含めるともう少しいろいろ話せることはありますが、軸となる処理は parse_str があったおかげでそこまで裏技的なことをせずに実装することができました。またもう一つ特に 悩むだろうなと予想されたのは、親構文要素から子構文要素を探索していく再帰処理部分 でしたが、syn::visit::Visitトレイト があったおかげで案外苦労せずに実装することができました!

synクレートにおんぶに抱っこになれば、この手のツールは簡単に作れるということですね...他にも色々活用できそうです。

Visitトレイトはどういう仕組み?

残りのハンズオンパートでは Visit トレイトの話はしないのですが、仕組みが面白かったのでここで少し解説したいと思います。

visit モジュールにはこの Visit トレイトと大量の visit_xxx 関数が定義されており、 Visit トレイトが提供するメソッド Visit::visit_xxx のデフォルト実装にてこの関数 visit_xxx が呼ばれている構造になっています。

Rust
trait Visit<'ast> {
    // ...省略...
    fn visit_xxx(&mut self, i: &'ast crate::Xxx) {
        visit_xxx(self, i);
    }
    // ...省略...
}

visit_xxx 関数は、自身より小さい要素の関数 visit_yyy を呼び出すようになっており(例えば visit_stmt なら式の時に visit_expr を呼ぶなど)、この仕組みによって子要素の方にまで再帰的にアクセスできるようになっています!

Rust
pub fn visit_xxx<'ast, V>(v: &mut V, node: &'ast crate::Xxx)
where
    V: Visit<'ast> + ?Sized,
{
    // ...省略... crate::Yyy型のyを得る
    v.visit_yyy(&y);
} 

フックとして、 Visit トレイトのデフォルト実装に処理を追加してあげれば、 Xxx 要素に対するすべての探索にフック処理が入ります!

Rust
struct Counter {
    xxx_counter: usize,
}

impl<'ast> Visit<'ast> for Counter {
    fn visit_xxx(&mut self, i: &'ast syn::Xxx) {
        // フック処理
        self.xxx_counter += 1;

        syn::visit::visit_xxx(self, i);
    }
}

playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=96c6b7abc292aa287b8d682874b4ec2f

Visit トレイトが採用しているような仕組みは、再帰的に探索する時にフックを仕掛けたい時に使えるパターンみたいですね...覚えておくと便利かもしれません(多分)

軽くハンズオン

さらなる詳細について工夫した点のみピックアップしようと思ったのですが、却って機能説明がわかりにくくなりそうだったため、「Whole Matcher機能」すなわち「与えられた文字列がRust構文要素の何に対応するかを調べる」機能だけハンズオンで解説することにしました!というわけでLet's 実装!

クレート用意

まずはクレートを用意します。

crate new match_frags

「フラグメント指定子(Fragment Specifiers)」にマッチするかどうか、ということで match_frags という名前にしてみました。

次に必要なクレートを追加していきます。もちろん syn クレートと、今回はCLIアプリにしたいので clap クレートを入れます。

cargo add syn --features extra-traits --features full
cargo add clap --features derive

入れたfeatureの捕捉です。

  • syn クレート
    • featuresは使わないかもしれないですが適当に入れています
    • --features extra-traits: Debug 等を利用できるようになります
    • --features full: 「アレ...?この機能ない...」となりがちなので入れちゃいます
      • 裏を返せば軽量にしたければ省きやすい構造になっていてよいですね
  • clap クレート
    • --features derive: 宣言的に引数を書けるようになるので必須です

コマンドライン引数で文字列入力を受ける

確認したい対象となる文字列はコマンドライン引数で受けることにします。

src/main.rs
use clap::Parser;

#[derive(Parser)]
struct Input {
    /// 構文解析対象文字列
    parse_target: String,
}

fn main() {
    let Input { parse_target } = Input::parse();

    println!("{}", parse_target);
}

受けた文字列をそのまま出力するだけのプログラムです。

$ cargo run -q -- 'hoge(1 + 1)'
hoge(1 + 1)

syn::parse_str でマッチ!

歌ならもうサビの部分です、引数で取り入れた解析対象文字列 parse_target が構文要素にマッチするか調べます!

とりあえず、式( syn::Expr )の場合は以下のような感じです。

Rust
if syn::parse_str::<syn::Expr>(&parse_target).is_ok() {
    println!("{} is Expr", parse_target);
}
src/main.rs
// 省略

fn main() {
    let Input { parse_target } = Input::parse();

-    println!("{}", parse_target);
+    if syn::parse_str::<syn::Expr>(&parse_target).is_ok() {
+        println!("{} is Expr", parse_target);
+    }
}

これを全部の構文要素(順不同で syn::Filesyn::Blocksyn::Exprsyn::Identsyn::Itemsyn::Lifetimesyn::Litsyn::Metasyn::Pathsyn::Stmtsyn::Typesyn::Visibility ...多い!)でマッチするか確認していきます。プログラムの変更について柔軟性を持たせるため、マッチした要素を文字列として動的配列に追加することし、面倒なので宣言マクロを利用することにします!

src/main.rs
use clap::Parser;

#[derive(Parser)]
struct Input {
    /// 構文解析対象文字列
    parse_target: String,
}

fn main() {
    let Input { parse_target } = Input::parse();

    let mut parsable_frags = Vec::new();

    macro_rules! check_parsable {
        ($frag:ty) => {
            if syn::parse_str::<$frag>(&parse_target).is_ok() {
                let s = stringify!($frag).to_string();
                parsable_frags.push(s);
            }
        };
    }

    check_parsable!(syn::File); // 正確には構文要素ではないが、ついでに確認
    check_parsable!(syn::Item);
    check_parsable!(syn::Block);
    check_parsable!(syn::Stmt);
    check_parsable!(syn::Expr);
    check_parsable!(syn::Type);
    check_parsable!(syn::Path);
    check_parsable!(syn::Visibility);
    check_parsable!(syn::Ident);
    check_parsable!(syn::Lifetime);
    check_parsable!(syn::Lit);
    check_parsable!(syn::Meta);

    println!(
        "parsable fragments for `{}`: {:?}",
        parse_target, parsable_frags
    );
}

実はこれでもう完成です、与えられた文字列がRust構文要素の何に該当するかを判別できます!

$ cargo run -q -- 'hoge(1 + 1)'
parsable fragments for `hoge(1 + 1)`: ["syn::Expr", "syn::Meta"]
$ cargo run -q -- 'if a { b } else { c }'
parsable fragments for `if a { b } else { c }`: ["syn::Stmt", "syn::Expr"]
$ cargo run -q -- 'fn hoge() { /*...*/ }'
parsable fragments for `fn hoge() { /*...*/ }`: ["syn::File", "syn::Item", "syn::Stmt"]

そんなに難しい部分はなかったかと思います。 syn::parse_str が便利すぎた...

macro_rules スコープの小ネタ

地味に今まで紹介していなかった小ネタを今回利用しています。

Rustマクロの事前知識③「マクロのマナー 衛生性(健全性)」 の結論としては、マクロ内で宣言されていない変数へのアクセスはマナー的にやめたほうが良いのでした。そして実際、大抵の場合これはエラーになります。

しかし、実は macro_rules でマクロを宣言しているスコープ内に定義されている変数に関しては、その限りではなく普通にアクセス出来ます!

Rust
/* // こちらだとコンパイルエラー
macro_rules! p {
    () => { println!("{}", hoge); };
}
*/

fn main() {
    let hoge = "hoge!";

    // ここで定義するとhogeが見える
    macro_rules! p {
        () => { println!("{}", hoge); };
    }
    
    p!();
}

というわけで、一々マクロに parse_targetparsable_frags 変数を渡すのは面倒だったので、 main 関数内で定義するようにしていました。

まとめ・所感

syn::parse_str を利用した軽いハンズオンを通して、 syn クレートは手続きマクロだけでなくRust構文解析にまつわるツール作成にも用いられることを示しました。

syn は手続きマクロ作成専用というより、Rust構文全般のパースや操作に便利なクレートです。こうした多用途性を意識しておくと、手続きマクロの作成や応用を考えるときの解像度も上がるはずです...というわけで、皆さんもぜひ syn クレートを利用して面白いツールを作ってみてほしいです!

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?