続・競技プログラミングにprocedural macroを持ち込む


競技プログラミングにprocedural macroを持ち込む

前回の記事では競技プログラミングに手続き型マクロを持ち込みたい動機と、それを成すための(あまりスマートとは言えない)方法を紹介しました。そして先日「そういえばrust-analzyer (RA)はどうやって手続き型マクロを扱っているのか」と思いふとGitHub上でRAのソースコードのそれっぽい場所を眺めてみたところ、rpc, msg, stdin, stdoutといった単語が目に入ってきました。もしやと思いarchitecture.mdを読むとこんなことが書かれているではありませんか。

For proc macros, the client-server model are used. We pass an argument --proc-macro to rust-analyzer binary to start a separate process (proc_macro_srv). And the client (proc_macro_api) provides an interface to talk to that server separately.

(今は--proc-macroではなくサブコマンドproc-macroのようですが)試しにクエリListMacroのJSONを放り込んだところ普通に結果を返してくれました。

$ echo '{"ListMacro":{"lib":"./target/debug/deps/libfastout-2dbcc333cc21dae5.so"}}' | rust-analyzer proc-macro
{"ListMacro":{"macros":[["fastout","Attr"]]}}

もう一つのクエリExpansionMacroについてもproc-macro2から上手くJSONにシリアライズすればいけるはずです。これでwattを要求しなくてもよくなります。今回cargo-equipにRAを使った手続き型マクロの展開機能を実装し、v0.10.0としてリリースしました。本記事ではそれにあたり遭遇した問題とその解決方法を書いていこうと思います。

rust-analzyer(.exe)のダウンロード

JSONの形式についてはドキュメント化されておらず、ユーザが使うことも想定されていません。従ってこれからやることはhackであり、単純に$PATH内のrust-analzyer(.exe)を使うとRAの更新により動作しなくなる可能性が付き纏います。なので特定のバージョンのバイナリをGitHub Releasesからツール側でダウンロードして使うような形にしました。

メッセージのスキーマ

やりとりするメッセージのスキーマはproc_macro_api::msg::{Request, Response}で定義されています。tt::Subtreeを模した型を作り、SerializeDeserializeproc_macro2::Groupとの相互Fromを実装すればあとは簡単に扱うことができます。

proc-macroクレートのDLLの場所

proc-macroクレートは動的リンクライブラリとしてコンパイルされます。この場所ですが、cargo check --message-format jsonの出力をcargo_metadataクレートでパースすれば得ることができます。目当てであるcompiler-artifactはコンパイルがスキップされていても含まれるため、再コンパイルを試みる必要はありません。

proc-macroクレートをコンパイルするRustのバージョン

proc-macroクレートは1.47.0以上のRustでコンパイルされる必要があります。1.42.0でコンパイルしたものをRAに渡すとこのようなことになります。

今回はactive toolchainが1.47.0未満ならrustupにあるツールチェインを探してそれを使うという形にしました。

動作例

proconio-derivememoiseがAtCoder以外で使えるようになりました。

#[macro_use]
extern crate memoise as _;
#[macro_use]
extern crate proconio_derive as _;

#[fastout]
fn main() {
    for i in 0..=100 {
        println!("{}", fib(i));
    }
}

#[memoise(n <= 100)]
fn fib(n: i64) -> i64 {
    if n == 0 || n == 1 {
        return n;
    }
    fib(n - 1) + fib(n - 2)
}


Output
//! # Procedural macros
//!
//! - `memoise 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)`         licensed under `BSD-3-Clause`
//! - `proconio-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)` licensed under `MIT OR Apache-2.0`

/*#[macro_use]
extern crate memoise as _;*/
/*#[macro_use]
extern crate proconio_derive as _;*/

/*#[fastout]
fn main() {
    for i in 0..=100 {
        println!("{}", fib(i));
    }
}*/
fn main() {
    let __proconio_stdout = ::std::io::stdout();
    let mut __proconio_stdout = ::std::io::BufWriter::new(__proconio_stdout.lock());
    #[allow(unused_macros)]
    macro_rules ! print { ($ ($ tt : tt) *) => { { use std :: io :: Write as _ ; :: std :: write ! (__proconio_stdout , $ ($ tt) *) . unwrap () ; } } ; }
    #[allow(unused_macros)]
    macro_rules ! println { ($ ($ tt : tt) *) => { { use std :: io :: Write as _ ; :: std :: writeln ! (__proconio_stdout , $ ($ tt) *) . unwrap () ; } } ; }
    let __proconio_res = {
        for i in 0..=100 {
            println!("{}", fib(i));
        }
    };
    <::std::io::BufWriter<::std::io::StdoutLock> as ::std::io::Write>::flush(
        &mut __proconio_stdout,
    )
    .unwrap();
    return __proconio_res;
}

/*#[memoise(n <= 100)]
fn fib(n: i64) -> i64 {
    if n == 0 || n == 1 {
        return n;
    }
    fib(n - 1) + fib(n - 2)
}*/
thread_local ! (static FIB : std :: cell :: RefCell < Vec < Option < i64 > > > = std :: cell :: RefCell :: new (vec ! [None ; 101usize]));
fn fib_reset() {
    FIB.with(|cache| {
        let mut r = cache.borrow_mut();
        for r in r.iter_mut() {
            *r = None
        }
    });
}
fn fib(n: i64) -> i64 {
    if let Some(ret) = FIB.with(|cache| {
        let mut bm = cache.borrow_mut();
        bm[(n) as usize].clone()
    }) {
        return ret;
    }
    let ret: i64 = (|| {
        if n == 0 || n == 1 {
            return n;
        }
        fib(n - 1) + fib(n - 2)
    })();
    FIB.with(|cache| {
        let mut bm = cache.borrow_mut();
        bm[(n) as usize] = Some(ret.clone());
    });
    ret
}

// The following code was expanded by `cargo-equip`.

#[allow(clippy::deprecated_cfg_attr)]#[cfg_attr(rustfmt,rustfmt::skip)]#[allow(unused)]pub mod memoise{}
#[allow(clippy::deprecated_cfg_attr)]#[cfg_attr(rustfmt,rustfmt::skip)]#[allow(unused)]pub mod proconio_derive{}