コンテンツにスキップ

ADR-3: 内部型表現

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

内部の型オブジェクトレイアウト(OQ1〜OQ3の作業決定を含む)はライブである;Rigor::Type::*キャリア(carrier)とdocs/internal-spec/internal-type-api.md契約(contract)がそれを実現する。

ADR-3は、Rigorの内部型オブジェクトレイアウト(型モデルを実装するRubyのクラス、モジュール、メソッド、値オブジェクト)の設計空間を記録します。ADR-3はセマンティクスを再定義しません——それはADR-1と型仕様が所有します——そしてプラグイン契約(contract)も定義しません——それはADR-2が所有します。ADR-3は、ADR-1とADR-2が付着する解析器側のデータ形状を取り巻く根拠とオープンクエスチョンを捉えます。

安定した決定はdocs/internal-spec/internal-type-api.mdにおいて規範的です。そのドキュメントとこのADRが一致しない場合、仕様が拘束力を持ち、このADRはそれに合わせて更新されます。型仕様についても同様です。docs/type-specification/がこのADRと観察可能な動作について一致しない場合、型仕様が拘束力を持ちます。

Rigorは、垂直スライス(slice)実装が着地する前に内部型表現が必要です。型仕様は、表現がカバーしなければならない形式を列挙するのに十分なほど安定しています(docs/type-specification/rbs-compatible-types.mddocs/type-specification/rigor-extensions.mddocs/type-specification/special-types.mddocs/type-specification/structural-interfaces-and-object-shapes.mdを参照)。ADR-1は関係と動的起源代数を決定します(docs/adr/1-types.mddocs/type-specification/relations-and-certainty.mddocs/type-specification/value-lattice.md)。ADR-2は型値を消費する拡張サーフェス(surface)を決定します(docs/adr/2-extension-api.md、特に「型システムオブジェクトモデル」と「Scopeオブジェクト」セクション)。

残る決定は、解析器がRubyコード内でそれらの形式をどのように表現するか——どのクラスが存在するか、メソッドがどのようにグループ化されるか、関係的な回答がどのように返されるか、そして「決定済み」と「実装に委ねる」の境界がどこにあるべきか——です。

最も近い実用的な参照は、phpstan/phpstan-srcのPHPStanのTypeインターフェースとそのTrinaryLogicコンパニオンです。代表的なアップストリームパス:

phpstan-srcリポジトリはRigorのサブモジュールの一部ではありません——references/phpstanはウェブサイト(website/)のみを持ちます——そのため、これらの引用は外部ポインタです。Rigorのチェックアウト内のreferences/phpstan/website/src/developing-extensions/type-system.mdドキュメントが最も近いリポジトリ内の説明です。

Rigorがこの参照から採用するパターンは、後でどのオープンクエスチョンが解決されるかに関わらず:

  • 型を切り替えるためにinstanceofを使わない。PHPStanのインターフェースコメントは明示的です: 呼び出し元は$type instanceof StringTypeではなく$type->isString()->yes()と聞きます。Rigorは同じルールに従います。具体的なクラスは実装の詳細です。
  • モナドのような証人リストとしての空/非空配列。PHPStanのgetConstantStrings(): list<ConstantStringType>のようなメソッドは、解析器が定数文字列の証人を証明できない場合に空の配列を返し、そうでなければ非空のリストを返します。ユニオン(union、合併型とも)と積はメンバーリストを組み合わせることで合成します。Rigorはリファインメントプロジェクション用にこのパターンを採用します。
  • ブール値から分離された3値の結果。ケイパビリティの質問は3値の結果(yes/no/maybe)を返します。特定の結果クラスのみがその値を理由でラップします。RigorはRubyイディオムで同じ分離を採用します。
  • サブクラスではなくラッパーとしての複合型。PHPStanのIntersectionTypeUnionTypeGenericObjectTypeConstantArrayType、そしてアクセサリー型は、内部のType参照を保持することで合成します。Rigorのラッパー(DynamicRefinedUnionIntersectionDifference、汎用キャリア)は同じ合成に従います。

PHPStanはコードの再利用のために内部的にクラス継承も使用します(例: ConstantStringType extends StringType)。Rigorはここで意図的に分岐します: Rigorの型表現は型クラス間に継承がありません。次のセクションでその理由を説明します。

