末尾呼び出し最適化 その2

input

  • 末尾かどうか?
  • 末尾までに消費したスタック

output

  • FRAME の削除
  • SHIFT の挿入

準備

  • vm/compiler をいじれるように用意する(rev 560)
  • 必ず記録を残す(こまめにコミット)

分からない動作

列挙

pass3/$local-assign val は tail => #f
pass3/$global-assign val は tail => #f
pass3/$seq begin の末尾だけ tail を受け継ぐ。それ以外は #f
pass3/$asm-xx-arg #f
pass3/$if test は #f 。then/else は受け継ぐ
pass3/$define val は #f
pass3/compile-arg #f
pass3/$call 引数のコンパイルは #f 。body は引数の数+LET_FRAMEサイズ。SHIFT発行
pass3/$labmda 引数の数
pass3/$let tail?だったら vars-length + 2 を足す

はじめよう

  • 3imp を見て思い出す。思い出した。
  • vm.scm で pass2 の最適化を off にする。
  • vm.scm でコンパイル済みコードが出力されるようにする(rev 561)

末尾呼び出しがうまく行く例を見ながら考える。

(define (a x) x)
((lambda (o p q r) (a 3)) 3 4 5 6)

VM から見ると

  • frame 作成
  • 引数の o p q r の push
  • 引数 3 の push
  • 引数3 を 4 個分 shift (つまり o p q r)を捨てる
  • a にジャンプ
  • a から return で 3 を片付け、frame の戻り先へ。

コンパイラから見ると

  • 末尾で呼び出される手続き a は他の手続きと全く同じコンパイル結果。
  • 末尾位置の a の呼び出し時には frame 作らない
  • 末尾位置の a の呼び出しの直前に o p q r の数だけ SHIFT で消す。

という感じか。
コンパイラは「スタックからどれだけの引数をさっくり消さないといけないか?」を持ち回っている。(lambda に入るとリセットされる)
ここまで rev563。

(let ([o 2]
      [p 3]
      [q 4]
      [r 5])
  (a 3))

これは末尾呼び出しだが、外側に lambda がないと末尾呼び出し最適化は行わない。(と思う。)
例えば以下のようなものが末尾呼び出し最適化されれば良い。

((lambda (o p q r)
   (let ([x 1])
     (a 3)))
 2 3 4 5)

これは現状の Mosh でも OK 。
さて問題は何だっけか。$map1 がスタックオーバーフローするな。

(define (my-map1 f l)
  (if (null? l)
      l
      (cons (f (car l)) (my-map1 f (cdr l)))))

(my-map1 (lambda (x) x) (vector->list (make-vector 10000 3)))

これは gosh や ypsilon ではスタックオーバーフローにならない。うーなぜだ。
http://d.hatena.ne.jp/higepon/20081027/1225087816
ここの議論で shiro さんのコメントにより原因が分かる。スタックオーバーフローはエラーではなくて自動伸長するのか。(OSが用意するネイティブスタックと同じだね)


というわけでやることは主に2つ

  • stack自動伸長を実装する。
  • shift 1 0 は発行しなくてもよい。