コンテンツにスキップ

Rigor and Tapioca — Comparison and Strategy

ステータス: notes, 2026-05-09rigor-sorbet(ADR-11)の設計作業中に浮上した設計比較をまとめ、RigorのエコシステムへのRBI-emitモードを次の表面として提案する — Tapiocaのカバレッジを置き換えるのではなく補完する

このドキュメントは参考情報。プラグイン契約の拘束力のあるソースはADR-2。Sorbetインプットプラグインの拘束力のあるソースはADR-11

  • RigorとTapiocaは同じ問題領域を共有する — Sorbetのブラインドスポット(DSL生成メソッド、gemの内部実装、メタプログラミング由来のAPI)— だが正反対の端からアプローチする:
    • Tapioca: ランタイムでアプリをロードし、リフレクトし、RBIを生成する
    • Rigor: 静的にパースし、解析し、診断を出力する
  • RigorはTapiocaを置き換える計画はない。ふたつのツールは補完的。両方を使うプロジェクトはそれぞれのカバレッジの和集合を得る。
  • 戦略的機会: RigorにRBI-emitモードを追加し、Rigorの静的推論をTapiocaが書き込むsorbet/rbi/ツリーに流し込む。TapiocaのランタイムイントロスペクションOutputに代わるのではなく補完する
  • RigorのRBI emitが独自の価値を持つ場面:
    • Bundler.requireが失敗するか好ましくない、サンドボックス化された/ロード不可能なコードベース。
    • Rigorのプラグインが既に静的に理解しているDSLサーフェス(rigor-activerecorddb/schema.rbを読む、rigor-routesconfig/routes.rbを読む等)。
    • Tapiocaのランタイムリフレクションパスが捉えられないRBS::Extended精度(リファインメント、狭められた型、フローファクト)。
  • Tapiocaが適切な場面:現時点での広いDSLカバレッジ(39の組み込みコンパイラ)、ランタイム専用ファクト(ソースに現れないdefine_method生成メソッド)、確立したTapiocaパイプラインを持つ大規模Railsモノレポ。

両ツールは同じギャップを対象にする: 静的型チェッカー(Sorbet、Rigor)はメタプログラミングで生成されたメソッド、ランタイムロードされた定数、ソースが利用できないgemの内部実装、ロード時にクラス/メソッドを合成するDSLマクロを見ることができない。主流の解決策:

  • Tapioca(Shopify、2021年〜) — アプリケーションをランタイムでロードし、定義されたすべてのクラス/メソッドをリフレクトしてsorbet/rbi/{gems,annotations,dsl,shims}/を生成する。
  • Rigor(2026年〜) — プラグイン拡張API(ADR-2)を提供し、DSLごとのプラグイン(rigor-activerecordrigor-routes等)がアプリケーションコードを実行することなくプロジェクトのソースを読み込み、メソッドシグネチャとフローファクトをアナライザーに提供できるようにする。

両者は異なるエコシステムの前提から同じ問題空間に到達した: TapiocaはアプリケーションがすでにRails上で実行可能であると前提とする。Rigorは静的専用のスタンス(ADR-2 §「プラグインの信頼とI/Oポリシー」に従う)を前提とする。

