仕様をテストスイートで定義する — 「仕様とは roast を通ることである」

Perl 6 は Apocalypse・Exegesis・Synopsis という 3 層の設計文書体系を持っていた。そして最終的に、仕様の正をテストスイート roast へ移した。散文の仕様には解釈が割れる・実装と乖離する・準拠を検査できないという 3 つの問題がある。テストはそれを解く。しかし代わりに手放したものがある — 散文は全部について曖昧に語り、テストは一部について厳密に語る。

perlrakuroastspecificationtestinglanguage-designprogramming-languages

前回、自分の言語の話でこう書いて終わった。

オラクル自身が間違っていたら、5 本揃って間違う。

今回はその問いを、Perl 6 がどう扱ったかの話である。 何をもって「Perl 6 として正しい」と言うのか。

3 層の設計文書

第 3 話で、361 本の RFC を Larry Wall が消化し直したところまで書いた。 その出力が Apocalypse である。そして実際には、文書は 3 種類あった。

文書 書き手 役割 比喩
Apocalypse(黙示録) Larry Wall 設計判断とその理由。なぜそうするか 啓示
Exegesis(釈義) Damian Conway Apocalypse を例で解説する 注解
Synopsis(概要) 複数 仕様として使える要約。実装が参照する 要約

命名はいずれも聖書由来で、Perl 文化らしい言葉遊びになっている。

Apocalypse の番号は 『Programming Perl』(ラクダ本)の章番号に対応している。 「ラクダ本の第 N 章にあたる部分を、Perl 6 ではどうするか」という構成だ。

番号 主題
A1 The Ugly, the Bad, and the Good — 全体方針
A2 基本データ型、シジル、リテラル
A3 演算子
A4 制御構造
A5 パターンマッチング — 正規表現の再設計。最も影響が大きかった 1 本
A6 サブルーチン — シグネチャ、多重ディスパッチ
A12 オブジェクト

全部は書かれていない。 番号が飛んでいるのはそのためだ。

Synopsis が実質の仕様になった

実装者が日々参照したのは、Apocalypse ではなく Synopsis(S01〜S32) だった。

理由ははっきりしている。Apocalypse は「なぜ」を語る散文で、長い。 仕様書として引くには向いていない。 一方 Synopsis は仕様として引ける形に整理されており、しかも更新され続けた。

番号 主題
S02 語彙・データ型・シジル
S03 演算子(メタ演算子を含む)
S05 正規表現と grammar
S06 サブルーチンと多重ディスパッチ
S12 オブジェクト
S14 Role と型
S17 並行性
S32 標準ライブラリ

ここまでが「散文の仕様」の時代である。

散文の仕様が持つ 3 つの問題

そして、この形には固有の問題があった。

1. 解釈が割れる

実装者ごとに読み方が違うと、実装同士が食い違う。 第 5 話で書いたとおり、この時期の Perl 6 には複数の実装があった。 Pugs があり、Parrot 上のものがあり、後に Niecza(.NET 上)も現れる。

同じ Synopsis を読んで、違う挙動を実装することが起きる。 どちらが正しいかを決める手続きが、散文には無い。

2. 実装と乖離する

文書の更新が止まっても、誰も気づかない。

これは設計文書一般の問題だ。文書は実行されない。 だから間違っていても、古くなっても、赤くならない。 第 5 話で書いた「検証されていない決定が、検証されないまま積み上がる」と同じ構造である。

3. 「準拠している」を検査できない

これが最も実務的な問題だ。

ある実装が「Perl 6 に準拠している」と主張したとき、 その主張を検証する手続きが無い。 散文を読んで、人が判断するしかない。

roast — 仕様をテストへ移す

そこで Perl 6 は、仕様の正を roast というテストスイートに移した。

Repository Of All Spec Tests。 起源は第 5 話で書いたとおり、Audrey Tang が Pugs で始めた 「実装するたびに公式のテストスイートにテストを書く」という運用である。

そして最終的に、こう宣言された。

仕様とは、roast を通ることである。

「6.c である」とは何を意味するか

第 8 話で扱う 2015 年の 6.c リリースは、roast の特定のコミットを凍結する という形で定義された。

つまり「6.c 準拠」とは「6.c 時点の roast を通る」という意味である。

これは驚くほど具体的だ。散文の解釈問題が消える。

  • 実装が「6.c 準拠」と主張するとき、その主張は機械的に検証できる
  • 新しい版は、新しい roast の凍結点として定義される
  • 古い版の挙動は、古い roast が保存されている限り再現できる

そして第 12 話で扱う「6.d が既定、6.e が策定中」という状態も、 roast の凍結点が 2 つあり、3 つ目を作っているという意味になる。

何を得て、何を手放したか

