コンテンツにスキップ

ADR-4: 型推論エンジンと`Scope#type_of`クエリ

ステータス: Accepted;実装・出荷済み

推論エンジン(Scope、ファクトストア、副作用効果モデル、ケイパビリティロール推論、正規化、RBS消去ルーティング)はライブである;docs/internal-spec/inference-engine.md契約(contract)がその規範的サーフェスである。

ADR-4は、静的型モデル(ADR-1・ADR-3)を動作する推論エンジンに変える設計決定を記録します。中心となる具体的な成果物は、Prism ASTノードと不変なRigor::Scopeを取り、その式がそのプログラム位置で生成すると証明されたRigor::Typeを返す解析器クエリです。これはPHPStanの$scope->getType($node)に対するRuby/Rigorの対応物であり、すべてのCLI規則・プラグイン・リファクタツールが最終的に呼び出すクエリです。

ADR-4はセマンティクスを再定義しません — それらはdocs/type-specification/にあります — また型オブジェクト公開契約(contract)も再定義しません — それはdocs/internal-spec/internal-type-api.mdにあります。ADR-4は、どのRubyモジュールが推論を実装するか、それらがどの順序で着地するか、そしてコードを書き始めるために必要なADR-3の未決事項に対する暫定回答を固定します。

このADRの規範的な側面 — Scope#type_ofの公開契約・フェイルソフトポリシー・不変性規律・エンジンロード境界 — はdocs/internal-spec/inference-engine.mdにあります。このADRとそのドキュメントが観測可能なRubyの挙動について食い違うとき、仕様が束縛し、このADRが一致するよう更新されます。

今日のRigorはPrismでRubyをパースし、CLIを通じてパース時診断を報告します。型表現・スコープ・推論はありません。ADR-1は型モデルセマンティクスを固定し、ADR-3は型オブジェクト表現を固定し、2つのdocs/internal-spec/ドキュメントはエンジンサーフェス(surface)と型オブジェクト公開契約を固定します。残る決定は解析器がASTをTypeにどう変えるか、どの順序で、どのシームでです。

PHPStanの$scope->getType($node)が標準的な参照です。これは(Scope, Node)からTypeへの純粋関数で、型オブジェクトカタログ・クラスレジストリ・メソッドディスパッチャー・スコープが運ぶ制御フローファクト(fact)を参照します。RigorはRuby慣用的な命名で同じ形状を採用します。

類似のPHPStanサーフェスは以下のとおりです。

Rigorは不変スコープ+純粋型付け器の分離を採用します。意図的に採用しないものは以下のとおりです。

  • PHPのparent::リフレクションモデル — Rubyのクラスレイアウトは異なり、レジストリはRBS駆動です。
  • PHPStanの深いビジター継承 — Rigorの型付け器は、ADR-3の「型クラス間の継承なし」規則と整合的に、パターンマッチングを通じてPrismノードでディスパッチします。

ADR-3は、推論コードを書き始める前に回答が必要な2つの未決事項を記録しています。ADR-4は、最初の垂直スライス(slice)が着地できるよう暫定回答にコミットします。決定は、Slice 1が出荷され選択肢が実コードで行使された後にのみ、ADR-3のWorking Decisionsへ昇格します。

OQ1: Constantスカラーとオブジェクトシェイプ — 暫定回答Option C(ハイブリッド)

Section titled “OQ1: Constantスカラーとオブジェクトシェイプ — 暫定回答Option C(ハイブリッド)”

統一されたRigor::Type::Constantキャリア(carrier)が、スカラーリテラル(IntegerFloatStringSymbolRationalComplextruefalsenil)に加えて、タプルスライスで用いる静的な整数端点Rangeリテラルを保持します。複合リテラル形状(TupleHashShapeRecord)は、その内部型参照とシェイプ(shape)ポリシーが単一のRubyの値に圧縮できないため、専用クラスを得ます。

スライスでハイブリッドを選ぶ根拠:

  • スカラーキャリッジはコンパクトでRuby慣用的なまま保たれます。1つのクラスが並列階層なしで9つのリテラル種別をカバーします。
  • 複合シェイプはどのみち必要な構造的検査可能性を保ちます。
  • リファインメント(refinement、篩型とも)合成(non-empty-stringpositive-int・hash-shape追加キーポリシー)は、rigor-extensions.mdで同じスカラー/複合境界に沿ってきれいに分割されます。

リスク(スライスレビューのために記録):

  • リテラル配列[1, 2, 3]は文書化された回答が必要です — Slice 5はこれを生の値を運ぶ定数配列シェイプではなくConstantTupleにするため、Tupleクラスは構造的、Constantクラスは点ごとです。
  • リファインメント射影が頻繁にクラスごとのディスパッチを必要とすることが判明したら、スライスが昇格する前にスカラーキャリッジをクラスごと(String::ConstantInteger::Constant…)に再検討・移行します。

OQ2: 3値返り述語の命名 — 暫定回答Option A(?を落とす)

Section titled “OQ2: 3値返り述語の命名 — 暫定回答Option A(?を落とす)”

Rigor::Trinaryを返すケイパビリティ(capability)クエリと関係的クエリは、?サフィックスなしの名詞/動詞名を使います。

type.string # Rigor::Trinary
type.integer # Rigor::Trinary
type.subtype_of(other) # Rigor::Type::SubtypeResult
type.has_method(name) # Rigor::Trinary
type.string.yes? # bool, the only ?-suffixed surface

根拠:

  • 戻り値型は名前形状にエンコードされます。?はRigor全体でMUSTブール値を意味し、これにはRigor::Trinary#yes?/no?/maybe?を含みます。
  • PHPStanのisString()スタイル(これもRubyの?スタイルではない)と整合し、?サフィックス付きメソッドがtrue/falseを返すというRubyの期待と整合します。
  • Option Bが導入する曖昧さ(?サフィックス付きメソッドから黙って非ブール値を返す)を回避します。

リスク:

  • Rubyの読み手は本能的にtype.string?と入力してNoMethodErrorを得るかもしれません。明確なクラスレベルのドキュメンテーション文字列と(slice 1で)落とした?形式を提案するカスタムmethod_missingを追加することでこれを緩和します。

Slice 1のレビューがOption C(双対API)の方が使いやすいと結論したら、ADR-3 OQ2は更新され、単一のフォローアップで?糖衣構文が型サーフェス全体に追加されます。

仮想ノードとメソッドディスパッチ境界

Section titled “仮想ノードとメソッドディスパッチ境界”

PHPStanは、Rigorが早期に採用する1つの機能を公開します。すなわち$scope->getType($node)は、実際のパーサノードと、Type値を直接埋め込んだ合成ノードの両方を受け付けます。PHPStanのTypeExprにより、呼び出し元は偽のASTを構築せずに「$scope->getType(new Add(new LNumber(1), new TypeExpr(new IntType())))は何を推論するか?」を尋ねることができます。プラグインは同じ形状を使ってリファクタをシミュレートし、値を絞り込み、メソッド戻り値型規則をプローブします。

Rigorはディスパッチャースライスを待つのではなく、Slice 1の強化でこれを導入します。契約はdocs/internal-spec/inference-engine.md仮想ノードにあります。出荷される最小サーフェスはRigor::AST::Node(マーカーモジュール)とRigor::AST::TypeNodeです。追加の合成種別(呼び出し式・コンテナリテラル・ナローイング(narrowing)ラッパー)は、それらを実際に消費するスライスと一緒に着地します。

拒絶された選択肢: 演算子メソッドディスパッチのために型クラスを特殊化する

Section titled “拒絶された選択肢: 演算子メソッドディスパッチのために型クラスを特殊化する”

もっともらしい代替は、1 + 2がレシーバー型に呼び出しを評価するよう問うことでディスパッチするように、演算子メソッドを持つRubyの組み込み型に対してRigor::Typeを特殊化することです — Rigor::Type::IntegerTypeが算術を知り、Rigor::Type::StringTypeが連結を知るなどです。この選択肢は拒絶されました。理由は以下のとおりです。

  • 型クラス間の継承(ADR-3で禁止)か、すべての型形式上の「これらの引数で:+を評価する」ためのオープンエンドなduck-type契約のいずれかを要求します。これはinternal-type-api.mdの薄い値オブジェクト規則に矛盾します。
  • PHPStan自身の設計も同じ関心事を分離しています。Type::Typeはケイパビリティと射影クエリに答えます。メソッドディスパッチはMethodReflection*ReturnTypeExtensionプラグインポイントを通じます。ConstantStringType extends StringTypeのようなサブクラスは、メソッドディスパッチ特殊化のためではなく表現特殊化のために存在します。
  • ADR-2のRigor拡張APIは、プラグイン作者が組み込みメソッドの挙動(フレームワーク知識・gem固有の慣用法)を追加またはオーバーライドすることを期待します。そのサーフェスを型クラスに集中させると、エンジンをサブクラス化せずに拡張するのが難しくなります。

選ばれた設計は代わりに、Rigor::Inference::MethodDispatcher(Slice 2で定数畳み込みスタブとして導入され、Slice 4でRBSルックアップで拡張)を通じてレイヤードルックアップでメソッドディスパッチをルーティングします。すなわち定数畳み込みルールブック・次にRBS環境・次に組み込み演算子/メソッドテーブル・次にADR-2プラグイン拡張です。型クラスは薄いまま保たれ、ディスパッチャーの入力は実際のノードと合成ノードにわたって統一的(上記の仮想ノード契約を介して)であり、演算子セマンティクスはプラガブルです。

各スライスは独立して出荷され、前のスライスをgreenに保ち、コードベースを倒さずに巻き戻すことができます。

Slice 1 — リテラル型付け器(このスライス)

Section titled “Slice 1 — リテラル型付け器(このスライス)”

公開成果物: Rigor::Scope#type_of(node)がリテラル式・ローカル変数読み込み・浅いArrayリテラルについて正しい型を返します。それ以外はDynamic[Top]にフォールバックします。Slice 1の強化はさらに、合成型付け位置が初日から使えるよう上記の仮想ノードインフラを着地させます。

