HOF インライン融合とクロージャ変換

Almide の ClosureConversion パスが、capture を持たないラムダを ClosureCreate に変換せず生の Lambda ノードのまま残す最適化。後段の WASM エンジンが HOF をインライン融合できるようにするためで、その代償が「生 Lambda を値として使うと詰む」制約になっている。

HOF と2つのコードジェネ戦略

HOF = Higher-Order Function(高階関数)。list.map / list.filter / list.fold など関数を引数に取る stdlib 関数。list.map(xs, (x) => x * 2) を WASM に落とすとき2通りある:

戦略やることオーバーヘッド
call_indirect 経由ラムダをトップレベル関数に lift → 関数テーブル登録。各要素ごとに間接呼び出し環境ポインタ受け渡し + 間接ジャンプ
インライン融合ラムダ本体(x * 2)を map のループ本体に直接展開。呼び出しそのものが消えるゼロ

融合は、エンジンの inline_lambda がラムダの params と body を取り出してループに埋め込む。

なぜ生 Lambda を残すのか

インライン融合には、エンジンがラムダ本体(IR)に直接アクセスできる必要がある。ClosureCreate に変換すると本体はトップレベル関数に lift され、呼び出し側からは func_name しか見えず、本体を取り出してループ展開できない。

だから ClosureConversion は capture が無いラムダ = 環境不要でインライン展開できるラムダを、あえて ClosureCreate に変換せず生の Lambda のまま残す。後段が HOF 引数として見たとき「これは生 Lambda だからループに融合できる」と判断し、オーバーヘッドゼロのコードを出せる。

list.map(xs, (x) => x * 2)
  非capturing ラムダ → 生 Lambda のまま温存 → ループ本体に x*2 を展開
                                              (call_indirect も関数テーブルも不要)

capturing は call_indirect 一択

逆に外側の変数を capture するラムダは環境が要るので、生 Lambda のまま展開できない。ClosureCreate に変換 → トップレベル関数へ lift + 関数テーブル登録 → HOF からは call_indirect で呼ぶ。これが「closures-as-values」で実装される2経路:

ラムダ表現HOF での呼び出し
非capturing生 Lambda 温存ループ本体にインライン融合(オーバーヘッド0)
capturingClosureCreate(lift + テーブル)call_indirect(環境ポインタ付き)

代償(設計の裏返し)

生 Lambda は lift されない = 関数テーブルにエントリが無い。そのため、その非capturing ラムダが HOF 以外の場所で値として使われると詰む:

let double = (x) => x * 2
apply(double, 5)   // ラムダを値として渡す/返す → テーブルエントリが無く call_indirect できない

融合のための「lift しない」判断が、値としての一般的な取り回しを壊す。最適化と一様な値表現のトレードオフであり、クロージャまわりの別のバグと同じく、クロージャ表現の場合分けが生む制約の一例。

関連

Graph