得たもの 手放したもの
準拠が機械的に検査できる テストに書かれていない挙動は未規定のまま残る
実装と仕様が乖離しない 「なぜそうなのか」がテストからは読めない
版を凍結できる(6.c / 6.d) テストが暗黙に実装の都合を固定してしまう危険

右列の 1 行目が重要だ。ここに、この連載の 8 つ目の型がある。

散文は全部について曖昧に語り、テストは一部について厳密に語る。

散文の仕様は、言語の全体について何かを言う。ただし曖昧に。 テストの仕様は、書かれた場所については厳密に決める。ただし、書かれていない場所には何も言わない。

これは優劣ではなく、沈黙の形が違うという話だ。

  • 散文は「ここは曖昧だ」と分かる形で沈黙する
  • テストは「ここはテストが無い」と気づかれない形で沈黙する

だから roast に移した後も、Raku は散文を捨ててはいない。 Synopsis は歴史文書として残り、言語ドキュメント(docs.raku.org)が実用上の説明を担っている。 正が roast に移った、という関係である。

他の言語はどうしているか

この判断は Perl 6 だけのものではない。

言語 仕様の正
Raku roast(テストスイート)
ECMAScript ISO/ECMA の散文仕様 + test262(共通テスト)
Ruby CRuby の実装 + ruby/spec
C / C++ ISO 規格(散文)
Go 言語仕様(散文)+ 単一の参照実装

ECMAScript が最も近い。 散文仕様と test262 の両方を持ち、 「準拠」は test262 で測られる。

Raku が特異なのは、長く実装が 1 つしかないのに、仕様を実装から分離し続けたことだ。 第 6 話で書いたとおり、Niecza が止まってからの Raku の実用実装は Rakudo だけだった。

普通、実装が 1 つなら「実装が仕様」で済む。Ruby がそうだ(CRuby が事実上の仕様)。 それでも Raku が roast を維持したのは、複数実装があった時代の遺産であり、 同時に単一実装になった後でこそ効く防波堤でもある。

そしてこの節の最後で書くとおり、2026 年にその遺産が効いた。

実装が 1 つのとき、実装のバグは「言語の仕様」として定着しうる。 roast があると、「それはテストに書かれているか」を問える。

前回の宿題に戻る

第 6 話をこう終えた。

オラクル自身が間違っていたら、5 本揃って間違う。

roast はこの問題を半分だけ解く。

解ける部分: 仕様が実装から独立した場所にある。 Rakudo がどう動くかと、Raku がどう動くべきかが、別のリポジトリに分かれている。 実装のバグが自動的に仕様になることはない。

解けない部分: roast に書かれていないことについては、何も言えない。 私の parity 仕掛けで言えば、インタプリタが間違っていて、 かつその挙動を確かめるテストを誰も書いていない領域は、依然として無防備である。

そして Perl 6 自身、この穴を別の方法で埋めていた。 複数の実装があったことだ。 Pugs と Parrot 系と Niecza が同じ roast を通る、 という状況では、片方にしかない挙動は「仕様の穴か、実装のバグか」のどちらかだと分かる。

2 つ目の実装は、1 つ目の実装が気づけない種類のバグを出す。

第 6 話で「2 人目の利用者が来なかった」と書いた。 仕様の検証という観点では、2 つ目の実装が去ったことにも同じ損失がある。

Niecza が止まってから、Raku は 10 年以上、単一実装で roast を維持してきた。 合理的だが、roast がどれだけ仕様を捉えているかを検証する手段は 1 つ減っていた。

そして 2026 年、2 つ目が戻ってきた

この原稿を書いている 2026 年、Rakudo 以外の実装が 2 つ動いている。

実装 言語 方式 roast(1,464 ファイル中、完全通過)
mutsu Rust バイトコード VM 1,433(97.9%)
Raku++(rakupp) C++17(外部依存なし) 手書きの字句・構文解析 + 木渡り評価器。ネイティブバイナリへのコンパイルも 676(46%)

どちらも roast を分母にして自分を測っている。 「Raku に準拠している」が主張ではなく数字になるのは、 仕様をテストスイートに移したからである。この回の主題が、そのまま効いている。

⚠️ 数字は 2026 年 9 月 18 日に確認したもの。両プロジェクトとも日々動いており、 mutsu の README は「適合度は毎日良くなっている」と明記している。引用するなら測り直すこと。

つまり、この節の懸念は部分的に解消された。 roast がどれだけ仕様を捉えているかを問い直す手段が、10 年ぶりに戻ってきたことになる。

2 つ目の実装は、1 つ目の実装が気づけない種類のバグを出す。

この型が、いま再び使える状態にある。


次回(第 8 話): Christmas。 2015 年 12 月 25 日、発表から 15 年後に最初の安定版が出る。 15 年とは何だったのかを、ここで一度まとめる。

← Back to Perl と Raku の系譜