二つの実行
アプリは 1 ファイルです。 それを二通りに走らせ、その二つが同じプログラムであることが、Rakugan の狙いのすべてです。 このページは、それぞれの実行が何でできていて、二つが何を共有していて、同じでない数少ない場所がどこかの話です。
二つのうち Perl なのは片方だけです。 書いているあいだ、ファイルを動かすのは perl そのものです。 配るときは別の言語に翻訳してコンパイルするので、バイナリの中に perl はありません。 二つの実行の違いは、すべてこの非対称性から来ます。 方言が言語全体ではなく部分集合なのも、そのためです。
二つの言語と一つのファイル
Rakugan は独自の言語を定義していません。 書けるのは Perl で、それが一度狭められ、そこにライブラリが足されています。
| 受け取る範囲 | |
|---|---|
| perl | Perl のすべて。書いているあいだアプリを動かすのはこれで、ファイルを先に読むのもこれです。perl が撥ねた書き方が翻訳器に届くことはありません。 |
| Rakugan | 翻訳器がコンパイルする実行まで運べる範囲。名前ごとに一つの型、Bool である条件、既定値つきのハッシュの読み、書き出されたパターン。eval も local も tie も ref もありません。それ以外は、何かを組み立てる前に rakugan check が断ります(Rakugan が断る書き方)。 |
この引き算の上に Rakugan が足すものは一つで、それは言語ではなくライブラリです。
画面を組み立てるサブルーチンと、run、every、task、sqlite_exec とその仲間です。
クラスもメソッドも正規表現も Perl 自身の関数も、Perl のものです。
書いているあいだに走るもの
解釈実行はこれだけです。 perl がそのファイルを読みます。 読み込み経路に置くのは、Rakugan の Perl 側と XS モジュールです。 XS モジュールはエンジンの C API を開いて、その関数を宣言します。 XS モジュールより上には、どちらの実行なのかを知っている場所はありません。
ここでのアプリは本物の Perl で、動かしているのは本物のインタプリタです。 クラスもサブルーチンも正規表現もごみ集めも、perl のものです。
配るときに走るもの
$ rakugan translate demo/counter.pl
emitted: demo/.gate/counter/src/main.pix
$ cd demo/.gate/counter && pixie build
翻訳器は PPI で Perl を読み、pixie を書きます。
pixie は、羊羹が Python から出すのと同じ、検査を受ける中間のソースです。
それをエンジンと一緒にコンパイルして 1 本のネイティブバイナリにします。
rakugan translate は最初の段階で止まり、.pix を書き出します。
中身は普通に読めるファイルです。
コンパイルしたバイナリはエンジンと翻訳したアプリを含み、システム自身のライブラリ以外は何もリンクしません。
二つが共に動かすもの
エンジンは一つで、C API の向こうにあります。
crates/pixie-capi(pixie のカーネルと、描画を受け持つ gpui)が、どちらの実行でも同じコードです。
解釈実行は XS モジュール越しに開き、コンパイルした実行はリンクします。
この API はわざと狭くしてあります。 要素は開いて、番号で書き込んで、閉じます。 ハンドラは XS モジュールが配る番号で、文字列のリストは文字列のリストとして渡ります。 エンジンは Perl の値を持ちません。 一つの実装が、相手がどちらなのかを知らないまま、インタプリタにもコンパイル済みバイナリにも応えられるのはそのためです。
その番号も手では書きません。
elements.toml が唯一の表で、すべての要素、そのキーワード、型、既定値が入っています。
tools/gen.pl がそこから、アプリが呼ぶ Perl のサブルーチンと、Perl のデータとしての表を書き出します。
どちらかが表より古ければ tools/gate_all.sh が落ちます。
だから、ある要素が Perl では一つの意味を持ち、描かれる側では別の意味を持つ、ということが起きません。
このエンジンの上のほかの三つの言語も、同じ表を読んでいます。
二つが同じでない場所
仕様は perl です。 二つが食い違ったら、正しいのは解釈実行で、翻訳のほうに誤りがあります。 違いは三種類あり、今どれを見ているのかを知っておくと役に立ちます。 三つとも、それを捕まえるものが違うからです。
1. 走る前に断られるもの
差の大半は、この引き算があらかじめ塞いでいます。 翻訳は通るのに振る舞いが変わる書き方は、その行と書き換え方を添えて断られます。 何かを組み立てる前、コンパイラを起動する前です。 一覧はRakugan が断る書き方にあり、文面を保持しているファイルから引いています。
そのうち二つはここで名前を挙げます。
Perl を書く人が、考えずに書いてしまう書き方だからです。
条件は Bool なので、if ($n) は if ($n != 0) と書きます。
keys %h は sort keys %h と書きます。
perl のハッシュの順序は perl を起動するたびに変わり、画面がそれに依存するわけにはいかないからです。
2. 答えが違うもの
Perl 自身の関数は、片方の実行では perl のもの、もう片方では Rust で書いた双子です。 コンパイルした実行に perl が入っていないからです。 perl と食い違う双子は二つの実行の違いになるので、ゲートが捕まえます。 ただし捕まえるのは、スクリプトが実際に触れたところだけです。 デモが触れるのは数十回の呼び出しであって、ライブラリ全体ではありません。
そこで、双子は perl に直接照らします。
Perl の名前を持つ関数はすべて、perl 自身が出力した表で測ります。
crates/rakugan-stdlib/tests/expected/ に、sprintf、文字列の関数、数の関数、List::Util、POSIX、正規表現にわたる 1000 行あまりが入っています。
cargo test -p rakugan-stdlib が、それを 1 件ずつ突き合わせます。
表は、その事例を perl に通し直して作ります。
だから perl が新しくなったときは、その差分が知らせになります。
正規表現に双子はありません。 パターンは翻訳のときにコンパイルされ、二つの実行は一つのエンジンで一致を取ります。 アプリが動いている最中に組み立てたパターンを断るのも、これが理由です。
フレームワーク自身のライブラリにも双子はありません。
fs_*、sqlite_*、http_*、jsondoc_*、クリップボードなどは、Rust の一つの実装です。
コンパイルした実行はそれをリンクし、解釈実行はエンジンの C API を通して同じコードに触ります。
そこでゲートが突き合わせているのは、一つのライブラリが二度答えたものです。
3. 同じプログラムで、速さが違うもの
コンパイルした実行のほうが速く走ります。
書いているあいだ遅く感じるだけの長い計算は、振る舞いの違いではありません。
すぐに戻らないハンドラは、どちらの実行でもウィンドウを固めます。
task があるのはそのためです。
時計
二つの実行は、一つの時計で動きます。
every で宣言したタイマーは、ウィンドウでは 1 フレーム、スクリプトでは advance:<ms> の 1 手順で発火します。
だから、同じ数の刻みが両方に届きます。
マシンの時計を読んでそのまま描くアプリを書くわけにはいきません。
それはゲートが捕まえる違いであり、clock_format_ms が時刻を自分で取らずミリ秒を受け取るのもそのためです。
そのうえで確かめるもの
ゲートです。 一つのスクリプトで二つの実行を動かし、1 バイトずつ比べます。
$ ./bin/rakugan gate demo/counter.pl --script "click:+1,dump"
GATE OK — 3 dump lines identical in both runs
緑のゲートが言うのは、スクリプトが触れたすべてについて二つの実行が一致した、ということです。
スクリプトが届かなかったところについては何も言いません。
まして、フレームワーク自身のライブラリについては何も言いません。
そこは一つの実装が両方に答えるので、その実装にある誤りは両方の誤りになります。
残りの二つの確かめがあるのはそのためです。
perl が出力した表がすべての双子を測り、tools/gate_all.sh が 41 本のデモをすべてゲートに通します。
三つ合わせて言えることが、Rakugan の主張です。
配るバイナリは、書いているあいだに動かした perl と同じように振る舞います。
そして名前が Perl のものであるところでは、どちらも perl のように振る舞います。