Ruby 3の型解析に向けた計画
遠藤 侑介
名古屋Ruby会議04 (2019/06/08)
1
自己紹介:遠藤侑介 (@mametter)
•クックパッドで働く
フルタイムRubyコミッタ
•Rubyプログラムを堅牢に
•テスト、Ruby 3の静的解析
•クックパッドに興味あったら
お声がけください
2
この発表では
•Ruby 3の静的解析のmatz構想
•型プロファイラの紹介と進捗
を話します
3
Rubyの型と名古屋の関係
•三浦さんからmrubyでの型推定の話を聞いて
Ruby 3の静的解析は回りだした
•型プロファイラはmruby-meta-circularの
パクリ
•細かい設計指針の違いはあとで
4
Ruby 3の静的解析のmatz構想
•目的: バグっぽいコードを指摘する
•高速化のための解析ではない(間違ってもいい)
•要件: Rubyのプログラミング体験を維持する
•型を書かない選択肢を残す
5
Ruby 3の静的解析の構想
1. 標準の型シグネチャフォーマット
2. 型シグネチャなし型検査器
+ 型シグネチャのプロトタイプ生成器
3. 型シグネチャあり型検査器
6
1. 型シグネチャフォーマット
•Rubyコードの型情報を示すもの
class Array[A]
include Enumerable
def []: (Integer) -> A
def []=: (Integer,A) -> A
def each:
{ (A) -> void } -> self
...
end
interface
generics
union type
option type
any type
Proposal:
ruby-signature
7
2. 型シグネチャなし型検査
•型エラーの可能性を指摘する解析器
•アプリの型シグネチャが無くても動作する
•false positive は許容する
def foo(s)
s.gsuub!(//, "")
s + 42
end
foo("foo")
NoMethodError?
TypeError?
Proposals:
• mruby-meta-circular
• 型プロファイラ
8
2'. 型シグネチャプロトタイプ生成
•無注釈コードから型シグネチャを推測する
•メソッドの呼び出しを追う
def foo(s)
s.to_s
end
foo("foo")
foo(42)
(String | Integer)
-> String ?
Proposals:
• mruby-meta-circular
• 型プロファイラ
9
3. 型シグネチャあり型検査
•型エラーの可能性を指摘する解析器
•型シグネチャとコードの整合性を検査する
•わりと普通の型検査
•ツール定義の
inline annotation も def foo(s)
s.gsuub!(//, "")
s + 42
end
TypeError!
def foo:
(String) -> Integer
Proposals:
• Steep
• Sorbet
NoMethodError!
10
Ruby 3の静的解析の図
Library code
type signature
Sorbet
Steep
RDL
Type error
warnings
Application code
mmc
Type Profiler
Type error
warnings
type signature
11
ユースケースに合わせた使い方
•緩く検査したい派:型シグネチャなし型検査
•きっちり検査したい派:
•コーディング→型シグネチャプロトタイプ→
型シグネチャあり型検査
•型ドリブン開発派:
•型シグネチャ手書き→コーディング→
型シグネチャあり型検査
12
Ruby 3の静的解析の進捗
•ruby-signature: ツール開発者間で議論中
•https://github.com/ruby/ruby-signature
•Type-Profiler: 試験実装状態
•mruby-meta-circular: (次の発表で)
•Steep: Siderで試用中、型注釈が結構必要
•Sorbet: Stripeで試用中、duck typingがない
13
Ruby 3には何が入る?
•「型シグネチャ」はバンドルしたい
•型シグネチャフォーマットのパーサ
•標準添付ライブラリの型シグネチャファイル
•型シグネチャ対応版RubyGems
•ツール群はバンドルの予定なし
•当面は外部ライブラリとして提供される
14
型プロファイラの
紹介と進捗
15
型プロファイラとは
•目的:型レベルテストとシグネチャ推定
•型エラーの可能性を指摘する
•型シグネチャのプロトタイプを生成する
•対象:型注釈のない素のRubyコード
16
型エラーの指摘
•NoMethodErrorやTypeErrorの可能性を示す
def foo(n)
if n < 10
n.timees {|x|
}
end
end
foo(42)
Type
Profiler
t.rb:3: [error] undefined method:
Integer#timees
Typo
17
型シグネチャ推定
•型シグネチャのプロトタイプを生成する
def foo(n)
n.to_s
end
foo(42)
Type
Profiler
Object#foo ::
(Integer) -> String
18
型プロファイラの動作イメージ
•Rubyコードを「型レベル」で実行する
普通のインタプリタ
def foo(n)
n.to_s
end
foo(42)
Calls w/
42
Returns
"42"
型プロファイラ
def foo(n)
n.to_s
end
foo(42)
Calls w/
Integer
Returns
String
Object#foo ::
(Integer) -> String
19
デモ:オーバーロードの例
def my_to_s(x)
x.to_s
end
my_to_s(42)
my_to_s("STR")
my_to_s(:sym)
Type
Profiler
Object#my_to_s :: (Integer) -> String
Object#my_to_s :: (String) -> String
Object#my_to_s :: (Symbol) -> String
20
デモ:再帰関数の例
def fib(n)
if n > 1
fib(n-1) + fib(n-2)
else
n
end
end
fib(10000)
Type
Profiler
Object#fib ::
(Integer) -> Integer
21
デモ:ユーザ定義クラスの例
class Foo
end
class Bar
def make_foo
Foo.new
end
end
Bar.new.make_foo
Type
Profiler
Bar#make_foo :: () -> Foo
22
型プロファイラと分岐
•実行を「フォーク」する
def foo(n)
if n < 10
n
else
"error"
end
end
foo(42)
Fork!
イマココ
n<10 の真偽は
わからない
Returns
Integer
Returns
String
Object#foo ::
(Integer) ->
(Integer | String)
23
最大の問題:状態爆発
•RubyKaigi時点(4月)の実験結果
•型プロファイラの型プロファイル:約10分
•optcarrotの型プロファイル:約3分
a=b=c=d=e=nil
a = 42 if n < 10
b = 42 if n < 10
c = 42 if n < 10
d = 42 if n < 10
e = 42 if n < 10
Fork!
Fork!
Fork!
Fork!
Fork!
2
4
8
16
32
状態数
24
RubyKaigi からの進捗 (1)
•解析方式を大きく変えた(後述)
•state merging 風の改良を加えた
•改善後の実験結果
•セルフ型プロファイル:約10分 ➔ 約2.5秒
•optcarrotの型プロファイル:約3分 ➔ 約6秒
25
解析にかかる時間
0
200
400
600
800
self-profiling optcarrot
seconds
old new
26
RubyKaigi からの進捗 (2)
•Flow sensitiveな解析
•右の例で警告が出ない
•分岐で単純フォークすると
誤警告が出てしまう
def foo(x)
if x.is_a?(Integer)
x + 42
else
x + "str"
end
end
foo(42)
foo("str")
27
解析アルゴリズム
•環境:各変数がとりうる型
•{ x: Integer or String, y: Integer } とか
•行ごとの「環境」を更新していく
•正確に言うとYARVのバイトコードごと
•「環境」が更新されなくなったら終わり
28
解析の例
1: def foo(a)
2: if a < 10
3: b = 42
4: else
5: b = "str"
6: end
7: c = b
8: c
9: end
10:
11: ret = foo(42)
行番号 a b c
1 ∅ ∅ ∅
2 ∅ ∅ ∅
3 ∅ ∅ ∅
4 - - -
5 ∅ ∅ ∅
6 -
7 ∅ ∅ ∅
8 ∅ ∅ ∅
行番号 a b c
1 { Int } ∅ ∅
2 ∅ ∅ ∅
3 ∅ ∅ ∅
4 - - -
5 ∅ ∅ ∅
6 -
7 ∅ ∅ ∅
8 ∅ ∅ ∅
行番号 a b c
1 { Int } ∅ ∅
2 { Int } ∅ ∅
3 ∅ ∅ ∅
4 - - -
5 ∅ ∅ ∅
6 -
7 ∅ ∅ ∅
8 ∅ ∅ ∅
行番号 a b c
1 { Int } ∅ ∅
2 { Int } ∅ ∅
3 { Int } ∅ ∅
4 - - -
5 { Int } ∅ ∅
6 -
7 ∅ ∅ ∅
8 ∅ ∅ ∅
行番号 a b c
1 { Int } ∅ ∅
2 { Int } ∅ ∅
3 { Int } ∅ ∅
4 - - -
5 { Int } ∅ ∅
6 -
7 { Int } { Int } ∅
8 ∅ ∅ ∅
行番号 a b c
1 { Int } ∅ ∅
2 { Int } ∅ ∅
3 { Int } ∅ ∅
4 - - -
5 { Int } ∅ ∅
6 -
7 { Int } { Int } ∅
8 ∅ ∅ ∅
行番号 a b c
1 { Int } ∅ ∅
2 { Int } ∅ ∅
3 { Int } ∅ ∅
4 - - -
5 { Int } ∅ ∅
6 -
7 { Int } { Int, Str } ∅
8 ∅ ∅ ∅
行番号 a b c
1 { Int } ∅ ∅
2 { Int } ∅ ∅
3 { Int } ∅ ∅
4 - - -
5 { Int } ∅ ∅
6 -
7 { Int } { Int, Str } ∅
8 ∅ ∅ ∅
行番号 a b c
1 { Int } ∅ ∅
2 { Int } ∅ ∅
3 { Int } ∅ ∅
4 - - -
5 { Int } ∅ ∅
6 -
7 { Int } { Int, Str } ∅
8 { Int } { Int, Str } { Int, Str }
Object#foo ::
(Int) -> (Int|Str)
29
他の問題
•嘘の警告・推定が出ることがある
•原理的に扱えない言語機能がある
•sendメソッド・特異クラス
30
# b: Integer or String
c = b
# c: Integer or String
# 「bとcの型は同じ」という条件が消えるので
b + c # 「Integer + String の可能性あり!」
mmcとの方針の違い
•mmc: ヒューリスティクス盛りだくさん
•ターゲットアプリで正しい推定をするように
•mmcを更新したら推定結果が結構変わったり
•型プロファイラ:理解可能な動きをすること
•解析方法を複雑化し、正しく推定するより
•解析方法を単純化し、なぜ間違えるか
ユーザが理解できるようにしたい
31
実装進捗:できてること
•基本的な言語機能のサポート
•変数、メソッド、ユーザ定義クラスなど
•一部の組み込みクラス
•ブロックと配列(たぶん)
•解析アルゴリズムの基本設計
32
実装予定:やっていくこと
•言語機能のサポート
•ハッシュ
•複雑な引数(可変長やキーワード)
•例外
•モジュール
•型シグネチャ言語の読み書き
•実用性向上
33
関連研究
•mruby-meta-circular (Hideki Miura)
•型プロファイラの元ネタ
•HPC Ruby (Koichi Nakamura)
•抽象解釈でRubyからCへ変換(HPC向け)
•pytype (Google's unofficial project)
•型解析のための抽象解釈器
34
謝辞
•Hideki Miura
•Matz, Akr, Ko1
•PPL paper co-authors
• Soutaro Matsumoto
• Katsuhiro Ueno
• Eijiro Sumii
•Stripe team & Jeff Foster
•And many people
35
まとめ
•Ruby 3の静的解析の構想を説明しました
•型プロファイラの概略を説明しました
•お便りお寄せください!
•https://github.com/mame/ruby-type-profiler
36

Ruby 3 の型解析に向けた計画