追加されるコードサーフェス:

  • yes/no/maybeのflyweightとand/or/negateを持つRigor::Trinary
  • Rigor::Typeドキュメンテーション専用ducktypeモジュール。
  • Rigor::Type::TopBotDynamicNominalConstantUnion
  • Rigor::Type::Combinatorファクトリー: uniondynamicnominal_ofconstant_of
  • IntegerFloatStringSymbolNilClassTrueClassFalseClassObjectBasicObjectのハードコードエントリーを持つRigor::Environment::ClassRegistry
  • レジストリをラップするRigor::Environment公開エントリー(RBSローダーはSlice 4で追加)。
  • Rigor::Scope.empty(environment:)#with_local#local#type_of
  • サポートされるノードに対するRigor::Inference::ExpressionTyper#type_of(node, scope)
  • Rigor::AST::NodeマーカーモジュールとRigor::AST::TypeNode合成ノード。型付け器によってPrismノードと並べてディスパッチされます。
  • Rigor::Inference::Fallback値オブジェクトとRigor::Inference::FallbackTracerオブザーバー。Scope#type_of(node, tracer: ...)を通じてスレッドされます。Slice 1以降カバレッジリグレッションが観測可能であるよう、すべてのフェイルソフトフォールバックを記録します。後のスライスは同じトレーサー上にrecord_dispatch_missrecord_budget_cutoffなどを追加します。
  • Rigor::Source::NodeLocator(ソーステキストとAST位置決めユーティリティのための新しいRigor::Source名前空間下)は(source, line, column)またはバイトオフセットを最深の囲みPrismノードにマップし、Rigor::Source::NodeWalkerはDFS事前順ですべてのPrismノードを生成します。
  • Rigor::Inference::CoverageScannerは新鮮なFallbackTracerで歩行された各ノードに対してScope#type_ofを実行し、最初に記録されたイベントのnode_classが訪問したノードのクラスと一致するときノードを直接認識されないとして分類します。これによりパススルーラッパー(ProgramNodeStatementsNodeParenthesesNode)の二重カウントが回避されます。
  • rigor type-of FILE:LINE:COL CLIサブコマンドはロケータとScope#type_ofをラップします。推論された型とRBS消去(テキストまたは--format=json)を表示します。--traceFallbackTracerを取り付け、記録されたイベントを報告します。これはエンジンサーフェスに対する最初のドッグフードループであり、単一位置のフェイルソフトカバレッジを検査する主要なツールです。
  • rigor type-scan PATH... CLIサブコマンドは、ファイル全体とディレクトリに対するCoverageScannerをラップし、クラスごとの訪問/認識されないカウントを集約し、フォールバック位置のサンプルを表面化します。--threshold=RATIOはそれをCIで実行可能にします。すなわち認識されない比率がしきい値を超えるとコマンドは非ゼロで終了し、カバレッジリグレッションがrigor checkに到達する前にビルドを破ります。

Slice 1で認識されるPrismノード:

IntegerNodeFloatNodeStringNodeSymbolNodeTrueNodeFalseNodeNilNodeLocalVariableReadNodeLocalVariableWriteNodeLocalVariableTargetNodeArrayNode(浅い、ナローイング不要)。

他のすべてのノードはtype_ofからDynamic[Top]を返します。フェイルソフト経路の契約はdocs/internal-spec/inference-engine.mdで規範的です。

Slice 2 — メソッドディスパッチ(定数畳み込みスタブ)

Section titled “Slice 2 — メソッドディスパッチ(定数畳み込みスタブ)”

ロードマップは元々ここにローカル・結合・文を、その後にメソッドディスパッチ(RBSバック)を置いていました。rigor type-scan libドッグフードループが着地したとき順序が入れ替わりました。すなわちこのコードベース内の認識されない式全体の約28%がPrism::CallNodePrism::ArgumentsNodeで、他のSlice 2候補の付加価値を圧倒していました。ローカル/結合は引き続き次に出荷され、Slice 3となります。

追加:

  • dispatch(receiver_type:, method_name:, arg_types:, block_type:)を持つRigor::Inference::MethodDispatcher(エントリーモジュール)とRigor::Inference::MethodDispatcher::ConstantFolding(ルールブック)。ディスパッチャーは呼び出しを畳み込めるときRigor::Typeを返し、「規則なし」のときnilを返すため、型付け器がフェイルソフトフォールバックを所有します。
  • Constant引数を持つRigor::Type::Constantレシーバー上の二項数値(+ - * / % < <= > >= == != <=>)・文字列(+ * == != < <= > >= <=>、暴走出力を避けるためのSTRING_FOLD_BYTE_LIMITキャップ付き)・symbol(== != <=> < <= > >=)・boolean(& | ^ == !=)・nil(==, !=)演算子をカバーする定数畳み込みルールブック。ホワイトリスト外はnilを返します。畳み込み中の実行時例外もrescueされnilにダウングレードされます。
  • ExpressionTyperPrism::CallNode(ディスパッチャー経由でルーティング、ミス時にはDynamic[Top]にフォールバック)とPrism::ArgumentsNode(非値位置として扱われ、カバレッジスキャナがそれをフラグするのを止めます。CallNodeハンドラがその子を直接読みます)を認識します。
  • ExpressionTyper#type_ofPRISM_DISPATCHハッシュとして書き直され、認識ノードカタログが循環的複雑度予算を再びトリップさせずに将来のスライスで成長できるようになります。
  • 強化ラウンドは算術を超えてカタログを広げます。ディスパッチハッシュは現在以下もカバーします。
    • Rigor::Environment::ClassRegistry#nominal_for_nameを介して解決されるPrism::ConstantReadNodePrism::ConstantPathNode。レジストリのハードコードリストはSlice-1の9個から〜35のコアクラス(ArrayHashRangeRegexpProcMethodModuleClassNumericComparableEnumerable・標準的なException格子、加えてIOFileDirEncoding)に成長します。未登録の名前は依然としてDynamic[Top]にフェイルソフトし、フォールバックイベントを発出します。
    • コンテナリテラル: Prism::HashNode/Prism::KeywordHashNodeNominal[Hash]として、Prism::InterpolatedStringNodeNominal[String]として、Prism::InterpolatedSymbolNodeNominal[Symbol]として、Prism::EmbeddedStatementsNodeはその本体型を伝播します。
    • 定義式: Prism::DefNodeConstant[:method_name]として、Prism::ClassNode/Prism::ModuleNode/Prism::SingletonClassNodeはその本体型を伝播し(空の場合はConstant[nil])、Prism::AliasMethodNode/Prism::AliasGlobalVariableNode/Prism::UndefNodeConstant[nil]として扱います。
    • 変数代入は単一のtype_of_assignment_writeハンドラを共有し、すべての*WriteNode(constant/instance/class/global/local、加えて*OperatorWriteNode*OrWriteNode*AndWriteNodeIndexOperatorWriteNode/IndexOrWriteNode/IndexAndWriteNodeMultiWriteNodeの各種)を.value右辺値の型として型付けします。
    • 「認識するがまだ絞り込まない」位置は静かにDynamic[Top]として型付けされます(フォールバックイベントなし)。すなわちPrism::SelfNode・読み込み側の*VariableReadNodeファミリー・Prism::BlockNodePrism::ForwardingSuperNode、加えて純粋な非値位置(ArgumentsNodeParametersNodeとすべての引数サブ種別・BlockParametersNodeBlockArgumentNodeAssocNodeAssocSplatNodeSplatNodeLocalVariableTargetNodeEmbeddedVariableNodeImplicitRestNodeForwardingParameterNodeNoKeywordsParameterNode)です。
  • rigor type-scan libでのカバレッジ向上: 定数畳み込みスタブ後の48.0%認識されないから26.1%まで下がりました。残る認識されない量は、Slice 3の制御フローノード(IfNodeUnlessNodeWhenNodeElseNodeCaseNodeAndNodeOrNodeBeginNodeRescueNodeReturnNodeBreakNodeNextNodeYieldNode)と、Slice 4のRBSバックディスパッチャーを待つユーザー定義の定数/呼び出しが支配しています。

Slice 3は2つのフェーズで着地します。

Phase 1(このスライスが最初に出荷):すべての制御フロー式はExpressionTyperを介してレシーバースコープで型付けされ、このファミリーのいずれのノードクラスも認識されないままにはなりません。IfNode/UnlessNodeの両分岐、CaseNode/CaseMatchNodeのすべてのWhenNode/InNode本体、およびBeginNodeの本体/rescueチェーン/else節は型付けされてunionされます。AndNode/OrNodeはそのオペランドをunionします(真偽値ナローイングはまだ。それはSlice 6で着地します)。RescueModifierNodeexpr rescue fallback)は同じunionです。WhileNode/UntilNodeConstant[nil]として型付けされます。ReturnNode/BreakNode/NextNode/RetryNode/RedoNodeBotとして型付けされ、unionの下できれいに吸収されるため、ジャンプする分岐は周囲の制御フローの値から静かに落とされます(if c; return; else; 7; endは正しくConstant[7]として型付けされます)。YieldNode/SuperNode/ForNode/DefinedNode/MatchPredicateNode/MatchRequiredNode/MatchWriteNodeは、後のスライスがそのセマンティクスを追加するまで静かにDynamic[Top]として型付けされます。LambdaNode/RangeNode/RegularExpressionNode/InterpolatedRegularExpressionNodeはリテラルキャリアをNominal[Proc]/静的なConstant[Range]またはNominal[Range]/Nominal[Regexp]として完成させます。Rigor::Scope#join(other)はPhase 2が用いる構造的unionジョインとして今出荷されます。これは束縛された名前を交差させ、各ペアをType::Combinator.unionを通じて実行します。

