Because .loop_3 is lifted out of the scope of
.f_2, which occurs free within the original code
for .loop_3, .f_2 must become an
additional formal parameter to .loop_3, and all calls
to .loop_3 must be changed to pass this additional
parameter.

Lambda lifting is one of the most complex optimizations performed
by Twobit, mainly because of complications that arise with groups
of mutually recursive known local procedures. Consider

Notice that g now escapes its scope, negating the earlier
closure analysis. Worse yet, if g were lifted to the outer
lambda expression, it would have
to take an extra parameter y.
That means f would have
to supply y as an extra argument when it calls g.

But f is now outside the scope of y!
Lifting g can therefore add extra arguments not only to
g but to procedures that call it.
Adding extra arguments to those procedures
may in turn involve adding extra arguments to procedures that call
the procedures that call g, and so on.

Eventually this process of adding arguments will stabilize.
For this example the result is

This is suboptimal: f is back within
the scope of g, so it is unnecessary to pass
g to f.

The flow equation for incremental lambda lifting

The solution is to lift each group of known local procedures as a
unit, after performing a flow analysis to determine
precisely which parameters must be added to each procedure.
Let V be the set of parameters for the lambda expression
at whose head a group of known local procedures is defined,
let f1, ... be the names of those procedures, and let FV(fi)
be the variables that occur free in the code for fi.
The set of variables Ai that need to be added as parameters to fi
is defined by the recursive flow equation

Ai = (FV(fi) intersect V) union (union {Aj | fi calls fj})

Compare this with the flow equation for conventional lambda
lifting:

Ai = FV(fi) union (union {Aj | fi calls fj})

The only difference between these flow equations is that,
by intersecting with V, incremental lambda lifting limits the
flow problem to the formal parameters of the lambda expression
from which the known local procedures are being lifted.
Another way of looking at it is that conventional lambda
lifting takes V to be the set of all variables that occur
within the program.

Solving these flow equations takes O(m^3 * n^2)
time, where m is the number of procedures being lifted and
n is the number of variables in the base set V.
For incremental lambda lifting, m and n are usually quite small.
On the other hand, incremental lambda lifting requires O(m)
different flow problems to be solved.

Twobit currently represents sets of variables as lists,
so it actually uses an O(m^3 * n^3) algorithm to solve these
flow equations. When compiling Twobit itself, Pass 2 spends
about 6% of its time solving flow equations. This is less
than 1% of the total compilation time.

Experiments using an O(m^3 * n^2) algorithm on synthetic benchmarks
have shown that incremental lambda lifting can be either faster or
slower than conventional lambda lifting, depending on the structure
of the program being compiled.

In short, compilation time is not a major issue for incremental
lambda lifting.

When and how to lift

Incremental lambda lifting is flexible because the lifting
process can be halted at an intermediate scope.
Heuristics
are applied at each stage of lifting to determine
whether further lifting is desirable.