次元RigorとTapiocaの両方
対象ドメインSorbetのブラインドスポット — DSL生成メソッド、gemの内部実装、メタプログラミング
プラグイン/コンパイラアーキテクチャDSLごとの拡張。コアにハードコードされていない
Rails対応ActiveRecord/ActionPack/ActiveJobをファーストクラスでサポート
ユーザー拡張性非標準DSL用のカスタムコンパイラ/プラグイン
RBIが合流点Tapiocaが書く。Rigorが読む(rigor-sorbetスライス4)
Sorbetエコシステムとの整合両者とも実プロジェクトでSorbetを有用にするために設計
TapiocaRigor
アプローチアプリケーションをロードするBundler.requirerequire "config/application")してRubyのランタイムAPI(Module#instance_methodsClass#ancestors等)でリフレクトする。アプリケーションコードを実行しない(ADR-2 §「プラグインの信頼」)。純粋なPrism ASTウォーク。
Tapiocaのコアlib/tapioca/runtime/reflection.rb — 安全なリフレクションのためにbind_call経由でKernel.instance_method(:class)等をラップ。プラグイン契約flow_contribution_for(call_node:, scope:)diagnostics_for_file(path:, scope:, root:)
プラグイン作成リフレクション駆動(短いが、gemのロードが必要)。AST駆動(より多くのコードだが、解析コードへのランタイム依存なし)。

このひとつの違いが他のすべての設計上の乖離を引き起こす。

  • Tapioca.rbiファイルをディスクに出力 (sorbet/rbi/gems/<gem>@<version>.rbisorbet/rbi/dsl/<class>.rbi等)。 Tapiocaはコードジェネレータである
  • Rigor → 診断をstdoutに出力(rigor check)、 オプションでCIベースライン用にJSON形式。 Rigorはアナライザーである

Tapiocaの出力はSorbetのsrb tcが消費する。Rigorの出力はユーザー/エディタが直接消費する。

  • Tapioca: 密に結合。RBIが唯一の出力。SorbetがTo唯一のコンシューマー。TapiocaはSorbetのために存在する。
  • Rigor: スタンドアロン。RBS(Rubyチームの公式型言語)が標準的な入力。Sorbetサポートはプラグイン(rigor-sorbet、ADR-11)であり、プラグイン境界でSorbetの語彙をRigorのRBSスーパーセット内部キャリアに変換する。
  • Tapioca: 39の組み込みDSLコンパイラ(AASMActionMailerActiveRecord*ファミリー、FrozenRecordGraphQLIdentityCacheJsonApiClientKredisProtobufSidekiqWorkerSmartPropertiesStateMachinesUrlHelpers等)。Shopifyで本番稼働。長年のイテレーション。
  • Rigor: 7つの作業例プラグイン(lisp-evalpatternunitsstatesmandeprecationsroutesactiverecord)と1つのエコシステムアダプタ(rigor-sorbet、ADR-11)。Railsプラグインファミリーはdocs/design/20260508-rails-plugins-roadmap.mdでロードマップ化済み。カバレッジではTapiocaが数年先行している。これは正直な評価
  • Tapioca: アプリケーションがロード可能であることを信頼する。gemのinitializeが失敗したりRailsイニシャライザがクラッシュすると、Tapiocaもクラッシュする。
  • Rigor: アプリケーションコードを実行しない。敵対的/不馴れなコードベースも安全。
  • Tapioca → RBI(Sorbetのフォーマット)
  • Rigor → RBS(Rubyチームのフォーマット)エクスポート時。RBSが自然に記述できないリファインメントにはRBS::Extended%a{rigor:v1:…}コメントアノテーションを使用。
  • Tapiocaコンパイラ: tapioca dsl実行ごとに1回呼び出され、ファイルを書き込む。呼び出しサイトごとのロジックはない。
  • Rigorプラグイン: 呼び出しサイトごと(flow_contribution_for)とファイルごと(diagnostics_for_file)に呼び出される。アナライザーとの継続的なインタラクション。

PHPのアナロジー(参考として)

Section titled “PHPのアナロジー(参考として)”

PHPの静的型付けエコシステムは数年前に同じように分裂した:

陣営PHPRuby
ランタイムイントロスペクション+ファイル生成barryvdh/laravel-ide-helperTapioca
静的拡張(アナライザー時プラグインAPI)PHPStan拡張 / PsalmプラグインRigor

マッピングは密接に対応する:

  • php artisan ide-helper:generatetapioca gem
  • php artisan ide-helper:modelstapioca dsl
  • _ide_helper.php / _ide_helper_models.phpsorbet/rbi/{gems,dsl}/*.rbi
  • phpstan-stubs(コミュニティ提供のスタブファイル) ≈ rbi-centralアノテーション

Rigorのプラグイン契約は明示的にPHPStanをモデルにしているADR-2 §「コンテキスト」: 「PHPStanはこの設計部分の最強の参照点である」)。PHPは両方のフレーバーの共存を許容する。RigorはPHPStanスタイルの静的拡張フレーバーをTapiocaの既存のランタイムイントロスペクションフレーバーと並んでRubyにもたらす。

Rigorは現在診断を出力し、RBIは出力しない。RBI-emitモードを追加することでRigorの静的推論をTapiocaが書き込むsorbet/rbi/ツリーに流し込める。3つの独自の価値提案:

1. Rigorの静的推論からのRBI(アプリロード不要)

Section titled “1. Rigorの静的推論からのRBI(アプリロード不要)”

できないか望まないプロジェクトに対してBundler.requireを実行する代わりに:

  • サンドボックス化されたCI環境(ネットワークなし、gem installなし)。
  • 敵対的/部分的に信頼されたgemソース。
  • ロード前のCIパス(インテグレーションテストより前のリント)。
  • パフォーマンスに敏感なパス(TapiocaのフルRailsブートは数秒かかる。Rigorの静的ウォークは同じコードでサブ秒)。

Rigorはソースのみから証明できるものについてRBIを生成する — クラス定義、メソッドシグネチャ(RBS/RBS::Inline/推論された戻り型から)、定数型。重いメタプログラミングではTapiocaのカバレッジに及ばないが、「コミット前のスモークテスト」ワークフローには十分。

2. RigorプラグインからのDSL対応RBI

Section titled “2. RigorプラグインからのDSL対応RBI”

DSLを理解する各Rigorプラグインは静的ウォーカーが記録できるファクトを持つ:

  • rigor-activerecord: モデルクラス名、スキーマ由来の属性メソッド(診断のために既に検出済み — RBIの出力は追加の1ステップ)。
  • rigor-routes: config/routes.rbからの*_path/*_urlヘルパー。
  • rigor-rails-i18n(計画中): t('key.path')のキー。
  • rigor-actionmailer(計画中): メーラーメソッド。

これらのプラグインはすでにDSLソースを静的にパースしている。プラグインごとにRBI-emitパスを追加すれば、RigorはTapioca-DSLコンパイラの代替としてRailsをロードしないことを好むプロジェクトにとって実用的になる。

Rigorの内部型はRBS(したがってRBI)が自然に記述できないリファインメント精度を保持できる — %a{rigor:v1:return: non-empty-string}はRigor固有。RBIを出力する際:

  • RBIに適合する厳格な型(リテラルの非空リスト、タプルシェイプ)はそのまま変換される。
  • 適合しないリファインメントは以下のどちらかになる:
    • コメント形式での保存: RBIのメソッド宣言の隣に # @rigor:return: non-empty-stringを出力。Sorbetはそれを無視し、RBIが後で消費された場合はRigorが読み戻す。
    • 保守的な消去: ADR-1 § 「rbs-erasure.md」に従い、 リファインメントを最も広いRBS形式に消去(例えば non-empty-stringString)。Sorbetは消去された形式を見る。精度はRBS::Extendedアノテーション経由でRigor側のみで保持される。

これにより、すでにTapiocaを使っているプロジェクトがTapioca生成のRBI上にRigor由来の精度を重ねる手段が得られる。sorbet/rbi/rigor/ディレクトリか、Tapiocaのshimスタイルのオーバーレイとして提供される。

  • SorbetのRBI語彙はRBSと異なる。RigorにはRBIの方向のトランスレーターが必要になる(ADR-11スライス3のrigor-sorbetのインプット側変換の逆)。難しい部分: T::Class[T]/T.attached_class/T.self_typeは慎重な近似が必要。SorbetのT.untypedセマンティクスはRBSのuntypedと異なる(漸進的vs.損失あり)。
  • ADR-1の不変条件が適用される。Rigor → RBIはRigor → RBS(rbs-erasure.md)とRBS → Rigor(情報無損失)と並ぶ第3の脚である。新しい脚には独自の規範的ドキュメントとラウンドトリップルールが必要。
  • カバレッジのマッチは正直に示す。Rigorの静的ウォークはランタイム専用の定義(ランタイム計算された名前に対するdefine_method、文字列のclass_evalif Rails.env.production?を通じて条件付きロードされるモジュール)を見ることができない。Tapiocaはこれらを捕捉する。RigorのRBI emitは捕捉しない。ドキュメントは明示的であるべき。

正直な承認: Tapiocaはなくならない。RigorのRBI-emit野心はそれを変えない。

  • 重いメタプログラミング。アプリケーションブート時にdefine_method、計算された文字列のclass_eval、ランタイムレジストリ上のmethod_missingで生成されたメソッド。静的解析はここで根本的に限界がある。ランタイムイントロスペクションが適切なツール。
  • 現時点での広いDSLカバレッジ。人気のRails/Shopifyスタックエコシステムのほとんどをカバーする39の組み込みコンパイラ。Rigorが追いつくには長年のプラグイン作成が必要。
  • 確立されたTapiocaパイプライン。CIインテグレーション、カスタムコンパイラ、shim管理ワークフローを持つ大規模モノレポ。Rigorがtapiocaが根本的にできないことを提供しない限り、切り替えコストが便益を上回る。
  • Sorbet固有の型strictness# typed: strict/# typed: strong — これらはSorbet-staticの機能であり、Rigorはそれを再現しない(Rigorはseverity_profileを類似フィルタリングに使うが、ファイル単位モードモデルはSorbetのもの)。

両ツールを使うプロジェクトが最強の構成である:

sorbet/rbi/
├── gems/ ← Tapioca(インストール済みgemへのランタイムリフレクション)
├── annotations/ ← Tapioca(rbi-centralコミュニティアノテーション)
├── dsl/ ← Tapioca(Rails DSLランタイムイントロスペクション)
├── shims/ ← 手書きのオーバーライド
└── rigor/ ← Rigor(静的 + RBS::Extended精度オーバーレイ)

Rigorはrigor-sorbetスライス4を通じてツリー全体を読む。プラグインのカタログはプロジェクトソースの.rb sigとRBI sigの間で共有されるため、Tapioca生成のRBIを既に持つプロジェクトはgemとDSLサーフェスについて無料のRigorカバレッジを得る。

rigor/オーバーレイはRigorの静的推論が着地する場所 — ユーザーがRigorに尊重させたいが、Tapiocaのランタイムパスに上書きされたくないリファインメント付きのsig。

将来のADR(おそらくADR-12)が設計を確定する。想定される構造:

  1. rigor rbi-emit CLIコマンドtapioca gem/tapioca dslに類似。paths:を探索し、推論を実行し、sorbet/rbi/rigor/<file>.rbiを出力する。
  2. プラグインのオプトインproduces_rbi:宣言 — RBI形式のファクトを提供するプラグインがマニフェスト経由で宣言。ランナーが集約する。
  3. RBI方向トランスレーターrigor-sorbetのインプット側変換の逆。コアに存在する(lib/rigor/rbs_extended.rbの消去側に類似)。
  4. RBS::Extendedコメント保存 — メソッド宣言の隣に# @rigor:…アノテーションを出力し、Rigor → Tapiocaフォーマットのあるいは → Rigorのラウンドトリップでリファインメントが保持されるようにする。
  5. rigor-sorbetとの合成 — 出力されたRBIはrigor-sorbetスライス4の有効なインプットである。ラウンドトリップテスト: 出力、再読み込み、再出力。2回目の出力は1回目に一致しなければならない。

スライスの順序はADR-11のパターンを反映する: 最初にemitインフラ(スライス1)、次にプラグインごとのRBI提供(スライス2-N)、最後にRBS::Extended精度(スライス≥N+1)。

次元TapiocaRigor
実行モデルランタイムイントロスペクション(require + リフレクト)静的AST解析(Prism、実行なし)
出力RBIファイル(sorbet/rbi/**/*.rbi診断(rigor check)。将来: RBI emit
対象コンシューマーSorbetのsrb tcエンドユーザー(CLI/エディタ)
プラグイン作成リフレクション駆動(短いが、ランタイムが必要)AST駆動(長いが、ランタイム依存なし)
信頼スタンスコードが実行されることを信頼コードを実行しない
RBSサポートなし(Sorbet-RBIのみ)ネイティブ(RBSが標準)
RBIサポートネイティブ出力rigor-sorbet経由で入力。将来: ネイティブ出力
組み込みDSLカバレッジ39コンパイラ(成熟)7例+Railsロードマップ(初期)
gem依存関係処理tapioca gemが自動でRBIを生成RBS優先。ADR-10オプトインソースウォーク
リファインメント精度RBS::Extended未サポートファーストクラス。Rigorを通じてラウンドトリップ
エコシステムの成熟度Shopifyで本番稼働、長年のイテレーションv0.1.xプレビュー
サンドボックス環境実行不可(Bundler.requireが必要)実行可(実行なし)

TapiocaはアプリをRunしてRBIを書く。RigorはRBS(およびrigor-sorbet経由でRBI)をソースをパースして読む。両者は正反対の端から同じ問題に取り組む。RigorのNEXT-tier野心は診断と並行してRBIを出力することであり、Tapiocaを置き換えることではなく、Tapiocaのランタイムパスが提供できない静的パスカバレッジやRBS::Extended精度を必要とするプロジェクトにとっての補完的存在となること

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