Phase 2(このサブフェーズはこのコミットで出荷) — StatementEvaluator(ローカルが文をまたいで伝播)Rigor::Inference::StatementEvaluator#evaluate(node) -> [Rigor::Type, Rigor::Scope]を導入し、Scope#joinをすべての文レベル構成に通すため、ある分岐で束縛されたローカルがマージ点後にunionされた束縛へ流れます。このクラスは(依然として純粋な)Scope#type_ofのRuby側補完です。すなわちすべての公開呼び出しは、レシーバースコープを変更せずに新鮮な[type, scope']ペアを返します。追加または拡張されたコンポーネント:

  1. Rigor::Inference::StatementEvaluatorが新しいエントリーポイントです。構築は入口のscope:に加えてオプショナルなtracer:を取ります。evaluate(node)はフローズンなHANDLERS = { Prism::*Node => :handler_method }テーブルでディスパッチし、カタログが特殊化しないノードについては[scope.type_of(node, tracer:), scope]にフォールバックします(したがって認識されない文的ノードはMUST NOTraiseしません — Slice 1のフェイルソフトポリシーは文レベルでも維持されます)。
  2. Slice 3 phase 2のカタログは、StatementsNode/ProgramNode(逐次スレッディング)・LocalVariableWriteNodeScope#with_localを介して右辺値の型を束縛)・IfNode/UnlessNode/ElseNode(述語、それから分岐+マージ)・CaseNode/CaseMatchNode/WhenNode/InNode(N項分岐+マージ)・BeginNode/RescueNode/EnsureNode(本体+rescueチェーン+結合された出口スコープに重ねられたensure)・WhileNode/UntilNode(条件+本体、後置スコープは0回反復とN回反復をジョイン)・AndNode/OrNode(LHSは常に実行、RHSはときどき実行。結果はunion、後置スコープはnil注入付きジョイン)・ParenthesesNode(内側式を通じてスコープをスレッドし、(x = 1; x + 2)xを束縛してConstant[3]を生成)です。
  3. 分岐マージ実装は、Scope#joinに委ねる前に半束縛の名前についてConstant[nil]を注入します。これはScope#joinが「文レベル評価器の責任」として文書化する契約を満たします。すなわちif cond; x = 1; end; xは今やConstant[1] | Constant[nil]として型付けされ、case kind; when 1 then x = 1; when 2 then x = 2; y = 9; endx: Constant[1] | Constant[2] | Constant[nil]y: Constant[9] | Constant[nil]として型付けされます。N項マージは繰り返しのペアごとのnil注入付きジョインで還元されます。還元順序は結果に影響しません。
  4. Rigor::Scope#evaluate(node, tracer: nil)は、呼び出し元が自分でStatementEvaluatorをインスタンス化しなくて済むよう、公開デリゲートとして出荷されます。レシーバースコープは入口スコープとして扱われます。戻り値は評価器が生成するのと同じ[type, scope']ペアです。

具体的な向上: x = 1; y = x + 2; yは今やConstant[3]として型付けされ、後置スコープではx: Constant[1]y: Constant[3]になります(定数畳み込みは束縛されたローカルを通じて流れます)。xs = [1, 2, 3]; xs.firstConstant[1] | Constant[2] | Constant[3]として型付けされます(Slice 5 phase 1のディスパッチ経路は束縛されたローカルを通じて解決します)。h = {a: 1, b: 2}; h.fetch(:a)Constant[1] | Constant[2]として型付けされます。

境界: Slice 3 phase 2は任意の式内部を通じてスコープをスレッドしませんfoo(x = 1)[1, x = 2]は依然として後置スコープからxを落とします)。意図的なPhase 2の簡略化は、後のスライスがカタログを成長させる間、StatementEvaluatorのサーフェスを安定に保ちます。DefNode対応スコープビルダー(後述)は、メソッド本体が自身の引数を見るよう、先に言及した2つ目の境界を持ち上げます。

CLI統合(このコミットも出荷): CLIコマンドrigor type-ofrigor type-scanは今や、新しいRigor::Inference::ScopeIndexer.index(root, default_scope:)ヘルパーを通じて間接的にScope#evaluateを消費します。インデクサーは新鮮なStatementEvaluatoron_enter:コールバックを配線し、プログラムを1度歩き、ルックアップが各ノードで見える入口スコープを生成する同一性比較のHash{Prism::Node => Rigor::Scope}を返します — 評価器が訪れない式内部の子に親のスコープを伝播します。CLIコマンドはそれからプローブごとにindex[node].type_of(node, tracer:)を実行し、ファイル内で先に束縛されたローカルが、後のノードを型付けするのに使われるスコープへ流れます。インデクサーは内部評価器をトレーサーフリーで実行します。CLI呼び出し元はインデックス後のtype_ofプローブにのみトレーサーを取り付け、二重記録されるフォールバックイベントを回避します。

追加:

  1. Rigor::Inference::StatementEvaluator#initialize(on_enter:)キーワード(デフォルトはnil)。非nilのとき、callableは(node, scope)とともにすべてのevaluate(node)呼び出しの開始時に1度呼び出され、すべての再帰的sub_evalを通じてスレッドされます。契約はdocs/internal-spec/inference-engine.mdの「文レベル評価」で束縛されます。
  2. Rigor::Inference::ScopeIndexerモジュールに、訪れていない式内部ノードのスコープエントリーを埋めるindexファクトリーとpropagate DFSウォーカーを持ちます。
  3. Rigor::CLI::TypeOfCommandRigor::Inference::CoverageScanner#scanは、ノードごとのtype_of呼び出しをインデクサーのルックアップを通じてルーティングします。

具体的な挙動の向上(CLIスモークプローブを通じて検証):

  • x = 1; y = x + 2; yを3行目1列(yの読み込み)で型付けするとConstant[3]を返します。2行目5列(右辺値内のxの読み込み)で型付けするとConstant[1]を返します。統合前は両プローブがDynamic[Top]を返しました。
  • xs = [1, 2, 3]; result = xs.first; resultを3行目で型付けするとConstant[1] | Constant[2] | Constant[3]を返します(Tuple対応ディスパッチが束縛されたローカルを通じて流れます)。統合前は、xsが見えなかったためresultプローブがDynamic[Top]を返しました。

type-scan libカバレッジは13.71%から13.70%認識されないへ移動します — ノイズの範囲内です。lib/はユーザー定義のConstantReadNode/ConstantPathNode参照、ユーザー型レシーバー(そのRBSは登録されていない)に対するCallNode、加えてローカルがメソッド引数(StatementEvaluatorは束縛しない)であるメソッド本体が支配しています。統合の価値はトップレベルローカル変数パターンを持つコードで実在し測定可能です。ドッグフードサンプルlib/はそのパターンを頻繁に行使しません。上記のCLI挙動向上が観測可能な証明です。

DefNode対応スコープビルダー(このコミットも出荷): StatementEvaluatorのカタログは現在Prism::ClassNodePrism::ModuleNodePrism::SingletonClassNodePrism::DefNodeを含みます。クラス/モジュール本体とメソッド本体は新鮮なスコープ下で評価され(Rubyのクラススコープとメソッドスコープは外側のローカルを見ません)、評価器はClassFrame.new(name:, singleton:)フレームの小さなclass_context:スタックをスレッドし、ネストされたdefがそのレキシカルなオーナーを知るようにします。新しいRigor::Inference::MethodParameterBinder.new(environment:, class_path:, singleton:).bind(def_node)はdefの引数リストをname -> Rigor::Typeマップに翻訳し、すべての名前をデフォルトでDynamic[Top]にし、利用可能な場合は周囲のクラスのRBSシグネチャからオーバーライドします。引数型は、マッチするスロットを持つすべてのオーバーロードにわたってunionされます(したがって並列の()オーバーロードがそれを省略しても、Array#first(n)(int)オーバーロードは依然としてnを束縛します)。位置スロットは位置でマッチされ、キーワードスロットはrequiredとoptional両方のキーワードマップにわたって名前でマッチされます。*rest**kw_restArray[T]Hash[Symbol, V]としてラップされます。def self.fooclass << self内のdef fooは両方ともRbsLoader#singleton_methodを通じてルーティングされます。追加または拡張されたコンポーネント: 8. Rigor::Inference::MethodParameterBinderは「defの引数リストを束縛マップに翻訳する」ための新しい公開サーフェスです。その契約はdocs/internal-spec/inference-engine.mdの「メソッドパラメータ束縛」で束縛されます。 9. Rigor::Inference::StatementEvaluatorは今やeval_defeval_class_or_moduleeval_singleton_classハンドラを定義し、すべてのsub_evalを通じてclass_context:スタックをスレッドします。フレームの修飾名はPrism::ClassNode#constant_pathからレンダリングされるため、class A::B; class C"A::B::C"class_pathを生成し、class << selfは最内フレームをシングルトンモードにフリップします。

具体的な挙動の向上(CLIスモークプローブを通じて検証):

  • class Integer; def divmod(other); other; end; endは今や本体内のother読み込みをFloat | Integer | Numeric | Rationalとして型付けします(Integer#divmodの4つのRBSオーバーロードにわたるunion)。バインダー前は読み込みがDynamic[Top]を返しました。
  • class Foo; def bar(x); x; end; endFooはRBSに知られていない)はxDynamic[Top]として型付けし、raiseしません — バインダーのフェイルソフト契約が成立します。
  • def add(a, b); a + b; endをトップレベル(クラスコンテキストなし)で実行すると、abの両方がDynamic[Top]として型付けされます — バインダーは囲みクラスを必要とするためRBSルックアップは試みられません。

type-scan libカバレッジは13.71%から13.45%認識されないへ移動します。Prism::CallNodeは35.9%→34.7%。改善は、RBS既知のメソッドを実際にオーバーライドする(引数が今や型情報を運ぶ)クラスに集中しており、ほとんどのlib/rigorメソッドがRigorがまだRBSを書いていないクラスに属するという事実によって上限が決まります。type-scan specは同じクエリで31.4%から30.98%認識されないへ移動します。

元々予期されたSlice 3境界自身でのカバレッジ向上はすでにPhase 1で実現されました(26.1%→22.3%認識されない)。Slice 4/Slice 5 phase 1後の認識されない量(13.5%)は、ユーザー定義のConstantReadNode/ConstantPathNode参照とユーザー型レシーバーに対するCallNodeが支配しています。両方とも、ローカル変数伝播ではなく、後のRBSロードとプロジェクト対応の作業を待ちます。

Slice 4 — メソッドディスパッチ(RBSバック)

Section titled “Slice 4 — メソッドディスパッチ(RBSバック)”

Slice 2の定数畳み込みルールブックの背後にRBSバックのディスパッチティアを重ねます。Slice 4は2つのフェーズで着地します。

Phase 1(このスライスが最初に出荷):エンジンはレシーバークラスメソッドディスパッチと定数名解決のためにRBSコアシグネチャを参照します。引数駆動のオーバーロード選択・ジェネリクスのインスタンス化・インターセクション型(intersection type、交叉型とも)・インターフェイス型・stdlib/gemのRBSロードはPhase 2に委ねられます。各メソッドの最初のオーバーロードが勝ち、これだけでInteger#succInteger#to_sString#upcaseArray#length1.zero?、および「メソッドが既知のクラスに存在し、戻り値型が単一の具象クラスインスタンス」のロングテールをカバーします。

追加:

  • Rigor::Environment::RbsLoaderRBS::EnvironmentLoader.new(コアのみ)と遅延構築されるRBS::DefinitionBuilderをラップします。デフォルトのローダーはフローズン・プロセス共有のシングルトンで、クラスごとの単調な定義キャッシュを持ちます。重いRBS::Environmentは最初のメソッド/クラスクエリで構築されるため、RBSに当たらないテスト実行は起動コストを払いません。
  • Rigor::Inference::RbsTypeTranslatorはハッシュベースのディスパッチテーブルを通じてRBS::Types::*Rigor::Typeに翻訳します。ジェネリクス引数は落とされ(Array[Integer]Nominal[Array])、Optional[T]Union[T, Constant[nil]]になり、boolUnion[Constant[true], Constant[false]]になり、self/instanceは提供されたときself_type:キーワードを置換し(レシーバークラス)、それ以外はDynamic[Top]に劣化します。AliasIntersectionVariableInterfaceDynamic[Top]に劣化します。
  • Rigor::Inference::MethodDispatcher::RbsDispatch(receiver, method_name)をRBSインスタンスメソッドに解決します。レシーバークラス名はConstantvalue.class.nameを介して)・Nominalclass_name)・Dynamicstatic_facetへ再帰)から導出されます。TopBot・他のレシーバーはnilを返します。Unionレシーバーは各メンバーを順次ディスパッチします — すべてのメンバーが解決すると結果はunionされます。任意のメンバーがミスしたら、ディスパッチ全体がnilを返します。
  • MethodDispatcher.dispatchenvironment:キーワードを受け付け、ConstantFoldingRbsDispatchをチェーンします。定数畳み込みは適用可能なときも依然として勝つため、1 + 2Constant[3]の精度を保ちます。畳み込み器が証明できない呼び出しのみがRBSへフォールスルーします。
  • Rigor::Environment#nominal_for_name(name)はまず静的クラスレジストリを参照し、次にRbsLoader#class_known?に尋ね、名前のためにNominalを合成します。ExpressionTyper#type_of_constant_readtype_of_constant_pathはこの組み合わせルックアップを使うため、Encoding::Converterや他のRBS専用コア定数はハードコードレジストリを膨らませずに解決されます。
  • ExpressionTyper#call_type_forはディスパッチャー後にDynamic起源伝播ティアを追加します。すなわちレシーバーがDynamic[T]で正の規則がどれも解決しなかったとき、結果はフォールバックトレーサーを発火させずに静かにDynamic[Top]へ劣化します。これは認識されたセマンティック結果(Dynamicが感染する)であり、フェイルソフト的妥協ではありません。inference-engine.mdメソッドディスパッチ境界に文書化されています。

