Gauche pass3 メモ - $CALL

関連記事。

コメントの訳

$CALL
$CALL ノードにはいくつかのバリエーションがある。それらには更に tail-call 版と non-tail-call 版があるだろう。

1. Local call:

'local' フラグをもつ $CALL ノードでローカル手続きの call を表す。呼び出し時の引数は手続きのうけとる引数の形に調整済みである。
PROC スロットにはローカル手続きを指す $LREF ノードが格納されている。

2. Embedded call:

'embed' フラグを持つ $CALL ノードで2つ以上の場所から呼び出されるエントリポイントを持つ。(1回しか呼び出されない手続きは $LET ノードになっているので、ここでは考えなくてよい)。
呼び出し時の引数は手続きのうけとる引数の形に調整済みである。
PROC スロットには embedded(埋め込まれた)$LAMBDA ノードが格納される。embedded $LAMBDA ノードの body は $LABEL ノードである。
生成されるコードは $LET ノードのそれとほとんど同じである。違うのは label が LOCAL-ENV の直後に置かれることである。

またこのノードに RENV を保持する。RENV は後で jump call ノードにおいていくつの environment frame を破棄すべきかを決定するのに使われる。
(ここでは embed ノードは関連する jump ノードの前に pass3 に渡されることを前提としている)。

3. Jump call:

'jump' フラグを持つ $CALL ノードでインライン展開されたローカル手続きへに移動することを表すノードである。この $CALL ノードの body はどこか別の場所に 'embedded call' ノードにより埋め込まれている。
PROC スロットは embed $CALL ノードを格納している。
このノードをコンパイルすると LOCAL-ENV-JUMP インストラクションになる。

4. Head-heavy call:

フラグがない $CALL ノードで全ての引数がシンプルな式(const か lref)であり、operator が $LET であるもの。
通常の call の流れでは引数の push のあとに operator が評価される。これにより $LET は 'top' context で評価されることになり、余計な continuation を push することが必要になる。
もし全ての引数がシンプルであるならば operator を先に評価し、引数の評価時にも VAL0 にその値を保持し続けることができる。
特に目立って名前付きlet が head-heavy call になる傾向があり、これを特別扱いする価値がある。
この head-heavy call 最適化は CONST-PUSH、LREF-PUSH などの合成インストラクション が使われることが前提である。
なぜならばもしこれらの合成が off になっているばあい VAL0 が引数で上書きされてしまうからである。

5. その他の call ノードは標準的な call のコードとしてコンパイルされる。

embedded と jump の違い

最初考えたときは embedded と jump の違いが分からなかった。すべて embedded で良いではないかと思ったが勘違い。
embedded の目的はクロージャを作らずインライン展開することだけでなく、埋め込まれたものを複数の箇所で共有すること。
例えば3箇所から同じクロージャが call されていれば1つは embed にして残り2つはその embed に対する jump にすれば良いというからくり(のはず)。

lambda の最適化

$lambda.calls に call site が記録してあるので、それぞれを locals recs tail-recs に分類する。

(1)local が1つ、それ以外は tail-rec の場合、local は embeded に、tail-rec は jump になる。
(2)recもtail-rec もなくて local のみの場合、サイズが小さいクロージャならすべてインライン展開される。