コンテンツにスキップ

検証と配布

ゲートが、若草の約束そのものです。 このツアーのほかの部分では、そのゲートを通るものをどう書くかを説明しています。

ウィンドウなしの実行

PIXIE_SCRIPT が人の操作の代わりになります。 エンジンが木を組み、そこに書かれた手順のとおりに動かして、求められたものを出力します。

手順 すること
click:<label> 表示されている文字でボタンを押す
input:<text> 欄に打つ
submit その欄で改行する
slide:<n> つまみを動かす
select:<option> 選択肢を選ぶ
click@1:<label> 同じ文字のボタンの二つ目(n はツリー順に 0 から数える)、input@n:submit@nslide@n:select@n: も同じ
key:<chord> shortcut に結ばれた打鍵
keydown:<key> / keyup:<key> キーを押したままにする、離す
menu:<item> メニュー項目を選ぶ
file:<path> ファイルダイアログの答え
drop:<path> ウィンドウにファイルを落とす
hover:<i> / hover: チャートの i 番目の点にポインタを載せる(n 番目のチャートなら hover@n:<i>)、外す
advance:<ms> 時計を進める
theme:dark / theme:light 配色を切り替える
dump 要素のツリーを出力する
a11y 画面読み上げに渡るものを出力する
mem エンジンが持っているオブジェクトの数を出力する

同じ説明にあてはまる要素が複数あるときは、@n でどれかを選びます。 click@1:donedone と書かれた二つめのボタンを押し、input@0: は最初の欄に打ちます。 手順はカンマで区切り、手順の文字そのものに含まれるカンマは \, と書きます。

なかでもよく使うのが dump です。 要素のツリーを文字にして出力します。 実行が最初と最後に出力するのと同じバイト列なので、途中で dump するスクリプトは、実行の終わりだけでなく途中も比べていることになります。

ゲート

wakakusa gate は、一つのスクリプトでアプリを二回走らせます。 片方は CRuby が C ABI 越しに走らせるもので、もう片方は同じ ABI をリンクしたコンパイル済みのバイナリです。 最後に、二つの記録を一バイトずつ比べます。

$ wakakusa gate demo/counter.rb --script "click:+1,dump,input:Momo"
GATE OK — 3 dump lines identical in both runs
  script:   click:+1,dump,input:Momo
  emitted:  demo/.gate/counter.c
  binary:   demo/.gate/counter (16.2 MB)

食い違ったときは、その場所を一行ずつ示します。

GATE FAILED — the two runs diverge:
  cruby:    Text("total: 3")
  compiled: Text("total: 0")

ファイルやデータベースを残すアプリは、どちらの実行も同じ「何もない」状態から始めなければなりません。 そのためにあるのが --fresh です。 実行のたびに、指定した場所を消します。 --fresh は何度でも指定できます。

$ wakakusa gate demo/ledger.rb --fresh demo/.gate/ledger.db \
    --script "click:reset,input@0:o'brien,input@1:250,click:food,dump"

wakakusa check は、ゲートの前にもビルドの前にも毎回走ります。 だから、コンパイラが起動するより先にエラーが出ます。 その一覧が若草が断る書き方です。

tools/gate_all.sh が全デモのゲートです。 すべてのデモを通し、続けて二つの言語のツアーとこのサイトにある完全な例をすべて通します。 要素の表やドア、エンジンに手を入れたときは、これを通す必要があります。

ゲートが証明するもの、しないもの

ゲートが証明するのは、渡した手順のもとで、あなたのアプリの二つの実行が同じ画面を描くことです。 その手順がアプリを網羅していることは証明しません。 誰も書かなかった操作は、誰も比べなかった操作です。

証明しなくてよいことが一つあります。 インタプリタ側の実行が Ruby として正しいかどうかです。 その実行は CRuby そのものだからです。 だからゲートが通れば、コンパイルされた実行、つまりもう一つの Ruby の実装が、スクリプトが通った範囲では本物のインタプリタと一致したということです。 食い違ったときに正しいのは、定義上 CRuby の側です。 二つが同じにならない数少ない点は、二つの実行に挙げてあります。

リリース

$ wakakusa build demo/todo.rb --release --app
built: demo/.gate/todo (12.3 MB)
bundle: demo/dist/todo.app (12.5 MB)
  not gate-checked — `gate` with a script proves the two runs agree

--release は symbol table を落とします。 これがファイルの五分の一を占めています。 --app はバイナリを dist/ の下の macOS アプリケーションバンドルに包み、ad-hoc 署名をつけます。 アプリと同じ場所に <stem>.icns<stem>.png を置いておくと、それがアイコンになります。

バイナリはエンジンとコンパイル済みの Ruby を含んでいて、システム自身のライブラリ以外は何もリンクしません。 だから、このバンドルだけでプログラムが完結します。 Ruby もコンパイラも入っていないマシンで、そのまま開きます。

wakakusa translate は、コンパイルされた実行のもとになる C を書き出します。 途中のどの段階でも呼べて、ゲートがバイナリの横に残すのと同じファイルです。

数字

macOS の arm64 で、共有のビルドディレクトリが温まった状態で測ったものです。

もの
wakakusa check 0.1 秒
画面を出力するウィンドウなしの実行 0.7 秒
コンパイラが書き出す C 10 ミリ秒未満、120 KB ほど
コンパイルされた実行の cc リンク 0.29 秒
コンパイルしたバイナリ 16.2 MB
リリースしたバイナリ(--release 12.3 MB
アプリケーションバンドル(--app 12.5 MB
起動してウィンドウが出るまで 0.3 秒未満
ゲート一往復(エンジンはビルド済み) 2.1 秒

まだできないこと

  • 種を与えた Random は、二つの実行で同じ生成器になりません。 どちらの実行でも同じ数列がほしいなら、生成器を自分で書きます。 二つのゲームがそうしていて、算術だけの六行で足ります。
  • 決まった形で書かなければならないところが三つあります。 ほかの書き方をコンパイラがまだ受け取れないためです。 状態はグローバル変数ではなくアプリのオブジェクトに持たせること、ハンドラはリテラルのブロックか、引数を取らない proc をキーワード引数で渡すこと、配列は list + [item] ではなくコピーしてから足すことです。 どれも、何がだめかと書き直し方がエラーに出ます(若草が断る書き方)。
  • check は、コンパイラが取りこぼすものをすべて見ているわけではありません。 見つけるのはその三つとほかの五つで、残りを見つけるのは今もゲートです。
  • アプリが自分で立てたスレッドは、どちらの実行でも走ります。 ただし、ある時点までにどこまで進んでいるかは一致しません。 task があるのはそのためです。
  • macOS と Linux です。 --app は macOS の形なので、そこ以外では名前を挙げて止まります。 build が書くネイティブバイナリはどちらでも動きます。 バイナリが描画に使うエンジンを含むので、小さなアプリでも 12 MB ほどになります。