rigor type-scan libでのカバレッジ向上: Slice 3 phase 1後の22.3%認識されないからSlice 4 phase 1後の15.1%まで下がりました。CallNode認識されない率は82.8%から38.5%に下がります。残る認識されない量は、ユーザー定義のConstantReadNode/ConstantPathNode(Rigor自身のRigor::*型はコアRBSにありません)と、Nominal[<user type>]レシーバーに対するCallNodeが支配しています。Slice 4 phase 2(プロジェクトRBSロードとstdlib登録)とSlice 5(ジェネリクス・オーバーロード・シェイプ推論)が両方のバケットを削っていきます。

Phase 2(サブフェーズに分割、それぞれ独立して出荷):

  • Phase 2a — プロジェクト+stdlib RBSロードRigor::Environment::RbsLoader#initializelibraries:"pathname"/"json"のようなstdlibライブラリ名の配列)とsignature_paths:(ユーザーの.rbsファイルを含むディレクトリの配列)を受け付けます。デフォルトのローダー(RbsLoader.default)はコアのみのままなので高速経路は変わりませんが、新しいRigor::Environment.for_project(root:, libraries:, signature_paths:)ファクトリーは<root>/sigを自動検出して任意のstdlibオプトインをロードするEnvironmentを構築します。未知のstdlib名はRBS::EnvironmentLoader#has_library?を介してフェイルソフトします(古い.rigor.ymlはMUST NOT解析器をクラッシュさせません)。存在しないシグネチャパスは静かにフィルタされます。CLI type-oftype-scanコマンドは今やEnvironment.for_projectを通じてスコープを構築するため、プロジェクトに対するプローブとスキャンは明示的な設定なしにローカルのsig/ツリーを拾います。rigor type-scan libでのカバレッジ向上: 14.9%→14.4%(小さなデルタは、Rigor自身のsig/rigor.rbsがまだスタブであることを反映します。インフラはsigが成長する準備ができました)。残る支配的な量 — ユーザー型レシーバーに対するPrism::CallNode — は移動するためにPhase 2bがクラスメソッドディスパッチを着地させる必要があります。
  • Phase 2b — クラスメソッド(シングルトンスコープ)ディスパッチ(このサブフェーズはこのコミットで出荷)。その住人がFooのインスタンスではなくクラスオブジェクトFoo自体であるシングルトンクラス型キャリアRigor::Type::Singleton[name]を追加します。Singleton[Foo]Nominal[Foo]class_nameを共有しますが構造的に区別されて比較されるため、型モデルは今や2つの値をきれいに区別します。配線は5か所で着地します:
    1. Rigor::Type::Combinator.singleton_of(class_or_name)は、既存のnominal_ofと並ぶ公開構築ヘルパーです。
    2. Rigor::Environment::RbsLoader#singleton_definition(class_name)#singleton_method(class_name:, method_name:)は、RBS::DefinitionBuilder#build_singletonを介して構築されたRBSシングルトンクラス定義をキャッシュします。それらはインスタンス側ヘルパーと名前空間が互いに素です — 例えばModule#instance_methodsはシングルトン側で解決され、Rubyの実行時セマンティクスと一致してインスタンス側では静かに不在です。
    3. Rigor::Inference::RbsTypeTranslator.translateinstance_type:キーワードを受け付けます。Bases::Selfself_type:を置換します(クラスメソッド本体ではSingleton[C]、インスタンスメソッド本体ではNominal[C])。Bases::Instanceは常にマッチするNominal[C]を置換します。singleton(::Foo)自体はNominal[Class]に劣化するのではなく、直接Singleton[Foo]に翻訳されます。
    4. Rigor::Inference::MethodDispatcher::RbsDispatchSingletonレシーバーを検出し、instance_methodの代わりにsingleton_methodを通じてルーティングし、適切なself_type/instance_typeペアを翻訳器に渡すよう学習します。Unionレシーバーはメンバーごとのディスパッチを続けます。1つのunion内でインスタンスとシングルトンのメンバーを混ぜることは自動的にサポートされます。
    5. Rigor::Environment#singleton_for_namenominal_for_nameをミラーリングし、定数のためのキャリアを生成します。ExpressionTyper#type_of_constant_readtype_of_constant_pathは今やそれを使うため、式IntegerSingleton[Integer]として型付けされ、Integer.sqrt(4)は正しくシングルトンメソッドティアを通じてNominal[Integer]に解決されます。Foo.newは登録されたクラスについてClass#newを通じて解決されます。既知のクラスでの認識されないクラスメソッドは依然としてDynamic[Top]にフォールバックし、フォールバックイベントを発出します。rigor type-scan libでのカバレッジ向上: 14.4%→13.9%認識されない。以前は誤った「クラスオブジェクトに対するインスタンスルックアップ」呼び出しが今や正しく答えられるようになり、CallNode認識されない率は38.5%から36.7%に下がります。
  • Phase 2c — 引数型付きオーバーロード選択(このサブフェーズはこのコミットで出荷)。すべての具象型にRigor::Type#accepts(other, mode:)を追加し、Rigor::Type::AcceptsResult値オブジェクト(Trinary+mode+reasons)を返し、それをRBSバックのディスパッチャーに通すことで、同じメソッドの異なるオーバーロードを呼び出し元の実際の引数型に基づいて選択できるようにします。追加されたコンポーネント:
    1. Rigor::Type::AcceptsResultは将来のSubtypeResultの双対です。3値の答え・境界mode:gradualが現在出荷、:strictは予約)・順序付きでフローズンなreasons配列を運びます。述語yes?/no?/maybe?は運ばれたTrinaryに委ねられ、with_reasonは1つの追加reasonが追加された不変コピーを生成します。
    2. 各具象Rigor::Type形式(TopBotDynamicNominalSingletonConstantUnion)は新しいRigor::Inference::Acceptanceモジュールに委ねるaccepts(other, mode: :gradual)を得ます。共有モジュールはケース解析をホストするため、型インスタンスは(ADR-3に従って)薄いまま、internal-type-api.mdの公開API契約を満たします。
    3. 受理代数。Topはすべてを受理します。BotはBotのみを受理します。漸進的(gradual)モードのDynamic[T]はすべての具象型を受理します(どちらの側のDynamicも短絡してyesになります)。Nominal[C]は、Object.const_getを介したRubyの実際のクラス階層を使い、D <= C / v.is_a?(klass(C))のときNominal[D]/Constant[v]を受理します(クラスがロードできないときmaybeを生成)。Singleton[C]はサブクラスの別のシングルトンのみを受理します。Constant[v]は構造的に等しいConstant[v’]のみを受理します。Unionはメンバーごとに、両側で自然なOR/ANDを使ってディスパッチします。
    4. Rigor::Inference::MethodDispatcher::OverloadSelectorRBS::Definition::Methodに加えて実際のarg_typesを消費し、メソッドタイプを位置アリティ(required・optional・rest・trailing)でフィルタし、required keywordsがキーワードレス呼び出し形状で満たせないオーバーロードをスキップし、それからacceptsからすべての(param, arg)ペアがyesまたはmaybeを返す最初のオーバーロードを選びます。どのオーバーロードもマッチしないとき、phase 1/2bからのフェイルソフト契約を保つためにセレクタはmethod_types.firstにフォールバックします。
    5. RbsDispatch.dispatch_oneは常にmethod_types.firstを取るのではなくセレクタを参照し、選ばれたオーバーロードの戻り値型をRbsTypeTranslator.translate(... self_type:, instance_type:)を通じてスレッドします。 具体的な向上: [1, 2, 3].first(引数なし)と[1, 2, 3].first(2)(1つのInteger引数)は今や異なる型を返します(Dynamic[Top]Nominal[Array])。phase 2bは両方に対して最初のオーバーロードのElemを返していました。Array.new(3)と引数クラスがミスマッチしたInteger#+(例えば1 + 1.5、定数畳み込みは助けにならない)も同様に正しいRBSオーバーロードを選択します。rigor type-scan libでのカバレッジ: 13.9%→13.6%認識されない。Prism::CallNodeは36.7%→35.8%。翻訳器のBases::Class劣化経路が今や残る支配的なCallNodeフォールバックソースです — その作業はPhase 2dで進みます。
  • Phase 2d — ジェネリクスのインスタンス化(このサブフェーズはこのコミットで出荷)Rigor::Type::Nominalに型引数を運び、それをエンジンのすべての層に通すため、Array[Integer]#firstElemを置換してDynamic[Top]に劣化するのではなくIntegerを返します。追加または拡張されたコンポーネント:
    1. Rigor::Type::Nominalは今や順序付き・フローズンなtype_args配列を運びます。空配列は「素」形式(Nominal["Array"])です。空でない配列は適用済みジェネリック(Nominal["Array", [Nominal["Integer"]]])を表します。構造的等価性とhashtype_argsを参照します。describe/erase_to_rbsは引数をArray[Integer]としてレンダリングします。同じクラスの2つの素キャリアと適用済みキャリアは異なる値です。したがって格子は静かに一方を他方に強制しません。
    2. Rigor::Type::Combinator.nominal_of(class_or_name, type_args: [])は公開構築ヘルパーです。キーワードはまだジェネリクスを運ばない呼び出し元から邪魔にならないままです。
    3. Rigor::Inference::Acceptance.accepts_nominaltype_argsに対して要素ごとに再帰します(共変(covariant)。宣言された分散はSlice 5以降で着地)。どちらかの側が素のとき、ヘルパーは寛容に短絡します — 素のselfは任意のインスタンス化を受理(yes)、適用済みself上の素のotherはmaybeを生成 — そのためまだジェネリクスを学んでいないphase-2c呼び出し位置は動作し続けます。アリティ不一致はnoに縮退します。
    4. Rigor::Inference::RbsTypeTranslator.translate(..., type_vars: {})はRBS変数のnameシンボルでキー付けされた置換マップを受け付けます。RBS::Types::Variableはマップを参照し、存在するとき束縛されたRigor::Typeを返します。束縛されていない変数はDynamic[Top]に劣化するため、インスタンス化されていないジェネリクスはフェイルソフトな挙動を保ちます。RBS::Types::ClassInstanceは今やargsを再帰的に翻訳するため、Array[Integer]Nominal["Array", [Nominal["Integer"]]]にラウンドトリップし、ネストされたジェネリクスは無傷のままです。
    5. Rigor::Environment::RbsLoader#class_type_param_names(class_name)はクラスの宣言された型パラメータシンボルを返します(Arrayについて[:Elem]Hashについて[:K, :V])。Array.newのようなシングルトンメソッドが同じElemでパラメータ化されるため、インスタンス定義から読みます。
    6. Rigor::Inference::MethodDispatcher::RbsDispatchはレシーバーのtype_argsをクラスのtype_param_namesに対してジップして置換マップを構築し、それからそのマップをOverloadSelector.select(..., type_vars:)と最終的なRbsTypeTranslator.translate(..., type_vars:)の両方を通じてスレッドします。アリティ不一致と素のレシーバーはマップを空にしておくため、自由変数は以前と同じように劣化します。
    7. Rigor::Inference::ExpressionTyper#array_type_forは今やリテラルの要素型のunionからNominal[Array, [Element]]を構築します。type_of_hashはKとVの両方で同じことをします。空リテラルは、解析器が持っていないBotの証拠を製造するのを避けるため、素のままです。 具体的な向上: [1, 2, 3].firstDynamic[Top]の代わりにConstant[1] | Constant[2] | Constant[3](リテラルの要素のunion)に解決されます。[1, 2, 3].first(2)Array[Constant[1] | Constant[2] | Constant[3]]を返します。{a: 1, b: 2}.fetch(:a)Constant[1] | Constant[2]を返します。rigor type-scan libでのカバレッジ: 13.6%→13.4%認識されない。Prism::CallNodeは35.8%→35.3%。利得は解決された呼び出しの数ではなく解決された呼び出しの精度にあるため、向上は2cのものより小さいです — 残余のCallNode量は今やユーザー定義のレシーバー(Rigor::*型)と、引数型自体がDynamicである呼び出し位置が支配しています。

