ytljitの型推論の説明(その3)

しつこいようですが、シグネチャーはメソッド呼び出しの際の引数を推論した結果の型オブジェクトの配列です。そして、型推論の際には必ずシグネチャが必要になります。
この2つのことから勘のいい人は気づくかも知れませんが(私はプログラムしてバグるまで気づかなかった)、シグネチャーを作るのにシグネチャーがいるのです。普通のメソッドだとそれほど問題ではないのですが、ブロック付きのメソッドの場合、シグネチャの型が一発で決まらないので、この辺の話が重くのしかかります。これが(その1)で書いたバグの根本の原因であろう(まだ取れてないので想像)と思うのですが、この辺はよくわかってないので分かったら書きます。

 次にメソッドの引数の話をします。ytljitではユーザが指定する普通の引数のほかに隠れ引数を渡します。隠れ引数は現在のところ次の3つです(例外処理とかで増えるかもしれない)。

  1. 親のブロックまたはメソッドのフレーム
  2. メソッド呼び出しの際に渡されたブロックのコードの先頭アドレス
  3. self

これらの型オブジェクトもシグネチャーの配列に入ります。1番目の親のブロックの型はフレームは現在のところ、常にNilClassです。
2番目のブロックの先頭アドレスのの型はブロックの戻り値の型が入ります。この、引数のおかげで違う型のブロックを渡しても破たんすることなく推論できます。

 # 両方がどっかで同じプログラム中に出てくる
  [1, 2, 3].map { |e| e.to_s }  # 文字列を返すブロックをmapに渡す
  [1, 2, 3].map { |e| e.to_f }  # Floatを返すブロックをmapに渡す

こんなコードすごくありがちですから、これが全部動的に型チェックされると大変です。
ブロックの戻り値の型を決めるためには、メソッドコールの際に渡しているブロックを型推論する必要があるのですが、そのためにはメソッド中のyieldの引数の推論を行う必要があって、このシグネチャーが必要になるのです。堂々めぐりですが、何とかなりそうです。「なります」と言い切れないのがつらいところ。

selfは問題ないでしょう。推論出来なければダイナミックメソッドサーチが必要になるので型チェックが増えるどころの騒ぎじゃないです。

なんか、わけのわからない愚痴っぽい文になってしましたが、次回は型推論メソッド(collect_candidate_type)が何をやっているか書きたいと思います。

続く (と思う)

(追記)

うっかり、前回の伏線を回収し忘れたので書きます。型推論contextのcurrent_method_signature_node とcurrent_methodの話です。
何度も書いている通り、型推論では必ずシグネチャーが必要になります。そこで、現在推論中のメソッドのシグネチャ型推論contextに持たせています。
ただ、シグネチャーは推論が進んでいくと変わる可能性があるので、型オブジェクトの配列という形で持たず毎回推論するようにしています。
具体的には今推論中のメソッドを呼び出したsend命令の引数を表現するノードの配列(current_method_signature_node)とメソッドノード(current_method)いう形で保持しています。そして、シグネチャーが必要になったら毎回推論してシグネチャーを作っています。
ここがかなり重くて全体の実行時間の6割を費やしているので何とかキャッシュできないか考えています。
さて、シグネチャーを作るために型推論する場合、シグネチャーが必要になります。型推論contextには呼び出し元の引数のノードが入っているので、呼び出し元のメソッドのシグネチャーが必要になります。同様に、呼び出し元のメソッドのシグネチャーを推論する場合はその呼び出し元の呼び出し元がが必要になります。つまり、呼出し履歴の全部のノードを取っておく必要があります。
そういう理由でcurrent_method_signature_nodeとcurrent_methodはスタックのような構造になっています。