コンテンツにスキップ

付録 a3 道具(実 Rigor の CLI と dispatch カスケード)

これは参照付録です。本書(chibirigor)は「動く最小版」を手で作るのが主目的なので、本編の各所では実物のRigorのツール挙動を1行ポインタで送りました。その送り先がここです。chibirigorの最小版と実Rigorの差を「本書では素朴/実物はこう」という橋渡しで並べます。

ここに書くRigorの事実記述は本編の原稿と一致させています(5段の順序、名称は原稿どおりです)。本書のコード挙動を変える記述ではありません。


a3-1. rigor check --explain(fail-softの地図を出す)

Section titled “a3-1. rigor check --explain(fail-softの地図を出す)”

通常のrigor checkは、証明できた問題だけを診断として報告し、Dynamic[Top](本文の最小版Dynamicの内部表記=untyped。付録a1-1参照)にfail-softした箇所については黙っています(前編Part 2「知らなければ黙る」、Part 9「わざと見逃す」の実物です)。これは誤検知を出さないための態度ですが、裏を返せば「静かに見逃している」でもあります。

--explainを付けると、そのfail-softした全箇所が:info診断として現れます。「ここで型を見失いました」という地図が出力されます。

Terminal window
$ rigor check --explain app/models/user.rb
app/models/user.rb:42:7: info: fell soft to Dynamic[Top] here (RBS not found for `external_call`)
app/models/user.rb:51:3: info: fell soft to Dynamic[Top] here (param `opts` is untyped)

使い道はこうです:

  • 「このバグを見落としているのでは?」という疑問が出たとき、--explainの出力で「どこで型が消えたか」を遡る。
  • たどり着いたfail-soft地点から、RBSの不足、プラグイン未設定、型注釈の抜け漏れを発見できる。

なぜ「地図」が描けるのか(Dynamic[Top]マーカーの回収)

Section titled “なぜ「地図」が描けるのか(Dynamic[Top]マーカーの回収)”

この一覧が成り立つのは、前編Part 1で触れたDynamic[Top]Dynamicマーカーが、fail-softした箇所に消えずに残っているからです。untypedを「ただの穴」ではなく「Dynamicという印の付いたTop」として持つことで、「どこで黙ったか」が構造として残ります。だからこそ、後から--explainで一覧に起こせます。

本書(chibirigor)実物(Rigor)
untypedの正体Type::Dynamic(印だけ)Dynamic[Top]TopDynamicマーカー)
fail-soft地点黙ってDynamicを返すだけ地点を構造に保持し、--explainで一覧化
沈黙の可視化仕組みなし(最小版の対象外)check --explain:info診断で地図を出す

chibirigorの「知らなければ黙る」は誤検知を防ぎますが、--explainその沈黙そのものを可視化する道具です。

a3-1x. 発展:chibirigorにも極小--explainがある

Section titled “a3-1x. 発展:chibirigorにも極小--explainがある”

上の表は本書を「沈黙の可視化=仕組みなし」としていますが、ここにも極小版を足しました。check --explainを付けると、推論がuntypedに倒した地点(未知メソッドのディスパッチ)を:info診断として併せて出します:

Terminal window
$ printf 'x = mystery_call\ny = x + 2\n' > demo.rb
$ ruby exe/chibirigor check --explain demo.rb
demo.rb:1:5: info: fell to untyped here (can't look up the type of `mystery_call`)
x = mystery_call
^^^^^^^^^^^^
demo.rb:2:5: info: fell to untyped here (can't look up the type of `+`)
y = x + 2
^^^^^

注目したいのは2行目です。mystery_callの型がわからずxuntypedになり、そのxに対する+も型を引けずにuntypedへ倒れています。沈黙が伝播していく様子が地図に出ます。--explain無しなら(誤検知を出さないので)No type errorsと黙るだけです。:infoは終了コードを汚さない(exit 0)ので、CIを止めずに「どこで型が消えたか」だけ覗けます。

実物との差は、実RigorがDynamic[Top]Dynamicマーカーを構造に保持してあらゆるfail-soft(RBS不足、未注釈引数、プラグイン未設定など)を一覧化するのに対し、chibirigorが拾うのは未知ディスパッチ1種だけ、という点です(実装はlib/chibirigor/dispatch.rbsignatureがnilの枝でprovenanceを1行積むだけです)。沈黙を地図にするという発想は同じです。


a3-2. Rigorの型表示(内部の精密型とRBS境界の保守型)

Section titled “a3-2. Rigorの型表示(内部の精密型とRBS境界の保守型)”

本書のannotateは、推論した型を1つそのまま見せます。実Rigorもannotate(本書と同じ名前の道具です)やエディタのhoverで型を見せますが、実物には本書に無い二段構えがあります:

  1. Rigor内部での精密な型(例:Constant<"FOO">)。
  2. RBSの境界で粗い型に落とした後の保守的な型(例:String)。

chibirigorのannotateは内部型だけを見せます。実物は「内部では精密に知っているが、境界では捨てる」という二重構造を持つので、「推論はもっと知っていそうなのに、シグネチャ(RBS)はなぜこんなに粗いのか」という状況が起こります。その答えが、この二段構えです。

本書(chibirigor)実物(Rigor)
型の見せ方annotateが行単位で内部型を並べるannotate/hoverで見せ、境界ではerasureで丸める
見せる型内部型1つ内部の精密な型、境界で丸めた保守的な型の二段
ズレの正体(境界が無いので起きない)内部精密型とRBS境界型の食い違い(erasure)

そしてchibirigorもdump_typeを基本機能として持っています。道具はcheckannotateの2つに絞ったままです。これはコマンドではなく、checkが認識するです。includeも要らず、dump_type(式)と書いてcheckするだけです:

Terminal window
$ printf 'x = c ? 1 : "a"\ndump_type(x)\n' > demo.rb
$ ruby exe/chibirigor check demo.rb
demo.rb:2:1: info: dump_type: 1 | "a"
dump_type(x)
^^^^^^^^^^^^

:infoなので診断は赤くならず(終了コードは0)、本物の型エラーと一緒に出しても邪魔をしません。仕組みは前編Part 9で作った:info診断そのままです。type_ofdump_type(式)を見つけたら、引数の推論型を:info診断に記録し(checkがそれを表に出します)、値はそのまま返します(だからdump_type(x)の型はxの型のままです)。実装はlib/chibirigor/type_of.rb、挙動の網羅はtest/test_dump_type.rbです。


a3-3. dispatch 5段カスケード(表引きの実物)

Section titled “a3-3. dispatch 5段カスケード(表引きの実物)”

前編Part 2は、メソッド送信の型付けを素朴な表引き一段に留めました((クラス,メソッド)METHODS表を引き、見つかれば戻り型、見つからなければuntyped)。実物のRigorは、この「表引き」を5段のカスケードにしています。上の段から順に当て、その段が当てられなければ次の段へ落ちます(fall through)。各段が何を解決し、外れたら何に渡すかを示します:

名前何を当てるか外れたら
定数畳み込み1 + 2のように両辺が既知の定数なら、その場で実際に計算して結果の型(3)を出す② へ
shape dispatchHashShapeのキー読み出しなど、型の構造に直接触れる操作を構造から直接解く③ へ
RBSコア、stdlib、プラグインが提供するRBSの型で引く(本書の手書きMETHODS表の実物)④ へ
in-source(本体推論)RBSに無いメソッドは、本体を推論して戻り型を合成する(前編Part 8の戻り型合成の実物)⑤ へ
fallbackどの段でも当たらなければDynamic[Top]にdegrade(脅かさない)(ここで止まる)

ひとつの呼び出しは、上から順に「この段で解けるか?」を問われ、解けた段で打ち止めになります。解けない段は黙って次に渡すだけです。誤検知を出さず、最後は必ずDynamic[Top]で受けるので、未知の呼び出しでも止まりません(前編Part 2「知らないメソッドは脅かさない」の実物です)。

receiver.method(args)
① 定数畳み込み ── 当たる ─→ 結果の型(例: 3)
│ 外れ
② shape dispatch ─ 当たる ─→ 構造から直接解いた型
│ 外れ
③ RBS ────────── 当たる ─→ RBS 由来の戻り型
│ 外れ
④ in-source ──── 当たる ─→ 本体推論で合成した戻り型
│ 外れ
⑤ fallback ───────────────→ Dynamic[Top](untyped)

優先順位が効く例(なぜ ③ が ④ に勝つか)

Section titled “優先順位が効く例(なぜ ③ が ④ に勝つか)”

カスケードは順序そのものが意味を持ちます。たとえばRBS::Extendedディレクティブで宣言した型が、メソッド本体の推論に勝つのは、③ RBSが ④ in-sourceより先に当たるからです。「宣言を本体に優先する」という設計判断が、段の並び順として表れています。

本書(chibirigor)実物(Rigor)
dispatchの段数1段(METHODS表を引くだけ)5段(① 定数畳み込み、② shape、③ RBS、④ in-source、⑤ fallback)
表の中身手書きのMETHODS Hash③ がRBS(コア、stdlib、プラグイン由来)
本体推論annotateで別途(前編Part 8)④ in-sourceとしてdispatchに組み込み
未知の扱いDynamicを返す⑤ fallbackでDynamic[Top]
宣言vs推論の優先(区別なし)段の順序(③ が ④ より先)で表現

a3-3b. rigor trace(推論の手順をコマ送りで見る)

Section titled “a3-3b. rigor trace(推論の手順をコマ送りで見る)”

ここまでの道具(--explain、型表示の二段構え、カスケード)は、推論の答え地図を見せるものでした。実Rigorにはもう一つ、推論の手順そのものを見せる道具があります。rigor traceです。checkが走らせるのと同じ推論をファイルに対して再走させ、記録した推論イベントを端末のコマ送りアニメーションとして再生します。1コマ=推論の1場面で、ローカルがscopeに入る瞬間(bind)、分岐の型が1つのunionに溶ける瞬間(union)、メソッド送信が解決する(またはDynamic[top]にfail-softする)瞬間(dispatch)を、評価される範囲をハイライトしながら見せます。

Terminal window
rigor trace lib/example.rb # キー押しでコマ送り
rigor trace --delay=0.5 lib/example.rb # 自動再生
rigor trace --format=json lib/example.rb # 生のイベント列

--verboseは式ごとのenter/resultまで全部出し、既定では上の3種の「教えどころ」だけに絞ります。JSONのイベント列は安定しているので、教材の図や講義資料の素材にできます。

a3-3bx. 発展:chibirigorにも極小traceがある

Section titled “a3-3bx. 発展:chibirigorにも極小traceがある”

これは付録の中でも珍しく、本書側にも実物とほぼ同じ道具がある節です(実装はlib/chibirigor/tracer.rb)。本書で各Partを写経して作った部品(scopeへの束縛、Type.unionの畳み込み、dispatchの表引き)が、動く順番で目の前を流れていきます。読者が「評価順はこうだろう」「ここでunionになるはず」と頭の中で組み立てた推論を、目で確かめるための学習用の道具です。

3行の例で動かしてみます。代入、三項演算子、メソッド呼び出しを1つずつ含みます:

Terminal window
$ printf 'x = 5\ny = x > 0 ? 1 : -1\nz = y + 2\n' > demo.rb
$ ruby exe/chibirigor trace demo.rb

端末では1コマずつEnterで送ります(qで終了)。全17コマのうち、要点のコマだけ抜き出すと:

chibirigor trace ─ step 2/17
────────────────────────────────────────────────────────────────
1 x = 5
2 y = x > 0 ? 1 : -1
3 z = y + 2
────────────────────────────────────────────────────────────────
type env : x: 5
evaluating : (top level)
► bind: x ← 5 (added to type env)
chibirigor trace ─ step 5/17
type env : x: 5
evaluating : if (incl. ternary) › call to >
► dispatch: 5.>(0) → untyped (not in table → fail-soft to untyped)
chibirigor trace ─ step 7/17
evaluating : if (incl. ternary)
► union: 1 , -1 → 1 | -1
chibirigor trace ─ step 9/17
type env : x: 5 y: 1 | -1
evaluating : (top level)
► bind: y ← 1 | -1 (added to type env)
chibirigor trace ─ step 12/17
evaluating : call to +
► dispatch: 1.+(2) → 3 (constant folding)
chibirigor trace ─ step 13/17
► dispatch: -1.+(2) → 1 (constant folding)
chibirigor trace ─ step 14/17
► union: 3 , 1 → 3 | 1
chibirigor trace ─ step 15/17
► dispatch: 1 | -1.+(2) → 3 | 1 (distribute Union to members)
chibirigor trace ─ step 17/17
type env : x: 5 y: 1 | -1 z: 3 | 1
evaluating : (top level)
► bind: z ← 3 | 1 (added to type env)
── playback done (17 steps total) ──

この17コマで、本編の部品が動く順に並びます。xの束縛(step 2)から、三項の条件x > 0表に無い>なのでfail-softでuntypedに倒れ(step 5)、2本のアーム1-1unionに溶けてyに束縛され(step 7、9)、y + 2y1 | -1なのでメンバへ分配して各メンバを定数畳み込みし(step 12、13)、結果をまたunionでまとめて(step 14、15)z3 | 1に束縛する(step 17)という流れです。各Partで別々に作った仕組みが、1つの式でどう連動するかが一望できます。--verboseを付ければ、間引いた式ごとのenter/resultも全部出ます。JSONで出したいときは--json、自動再生は--delay 0.5です。

仕組みは実物と同じ発想で、コアには一切手を入れていませんtype_ofeval_statementType.unionDispatch.dispatchModule#prependでフックを差し込むだけで、レコーダがnil(トレース中でない)ときフックは即superします。だからcheckannotateの挙動は変わらず、本編で写経したコードを1行も汚しませんtracer.rb冒頭コメント参照)。

本書(chibirigor)実物(Rigor)
見せるもの推論イベントのコマ送り(binduniondispatch同じ(推論のderivationを再生)
コアへの干渉Module#prependのフック、レコーダnilなら即supercheckと同じ推論に乗る記録から再生する探針
出力形式端末アニメーション、--json--verbose--delay端末アニメーション、--format=json--verbose--delay--line

実物が--line=Nで1行だけに絞れるのに対し、chibirigorは行フィルタを持たない、といった枝葉の差はありますが、推論の手順を再生して見せるという発想と3種のイベントは同じです。本書の他の道具が「Rigorの実物から極小版」だったのに対し、traceは珍しく本書側がほぼ実物と同型で並びます。


a3-4. まとめ(「素朴/実物」対応の早見)

Section titled “a3-4. まとめ(「素朴/実物」対応の早見)”

本付録で橋渡しした4つを一枚に:

仕組み本書での扱い実物の挙動戻りポインタ
rigor check --explain極小版あり(未知ディスパッチを:info地図化、§a3-1x)Dynamic[Top]マーカーを手がかりにfail-soft地点を:infoで地図化前編Part 9
型表示の二段構え(erasure)annotateは内部型1つ(境界が無いのでズレない、§a3-2)annotate/hoverの裏で内部精密型とRBS境界型をerasureで丸める前編Part 1
rigor trace fileほぼ実物と同型の極小版あり(binduniondispatchをコマ送り、§a3-3bx)推論の手順を端末アニメーションで再生(--verbose--line--format=json(なし)
dispatch 5段カスケード1段の表引き(METHODS、極小版は設けず)① 定数畳み込み、② shape、③ RBS、④ in-source、⑤ fallback前編Part 2

いずれも、本書で手作りした骨格(Dynamicマーカー、annotateMETHODS表)が、実Rigorでは同じ骨格を拡大した形で動いている、という対応で読めます。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.