4つすべてのサブフェーズはフェイルソフトなDynamic[Top]ポリシーを無傷に保つため、部分的な移行はエンジンサーフェスを決して破りません。

Slice 5は2つのフェーズで着地します。ロードマップは元々TupleHashShapeRecordを一緒にまとめていました。Slice 5 phase 1のコミットは2つのリテラル駆動キャリア(TupleHashShape)を出荷し、Record(推論されたオブジェクトシェイプ、structural-interfaces-and-object-shapes.mdを参照)はオブジェクトシェイプの証拠がリテラル駆動ではなくケイパビリティロール推論と並んで着地するため、phase 2に委ねます。

Phase 1(このサブフェーズはこのコミットで出荷) — Tuple+HashShapeキャリアとリテラルアップグレード。追加されたコンポーネント:

  1. Rigor::Type::TupleRigor::Type要素値の順序付き・フローズン配列を運びます。住人は、長さがelements.sizeと一致し、位置iの要素がelements[i]に住むRubyのArrayインスタンスです。describe/erase_to_rbs[A, B, C]をレンダリングします。等価性とhashelementsに対して構造的です。array_type_forが(アリティを固定する要素の証拠がないため)[]を素のNominal[Array]として保つにもかかわらず、空のTuple Tuple[]は有効な値オブジェクトです。
  2. Rigor::Type::HashShapeは順序付き・フローズンな(Symbol|String) -> Rigor::Typeマップに加えて、required key・optional key・read-only・open/closed extra-keyポリシー(rigor-extensions.mdのRigor拡張)を運びます。describeは必須symbolキーについて{ a: T }、optionalキーについて{ ?b: T }、stringキーについて{ "k": T }をレンダリングし、openシェイプには...を追加します。厳密にclosedなsymbolキー付きシェイプはRBSレコード構文({}とoptionalフィールドを含む)に消去されます。stringキー付きシェイプはHash[K, V]に劣化し、型付きextra境界のないopenシェイプはHash[top, top]に劣化します。等価性はエントリーについてRubyのHash#==に従い、ポリシーフィールドを含みます。
  3. Rigor::Type::Combinator.tuple_of(*elements)Combinator.hash_shape_of(pairs, **options)は公開ファクトリーです。tuple_of()は空のTupleを生成します。hash_shape_of({})は空のclosed HashShapeを生成します。
  4. Rigor::Inference::Acceptanceは2つの新しい経路を学習します。Tuple[A1..An].accepts(Tuple[B1..Bn])はアリティチェック後に共変な要素ごとの比較を行います。Tupleでないotherは、解析器がジェネリックnominal単独からアリティを証明できないため拒否されます。HashShape{k: T,...}.accepts(HashShape{...})は共有キーで深さ共変であり、ターゲットのすべてのrequiredキーがソースでrequiredであることを要求し、不在のoptionalキーを許し、ターゲットがclosedのときextra/openソースを拒否します。逆経路 — Nominal[Array, [E]].accepts(Tuple[*])Nominal[Hash, [K, V]].accepts(HashShape{...}) — はシェイプを基底のnominalに射影し、既存のジェネリック受理パイプラインに再入します。
  5. Rigor::Inference::RbsTypeTranslator.translate_tupletranslate_recordは、RBS::Types::TupleRBS::Types::Recordを(phase 2dのようにNominal[Array] / Nominal[Hash]に消去するのではなく)新しいシェイプキャリアにマップします。要素/値型は呼び出し元のself_type/instance_type/type_varsコンテキスト下で再帰的に翻訳されるため、tuples/records内のジェネリクスが保たれます。RBSレコードのoptionalフィールドはoptional HashShapeキーにマップされ、レコードはclosedです。
  6. Rigor::Inference::MethodDispatcher::RbsDispatch.receiver_descriptorはシェイプを運ぶレシーバーを基底のnominalに射影し、既存のジェネリック型付きディスパッチパイプラインが重複なく再利用できるようにします。すなわちTuple[Integer, String]Array[Integer | String]としてディスパッチされ、HashShape{a: Integer}Hash[Symbol, Integer]としてディスパッチされます。Tuple対応の精緻化(例: 精密なメンバーを返すtuple[0]、分解代入)はphase 2に委ねられます。それらはRbsDispatchの上のより高優先度のディスパッチティアとして実行されます。
  7. Rigor::Inference::ExpressionTyper#array_type_forは、すべての要素が非splat値である空でない配列リテラルをTupleにアップグレードします。splatを含むリテラルは、[*xs, 1]が依然として推論可能な要素型を生成するよう、Slice 4 phase 2dのNominal[Array, [union]]経路を保ちます。type_of_hashは、すべてのエントリーがキーが静的なSymbolNodeまたはStringNodeリテラルであるAssocNodeであるハッシュリテラルをHashShapeにアップグレードします。動的キー・ダブルsplat・重複キーを持つエントリーは、ジェネリックなHash[K, V]形式にフォールスルーします。

具体的な向上: [1, 2, 3]Tuple[Constant[1], Constant[2], Constant[3]]として型付けされます(以前はNominal[Array, [Constant[1] | Constant[2] | Constant[3]]])。{ a: 1, b: 2 }HashShape{a: Constant[1], b: Constant[2]}として型付けされます(以前はNominal[Hash, [Symbol-union, Integer-union]])。キャリアを通じたメソッドディスパッチは射影を介して同じ戻り値型精度を保ちます。すなわち[1, 2, 3].first(2)は依然としてArray[Constant[1] | Constant[2] | Constant[3]]に解決され、{ a: 1 }.fetch(:a)は依然として値のunionにVを置換します。rigor type-scan libでのカバレッジ: 13.4%→13.5%認識されない。小さなぐらつきは精度のリグレッションではなく、新しいlibファイル(Tuple/HashShapeキャリア)が独自の定数参照を寄与していることを反映しています。

Phase 2はサブフェーズで着地。phase 1で出荷されたキャリアと射影ベースのディスパッチは、漸進的な精度向上の余地を残します。