RigorはRubyを対象とします。Rubyの3つの特性がPHPStanモデルからの偏差を駆動します:

  • すべての値はオブジェクトです。PHPのスカラー値とオブジェクト値の分割はRubyには存在しません。整数リテラル1はすでに1.class == Integerを通じてクラス情報を持ちます。「定数文字列」型と「定数整数」型は原則として、識別がvalue.classである単一のRubyキャリアを共有できますが、クラスごとのレイアウトも可能です。これはオープンクエスチョン1の実質です。
  • ?サフィックスのメソッドは慣例的にブール値を返します。Rubyの読者はstring?trueまたはfalseを返すことを期待します。Rigorのケイパビリティクエリは3値の結果を返します。命名規則は?を削除するか、ローカルで再定義するか、または2つの並行サーフェスを公開する必要があります。これはオープンクエスチョン2の実質です。
  • ミックスイン基盤の合成が慣用的です。Rubyモジュールはクラス階層を強制せずにトレイトのような動作を共有できます。Rigorはモジュールを、型分類ではなく共有の構造的等値性とidentity契約に狭く使用します。

このADRの残りは、設計の根拠、作業決定、却下/延期された選択肢、および計画チェックリストを記録します。安定した決定はdocs/internal-spec/internal-type-api.mdにおいて規範的です。このADRとそのドキュメントが一致しないように見える場合、仕様が拘束力を持ちます。

内部型表現の決定済みの部分——不変値オブジェクト、構造的等値性、型クラス間の継承なし、Rigor::Trinaryを返すケイパビリティクエリ、Array<Type>を返すリファインメントプロジェクション、ラッパーとしての複合形式、結果オブジェクトを返す関係クエリ、ファクトリールートの正規化、メソッドサーフェス、モジュールレイアウト、診断表示ルーティング——はdocs/internal-spec/internal-type-api.mdにおいて規範的です。エンジンとプラグインのコードはそのドキュメントに従わなければなりません(MUST)。このADRは設計の根拠、以下の却下/延期オプション、および計画チェックリストのために保持されます。移動した契約に対して拘束力があるものとして扱ってはなりません(MUST NOT)。

それらの型オブジェクトを取り囲むエンジンサーフェス契約(Scope、ファクト(fact)ストア、エフェクトモデル、ケイパビリティロール推論、正規化、RBS消去ルーティング、公開安定性ルール)はdocs/internal-spec/implementation-expectations.mdにおいて規範的です。

3つの設計上の問いは、最初は選ばれた回答を実際のコードで試せるように延期されていました。最初の2つは既存の実装によって解決されています。3つ目は後続のスライスが一貫したターゲットを持てるように設計レベルで解決されています。以下の各セクションは作業決定、それを着地させた根拠、および却下/延期された選択肢をその元の「検討されたオプション」形式で記録します。

オープンクエスチョン1: 定数スカラーとオブジェクトシェイプ

Section titled “オープンクエスチョン1: 定数スカラーとオブジェクトシェイプ”

解析器が値が特定のRubyリテラル(1"aaa":symtruefalsenil)と等しいことを証明できる場合、その事実はどのように型表現に持ち込まれるべきですか?

作業決定: オプションC(ハイブリッド)。単一のRigor::Type::Constantクラスがスカラーリテラル(IntegerFloatStringSymbolRationalComplextruefalsenil、整数エンドポイントのRange)を持ちます。専用のキャリア(TupleHashShapeIntegerRangeなど)は、内部構造が単一のRuby値に圧縮できない複合形状とリファインメント形状を持ちます。実装はlib/rigor/type/constant.rbSCALAR_CLASSESが受け入れるクラスを列挙)、lib/rigor/type/tuple.rblib/rigor/type/hash_shape.rblib/rigor/type/integer_range.rbにあります。

ハイブリッドは元の分析でスコアが最も高かったのと同じ理由で着地しました。スカラーキャリアはコンパクトでRubyイディオムを保ち、既存のキャリアはすでに内部型参照とシェイプ(shape)ポリシーのために独自の構造を必要とし、「スカラーリテラル」と「複合シェイプ」の境界はrigor-extensions.mdの概念的な分離と一致します。

