競技プログラミング用に、Rustのコードを単一ファイルにバンドルする

## What I did

[cargo-equip](https://github.com/qryxip/cargo-equip)という、競技プログラミング用にライブラリを一つの`.rs`ファイルにバンドルするツールを作りました。

## 動機

競技プログラミングで提出するコードは、外部ライブラリへの依存を持たない単一ファイルに収める必要があります。

コンテストに何度も参加していると、一から手書きするのがキツいデータ構造や、痒いところに手を伸ばすユーティリティだったりを「ライブラリ」として事前に用意しておきたくなります。そのような「ライブラリ」を極力要求しないようにしている[AtCoder](https://atcoder.jp)においても同様です。AtCoderでは今年から[39個のRustのクレート](https://github.com/rust-lang-ja/atcoder-rust-resources/wiki/2020-Update)が使えるようになりましたが、足りない機能は多いです。

しかし競技プログラミングでは通常のプログラミングのようにパッケージマネージャからライブラリを使用する、といったことができません。毎回それらを提出するファイルに含める必要があります。手でコピー&ペーストするというのが一般的な手法です。

これを自動化する方法を考えてみます。Rustには[手続き型マクロ](https://doc.rust-lang.org/reference/procedural-macros.html)のために、容易に構文木を解析, 編集, 生成することができます。手続き型マクロの要領で自動でバンドルするツールを作る、という考えは以前から持ってましたし多くの競プロRust使いも同様だったと思います。

まず思いつくのが外部クレートとして使ったライブラリを「展開」する、というものですが、これにはざっと思いつくだけでも様々な問題があります。まず各アイテムのパスを全部一段ずらす必要がありますが、Rustのモジュールの挙動は機械どころか人間にとっても非自明です。部分的な対応だけして各ファイルがちゃんと展開されるかのテストを書くようにする、という方針もありますが`#[macro_export]`をどうするかという問題だってあります。

次に私が考えたのは`mod foo;`を展開するツール、[cargo-expand-mods](https://github.com/qryxip/cargo-expand-mods)を作り、使う際一つ一つのモジュールを`#[path = ".."]`で指定して使用するといったものですがこれは煩雑すぎました。

そもそもコード片を持ち込むだけならエディタか`cat(1)`でファイルを開き、コピー&ペーストすれば事足ります。数時間のコンテストならその手間は誤差です。

スニペットにするという方法だってあります。スニペットの管理には[cargo-snippet](https://github.com/hatoo/cargo-snippet)という優れたツールがあります。

[Rustで競技プログラミングをするときの"スニペット管理"をまじめに考える(cargo-snippetの紹介) - Qiita](https://qiita.com/hatoo@github/items/5c6814e72ddd2ecaf48f)

手持ちのコードを全部まとめてminify、するツールはまだ無いのでそれっぽく一行に圧縮したものをソースファイルのテンプレートに含めるという方法だってあります。多くの競プロプラットフォームのサイズ制限は64KiBですが、AtCoder等であれば512KiBのコードまで提出できます。AtCoderであればバイナリ提出も **まだ** 許されています。そのためバンドルするツールの必要性は感じていませんでした。

ところが最近このようなツイートを偶然見かけました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Rust のマクロ全然詳しくないんだけど、競プロやる上でこういうの欲しくなる。 <a href="https://t.co/Z3lfJ25pP9">pic.twitter.com/Z3lfJ25pP9</a></p>&mdash; 宇宙ツイッタラーX (@kenkoooo) <a href="https://twitter.com/kenkoooo/status/1299590003488555008?ref_src=twsrc%5Etfw">August 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こういう方法なら問題点が完全ではないにしろ解決するのではないか、と感じました。ダミーのアトリビュートが付いたアイテムを外部ツールで拾っていく、という方法はcargo-snippetが既にやっています。そして作ったのがcargo-equipです。

これがcargo-equipの提供する`#[kore_tenkai_shitekureya]`です。

```rust:src/bin/foo.rs
#[cfg_attr(cargo_equip, cargo_equip::equip)]
use ::__my_lib::{a, b::Foo, c::Bar};
```

cargo-equipは上のコードをこのように展開します。

```rust
use self::b::Foo;
use self::c::Bar;

pub mod a { /* `a.rs`の中身そのまま */ }
pub mod b { /* `b.rs`の中身そのまま */ }
pub mod c { /* `c.rs`の中身そのまま */ }
pub mod other_dependent { /* `other_dependent.rs`の中身そのまま */ }
```

## 使用例

<https://qryxip.github.io/oj-verify-playground/verify/verification/library-checker/src/bin/point_add_range_sum_test.rs.html>

"Code"の右上の"Bundle"/"Unbundle"をクリックすることで展開前/後を切り替えることができます。

## ライブラリの制約

cargo-equipで扱えるライブラリには以下の制約があります。

1. 非inline module (`mod $name;`)は深さ1まで
2. 深さ2以上のモジュールはすべてinline module (`mod $name { .. }`)
3. crate root直下には`mod`以外の`pub`なアイテムが置かれていない (置いてもいいですが使わないでください)
4. `#[macro_export] macro_rules! name { .. }`は`mod name`の中に置かれている (それ以外の場所に置いていいですがその場合`#[macro_use]`で使ってください)
5. `#[macro_export]`には組み込み以外のアトリビュート(e.g. `#[rustfmt::skip]`)を使用しない (原理的に展開すると壊れる)

1.と2.はそのうち対応しようと思います。

このように薄く広く作ってください。

```
my_lib/src
├── a.rs
├── b.rs
├── c.rs
├── lib.rs
└── other_dependent.rs
```

```rust:my_lib/src/lib.rs
pub mod a;
pub mod b;
pub mod c;
pub mod other_dependent;
```

また、`Cargo.toml`にモジュール間の依存関係を手で記述してください。直接`use`したモジュールと、その連結成分だけを展開します。欠けている場合、warningと共にすべてのモジュールを展開します。

```toml
[package.metadata.cargo-equip-lib.mod-dependencies]
"a" = []
"b" = []
"c" = ["other_dependent"]
"other_dependent" = []
```

ある一つのモジュールを分割したい場合、クレートを作るときのように共通のprefixを付けたモジュールを作り、それらをre-exportしてください。(参考: [num](https://crates.io/crates/num))

これらの制約により各ファイルの中身をそのまま展開できます。これにより壊れにくくなっているはず。

今のところ制約の違反を検出してエラーにする機能はありません。

## 展開方法

leading colonは必須です。

```
#[cfg_attr(cargo_equip, cargo_equip::equip)]
use ::{__my_lib::{a, b::Foo, c::Bar}};
    ^^
```

そうするとパスの第一セグメントは"extern crate name"だと言えるので、そこから対象が`dependencies`の中のどれなのかを特定します。誤って`#[cargo_equip::equip]`以外で直接使わないよう、ライブラリの`lib` targetの名前はその`Cargo.toml`か使う側の`Cargo.toml`の中でリネームしておくことを強く推奨します。

~~複数のライブラリの展開は未実装です。~~ [対応しました](https://github.com/qryxip/cargo-equip/pull/11)。

```
#[cfg_attr(cargo_equip, cargo_equip::equip)]
use ::{__my_lib::{a, b::Foo, c::Bar}};
       ^^^^^^^^
```

先述したライブラリの制約より、パスの第二セグメントはモジュールとみなします。これらのモジュールと、先程書いた`mod-dependencies`で繋がっているモジュールが展開されます。

```
#[cfg_attr(cargo_equip, cargo_equip::equip)]
use ::{__my_lib::{a, b::Foo, c::Bar}};
                  ^  ^       ^
```

第三セグメント以降は`use self::$name::{..}`と展開されます。

```
#[cfg_attr(cargo_equip, cargo_equip::equip)]
use ::{__my_lib::{a, b::Foo, c::Bar}};
                      ^^^^^   ^^^^^
```

ソースコード全体はこのように変換されます。

```rust
//! # Bundled libraries
//!
//! ## [`my_lib`]({ a link to Crates.io or the repository })
//!
//! ### Modules
//!
//! - `::__my_lib::a` → `$crate::a`
//! - `::__my_lib::b` → `$crate::b`
//! - `::__my_lib::c` → `$crate::c`
//! - `::__my_lib::other_dependent` → `$crate::other_dependent`

/*#[cfg_attr(cargo_equip, cargo_equip::equip)]
use ::{__my_lib::{a, b::Foo, c::Bar}};*/

fn main() {
    todo!();
}

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

use self::b::Foo;
use self::c::Bar;

pub mod a { /* `a.rs`の中身そのまま */ }
pub mod b { /* `b.rs`の中身そのまま */ }
pub mod c { /* `c.rs`の中身そのまま */ }
pub mod other_dependent { /* `other_dependent.rs`の中身そのまま */ }
```

## 展開結果のチェック

展開結果をチェックするために`--check`というオプションを用意しています。出力する前に`cargo check`します。

```console
$ cargo equip --check -o /dev/null
    Bundling code
    Checking cargo-equip-check-output-r3cw9cy0swqb5yac v0.1.0 (/tmp/cargo-equip-check-output-r3cw9cy0swqb5yac)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
```

ただworkspaceを一時的に拡張する方法がわからなかったので、一時パッケージを作ってその上で`cargo check --target-dir {元のtarget directory}`する方法になりました。 (折角依存ライブラリが小さめになったのに`cargo`クレートを導入したくなかったので)

一応衝突を避けるため、パッケージ`cargo-equip-check-output-r3cw9cy0swqb5yac`上で`src/bin/cargo-equip-check-output-r3cw9cy0swqb5yac.rs`を実行、ということをしています。

[trybuild](https://crates.io/crates/trybuild)がどうやってコンパイルを行っているのかがわからないのですが、調べて真似できそうならそうしたいと思っています。

## 競プロツールとの統合

[cargo-atcoderの代替品を作った - Qiita](https://qiita.com/qryxip/items/bff57848ac9310d27f1a)

[cargo-compete](https://github.com/qryxip/cargo-compete)の方でも提出にcargo-equipを使えるようにする設定を追加しておきました。

```toml
[submit.transpile]
kind = "command"
args = ["cargo", "equip", "--oneline", "mods", "--rustfmt", "--check", "--bin", "{{ bin_name }}"]
#language_id = ""
```

`--bin a`の代わりに`--src ./src/bin/a.rs`のように指定できるようにもしてあるので、他ツールとの統合もやりやすいはずです。