Phase 2 sub-phase 1(このサブフェーズはこのコミットで出荷) — シェイプ対応要素ディスパッチConstantFoldingRbsDispatchの間に挿入される新しいティアであるRigor::Inference::MethodDispatcher::ShapeDispatchを追加します。契約はdocs/internal-spec/inference-engine.mdの「メソッドディスパッチ境界」で束縛されます。ティアは、TupleHashShapeの要素アクセスメソッドを射影されたArray#[]/Hash#fetchの答えではなく位置ごと/キーごとの精密な型に解決します。追加されたコンポーネント:

  1. ShapeDispatch.try_dispatch(receiver:, method_name:, args:)は精密な要素/値型を返すか、次のティアに委ねるためにnilを返します。認識されるTupleカタログはfirst/last/size/length/count(無引数)に加えて単一のConstant[Integer]引数を伴う[]/fetchです。認識されるHashShapeカタログはsize/length(無引数)に加えて単一のConstant[Symbol|String]引数を伴う[]/fetch/digです。範囲外インデックス・キー欠落のfetch・複数引数dig・非静的キーはRbsDispatchに委ねられ、射影の答えが適用され続けます。
  2. MethodDispatcher.dispatchは新しいティアをRbsDispatchの上にスレッドします。phase 1の射影は依然としてミス時に適用されます。すなわちtuple.mapshape.transform_values・他の反復呼び出しは以前の挙動を保ちます。
  3. 負のタプルインデックスは長さで正規化されます(tuple[-1]は最後の要素を返します)。キー欠落の解決はRubyのセマンティクスをミラーします。すなわちshape[:missing]shape.dig(:missing)Constant[nil]に解決され、shape.fetch(:missing)は実行時にKeyErrorをraiseするため委ねます。

具体的な挙動の向上(CLIスモークプローブを通じて検証):

  • [1, 2, 3].firstConstant[1]として型付けされます(以前はConstant[1] | Constant[2] | Constant[3])。
  • [1, 2, 3][-1]Constant[3]として型付けされます。[1, 2, 3].sizeConstant[3]として型付けされます。
  • { name: "Alice", age: 30 }[:name]Constant["Alice"]として型付けされます(以前は射影された値のunion)。
  • { a: 1 }[:missing]Constant[nil]として型付けされます。{ a: 1 }.fetch(:missing)は射影の答えを保ちます。

rigor type-scan libでのカバレッジ: 13.8%→13.6%認識されない。Prism::CallNodeは35.7%→35.1%。利得はタプルとハッシュシェイプをローカルで構築するコードに集中しています。ユーザー型レシーバー(Rigor自身のRigor::*型)はさらなるカバレッジのためにRBSオーサリングを依然として待ちます。Slice 4 phase 2c/dで以前記録された向上引用([1, 2, 3].firstをunionとして、{ a: 1, b: 2 }.fetch(:a)を値のunionとして)はそのスライスのコミット時の挙動を反映し、ここで置き換えられます。すなわちそれらの式は今やShapeDispatchを通じて精密な最初のメンバー/値に解決されます。