検討して却下されたオプション:

  • オプションA — 統一キャリア(複合リテラルを含むすべてのものに単一のConstant)。複合シェイプ(TupleHashShapeRecord)は内部のRigor::Type参照とelement単位のポリシーを持ち、単一のRuby値に圧縮できないため却下。統一されたConstantはすべてのインスタンスにそれらのポリシーを埋め込む必要があり、スカラーキャリアとシェイプポリシーを混同します。
  • オプションB — Rubyクラスごとの専門化(String::ConstantInteger::Constantなど)。クラスごとのレイアウトはサポートされるリテラル種類に比例してクラス数を増やし、Rubyでそのレイアウトが購入するクラスごとの動作は何も必要ありません——統一キャリアでのvalue.classディスパッチはクラスパターンマッチと同様に直接的であり、リファインメントプロジェクションは統一されたシェイプに対してクリーンに合成するため却下(OQ3の作業決定を参照)。

オープンクエスチョン2: 3値を返す述語の命名

Section titled “オープンクエスチョン2: 3値を返す述語の命名”

ケイパビリティメソッドはRigor::Trinaryを返し、Rubyのブール値ではありません。Rubyの慣例は?サフィックスのメソッドがブール値を返すことです。この2つの事実が衝突します。

作業決定: オプションA(3値を返すメソッドの?を削除)。型側のケイパビリティと関係クエリはRigor::Trinarytype.toptype.bottype.dynamic)または結果オブジェクト(type.accepts(other, mode:)Type::AcceptsResultを返す)を返す名詞/動詞形式です。ブールクエリ——Trinary自体を含む——は、それが本当にブール値を返すから?サフィックスを保持します(Trinary#yes?Trinary#no?Trinary#maybe?AcceptsResult#yes?/#no?/#maybe?)。

実装はスライス1からこのルールと一致しています。すべてのRigor::Typeキャリアはtopbotdynamic(3値を返す、?なし)とaccepts(other, mode:)(結果オブジェクトを返す)を公開し、Trinaryはブールプロジェクションのためにyes?/no?/maybe?を公開します。

クロスカット要件は引き続き有効です:

  • Rigor::Trinary値オブジェクトはyes?no?maybe?メソッドを持たなければなりません(MUST)。
  • Rigor::Typeのすべてのメソッドが3値を返す場合、このルールに従わなければなりません(MUST)(クラスごとの偏差なし)。
  • ケイパビリティサーフェスと関係サーフェスは一致します。

検討して却下されたオプション:

  • オプションB — ?を保持して偏差を文書化。 ?サフィックスのメソッドから非ブール値を静かに返すことは広く保持されているRubyの期待と矛盾し、貢献者、RuboCop/Rubyルール、RBS著者、IDEのインレイヒントを混乱させるため却下。
  • オプションC — デュアルAPI(3値のためのtype.string.yes?上のBool糖衣構文のためのtype.string?)。サーフェスを2倍にし、呼び出し元が?をデフォルトに使用してmaybeを認識する動作を静かに失う誘惑を与え——relations-and-certainty.mdが警告する正確な失敗モード——2つの並行サーフェスを同期する保守負担はすべての新しいクエリメソッドとともに増大するため却下。

オープンクエスチョン3: リファインメントキャリア戦略

Section titled “オープンクエスチョン3: リファインメントキャリア戦略”

imported-built-in-types.mdはリファインメント名のカタログを予約します——non-empty-stringlowercase-stringnumeric-stringdecimal-int-stringpositive-intnon-empty-array[T]non-empty-hash[K, V]など——これらは既存の名前的型(nominal type、公称型とも)のサブセットを命名します。問いは、解析器がそれらのサブセットをどのように内部的に表現するかです。

作業決定: オプションC(2層ハイブリッド: 点除去Difference、述語サブセットRefined。カタログは自然な数学的境界に沿って分割されます:

  • 点除去リファインメント — 値集合が基底型から有限で静的に記述可能な値の集合を引いたもの — 既存のDifference[BaseType, RemovedSet]キャリアを使用:
    • non-empty-string = String - ""
    • non-zero-int = Integer - 0
    • non-empty-array[T] = Array[T] - []
    • non-empty-hash[K, V] = Hash[K, V] - {}
    • positive-intnon-negative-intはすでにIntegerRangeを通じて実現されています。
  • 述語サブセットリファインメント — 値集合が要素ごとの述語によって定義される — 基底型と述語識別子をラップするType::Refinedキャリアを使用:
    • lowercase-string = Refined[String, :lowercase]
    • uppercase-string = Refined[String, :uppercase]
    • numeric-string = Refined[String, :numeric]
    • decimal-int-stringoctal-int-stringhex-int-string — それぞれRefined[String, :…]
    • ADR-2を介したプラグイン提供の述語リファインメント

複合リファインメント名はIntersectionを通じて合成します: non-empty-lowercase-string = Difference[String, ""] & Refined[String, :lowercase]

正規名レジストリはケバブケース名をそのキャリア形状(DifferenceまたはRefined+述語)にマッピングします。

ステータス(v0.0.4以降):両方の半分と合成Intersectionキャリアはv0.0.4で出荷されました。Type::Differencelib/rigor/type/difference.rbType::Refinedlib/rigor/type/refined.rbType::Intersectionlib/rigor/type/intersection.rbにあります。

検討して却下されたオプション:

  • オプションA — アクセサリーキャリアごとのIntersectionType PHPStanスタイル。リファインメントごとのクラス成長はカタログに対して無限であり、点除去半分のDifferenceと述語半分の共有Refinedキャリアによってすでに表現可能な動作をクラスごとのレイアウトが購入するため却下。
  • オプションB — Difference型のみ。述語定義のリファインメント(lowercase-stringnumeric-string)をカバーできないため却下。

このカタログは規範的ではありません。型仕様が計画された表現によってカバーされているかのチェックリストです。

  • 特殊型: TopBotDynamicVoidUntypedは構築時にDynamic[Top]に解決されます。
  • 名前的型: NominalSingletonSelfInstanceClassMarker
  • 構造的型: InterfaceObjectShapeCapabilityMethodSignatureProcSignatureBlockSignature
  • コンテナ: ArrayShapeTupleHashShapeRecord
  • 定数: Constantはスカラーリテラルを持ちます(OQ1オプションCで解決)。
  • コンビネータ: Union(実装済み)、IntersectionDifferenceComplement
  • リファインメント: OQ3オプションCごとに、リファインメントは2層に分割されます。点除去はDifference[BaseType, RemovedSet]を使用。述語サブセットはRefined[BaseType, predicate]を使用。IntegerRangeは専用の有界整数キャリアとして残ります。
  • 汎用位置キャリア: GenericTemplateParameterVariance

すべてのエントリーはdocs/internal-spec/internal-type-api.mdのメソッドサーフェスを満たさなければなりません(MUST)。

ロードマップは情報提供のためであり、規範的ではありません。

OQ1+OQ2キャリアは着地しています: Type::ConstantType::TupleType::HashShapeType::IntegerRangeType::UnionType::TopType::BotType::DynamicType::NominalType::Singleton、3値を返すケイパビリティメソッド、Type::AcceptsResultなど。

OQ3はType::Differenceの着地(点除去半分)とType::Refined(述語サブセット半分)によって解決されています。

ステータス(v0.0.4以降):両方のフォローアップスライスが出荷しました。Type::Refined[base, predicate_id](述語レジストリ、正規名テーブル、カタログ層プロジェクションルール付き)とType::Intersection(合成されたnon-empty-lowercase-string/non-empty-uppercase-string名のため)がすべて着地しました。6つの述語リファインメントがカタログ化されています(lowercase-stringuppercase-stringnumeric-stringdecimal-int-stringoctal-int-stringhex-int-string)。

Rigorドキュメント:

背景となる研究ノート(外部文献のRigor観点レビュー):

  • docs/notes/20260518-matsumoto-2008-poly-records-rigor-review.md — 松本&南出2008(Ruby向けGarrigueカインド付き多相レコード)のレビュー。80行のプログラムに対して約57k個の束縛型変数まで膨張する型変数爆発と、ADR-3の名前的型優先のキャリア選択が回避している構造的レコードの限界を記録する。
  • docs/notes/20260518-matsumoto-2010-cfa-rigor-review.md — 松本&南出2010(SemiRubyに対するセミフローセンシティブなRuby CFA)のレビュー。Rigorのウォーカーごとの静的ディスパッチャー層と、論文のプログラムポイントごとのメソッド設定を対比する。

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