Rime+のススメ


某OB/OGの会では問題準備に [nya](http://qiita.com/nya) さんの作った[Rime](http://nya3jp.github.io/rime/)というツールを6年(?)[^1]くらい前から使っています。しかし、このRime、現代から見るといろいろ足りない機能があります。そこで、プロトタイプとして以前いくつか拡張プラグインを追加した [Rime+](https://github.com/icpc-jag/rime-plus) というのを昔作ったので紹介します。なお、一部機能は飽きてあまりテストされていなかったり、環境・バージョン依存が含まれているかもしれないですがその点ご了承ください。

[^1]: フルスクラッチで書き直される前のバージョン時代を含める

## Rime+ の特徴

Rimeが何であるかは公式サイトを見ていただくとして、Rime+ の特徴について紹介したいと思います。

### 1. pip によるインストール

Rime+ は、レポジトリからレポジトリへと秘伝のソースをコピペして受け継いでいた従来の Rime と違い、 pip で簡単にインストールすることができます。インストールは、

```bash:
$ pip install git+https://github.com/icpc-jag/rime-plus
```

アップデートは、

```bash:
$ pip install -U git+https://github.com/icpc-jag/rime-plus
```

アンインストールは、

```bash:
$ pip uninstall rime-plus
```

となります。

### 2. 雛形を作る労力を削減するための scaffolding 系コマンドの追加

Rime+ では、Rime にはない雛形を用意するためのコマンド群が用意されています。これらを利用することで PROJECT, PROBLEM, SOLUTION, TESTSET といった config ファイルを用意する手間が省けます。

まず、新規プロジェクトを始めるには、以下のように `rime_init` を使います。

```bash:
$ rime_init --git       # git
$ rime_init --mercurial # mercurial
```

ついでに VCS も初期化するので git か m ercurial を選んでください。また、このコマンドでは最新の testlib.h も落としてきます。

次に問題・解答・テストセットを追加するには、`rime add` を使います。`rime add`では、以下のように、追加する親ディレクトリ、追加する種類、追加するディレクトリ名をこの順で引数に取ります。[^2]

```bash:
# 問題ディレクトリとして "a_plus_b" をプロジェクトルート直下に追加
$ rime add . problem a_plus_b

# "a_plus_b" の下に解答ディレクトリとして "hiroshi-cl" を追加
$ rime add a_plus_b solution hiroshi-cl	

# "a_plus_b" の下にテストセットディレクトリとして "tests" を追加
$ rime add a_plus_b testset tests
```

`rime add` では、環境変数 `$EDITOR` に設定されたエディタで生成された config ファイルが自動的に開かれ、それを編集することができます。なお、`$EDITOR` が設定されていないときは vi になります。

[^2]: 引数の順が変なのは実装上を手抜きした都合です…

### 3. ジャッジシステムへのデプロイ関連コマンドの追加

Rime+ には、AtCoder 等へのデプロイの手間を削減するため、`rime pack`, `rime upload`, `rime submit`という3つのコマンドが用意されています。それぞれ、ジャッジシステム用にファイルを詰める、ジャッジデータをアップロードする、ジャッジ解を投げて正常にアップロードされているか確かめるコマンドです。

`rime pack` に関しては、`use_plugin('judge_system/atcoder')` の代わりに `use_plugin('judge_system/aoj')` や  `use_plugin('judge_system/mjudge')` とすることで pack 方法を AOJ や M-Judge ように変更することもできます。ただし、AOJ は[田山さん](http://qiita.com/tayama0324)の書いた古い資料を元にしたことや、M-Judge はそもそも現存しないことから正しいものを出力するかは謎です。

`rime upload`は、`--upload`を引数に指定しないと dry-run モードになるという、事故防止機能がついています。なお、実際のアップロードには AtCoder の中の人によるごにょごにょが必要であり、Rime+ 単体では何もできないのでご了承ください。

### 4. 現代的における多様な出題方式への対応

Rime+ では、現代における多様化した出題形式への対応を強化しています。なお、実験的な機能が多いので過信はしないことをおすすめします。

#### 4.1 カスタムジャッジ

従来から Rime には、数値計算問題や復元問題など単純な diff では比較できない問題に対応するため、カスタムジャッジを指定する機能があります。しかし、カスタムジャッジの入出力プロトコルが Rime 独自の謎仕様で、デファクトスタンダードとなった testlib.h と互換性がないという問題があり、某OB/OGの会などでは京都で作られた改造版 testlib.h (の子孫)が使われるという状況にありました。

この問題を解決するため Rime+ では、variant 指定が追加されました。"TESTSET" に以下のように書くと、"testlib.h" 形式のカスタムジャッジが直接使用できるようになります。

```python:TESTSET
cxx_judge(src='judge.cc', dependency=['testlib.h'], variant=testlib_judge_runner)
#cxx_judge(src='judge.cc', variant=rime_judge_runner) # default
```

現行の Rime+ では、途中で力尽きたので実装されていませんが、ICPC国内予選システムのように使用する終了コードが異なるジャッジシステム、あるいは、AOJのように終了コードではなく標準出力の有無を見るジャッジシステムなどに対応するための Runner を追加することも容易です。

#### 4.2 リアクティブ

現代ではリアクティブ問題も広く見られるようになりました。Rime+ では、"TESTSET"に以下のように書くことでKUPC形式のリアクティブジャッジを使用することができます。

```python:TESTSET
cxx_reactive(src='reactive.cc', dependency=['testlib.h', 'reactive.hpp'], variant=kupc_reactive_runner)
```

その他にもリアクティブジャッジには、testlib 形式、NEERC 形式などが存在するようですが、途中で力尽きたので実装されていません。

#### 4.3 サブタスク

現代では一部の簡単なテストケースに正解すると部分点を与える問題も見られます。Rime+ では、"TESTSET"に以下のように書くことで`rime test`コマンド使用時にサブタスクの採点を有効化することができます。

```python:TESTSET
subtask_testset(name='All', score=100, input_patterns=['*'])
```

さらに、"SOLUTION" に以下のように書くことで、想定部分点解を検証することもできます。

```python:SOLUTION
expected_score(100)
```

なお、`-k`もしくは`--keep-going`をつけなかった場合、正しく点数がつかないのでご注意ください。

#### 4.4 採点機能付きカスタムジャッジ

現代ではカスタムジャッジによる採点機能を利用して部分点をつける問題も一部見られます。[^3] Rime+ では、"TESTSET" に以下のように書くことでカスタムジャッジの出力した部分点情報を読み取るようになります。

```python:TESTSET
scoring_judge()
```

そういうふうに実装したつもりなのですけど、手元に資料が少なく、AtCoder の出力する点数とイマイチ合っていないなど、問題点が多く残っています。あなたのバグフィックスに期待しています。

[^3]: UTPC の Uinny, KUPC の自由研究など

#### 4.5 複数テストケース

手元実行時代の遺物のような複数テストケースですが、現代においても ICPC, GCJ など幾つかのコンテストでしぶとく生き残っています。Rime+ では、Rime の merged_test プラグイン同様、以下のように書くことで複数テストケースの入力ファイルを生成することができます。

```python:TESTSET
merged_testset(name=id + '_Merged', input_pattern='*.in')
```

ただし、Rime+ では以下のように merge 方式を定義する merger も指定する必要があります。

```python:TESTSET
icpc_merger(input_terminator='0 0\n')
```

これは、以下のように `output_replace` に `casenum_replace` を指定することで2011年までのWFのようにケース番号を出力する形式にも対応しできます。

```python:TESTSET
icpc_merger(input_terminator='0 0\n', output_replace=casenum_replace('Case 1', 'Case {{0}}'))
```

また、Rime+ では、GCJのように入力の最初にテストケース数がくる形式も対応しています。

```python:TESTSET
gcj_merger(output_replace=casenum_replace('Case 1', 'Case {{0}}'))
```

ところで従来の Rime の merged_test では、merge 前に入力検証とジャッジ解でのテストをするため、merge 後のファイルのフォーマットがあっているか検証されないという問題や、ジャッジ解を標準入力の終了と入力終端記号の両方で終了するように作る必要があり一貫性にリスクを抱えているという問題などがありました。Rime+ では、この問題に対処するため、すべて merge 後形式になるよう前処理してから、入力検証やジャッジ解でのテストをするようにしました。

例えば、GCJ形式のa+b問題があったとしましょう。まず、入力生成器が以下のように1ケースのみを含む入力ファイル"1.in"を生成します。

```text:1.in
1 1
```

merger はまずこのファイルを"1.in_orig"にリネームします。

```text:1.in_orig
1 1
```

ここから、前処理をした"1.in"を生成します。1ケースだけなので最初に1がつきます。

```text:1.in
1
1 1
```

この前処理済み入力ファイルを用いて入力検証やジャッジ解によるテストが実行されます。

また、同様に Merged の方も".in_orig"を用いて生成されます。"1.in"~"9.in"があったとすると以下のようになります。

```text:Merged.in
9
1 1
 :
9 9
```

### 5. その他細かい改良

その他にも Rime にあった問題の解決などで細かい改良があります。

#### 5.1 テスト結果のキャッシュ

Rime のヘルプでは "-C" をつけるとキャッシュされると表示されるのですが、未実装でした。Rime+ ではテスト結果のキャッシュが行われます。`rime wikify_full` などのユーザーにおすすめです。ソースコード等に更新があった際には(時々とちるようですが)無効化も行われます。

#### 5.2 -O2, -std=c++11 のデフォルト化

議論はいろいろあると思いますが、C/C++ではデフォルト引数が "-O2", 加えて C++ では "-std=c++11"がついています。現代なら14にした方がいいのでしょうか？

あと地味ですが、ついでに ".cpp" も C++ のファイルとして認識するようにしました。

#### 5.3 追加の言語対応

[KLab さんのレポジトリ](https://github.com/KLab/rime)から持ってきただけですが、JavaScript (node.js), C# (mono), Haskell (stack + ghc) に追加で対応しています。

また、最近の Rime では "env" を使った interpreter 指定ができなくなっていましたが、それも対応しています。

#### 5.4 並列テストの高速化

Rime では、並列テスト時に TLE になった場合、他のタスクを止めて排他的にした上で念のためもう1回走らせるという仕様になっていましたが、`rime wikify_full` のときに非常に遅いという問題がありました。 Rime+ では、全部並列のまま実行するように変更しています。正確な測定が必要なときは `-p` または `--precise` を使ってください。

ついでに Rime では不正確という理由で隠されていた並列実行時の実行時間も表示するように変更しています。

#### 5.5 共有ライブラリ

"PROJECT"ファイルないで`library_dir`に指定したディレクトリ(テンプレでは"common")においたファイルを、`dependency`で指定することで`rime build`で一緒にコンパイルしたり、`rime pack`で一緒にコピーしたりできるようにしました。"testlib.h"や"reactive.hpp"などに使用すると便利です。

#### 5.6 サマリの強化

某OB/OG の会内部版の wikify_full では、様々な情報を見ることができました。それを `rime build` や `rime test` のサマリでも表示するようにしました。ただし、デザインがあまり綺麗じゃなくなってしまったので、少し残念です。

#### 5.7 --keep-going でのエラーメッセージの改善

Rime では "-k" もしくは "--keep-going" を使ったとき、2個目以降のエラーメッセージが最初のエラーメッセージで上書きされ、最初のエラーと個数しか情報量がなくなる問題がありました。Rime+ では2個目以降も本来のエラーメッセージが表示されます。

#### 5.8 入力検証器の検証

Rime では、間違った入力を入力検証器で弾いているかをテストできていませんでした。Rime+ では正しくない入力を ".invalid" という拡張子のファイルで追加することで入力検証器をテストすることができます。

#### 5.9 パターンによる撃墜ケース指定

Rime では撃墜ケースを1個1個指定する必要がありましたが、Rime+ ではパターンで指定できるようになりました。それにともなって、Rime では撃墜ケース全部で撃墜できなければテスト失敗になりましたが、Rime+ では1個でも撃墜できたケースがあればテスト成功になるように変更してあります。

### 0. Rime との互換性

Rime+ には、オリジナル Rime とは挙動が異なる点が多く含まれていますが、オリジナルの Rime と同じ動作をさせることもできます。これは Rime+ はほぼすべて Rime プラグインとして作られているためです。PROJECT ファイルに `use_plugin('rime_plus')` と書くのをやめるだけでオリジナル Rime として使用することが可能になります。 

## つけようと思いつつついていない機能

- 設定ファイルのユーザーテンプレート機能
- 解答・入力生成器・入力検証器のテンプレ作成
- rime 用ファイルレポジトリ
- 問題名変更などリファクタリング機能
- 処理系のユーザ指定
- 問題文作成機能との連携

## 利用例

Rime+ は某OB/OGの会を含めてあまり使われていませんが、[blue jam さんのプロジェクト](https://github.com/blue-jam/ICPC2016JDCAutoJudge)などで使われています。

## 未来の開発者へのお願い

この Rime+ ですが、今は開発していません。理由としては、以下があります。

1. 大規模改修が必要で現行バージョンに手を入れることがためらわれる
	1. 開発時とは作問事情が大きく変わっていていろいろな機能を追加する必要がある
	2. アーキテクチャが複雑すぎる、特にプラグイン機能
	3. Python 3 への移行とかも
2. (動的型付け言語とかもう使いたくない。ぼそっ

では、なぜこの記事を書いたかというと、以下の理由からです。

1. 生 Rime より Rime+ を使ってほしい。
2. オレオレ作問ツールを作る前に作問ツールにどのような要件が求められているか理解してほしい。

Rime にも Rime+ にもいろいろ不満はあると思いますし、新しいものを作っていくことも大事ですが、Rime の資産をきちんと踏まえた上で新しい作問時代を作っていただけると嬉しいです。