Phase 2 sub-phase 2(このサブフェーズはこのコミットで出荷) — 分解代入・複数引数digHash#values_at。追加されたコンポーネント:

  1. Rigor::Inference::MultiTargetBinderは、Prism多重ターゲットツリー(MultiWriteNodeまたはMultiTargetNode)に対してRigor::Type値を分解し、name -> Rigor::Type束縛マップを返す純粋なモジュールです。タプル形状の右辺は要素ごとに射影します。すなわち前方ターゲットはインデックスで要素を読み(欠落スロットはConstant[nil]で埋めます)、restターゲットは中間要素のTupleに束縛され(ソースに余剰がないときTuple[])、後方ターゲットは対応するオフセットでテール要素を読みます。Tupleでない右辺はすべてのスロットをDynamic[Top]に束縛します。ネストされたMultiTargetNodeターゲットはスロットの型を新しい右辺としてリカースします。非ローカルターゲット(インスタンス/クラス/グローバル変数・定数・index/callターゲット・無名splat)は、ローカル変数スコープに観測可能な寄与がないため静かにスキップされます。バインダーは、sub-phase 2(文レベル分解)とSlice 6 phase C sub-phase 2(ブロック引数分解)の間で共有される標準サーフェスであるため、bind規則はMUST一度書かれて二度消費されます。
  2. Rigor::Inference::StatementEvaluatorは、エントリースコープ下で右辺を一度評価し、バインダーの束縛を後置スコープに折り畳むPrism::MultiWriteNodeハンドラを追加します。ペアの型はMUST右辺の型と等しくなります(Rubyの(a, b = [1, 2]) #=> [1, 2]セマンティクスと一致)。
  3. Rigor::Inference::MethodDispatcher::ShapeDispatchは3つの精密なハンドラを成長させます。すなわちTuple#dig(チェーン)・HashShape#dig(チェーン)・HashShape#values_atです。チェーンセマンティクスはMUST以下のとおりです。すなわち各ステップはそのキー/インデックスをルックアップし、それからchain_digは解決された値を新しいレシーバーとして続行します — Tuple/HashShapeメンバーは残りの引数でカタログに再ディスパッチし、Constant[nil]メンバーはチェーンをConstant[nil]に短絡し(RubyのArray#digHash#digは実行時にnilで短絡)、他の任意の中間キャリアは委ねて射影の答えが適用されます。チェーンステップに発生する範囲外インデックスは、RubyのArray#digがraiseするのではなく範囲外インデックスについてnilを返すため、MUSTConstant[nil]に解決されます。values_atは、位置ごとの値がキーごとの値(欠落キーについてはConstant[nil])であるTupleを返します。任意の引数が非静的のときカタログは委ねます。Range/start-length []とRigor拡張のhash-shapeポリシーはsub-phase 3で着地します。

具体的な挙動の向上(CLIスモークプローブを通じて検証):

  • pair = [10, 20]; a, b = pair; sum = a + bsumConstant[30]として型付けします。束縛前はabが複数書き込みを越えて束縛されておらず、sumDynamic[Top]に縮退しました。
  • users = { addr: { zip: "00100" } }; users.dig(:addr, :zip)Constant["00100"]として型付けされます。束縛前はチェーンが射影されたHash[Symbol, Hash[...]]の答えを通じてステップしリテラル値を失いました。
  • { a: 1, b: "two" }.values_at(:a, :b)Tuple[Constant[1], Constant["two"]]として型付けされます(以前は射影されたArray[Integer | String])。
  • a, *r, c = [1, 2, 3, 4]a -> Constant[1]r -> Tuple[Constant[2], Constant[3]]c -> Constant[4]を束縛します。

Recordキャリア(推論されたオブジェクトシェイプ、structural-interfaces-and-object-shapes.mdを参照)は後のスライスでケイパビリティロール推論と並んで着地します。リテラル駆動のHashShapeはそれまでハッシュ側をカバーし続けます。

Phase 2 sub-phase 3(このサブフェーズはこのコミットで出荷) — []のRangeおよびstart-length形式、加えてRigor拡張のhash-shapeポリシー。追加されたコンポーネント:

  1. ExpressionTyper#type_of_rangeは今や整数端点の範囲リテラルをConstant[Range]として運び、ShapeDispatchのために静的境界を保ちます。動的範囲はNominal[Range]のままです。
  2. ShapeDispatch[]についてtuple[start, length]tuple[range]を認識し、RubyのArray#[]スライスセマンティクスを使います。静的に成功するスライスはスライスされたTupleを返します。静的にnilなスライスはConstant[nil]を返します。fetchはそれらの形式を要求しません。
  3. HashShapeはrequired/optional/read-onlyキーセットとopen/closed extra-keyポリシーを得ます。厳密にclosedなsymbolキー付きシェイプはoptionalフィールドを含めてRBSレコードに消去されます。openまたはstringキー付きシェイプはHash[K, V]に消去されます。[]/digを通じたoptionalキー読み込みはnilを含み、optionalキーfetchはキーが不在の可能性があるため委ねます。
  4. Acceptanceは構造チェックを通じてポリシーをスレッドします。closedターゲットはextra既知キーとopenソースを拒否します。openターゲットは古い幅寛容な挙動を保ちます。requiredターゲットキーはソースでrequiredでなければならず、optionalターゲットキーは不在でかまいません。

Slice 6 — ナローイング(最小CFA)

Section titled “Slice 6 — ナローイング(最小CFA)”

Slice 6は2つのフェーズで着地します。Phase 1はIfNode/UnlessNodeでの真偽値性とnil?ナローイング、加えてAndNode/OrNodeの対応するRHSエントリーナローイングを出荷します。phase 2はクラスメンバーシップ述語(is_a?kind_of?instance_of?)・有限リテラル集合の等価ナローイング・docs/type-specification/control-flow-analysis.mdのヒープと関係的ファクトを駆動する正式なRigor::Analysis::FactStoreキャリッジを追加します。

Phase 1(このサブフェーズはこのコミットで出荷) — ローカル束縛での真偽値性とnilナローイング。追加されたコンポーネント:

  1. Rigor::Inference::Narrowingは、型レベルプリミティブ(narrow_truthynarrow_falseynarrow_nilnarrow_non_nil)と述語レベル解析器predicate_scopes(node, scope) -> [truthy_scope, falsey_scope]を公開する純粋なモジュールです。契約はdocs/internal-spec/inference-engine.mdの「ナローイング(Slice 6 phase 1)」で束縛されます。
  2. Slice 6 phase 1の述語カタログは、LocalVariableReadNode(束縛されたローカルの真値/偽値ナローイング)・recv.nil?と単項!recvCallNode(呼び出しが引数もブロックも運ばないときのみ)・ParenthesesNode/StatementsNode(本体/最後の文へ再帰)・短絡するAndNode/OrNodeScope#joinを通じてサブエッジを合成)です。それ以外は「ナローイングなし」にフォールスルーします — 両エッジはエントリースコープを変更せずに返すため、カバーされていない形状ではSlice 3 phase 2の挙動が保たれます。
  3. Rigor::Inference::StatementEvaluatorは今やナローイング対応です。eval_ifthen分岐を述語の真値スコープ下で、else分岐を偽値スコープ下で評価します。eval_unlessは2つをスワップします。eval_and_orはRHSをLHSの真値スコープ下(&&)またはLHSの偽値スコープ下(||)で入ります。分岐マージでの半束縛のnil注入は変わりません。

受理代数はクラスごとの述語ではなくnarrow_truthy/narrow_falseyに委ねられるため、型インスタンスは(ADR-3に従って)薄いまま保たれます。すなわちConstantはスカラーvalueを参照し、NominalNilClass/FalseClassの短いリストに対してclass_nameを参照し、Unionは要素ごとに再帰し、Top/Dynamicは、解析器がまだよりリッチなキャリアなしに差集合型を表現できないため、変更されずに通過します。

具体的な挙動の向上(CLIスモークプローブを通じて検証):

  • xs = [1, 2, nil]; y = xs.first; if y.nil?; "got nil"; else; y; endConstant["got nil"] | Constant[1] | Constant[2]として型付けされます。ナローイング前は、yがelse分岐で精緻化されなかったため結果にConstant[nil]が含まれていました。
  • Union[Integer, nil].evaluate("if x; x.succ; end")x.succNominal[Integer]に対して型付けします(レシーバーが絞り込まれているためディスパッチがきれいに解決します)。絞り込まれていないディスパッチはNilClass#succを証明できず、フォールバックしました。

rigor type-scan libでのカバレッジ: 13.45%→13.8%認識されない。ADR-4がSlice 6について予期したとおり、小さな上方ぐらつきは精度リグレッションではなく、新しいlib/rigor/inference/narrowing.rbファイル(その定数参照がまだRBSでカバーされていないRigor::*型に対する認識されないバケットに寄与)を反映します。挙動の向上はすでに型付けされた値に集中しています。

Phase 2 sub-phase 1(このサブフェーズはこのコミットで出荷) — クラスメンバーシップナローイング。追加されたコンポーネント:

  1. Rigor::Inference::Narrowingは2つの型レベルプリミティブnarrow_class(type, class_name, exact: false)narrow_not_class(type, class_name, exact: false)を成長させます。真値プリミティブは値格子(ConstantNominalUnionTupleHashShapeSingletonTopDynamicBot)を歩き、Object.const_getを介したホストRubyのクラス階層を使って:equal/:subclass/:superclass/:disjoint/:unknownの1つを計算します。:superclassケースは実用的なナローイング勝利を実装します — is_a?(Integer)下のNominal[Numeric]はsupertypeに留まるのではなくNominal[Integer]になります。:unknown(ホストRubyがロードしていないクラス)は入力を保つため、解析器は証明できないナローイングを決して主張しません。偽値ミラーはマッチするキャリアをBotに縮退し残りを保ち、解析器がよりリッチなキャリアなしに分離を証明できないsupertypeケースで意図的に保守的なまま残ります。
  2. Rigor::Inference::Narrowing.predicate_scopesは3つの新しいPrism::CallNode形状を認識します。すなわちrecv.is_a?(C)recv.kind_of?(C)recv.instance_of?(C)です。レシーバーはMUSTPrism::LocalVariableReadNodeであり、単一引数はMUST静的な定数参照(Prism::ConstantReadNodeまたはPrism::ConstantPathNode)です。修飾名は定数パスの親ウォークを通じてレンダリングされます。is_a?/kind_of?exact: falseを、instance_of?exact: trueを使います。それ以外(非定数引数・複数引数呼び出し・非ローカルレシーバー)は「ナローイングなし」にフォールスルーするため、エントリースコープは両エッジで変更されずに観測されます。
  3. StatementEvaluator統合は変わりません。すなわちeval_if/eval_unless/eval_and_orはすでにpredicate_scopesを消費し、新しいカタログは同じ[truthy_scope, falsey_scope]形状を通じて表面化します。単項!解析器は再帰呼び出しの真値/偽値エッジをスワップするため、unless x.is_a?(Integer)!x.is_a?(Integer)は形式ごとのコードなしに同じ機構を再利用します。
  4. docs/internal-spec/inference-engine.mddocs/type-specification/control-flow-analysis.mdの型仕様ポインタは、これらのナローイングプリミティブの束縛契約のままMUST留まります。内部仕様は、新しいキャリア規則と新しいPrism::CallNodeカタログエントリーを列挙するためにこのコミットで更新されました。

具体的な挙動の向上(CLIスモークプローブを通じて検証):

  • Union[Integer, String].evaluate("if x.is_a?(Integer); x; else; x; end")は各分岐のxをそれぞれNominal[Integer]Nominal[String]として型付けします。ナローイング前は両分岐がベアunionを見ました。
  • Nominal[Numeric].evaluate("if x.is_a?(Integer); x; end")はthen分岐のxNominal[Integer]として型付けします(:superclass順序がsupertypeを問われたクラスへ下方に絞り込みます)。ナローイング前はsupertypeが保たれていたため、x.bit_lengthはディスパッチしませんでした。
  • unless x.is_a?(Integer); x; else; x; endは既存のeval_unlessハンドラを通じてエッジをスワップするため、Union[Integer, String]は形式ごとのコードなしにunless分岐をNominal[String]として解決します。

rigor type-scan libでのカバレッジ: 13.8%→13.5%認識されない。これは、解析器がプロダクトコード内のis_a?呼び出しでいくつかの残余フェイルソフトフォールバックを排除することを反映する小さな下方ステップです(MethodDispatcher.expected_block_param_typesクエリとNarrowing解析器の両方が、以前ナローイングサーフェスから落ちていたcase node when ...分岐を含みます)。

Phase 2 sub-phase 2(このサブフェーズはこのコミットで出荷) — 等価ナローイング+FactStore。追加されたコンポーネント:

  1. Rigor::Analysis::FactStoreは各Scopeスナップショットが運ぶ不変なファクト束です。初期のバケット語彙(local_bindingcaptured_localobject_contentglobal_storagedynamic_originrelational)・target/fact値オブジェクト・targetの無効化・両方の入力エッジに存在するファクトのみを保持する保守的なジョインを定義します。Scope#with_localは再束縛されたローカルのファクトを無効化します。Scope#with_factScope#local_factsScope#facts_forは、可変なバケットストレージを公開せずに狭いクエリサーフェスを公開します。
  2. Rigor::Inference::Narrowingnarrow_equal(type, literal)narrow_not_equal(type, literal)を成長させます。String/Symbol/Integerリテラルは、すでに有限な信頼できるリテラルドメイン内でのみ絞り込みます。nil/true/falseのシングルトン値は、Integer | nilのような混合ドメインから抽出できます。Floatリテラルと広いドメイン(StringDynamic[Top])は、捏造されたリテラル精度を獲得しません。
  3. Narrowing.predicate_scopesは信頼できる静的リテラルについてlocal == literalliteral == local!=ミラーを認識します。等価エッジは新しいプリミティブを通じてローカルを再束縛し、FactStore::Factを記録します。すなわち型が変わったときはlocal_binding、比較が記憶されるが値型を絞り込むのに十分信頼されないときはrelationalです。
  4. StatementEvaluator#eval_and_orは今やa && bunion(narrow_falsey(a), b)として、a || bunion(narrow_truthy(a), b)として型付けします。これは既存のRHSエントリーナローイングとnil注入された後置スコープジョインを保ちながら、RubyのスキップされたLHS値セマンティクスと一致します。
  5. クラスメンバーシップナローイングは今やEnvironment#class_orderingを使います。これは静的レジストリを参照し、それからRBS::Definition#ancestorsに対するRbsLoader#class_orderingを参照します。述語ナローイングはもはやアドホックなObject.const_getを行いません。RBS専用のプロジェクトクラスは、解析器ホストによってロードされなくても階層ナローイングに参加できます。

クロージャに捕捉されたローカルの無効化はSlice 6 phase C sub-phase 3に委ねられたままです。このサブフェーズはそれが必要とするFactStoreターゲット/無効化サーフェスを与えます。

DefNode対応スコープビルダー(Slice 3 phase 2フォローアップ)はRBSからメソッド引数を束縛しました。このスライスはその対称的な対応物をPrism::BlockNodeに対して出荷します。

Sub-phase 1(このサブフェーズはこのコミットで出荷) — 受信側メソッドのRBSシグネチャによって駆動されるブロック引数束縛。追加されたコンポーネント:

  1. Rigor::Inference::BlockParameterBinderは薄い値オブジェクトです。BlockParameterBinder.new(expected_param_types: [...])は位置ごとのRigor::Type配列を消費し、Prism::BlockParametersNode#parametersを歩くことでname -> Type束縛マップを生成します。required・optional・trailingの位置引数はインデックスで期待配列に対してマッチされます。rest(*r)・keyword(k:/k: 0)・keyword rest(**kw)・明示的ブロック(&blk)スロットは保守的な型付きデフォルト(それぞれArray[Dynamic[Top]]Dynamic[Top]Hash[Symbol, Dynamic[Top]]Nominal[Proc])を得ます。MultiTargetNode分解(|(a, b), c|)と番号付き引数(_1/_2)は委ねられます。バインダーは整形式のPrismブロックノードでMUST NOTraiseしません。
  2. Rigor::Inference::MethodDispatcher.expected_block_param_types(receiver_type:, method_name:, arg_types:, environment:)は、バインダーのexpected_param_types:配列を供給する標準クエリです。内部ではRbsDispatch.block_param_typesを使い、これは既存のOverloadSelectorblock_required: trueフラグで拡張、ブロック付き呼び出しがブロックなしオーバーロードを通じて束縛しないように)を通じてオーバーロードを選択し、RBS::Types::Block#type Functionを取り出し、そのrequired_positionals + optional_positionals引数をRigor::Type値に翻訳します。ジェネリック置換は戻り値型ティアが用いるのと同じtype_varsマップを通じて流れるため、Array#eachElemブロック引数はレシーバーのtype_argsを通じて解決されます。Unionレシーバーは、すべてのメンバーが構造的に等しいブロック引数リストを生成しない限り空配列に劣化します。
  3. Rigor::Inference::StatementEvaluatorPrism::CallNodeハンドラを追加します。ハンドラは:
    • 既存のScope#type_ofに呼び出しの値型を尋ねます(したがって定数畳み込み/シェイプ/RBSディスパッチチェーンが依然として適用され、MethodDispatcher.dispatchが戻り値型の単一の真実の源です)。
    • MethodDispatcher.expected_block_param_typesで呼び出しの期待ブロック引数配列をプローブします。
    • 外側スコープをバインダーの束縛で拡張することでブロックのエントリースコープを構築します(Rubyのレキシカルスコーピング規則: ブロックは外側のローカルを見ます。ブロック引数がその上に重ねられます)。
    • Prism::BlockNodeに再帰します(これはsub_eval(body, scope)に委ねる独自のハンドラを持ちます)。これによりノードごとのスコープインデックスが引数束縛を見ます。
    • レシーバースコープを変更せずに返します。したがってブロック効果は後置呼び出しスコープに漏れません。ブロック内のみで束縛されたローカルは、control-flow-analysis.mdのクロージャ捕捉規則が着地するまで意図的に外側からは見えません。

