это если «замкнуть контекст на лямбду» и никак по-другому, то да, сложно, особенно если в llvm-ir такого нет :) А если «закат солнца в ручную», то что мешает выделять память для данных/указателей, копировать данные, передавать их функции через доп. параметр или через паблик указатель?
По-моему в IR нет замыканий. То есть, если тебе нужно нужно замыкание в IR, то ты хочешь чего-то не того. Если тебе нужно замыкание в реализуемом «языке», сделай его сам из примитивов, предлагаемых IR.
Но выглядит тормозным (это оно машинный код на стеке пишет во время выполнения) и неудобным (максимум один nest).
Поэтому на уровне фронтенда и рантаймовых фишек (чтобы не говорить «VM»). Например:
f(x, y) = x + y
g(x) = \y -> f x y
будет
struct f_int_int_int_capture_first { int x; int(&f)(int, int); };
int call(f_int_int_int_capture_first &fn, int y) { return fn.f(fn.x, y); }
int f(int x, int y) { return x + y; }
f_int_int_int_capture_first& g(int x) { return *new /* gc_new */ f_int_int_int_capture_first{x, f}; }
int main() { return call(g(1), 2); }
В фронтенде всё знаем — про типы и захваты, так что аллоцируем такие структуры и сами расставляем call, в IR для этого всё есть. Типичная проблема с необходимостью счётчика ссылок или GC для замыканий — тоже имеет место быть. Ну и разные агрессивные оптимизации к ним применимы, как в C++ или в Haskell.