123456789_123456789_123456789_123456789_123456789_

TypeProf: 抽象解釈に基づくRubyの型解析器

TypeProfの使い方 - CLIツールとして

app.rb を解析する。

$ typeprof app.rb

一部のメソッドの型を指定した sig/app.rbs とともに app.rb を解析する。

$ typeprof sig/app.rbs app.rb

典型的な使用法は次の通り。

$ typeprof sig/app.rbs app.rb -o sig/app.gen.rbs

TypeProfの使い方 - Language Serverとして

RubyKaigi 2024の発表資料を参照ください。

TypeProfの解析方法

TypeProfは、Rubyプログラムを型レベルで抽象的に実行するインタプリタです。 解析対象のプログラムを実行し、メソッドが受け取ったり返したりする型、インスタンス変数に代入される型を集めて出力します。 すべての値はオブジェクトそのものではなく、原則としてオブジェクトの所属するクラスに抽象化されます(次節で詳説)。

メソッドを呼び出す例を用いて説明します。

def foo(n)
  p n      #=> Integer
  n.to_s
end

p foo(42)  #=> String

TypeProfの解析結果は次の通り。

$ typeprof test.rb
# Revealed types
#  test.rb:2 #=> Integer
#  test.rb:6 #=> String

# Classes
class Object
  def foo : (Integer) -> String
end

foo(42)というメソッド呼び出しが実行されると、Integerオブジェクトの42ではなく、「Integer」という型(抽象値)が渡されます。 メソッドfoon.to_sが実行します。 すると、組み込みメソッドのInteger#to_sが呼び出され、「String」という型が得られるので、メソッドfooはそれを返します。 これらの実行結果の観察を集めて、TypeProfは「メソッドfooIntegerを受け取り、Stringを返す」という情報をRBSの形式で出力します。 また、pの引数はRevealed typesとして出力されます。

インスタンス変数は、通常のRubyではオブジェクトごとに記憶される変数ですが、TypeProfではクラス単位に集約されます。

class Foo
  def initialize
    @a = 42
  end

  attr_accessor :a
end

Foo.new.a = "str"

p Foo.new.a #=> Integer | String
$ typeprof test.rb
# Revealed types
#  test.rb:11 #=> Integer | String

# Classes
class Foo
  attr_accessor a : Integer | String
  def initialize : -> Integer
end

TypeProfの扱う抽象値

前述の通り、TypeProfはRubyの値を型のようなレベルに抽象化して扱います。 ただし、クラスオブジェクトなど、一部の値は抽象化しません。 紛らわしいので、TypeProfが使う抽象化された値のことを「抽象値」と呼びます。

TypeProfが扱う抽象値は次のとおりです。

クラスのインスタンスはもっとも普通の値です。 Foo.newというRubyコードが返す抽象値は、クラスFooのインスタンスで、少し紛らわしいですがこれはRBS出力の中でFooと表現されます。 42という整数リテラルはIntegerのインスタンス、"str"という文字列リテラルはStringのインスタンスになります。

クラスオブジェクトは、クラスそのものを表す値で、たとえば定数IntegerStringに入っているオブジェクトです。 このオブジェクトは厳密にはクラスClassのインスタンスですが、Classに抽象化はされません。 抽象化してしまうと、定数の参照やクラスメソッドが使えなくなるためです。

シンボルは、:fooのようなSymbolリテラルが返す値です。 シンボルは、キーワード引数、JSONデータのキー、Module#attr_readerの引数など、具体的な値が必要になることが多いので、抽象化されません。 ただし、String#to_symで生成されるSymbolや、式展開を含むSymbolリテラル(:"foo_#{ x }"など)はクラスSymbolのインスタンスとして扱われます。

untypedは、解析の限界や制限などによって追跡ができない場合に生成される抽象値です。 untypedに対する演算やメソッド呼び出しは無視され、評価結果はuntypedとなります。

抽象値のユニオンは、抽象値に複数の可能性があることを表現する値です。 人工的ですが、rand < 0.5 ? 42 : "str"の結果はInteger | Stringという抽象値になります。

コンテナクラスのインスタンスは、ArrayやHashのように他の抽象値を要素とするオブジェクトです。 いまのところ、ArrayとEnumeratorとHashのみ対応しています。 詳細は後述します。

Procオブジェクトは、ラムダ式(-> { ... })やブロック仮引数(&blk)で作られるクロージャです。 これらは抽象化されず、コード片と結びついた具体的な値として扱われます。 これらに渡された引数や返された値によってRBS出力されます。

TODO: write more