具体的な挙動の向上(CLIスモークプローブを通じて検証):

  • xs = [1, 2, 3]; xs.each { |x| y = x.succ }はブロック内のyNominal[Integer]として型付けします(ブロック引数xはタプル要素のunionに束縛され、Integer#succはディスパッチを通じて解決します)。束縛前はxが束縛されておらずx.succDynamic[Top]にフォールスルーしました。
  • [1, 2, 3].map { |n| n + 1 }のレシーバーnは同じタプル要素のunionとして型付けされます。したがってn + 1は各要素型上の定数畳み込みティアを通じて解決します。
  • foo { |x| x } — 受信側の呼び出しがRBSシグネチャを持たないとき、バインダーはxDynamic[Top]にデフォルトし、Slice 3 phase 2のフェイルソフト姿勢と一致します。

rigor type-scan libでのカバレッジ: 13.6%→13.5%認識されない(2 122 / 15 734ノード。ブロックが今やStatementEvaluatorのノードごとのスコープインデックスを通じて訪れられるため、合計ノード数が成長)。メトリックはRigor自身の定数参照が支配しており、それをさらに動かせるのはRBSオーサリング(候補A)のみです。

Sub-phase 2(このサブフェーズはこのコミットで出荷) — 分解ブロック引数・番号付き引数・ブロック戻り値型対応ディスパッチ。追加されたコンポーネント:

  1. Rigor::Inference::BlockParameterBinder#bind_required_paramPrism::MultiTargetNodeブロックターゲット(|(a, b), c|)を認識し、各分解スロットをスロットの期待要素型に対してRigor::Inference::MultiTargetBinderに委ねます。Type::Tupleスロットは要素ごとに分解されます。他のキャリアはすべての内部ローカルをDynamic[Top]に縮退します。内部ターゲットはブロック側でPrism::RequiredParameterNodeインスタンスです。MultiTargetBinderは両者が同じname:フィールドと同じ観測可能なセマンティクスを運ぶため、それらをいとこのPrism::LocalVariableTargetNodeと統一的に処理します。
  2. Rigor::Inference::BlockParameterBinder#bind_numbered_parametersPrism::NumberedParametersNodeを消費し、明示的な引数で用いるのと同じ位置ごとのexpected_param_types:配列によって駆動される:_1から:_maximumまでの束縛を実体化します。[1, 2, 3].map { _1 + 1 }は今や_1をレシーバーの射影された要素型に束縛するため、本体の_1 + 1は依然として精密な整数キャリアでディスパッチャーを参照します。
  3. Rigor::Inference::MethodDispatcher.dispatchは長く予約されていたblock_type:キーワードを尊重します。すなわち非nilのとき、RbsDispatch.try_dispatchはブロック付きオーバーロードを(OverloadSelectorblock_required: trueを介して)選択し、選択されたオーバーロードのブロック戻り値型が参照するメソッドレベル型パラメータを、戻り値型を翻訳する前にblock_typeに束縛します。配線は意図的に狭いです — 厳密なVariableブロック戻り形状のみが参加します — ため、ブロック戻り値がuntyped関数またはより精巧な型(例: タプル・構造的シェイプ)であるシグネチャは以前のフォールバックを保ちます。Array#map[U] { (Elem) -> U } -> Array[U]はこのスライスがブロック解除する標準的なケースです。
  4. Rigor::Inference::ExpressionTyper#call_type_forは単一のブロック対応ディスパッチサーフェスになります。すなわち呼び出しがPrism::BlockNodeを運ぶとき、StatementEvaluatorが構築するのと同じブロックエントリースコープ(外側スコープ + BlockParameterBinder.bind)を構築し、ブロック本体を型付けし、本体の型をblock_type:としてMethodDispatcher.dispatchに渡します。これによりStatementEvaluatorをループに含めることを要求せずに、すべての呼び出し位置(Scope#type_ofScopeIndexer・CLI rigor type-of / rigor type-scan)から結果型の向上が見えます。StatementEvaluatorのCallNodeハンドラは整合を保ちます。すなわち結果型についてはScope#type_ofに委ね、ノードごとのスコープインデックスのためにのみブロック本体を再評価します。

具体的な挙動の向上(CLIスモークプローブを通じて検証):

  • [1, 2, 3].map { |n| n.to_s }Array[String]として型付けされます(以前はArray[Elem]シェイプを通じて射影されたArray[Dynamic[Top]])。
  • [1, 2, 3].map { _1 + 1 }Array[Integer]として型付けされます。番号付き引数を束縛する前は、_1は束縛されていないローカルとして解決され、ブロック本体はDynamic[Top]に縮退しました。
  • arr.each_with_object({}) { |x, acc| acc[x] = true }は既存の射影の答えを保ちます(ブロック戻り値型はメソッドレベルの型変数ではないため、block_type:参加はきれいにフォールスルーします)。

クロージャに捕捉されたローカルの無効化はSlice 6 phase 2のFactStore作業と並んで着地します。それはこのサブフェーズのスコープ外です。

Slice 7 — リファインメント(最小)

Section titled “Slice 7 — リファインメント(最小)”

imported-built-in-types.mdからのnon-empty-stringpositive-intを持つRigor::Type::RefinedNominalを追加します。

モジュールスケッチ(Slice 1後)

Section titled “モジュールスケッチ(Slice 1後)”
lib/rigor/
├─ trinary.rb
├─ type.rb # ducktype module
├─ type/
│ ├─ top.rb
│ ├─ bot.rb
│ ├─ dynamic.rb
│ ├─ nominal.rb
│ ├─ constant.rb
│ ├─ union.rb
│ └─ combinator.rb # factory
├─ environment.rb # public entry
├─ environment/
│ └─ class_registry.rb # Slice 1 hardcoded built-ins
├─ scope.rb # public Scope#type_of
└─ inference/
└─ expression_typer.rb # AST → Type

Slice 2はlib/rigor/inference/method_dispatcher.rblib/rigor/inference/method_dispatcher/constant_folding.rbを追加します。Slice 4はlib/rigor/environment/rbs_loader.rbMethodDispatcher内のRBSバックディスパッチティアを追加します。Slice 6はlib/rigor/analysis/fact_store.rbを追加します。lib/rigor/analysis/ディレクトリは診断とランナーコードを保持し続けます。推論エンジンはlib/rigor/inference/下の別の関心事です。

class Rigor::Scope
def self.empty(environment:)
def with_local(name, type)
def local(name) # Rigor::Type or nil
def type_of(node) # Rigor::Type
def environment
end
module Rigor::Type::Combinator
def self.union(*types)
def self.dynamic(static_facet)
def self.nominal_of(class_object)
def self.constant_of(value)
end

Slice 1のサーフェスはinternal-type-api.mdのメソッドサーフェス契約と整合的です。後続のスライスはScope#type_ofの形状を変えることなくRigor::Type::CombinatorRigor::Inference::*に追加します。

ADR-15との境界(Ractor並行性モデル)

Section titled “ADR-15との境界(Ractor並行性モデル)”

ADR-15はアナライザーの段階的なRactorベース並行性モデルにコミットします。その移行のフェーズ2bはRigor::Environment::RbsLoaderを次に分割します:

  • Environment::Reflection — 読み取り専用RBSクエリサーフェス(class_known?instance_definitionsingleton_definitionclass_ordering、…)。構築後に凍結。Ractor間で共有。
  • Environment::CacheLayer — リフレクションファサードをラップするRactorごとの可変メモ化Hash(@class_known_cache@instance_definition_cache、…)。各Ractorが自身のものを所有;レイヤーはクロスRactorのCache::Storeからフィードされるため、ウォームアップがワーカープール全体で償却される。

上記で文書化されたScope契約は変更されません。Scope#environmentは引き続き同じ公開リードAPI(class_known?instance_definitionsingleton_definition、…)を公開する;ディスパッチはキャッシュレイヤー経由で行き、リフレクションファサードに遅延ルーティングされる。プラグイン作者がscope.environment.*を読むと、リファクタ全体で同一の戻り値が見える。

この境界は、Scopeから環境への配管をたどる読者がフェーズ2b分割のコンテキストにワンクリックで到達できるよう、ここに文書化されています。

  • 暫定OQ回答は後で覆る可能性があります。プロダクションコード経路はType::Combinatorを通じてルーティングされます。直接の型クラスコンストラクタは内部専用の脱出口です。CIリントは?サフィックス付きメソッドがTrinaryを返さないようガードします。Slice 1で追加されるケイパビリティ述語は最小なので、リネームは機械的です。
  • Prism APIの進化。型付け器はビジター継承ではなくRubyのパターンマッチング(case node in Prism::IntegerNode)を使うため、Prismクラス階層を拡張しません。将来のPrismリリースは型付け器を局所的に破ります。
  • RBS環境の起動コスト。RBSロードはSlice 4に委ねられます。Slice 1はハードコードレジストリで出荷し、Slice 2は定数畳み込み規則のみに依存します。Slice 4のローダーは実行とテストにわたるキャッシュを許すためにラップされています。
  • フェイルソフトなDynamic[Top]がリグレッションをマスクすること。Slice 1以降、型付け器はDynamic[Top]にフォールバックするときオプショナルにDiagnostic::Traceを記録します。トレースはノイズを避けるためオプトインですが、後のスライスがカバレッジリグレッションを検出できるよう配管されています。
  • スコープのエルゴノミクスevaluate(node, scope)から[Type, Scope']を返すのは(Slice 3)冗長です。明示的な不変性と引き換えに冗長性を受け入れます。ヘルパービルダー(scope.evaluate(node) { |type| ... })は2つか3つの呼び出し位置が存在し次第MAY追加できます。

外部(PHPStanソースコード、Rigorのサブモジュールの一部ではない):

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