\documentstyle[11pt,grasp]{article}
% \setcounter{secnumdepth}{0} % No section numbering for SPE
% Special symbols
\newcommand{\curlyL}{${\cal L}$}
\begin{document}
\title{A modular fully-lazy lambda lifter in \Haskell{}}
\author{Simon L Peyton Jones \\
Department of Computing Science, University of Glasgow, G12 8QQ
\and David Lester \\
Department of Computer Science, University of Manchester, M13 9PL}
\maketitle
\begin{abstract}
An important step in many compilers for functional languages is
{\em lambda lifting}. In his thesis, Hughes showed that by doing lambda
lifting in a particular way, a useful property called {\em full laziness}
can be preserved.[.hughes thesis.]
Full laziness has been seen as intertwined with
lambda lifting ever since.
We show that, on the contrary, full laziness can be regarded as a completely
separate process to lambda lifting, thus making it easy to use different
lambda lifters following a full-laziness transformation, or to use
the full-laziness transformation in compilers which do not require lambda
lifting.
On the way, we present the complete code for our modular fully-lazy
lambda lifter, written in the \Haskell{} functional programming language.
\end{abstract}
\section{Introduction}
Lambda lifting and full laziness are part of the folk lore
of functional programming, yet the few published descriptions of
fully-lazy lambda lifting have been either informal or
hard to understand, with the notable exception of
Bird.[.bird supercombinator.]
Our treatment differs from earlier work in the following ways:
\begin{itemize}
\item
The main technical contribution is to show how full
laziness can be separated from lambda lifting, so that
the two transformations can be carried out independently. This
makes each transformation easier to understand and
improves the modularity of the compiler.
\item
Our treatment deals with a language including let- and letrec-expressions.
Not only is this essential for efficient compilation in the later stages
of most compilers, but we also show that
eliminating let(rec) expressions, by translating them into
lambda abstractions, loses full laziness.
To our knowledge, this has not previously been realised.
\item
We show how to decompose each of the transformations further
into a composition of simple steps,
each of which is very easy to program, thus further improving modularity.
\item
We have developed an elegant use of parameterised data types, which
allows the type system to help express the purpose of each pass.
\end{itemize}
We present the complete source code for our solution, and we
document some of the experience we gained in developing it; indeed
the paper can be read as an exercise in functional programming.
The code is presented in the functional programming language \Haskell{},
and the source text for the paper is an executable literate \Haskell{} program.
Lines of code are distinguished by a leading @>@ sign, the rest of
the text being commentary.
We introduce all the notation and background that is required to understand
the paper. The first three sections
introduce the language to be compiled, the
\Haskell{} language, and the main data types to be used.
Following these preliminaries we develop a non-fully-lazy lambda lifter
and then refine it into a fully lazy one.
The paper concludes with some optimisations to the fully-lazy lambda lifter.
\section{The language}
We begin by defining a small language, \curlyL, on which our compiler will
operate.
The abstract syntax of the language is given by the following productions:
$$\begin{array}{lcll}
expression & ::= & name \\
& | & constant & \mbox{Literals and built-in functions} \\
& | & expression_1\ expression_2 & \mbox{Application}\\
& | & @let@\ defns\ @in@\ expression
& \mbox{Non-recursive definitions}\\
& | & @letrec@\ defns\ @in@\ expression
& \mbox{Recursive definitions}\\
& | & \lambda\ name @->@ expression
& \mbox{Lambda abstraction}\\
\\
defns & ::= & def_1 \ldots def_n \qquad (n>0) \\
\\
def & ::= & name\ @=@\ expression
\end{array}$$
The concrete syntax is conventional: parentheses are used to disambiguate;
application associates to the left and binds more tightly than any other
operator; the body of a lambda abstraction
extends as far to the right as possible; the usual infix operators are
permitted; and definitions are delimited using layout.
Notice that the bindings in let(rec) expressions are all {\em simple}; that
is, the left hand side of the binding is always just a variable.
(Functions are defined by binding variables to lambda abstractions.)
Here is an example to illustrate these points:
\begin{verbatim}
letrec
fac = \n -> if (n == 0) 1 (n * factorial (n-1))
in
fac 100
\end{verbatim}
An important feature of this language is that any pure high-level
functional programming language can be translated into it without loss
of expressiveness or efficiency. This process is described in some detail
by Peyton Jones.[.peyton book.]
Notice that let(rec)-expressions are retained, despite the fact that they
are responsible for many of the subtleties in the rest of the paper.
They can certainly be transformed into applications of lambda abstractions and
the @Y@ combinator, but doing so straightforwardly may result in
a serious loss of efficiency.[.peyton book <, Chapter 14>.]
For example, eliminating a let-expression introduces a new
lambda abstraction, which will (under the compilation scheme described
in this paper) be lambda-lifted, and thereby decompose
execution into smaller steps.
Similarly, a naive treatment of mutual local recursion using
@Y@ involves much packing and unpacking of
tuples, which can easily be avoided
if the letrec-expression is handled explicitly.
Finally, later on we show that laziness
can be lost if let-expressions are not handled specially%
%(Section \ref{fully-lazy-letrecs})%
.
\section{\Haskell{}}
\Haskell{} is a recently-designed common non-strict pure functional
language.[.Haskell{} report.] For the purposes of this paper it is fairly
similar to SML or Miranda\footnote{Miranda is a trade mark of Research
Software Ltd}, except in its treatment of abstract data types and modules.
We make little use of Haskell{}'s major technical innovation,
namely systematic overloading using type classes.
Haskell{} modules begin with a declaration naming the module, and
importing any modules it requires:
> module LambdaLift where
>
>
> import Utilities
Here, the module we will be defining is called @LambdaLift@, and it imports
an auxiliary module called @Utilities@, whose interface is given in
the Appendix.
\section{A data type for compilation}
Every compiler has a data type which represents the abstract syntax
tree of the program being compiled. The definition of this data type has
a substantial impact on the clarity and efficiency of the compiler, so
it merits careful thought.
\subsection{First failed attempt}
As a first attempt, the language described in the previous
section can translated directly into the
following \Haskell{} {\em algebraic data type} declaration\footnote{%
Notice that data constructors, such as @EVar@ and @ELam@,
and type constructors, such as @Expresssion@ and @Name@,
must both begin with capital letter in \Haskell{}.
}:
1> data Expression
1> = EConst Constant
1> | EVar Name
1> | EAp Expression Expression
1> | ELam [Name] Expression
1> | ELet IsRec [Definition] Expression
We choose to represent names simply by their character string, using
a {\em type synonym} declaration.
> type Name = [Char]
Constants may be numbers, booleans, the names of global functions, and
so on:
> data Constant = CNum Integer | CBool Bool | CFun Name
% deriving(Text)
The definition of the @Constant@ type can be changed without affecting
any of the rest of this paper.
As an example of the @Expression@ type,
the \curlyL-expression @a+3@ would be represented by
the \Haskell{} expression
\begin{verbatim}
EAp (EAp (EConst (CFun "+")) (EVar "a")) (EConst (CNum 3))
\end{verbatim}
Let-expressions can usually be treated in the same manner
as letrec-expressions, so the two are given a common constructor, and
distinguished by a flag of type @IsRec@. It is convenient to use
a boolean for this flag:
> type IsRec = Bool
> recursive = True
> nonRecursive = False
Each definition in the definition list of
an @ELet@ construction is just a pair:
1> type Definition = (Name, Expression)
\subsection{Second failed attempt}
It does not take long to discover that this is an insufficiently flexible
data type. Many compiler passes add information to the abstract syntax
tree, and we need a systematic way to represent this information.
Examples of analyses which generate this sort of information are:
free-variable analysis,
binding level analysis,
type inference,
strictness analysis,
and sharing analysis.
The most obvious way to add such information is to add a new constructor
for annotations to the @Expr@ data type, thus:
1> data Expression
1> = EVar Name
1> | ...
1> | EAnnot Annotation Expression
together with an auxiliary data type for annotations, which can be
extended as required%
\footnote{The type @Set a@ is a standard abstract data type for sets,
whose interface is given
in the Appendix, but whose implementation is not further defined.}%
:
1> data Annotation = FreeVars (Set Name)
1> | Level Integer
This allows annotations to appear freely throughout the syntax tree,
which appears admirably flexible. In practice, it suffers from two
major disadvantages
\begin{itemize}
\item
It is easy enough to {\em add} annotation information in the form
just described,
but writing a compiler
pass which {\em uses} information placed there by a previous pass is downright
awkward.
Suppose, for example, that a pass wishes to use the free-variable
information left at every node of the tree by a previous pass. Presumably
this information is attached immediately above every node, but the
data type would permit several annotation nodes to appear above each node,
and worse still none (or more than one) might have a free-variable annotation.
Even if the programmer is prepared to certify that there is exactly one
annotation node above every tree node, and that it is a free-variable
annotation, the implementation will still perform pattern-matching to
check these assertions when extracting the annotation.
Both of these problems, namely the requirement for uncheckable programmer
assertions, and some implementation overhead, are directly attributable to
the fact that every annotated tree has the rather uninformative type
@Expression@, which says nothing about which annotations are present.
\item
The second major problem is that further experimentation reveals that
{\em two} distinct forms of annotation are required.
The first annotates
expressions as above, but the second annotates the binding occurrences of
variables; that is, the occurrences on the left-hand sides of definitions,
and the bound variable in a lambda abstraction.
We will call these occurrences {\em binders}.
An example of the need for the latter comes in type inference, where it
the compiler infers a type for each binder, as well as for each sub-expression.
It is possible to use the expression-annotation to annotate binders,
but it is clumsy and inconvenient to do so.
\end{itemize}
\subsection{A happy ending}
We will address the second problem first, since it has an easy solution.
All we need do is parameterise the @Expression@ type with respect to the type
of binders, thus:
> data Expr binder
> = EConst Constant
> | EVar Name
> | EAp (Expr binder) (Expr binder)
> | ELam [binder] (Expr binder)
> | ELet IsRec [Defn binder] (Expr binder)
>
> type Defn binder = (binder, Expr binder)
%> deriving(Text)
@binder@ is a type variable, which in \Haskell{}
must begin with a lower-case letter.
We can easily recover a definition of our original @Expression@ type, in
which a binder is represented by a @Name@, using a type synonym declaration,
thus:
> type Expression = Expr Name
Alternatively, a data type in which binders are names annotated with a type
can be defined thus:
1> type TypedExpression = Expr (Name, TypeExpr)
where @TypeExpr@ is a data type representing type expressions.
Returning to annotations on expressions, we can re-use the same technique by
parameterising the data type of expressions with repect
to the annotation type.
We want to have an annotation on every node of the tree, so one possibility
would be to add an extra field to every constructor with the annotation
information. This is inconvenient if, for example, you simply want to
extract the free-variable information at the top of a given expression
without performing case analysis on the root node. This leads to the
following idea: each level of the tree is a pair, whose first component
is the annotation, and whose second component is the abstract syntax tree
node. Here are the corresponding \Haskell{} data type definitions:
> type AnnExpr binder annot = (annot, AnnExpr' binder annot)
>
> data AnnExpr' binder annot
> = AConst Constant
> | AVar Name
> | AAp (AnnExpr binder annot) (AnnExpr binder annot)
> | ALam [binder] (AnnExpr binder annot)
> | ALet IsRec [AnnDefn binder annot] (AnnExpr binder annot)
>
> type AnnDefn binder annot = (binder, AnnExpr binder annot)
Notice the way that the mutual recursion between @AnnExpr@ and @AnnExpr'@
ensures that every node in the tree carries an annotation.
The sort of annotations carried by an expression are now manifested
in the type of the expression. For example, an expression annotated
with free variables has type @AnnExpr Name (Set Name)@.
It is a real annoyance that @AnnExpr'@ and @Expr@ have to define two
essentially identical sets of constructors. There appears to be no way
around this within the Hindley-Milner type system.
It would be possible to abandon the @Expr@ type altogether, because
the @Expr a@ is nearly isomorphic to @AnnExpr a ()@, but
there are two reasons why we chose not to do this.
Firstly, the two types
are not quite isomorphic, because the latter distinguishes $@((),@\perp@)@$
from $\perp$ while the former does not. Secondly (and more seriously),
it is very tiresome to write all the @()@'s when building and pattern-matching
on trees of type @AnnExpr a ()@.
Finally, we define two useful functions, @bindersOf@ and @rhssOf@
(right-hand-sides of),
which each take the
list of definitions in an @ELet@,
and pick out the list of variables bound by the let(rec), and list of
right-hand sides to which they are bound, respectively:
> bindersOf :: [(binder,rhs)] -> [binder]
> bindersOf defns = [name | (name, rhs) <- defns]
>
> rhssOf :: [(binder,rhs)] -> [rhs]
> rhssOf defns = [rhs | (name,rhs) <- defns]
This definition illustrates the use of a {\em type signature} to express the
type of the function to be defined; and of
a {\em list comprehension} in the right hand side of @bindersOf@, which
may be read ``the list of all @name@s, where the pair @(name,rhs)@ is
drawn from the list @defns@''.
Both of these are now conventional features of functional programming
languages.
This completes our development of the central data type. The discussion
has revealed some of the strengths, and a weakness, of the
algebraic data types provided by all modern functional programming languages.
\section{Lambda lifting}
\label{lambda-lift}
Any implementation of a lexically-scoped
programming language has to cope with the fact that a function
may have free variables. Unless these are removed in some way, an
environment-based implementation has to manipulate linked environment
frames, and a reduction-based system is made significantly more complex
by the need to perform $\alpha$-renaming during substitution.
A popular way of avoiding these problems, especially in graph reduction
implementations, is to eliminate all free variables from function
definitions by means of a
transformation known as {\em lambda lifting}.
Lambda lifting is a term coined by Johnsson,[.johnsson lambda lifting.] but
the transformation was independently developed by Hughes.[.hughes thesis.]
A tutorial treatment is given by Peyton Jones.[.peyton book.]
The lambda-lifting issue is not restricted to functional languages.
For example, Pascal allows a function to be declared locally within another
function, and the inner function may have free variables bound by the
outer scope.
On the other hand, the C language does not permit such local definitions.
In the absence of side effects, it
is simple to make a local function definition into a global one: all
we need do is add the free variables as extra parameters, and add these
parameters to every call.
This is exactly what lambda lifting does.
In a functional-language context,
lambda lifting transforms an {\em expression} into a {\em set of
supercombinator definitions}, each of which defines a function of zero
or more arguments, and whose body contains no embedded lambda
abstractions. In this paper we will use the convention that
the value of the set of
definitions is the value of the supercombinator @$main@; this artifice
avoids the need to speak of ``a set of supercombinator definitions
together with an expression to be evaluated''.
To take a simple example, consider the program
\begin{verbatim}
let
f = \x -> let g = \y -> x*x + y in (g 3 + g 4)
in
f 6
\end{verbatim}
Here, @x@ is free in the abstraction @\y -> x*x + y@. The free variable can be
removed by defining a new function @$g@ which takes @x@ as an extra parameter,
but whose body is the offending abstraction,
and redefining @g@ in terms of @$g@, giving the following set of supercombinator
definitions:
\begin{verbatim}
$g x y = x*x + y
f x = let g = $g x in (g 3 + g 4)
$main = f 6
\end{verbatim}
(To stress the fact that this program is a set of supercombinator
definitions, we permit ourselves to write the arguments of the supercombinator
on the left of the @=@ sign.)
Matters are no more complicated when recursion is involved. Suppose that
@g@ was recursive, thus:
\begin{verbatim}
let
f = \x -> letrec g = \y -> cons (x*y) (g y) in g 3
in
f 6
\end{verbatim}
Now @x@ and @g@ are both free in the @\y@-abstraction, so the lambda lifter
will produce the following set of supercombinators:
\begin{verbatim}
$g g x y = cons (x*y) (g y)
f x = letrec g = $g g x in g 3
$main = f 6
\end{verbatim}
Notice that the definition of @g@ is still recursive, but the lambda lifter
has eliminated the local lambda abstraction. The program is now directly
implementable by most compiler back ends.
This is not the only way to handle local recursive function definitions.
The main alternative is
described by Johnsson,[.johnsson thesis.] who generates directly-recursive
supercombinators from locally-recursive function definitions; in our example,
@$g@ would be directly recursive rather than calling its parameter @g@, thus:
\begin{verbatim}
$g x y = cons (x*y) ($g x y)
f x = $g x 3
$main = f 6
\end{verbatim}
The lambda lifter he describes is
much more complex than the one we develop here. For Johnsson it is worth the
extra work, because the back end of his compiler can produce better code from
directly-recursive supercombinators, but it is not clear that this applies
universally to all implementations.
At all events, we will stick to the simple lambda lifter here, since our main
concern is the interaction with full laziness.
\subsection{Implementing a simple lambda lifter}
\label{simple-lambdalift}
We are now ready to develop a simple lambda lifter.
It will take an expression and deliver a list of supercombinator definitions,
so this is its type:
> lambdaLift :: Expression -> [SCDefn]
Each supercombinator definition consists of the name of the supercombinator,
the list of its arguments, and its body:
> type SCDefn = (Name, [Name], Expression)
It should be the case that there are no @ELam@ constructors
anywhere in the third component of
the triple.
Unfortunately, there is no way to express (and hence enforce)
this constraint in the type,
except by declaring yet another new variant of @Expr@ lacking
such a constructor. This is really another shortcoming of the type system:
there is no means of expressing this sort of subtyping relationship.
The lambda lifter works in three passes:
\begin{itemize}
\item
First, we annotate every node in the expression with its free variables.
This is used by the following pass to decide which extra parameters
to add to a lambda abstraction. The @freeVars@ function has type
> freeVars :: Expression -> AnnExpr Name (Set Name)
\item
Second, the function @abstract@ abstracts the free variables
from each lambda abstraction, replacing the lambda abstraction with the
application of the new abstraction (now a supercombinator) to the free
variables. For example, the lambda abstraction
\begin{verbatim}
(\x -> y*x + y*z)
\end{verbatim}
would be transformed to
\begin{verbatim}
(\y -> \z -> \x -> y*x + y*z) y z
\end{verbatim}
@abstract@ has the type signature:
> abstract :: AnnExpr Name (Set Name) -> Expression
Notice, from the type signature, that @abstract@ removes the free variable
information, which is no longer required.
\item
Finally, @collectSCs@ gives a unique name to each supercombinator,
collects all the supercombinator definitions into a single list, and
introduces the @$main@ supercombinator definition.
> collectSCs :: Expression -> [SCDefn]
\end{itemize}
The compiler itself is the composition of these three functions\footnote{%
Infix ``@.@'' denotes function composition.
}:
> lambdaLift = collectSCs . abstract . freeVars
It would of course be possible to do all the work in a single pass,
but the modularity provided by separating them has a number of advantages:
each individual pass is easier to understand, the passes may be reusable
(for example, we reuse @freeVars@ below), and modularity makes it easier
to change the algorithm somewhat.
As an example of the final point, the
\Haskell{} compiler under development at Glasgow
will be able to generate better code by omitting
the @collectSCs@ pass, because more is then known about the context in which
the supercombinator is applied.
For example, consider the expression, which might be produced by the
@abstract@ pass:
\begin{verbatim}
let f = (\v.\x. v-x) v
in ...f...f...
\end{verbatim}
Here @abstract@ has removed @v@ as a free variable from the @\x@-abstraction.
Rather than compiling the supercombinator independently of its context,
our compiler constructs a closure for @f@, whose code accesses @v@ directly
from the closure and @x@ from the stack. The calls to @f@ thus do not have
to move @v@ onto the stack.
The more free variables there are the more
beneficial this becomes.
Nor do the calls to @f@ become less efficient
because the definition is a local one; the compiler can see the binding
for @f@ and can jump directly to its code.
\subsection{Free variables}
We begin by giving the code for the free-variable pass, not because it
is particularly subtle, but because it serves to illustrate {\sc Haskell}
language notation.
> freeVars (EConst k) = (setEmpty, AConst k)
> freeVars (EVar v) = (setSingleton v, AVar v)
> freeVars (EAp e1 e2) =
> (setUnion (freeVarsOf e1') (freeVarsOf e2'), AAp e1' e2')
> where
> e1' = freeVars e1
> e2' = freeVars e2
> freeVars (ELam args body) =
> (setDifference (freeVarsOf body') (setFromList args), ALam args body')
> where
> body' = freeVars body
> freeVars (ELet isRec defns body) =
> (setUnion defnsFree bodyFree, ALet isRec (zip binders rhss') body')
> where
> binders = bindersOf defns
> binderSet = setFromList binders
> rhss' = map freeVars (rhssOf defns)
> freeInRhss = setUnionList (map freeVarsOf rhss')
> defnsFree | isRec = setDifference freeInRhss binderSet
> | not isRec = freeInRhss
> body' = freeVars body
> bodyFree = setDifference (freeVarsOf body') binderSet
> freeVarsOf :: AnnExpr Name (Set Name) -> Set Name
> freeVarsOf (free_vars, expr) = free_vars
In the definition of @defnsFree@, the boolean condition between the
vertical bar and the equals sign is a {\em guard}, which serves to
select the appropriate right-hand side.
The function @zip@ in the definition of @defns'@ is a standard function which
takes two lists and returns a list consisting of pairs of corresponding
elements of the argument lists.
The set operations @setUnion@, @setDifference@, and so on, are defined in
the utilities module, whose interface is given in the Appendix%
% ~\ref{utilities}%
.
Otherwise the code should be self-explanatory.
\subsection{Generating supercombinators}
\label{abstract}
The next pass merely replaces each lambda abstraction, which is
now annotated with its free variables, with a new abstraction
(the supercombinator) applied
to its free variables.
The full definition is given in the Appendix%
%~\ref{omitted}%
; and the only interesting
equation is that dealing with lambda abstractions:
1> abstract (free, ALam args body) =
1> foldl EAp sc (map EVar fvList)
1> where
1> fvList = setToList free
1> sc = ELam (fvList ++ args) (abstract body)
The function @foldl@ is a standard function; given a dyadic function
$\oplus$, a value $b$, and a list $xs\ =\ [x_1,...,x_n]$,
$@foldl@\ \oplus\ b\ xs$
computes $( \ldots ((b~ \oplus~ x_1)~ \oplus~ x_2)~ \oplus~ \ldots x_n)$.
Notice the way that the free-variable information is discarded by the pass,
since it is no longer required.
We also observe that @abstract@ treats the two expressions
@ELam args1 (ELam args2 body))@ and
@(ELam (args1++args2) body)@ differently.
In the former case, the two abstractions
will be treated separately, generating two supercombinators, while in the
latter only one supercombinator is produced.
It is clearly advantageous to merge directly-nested @ELam@s before
performing lambda lifting. This is equivalent to the $\eta$-abstraction
optimisation noted by Hughes.[.hughes thesis.]
\subsection{Collecting supercombinators}
Finally, we have to name the supercombinators and collect them together.
To generate new names, the main function
has to carry around a {\em name supply};
in particular, it must take the
name supply as an argument and return a depleted supply as a result.
In addition, it must return the collection of supercombinators it has found,
and the transformed expression.
Because of these auxiliary arguments and results,
we define a function @collectSCs_e@
which does all the work, with @collectSCs@ being defined in terms of it:
> collectSCs_e :: NameSupply -> Expression
> -> (NameSupply, Bag SCDefn, Expression)
> collectSCs e = [("$main", [], e')] ++ bagToList scs
> where
> (_, scs, e') = collectSCs_e initialNameSupply e
The name supply is
represented by an abstract data type @NameSupply@, whose interface is
given in the Appendix%
%~\ref{utilities}%
, but in which we take no further interest.
The collection of supercombinators is a {\em bag}, represented by the
abstract data type of @Bag@, which has similar operations defined on it
as those for @Set@.
The ``@_@'' in the last line of @collectSCs@ is a {\em wildcard} which
signals the fact that we are not interested in the depleted name supply
resulting from transforming the whole program.
The code is now easy to write.
> collectSCs_e ns (EConst k) = (ns, bagEmpty, EConst k)
> collectSCs_e ns (EVar v) = (ns, bagEmpty, EVar v)
> collectSCs_e ns (EAp e1 e2) =
> (ns2, bagUnion scs1 scs2, EAp e1' e2')
> where
> (ns1, scs1, e1') = collectSCs_e ns e1
> (ns2, scs2, e2') = collectSCs_e ns1 e2
In the case of lambda abstractions we replace the abstraction by a name,
and add a supercombinator to the result.
> collectSCs_e ns (ELam args body) =
> (ns2, bagInsert (name, args, body') bodySCs, EConst (CFun name))
> where
> (ns1, bodySCs, body') = collectSCs_e ns body
> (ns2, name) = newName ns1 "SC"
A common paradigm occurs in the case for let(rec):
> collectSCs_e ns (ELet isRec defns body) =
> (ns2, scs, ELet isRec defns' body')
> where
> (ns1, bodySCs, body') = collectSCs_e ns body
> ((ns2, scs), defns') = mapAccuml collectSCs_d (ns1, bodySCs) defns
>
> collectSCs_d (ns, scs) (name, rhs) =
> ((ns1, bagUnion scs scs'), (name, rhs'))
> where
> (ns1, scs', rhs') = collectSCs_e ns rhs
When processing a list of definitions, we need to generate a new list
of definitions, threading the name supply through each.
This is done by a new
higher-order function @mapAccuml@, which behaves like a combination
of @map@ and @foldl@;
it applies a function to each element of a list, passing an accumulating
parameter from left to right, and returning a final value of this
accumulator together with the new list. @mapAccuml@
is defined in the Appendix%
% ~\ref{omitted}%
.
\label{mapaccum}
This completes the definition of the simple lambda lifter.
We now turn our attention to full laziness.
\section{Separating full laziness from lambda lifting}
Previous accounts of full laziness have invariably linked it to
lambda lifting,
by describing ``fully-lazy lambda lifting'', which turns out to be
rather a complex process.
Hughes
gives an algorithm but it is extremely subtle
and does not handle let(rec) expressions.[.hughes thesis.]
On the other hand, Peyton Jones
does cover let(rec) expressions, but
the description is only informal and no algorithm is given.[.peyton book.]
In this section we show how full laziness and lambda lifting can
be cleanly separated.
This is done by means of a transformation involving let-expressions.
Lest it be supposed that we have simplified things in one way only
by complicating them in another, we also show that performing fully-lazy lambda
lifting without let(rec) expressions risks an unexpected loss of laziness.
Furthermore, much more efficient code can be generated for let(rec) expressions
in later phases of most compilers than for their equivalent lambda expressions.
\subsection{A review of full laziness}
We begin by briefly reviewing the concept of full laziness.
Consider again the example given earlier.
%in Section~\ref{lambda-lift}.
\begin{verbatim}
let
f = \x -> let g = \y -> x*x + y in (g 3 + g 4)
in
f 6
\end{verbatim}
The simple lambda lifter generates the program:
\begin{verbatim}
$g x y = x*x + y
f x = let g = $g x in (g 3 + g 4)
$main = f 6
\end{verbatim}
In the body of @f@ there are two calls to @g@ and hence to @$g@.
But @($g x)@ is not a reducible expression, so @x*x@ will
be computed twice. But @x@ is fixed in the body of @f@, so some work is being
duplicated. It would be better to share the calculation of @x*x@ between
the two calls to @$g@. This can be achieved as follows: instead of making @x@ a
parameter to @$g@, we make @x*x@ into a parameter, like this:
\begin{verbatim}
$g p y = p + y
f x = let g = $g (x*x) in (g 3 + g 4)
\end{verbatim}
(we omit the definition of @$main@ from now on, since it does not change).
So a fully-lazy lambda lifter will make each {\em maximal
free sub-expresssion} (rather than each {\em free variable})
of a lambda abstraction into an argument of the
corresponding supercombinator.
A maximal free expression (or MFE) of a lambda abstraction is an expression
which contains no occurrences of the variable bound by the abstraction, and is
not a sub-expression of a larger expression with this property.
Full laziness corresponds precisely to moving a loop-invariant
expression outside the loop, so that it is computed just once at the
beginning rather than once for each loop iteration.
How important is full laziness for ``real'' programs?
No serious studies have yet been made of this question, though we plan to
do so.
However, recent work by Holst
suggests that the importance of full laziness may be greater than
might at first be supposed.[.Holst improving full laziness.]
He shows how to perform a transformation which automatically enhances the
effect of full laziness, to the point where the optimisations obtained
compare favourably with those gained by partial
evaluation,[.mix jones sestoft.]
though with much less effort.
\subsection{Full-lazy lambda lifting in the presence of let(rec)s}
\label{fully-lazy-letrecs}
Writing a fully-lazy lambda lifter, as outlined in the previous section, is
somewhat subtle. Our language, which includes let(rec) expressions, appears
to make this worse by introducing a new language construct.
For example, suppose the definition of @g@ in our running example was slightly
more complex, thus:
\begin{verbatim}
g = \y -> let z = x*x
in let p = z*z
in p + y
\end{verbatim}
Now, the sub-expression @x*x@ is a MFE of the @\y@-abstraction,
but sub-expression @z*z@ is not since @z@ is bound inside the @\y@-abstraction.
Yet it is clear that @p@ depends only on @x@ (albeit indirectly), and so
we should ensure that @z*z@ should only be computed once.
Does a fully-lazy lambda lifter spot this if let-expressions are coded as
lambda applications? No, it does not. The definition of @g@ would become
\begin{verbatim}
g = \y -> (\z -> (\p -> p+y) (z*z)) (x*x)
\end{verbatim}
Now, @x*x@ is free as before, but @z*z@ is not. In other words, {\em
if the compiler does not treat let(rec)-expressions specially, it may
lose full laziness which the programmer might reasonably expect to
be preserved}.
Fortunately, there is a straightforward way to handle let(rec)-expressions,
as described by Peyton Jones,
namely to ``float'' each let(rec)
definition outward until it is outside any lambda abstraction in which
it is free.[.peyton book <, Chapter 15>.]
For example, all we need do is
transform the definition of @g@ to the following:
\begin{verbatim}
g = let z = x*x
in let p = z*z
in \y -> p + y
\end{verbatim}
Now @x*x@ and @z*z@ will each be computed only once.
Notice that this property should hold
{\em for any implementation of the language},
not merely for one based on lambda lifting and graph reduction.
This is a clue that full laziness and lambda lifting are not as closely related
as at first appears, a topic to which we will return in the next section.
Meanwhile, how can we decide how far out to float a definition?
It is most easily done by using
{\em lexical level numbers} (or {\em de Bruijn numbers}).
There are three steps:
\begin{itemize}
\item
First, assign to each lambda-bound variable a level number, which
says how many lambdas enclose it. Thus in our example,
@x@ would be assigned level number 1, and @y@ level number 2.
\item
Now, assign a level number to each let(rec)-bound variable (outermost first),
which is the maximum of the level numbers of its free variables, or zero
if there are none.
In our example, both @p@ and @z@ would be assigned level number 1.
Some care needs to be taken to handle letrecs correctly.
\item
Finally, float each definition (whose binder has level $n$, say)
outward, until it is outside
the lambda abstraction whose binder has level $n+1$, but still inside
the level-$n$ abstraction.
There is some freedom in this step about exactly where between the
two the definition should be placed.
\end{itemize}
Each mutually-recursive set of definitions defined in a letrec
should be floated out together,
because they depend on each other and must remain in a single letrec.
If, in fact, the definitions are {\em not} mutually recursive despite
appearing in the same letrec, this policy might lose laziness by
retaining in an inner scope a definition which could otherwise be
floated further outwards.
The standard solution is to perform {\em dependency analysis} on the definitions
in each letrec expression, to break each group of definitions into its
minimal subgroups. We take no further interest in this optimisation,
which is discussed in Chapter 6 of Peyton Jones.[.peyton book.]
Finally, a renaming pass should be carried out before the floating operation,
so that there is no risk that the bindings will be altered by the movement
of the let(rec) definitions. For example, the expression
\begin{verbatim}
\y -> let y = x*x in y
\end{verbatim}
is obviously not equivalent to
\begin{verbatim}
let y = x*x in \y->y
\end{verbatim}
All that is required is to give every binder a unique name to eliminate the
name clash.
\subsection{Full laziness without lambda lifting}
At first it appears that the requirement to float let(rec)s outward
in order to preserve full laziness merely further complicates the already
subtle fully lazy lambda lifting algorithm suggested by Hughes.
However, a simple transformation allows {\em all} the full laziness
to be achieved by let(rec) floating, while lambda lifting is performed
by the original simple lambda lifter.
The transformation is this: {\em before floating let(rec) definitions,
replace each MFE $e$ with the expression @let v = @$e$@ in v@}. This
transformation both gives a name to the MFE and makes it accessible to
the let(rec) floating transformation, which can now float out the new
definitions. Ordinary lambda lifting can then be performed.
For example, consider the original definition of @g@:
\begin{verbatim}
let
f = \x -> let g = \y -> x*x + y
in (g 3 + g 4)
in
f 6
\end{verbatim}
The subexpression @x*x@ is an MFE, so it
is replaced by a trivial let-expression:
\begin{verbatim}
let
f = \x -> let g = \y -> (let v = x*x in v) + y
in (g 3 + g 4)
in
f 6
\end{verbatim}
Now the let-expression is floated outward:
\begin{verbatim}
let
f = \x -> let g = let v = x*x in \y -> v + y
in (g 3 + g 4)
in
f 6
\end{verbatim}
Finally, ordinary lambda lifting will discover that @v@ is free in the
@\y@-expression, and the resulting program becomes:
\begin{verbatim}
$g v y = v + y
f x = let g = let v = x*x in $g v
in (g 3 + g 4)
$main = f 6
\end{verbatim}
A few points should be noted here.
Firstly, the original definition of a maximal free expression
was relative to a {\em particular} lambda abstraction.
The new algorithm we have just developed transforms certain expressions
into trivial let-expresssions.
Which expressions are so transformed? Just the ones which are MFEs of
{\em any} enclosing lambda abstraction. For example, in the expression:
\begin{verbatim}
\y -> \z -> (y + (x*x)) / z
\end{verbatim}
two MFEs are identified: @(x*x)@, since it is an MFE of the @\y@-abstraction,
and @(y + (x*x))@, since it is an MFE of the @\z@-abstraction.
After introducing the trivial let-bindings, the expression becomes
\begin{verbatim}
\y -> \z -> (let v1 = y + (let v2 = x*x in v2) in v1) / z
\end{verbatim}
Secondly, the
newly-introduced variable @v@ must either be unique, or the expression
must be uniquely renamed after the MFE-identification pass.
Thirdly, in the final form of the program @v@ is only referenced once,
so it would be sensible to replace the reference by the right-hand-side
of the definition and eliminate the definition, yielding exactly the
program we obtained using Hughes's algorithm. This is a straightforward
transformation, and we will not discuss it further here, except to note that
this property will hold
for all let-definitions which are floated out past a lambda.
In any case, many compiler back ends will generate the same code
regardless of whether or not the transformation is performed.
\subsection{A fully lazy lambda lifter}
Now we are ready to define the fully lazy lambda lifter. It can be decomposed
into the following stages:
\begin{itemize}
\item
First we must make sure that all @ELam@ constructors bind only a single
variable, because the fully-lazy lambda lifter must treat each lambda
individually.
It would be possible to encode this in later phases of the algorithm,
by dealing with a list of arguments, but it turns out that we can
express an important optimisation by altering this pass alone.
> separateLams :: Expression -> Expression
\item
First we annotate all binders and expressions with level numbers, which
we represent by natural numbers starting with zero:
> type Level = Int
> addLevels :: Expression -> AnnExpr (Name, Level) Level
\item
Next we identify all MFEs, by replacing them with trivial let-expressions.
Level numbers are no longer required on every sub-expression, only on
binders.
> identifyMFEs :: AnnExpr (Name, Level) Level -> Expr (Name, Level)
\item
A renaming pass makes all binders unique, so that floating does not
cause name-capture errors. This must be done after @identifyMFEs@, which
introduces new bindings.
> rename :: Expr (Name, a) -> Expr (Name, a)
\item
Now the let(rec) definitions can be floated outwards. The level numbers
are not required any further.
> float :: Expr (Name, Level) -> Expression
\item
Finally, ordinary lambda lifting can be carried out, using @lambdaLift@ from
% Section~\ref{simple-lambdalift}.
the section dealing with simple lambda lifting.
\end{itemize}
The fully lazy lambda lifter is just the composition of these passes:
> fullyLazyLift = lambdaLift . float . rename .
> identifyMFEs . addLevels . separateLams
\subsection{Separating the lambdas}
\label{separatelams}
The first pass, which separates lambdas so that each @ELam@ only
binds a single argument,
is completely straightforward.
The only interesting equation
is that which handles abstractions, which we give here:
1> separateLams (ELam args body) = foldr mkLam (separateLams body) args
1> where
1> mkLam arg body = ELam [arg] body
The other equations are given in the Appendix.
\subsection{Adding level numbers}
\label{levels}
There are a couple of complications concerning annotating an expression
with level-numbers.
At first it looks as though it is sufficient to write a function which
returns an expresssion annotated with level numbers; then for an
application, for example, one simply takes the maximum of the levels
of the two sub-expressions. Unfortunately, this approach loses too
much information, because there is no way of mapping the level-number
of the {\em body} of a lambda abstraction to the level number of the
abstraction {\em itself}. The easiest solution is first to annotate
the expression with its free variables, and then use a mapping
@freeSetToLevel@ from variables to level-numbers, to convert the
free-variable annotations to level numbers.
> freeSetToLevel :: Set Name -> Assn Name Level -> Level
> freeSetToLevel free_vars env =
> maximum (0 : map (assLookup env) (setToList free_vars))
> -- If there are no free variables, return level zero
The second complication concerns letrec expressions.
What is the correct level number to attribute to the newly-introduced variables?
The right thing to do is to take the maximum of the levels of the
free variables of all the
right-hand sides {\em without} the recursive variables, or equivalently
map the recursive variables to level zero when taking this maximum.
This level should be attributed to each of the new variables.
Let-expressions are much simpler: just attribute to each new variable the
level number of its right-hand side.
Now we are ready to define @addLevels@. It is the composition of two
passes, the first of which annotates the expression with its free variables,
while the second uses this information to generate level-number annotations.
> addLevels = freeToLevel . freeVars
We have defined the @freeVars@ function already, so it remains to
define @freeToLevel@. The main function will need to carry around the
current level, and a
mapping from variables to level numbers, so as usual we define @freeToLevel@
in terms of @freeToLevel_e@ which does all the work.
> freeToLevel_e :: Level -- Level of context
> -> Assn Name Level -- Level of in-scope names
> -> AnnExpr Name (Set Name) -- Input expression
> -> AnnExpr (Name, Level) Level -- Result expression
> freeToLevel e = freeToLevel_e 0 [] e
We represent the name-to-level mapping as an association list, with type
@Assn Name Level@. The interface of association lists is given in
the Appendix%
% ~\ref{utilities}%
.
but notice that it is {\em not} abstract. It is so convenient
to use all the standard functions on lists, and notation for lists,
rather than to invent their
analogues for associations, that we have compromised the abstraction.
For constants, variables and applications, it is simpler and more efficient
to ignore the free-variable information and calculate the level number
directly.
> freeToLevel_e level env (_, AConst k) = (0, AConst k)
> freeToLevel_e level env (_, AVar v) = (assLookup env v, AVar v)
> freeToLevel_e level env (_, AAp e1 e2) =
> (max (levelOf e1') (levelOf e2'), AAp e1' e2')
> where
> e1' = freeToLevel_e level env e1
> e2' = freeToLevel_e level env e2
This cannot be done for lambda abstractions, so we compute the level
number of the abstraction using @freeSetToLevel@.
We also assign a level number to each variable
in the argument list. At present we expect there to be only one such
variable, but
we will allow there to be several and assign them all the same level number.
This works correctly now, and turns out to be just what is needed to support
a useful optimisation later%
%(Section~\ref{redundant})%
.
> freeToLevel_e level env (free, ALam args body) =
> (freeSetToLevel free env, ALam args' body')
> where
> body' = freeToLevel_e (level + 1) (args' ++ env) body
> args' = zip args (repeat (level+1))
Let(rec)-expressions follow the scheme outlined at the beginning of this
section.
> freeToLevel_e level env (free, ALet isRec defns body) =
> (levelOf body', ALet isRec defns' body')
> where
> binders = bindersOf defns
> freeRhsVars = setUnionList [free | (free, _) <- rhssOf defns]
> maxRhsLevel = freeSetToLevel freeRhsVars
> ([(name,0) | name <- binders] ++ env)
> defns' = map freeToLevel_d defns
> body' = freeToLevel_e level (bindersOf defns' ++ env) body
>
> freeToLevel_d (name, rhs) = ((name, levelOf rhs'), rhs')
> where rhs' = freeToLevel_e level envRhs rhs
> envRhs | isRec = [(name,maxRhsLevel) | name <- binders] ++ env
> | not isRec = env
Notice that the level of the whole let(rec) expression is that of the body.
This is valid provided the body refers to all the binders directly or
indirectly. If any definition is unused, we might assign a level number
to the letrec which would cause it to be floated outside the scope of some
variable mentioned in the unused definition. This is easily fixed, but
it is simpler to assume that the expression contains no redundant
definitions\footnote{%
The dependency analysis phase referred to earlier could
eliminate such definitions.}.
Finally the auxillary function @levelOf@ extracts the level from an
expression:
> levelOf :: AnnExpr a Level -> Level
> levelOf (level, e) = level
\subsection{Identifying MFEs}
\label{identifyMFEs}
It is simple to identify MFEs, by comparing the level number of an
expression with the level of its context.
This requires an auxiliary
parameter to give the level of the context.
%
%> float = error "help"
%> identifyMFEs = error "help"
%
> identifyMFEs_e :: Level -> AnnExpr (Name, Level) Level -> Expr (Name, Level)
> identifyMFEs e = identifyMFEs_e 0 e
Once an MFE $e$ has been identified, our strategy is to wrap it in a
trivial let-expression of the form $@let v = @e@ in v@$; but not all
MFEs deserve special treatment in this way. For example, it would be
a waste of time to wrap such a let-expression around an MFE consisting
of a single variable or constant. Other examples are given below, in
the section on redundant full laziness.
%Section~\ref{redundant}.
We encode this knowledge of which MFEs
deserve special treatment in a function @notMFECandidate@.
> notMFECandidate (AConst k) = True
> notMFECandidate (AVar v) = True
> notMFECandidate _ = False -- For now, everything else is a candidate
@identifyMFEs_e@ works by comparing the level number of the expression with
that of its context. If they are the same, or for some other reason the
expression is not a candidate for special treatment, the expression is left
unchanged, except that @identifyMFEs_e1@ is used to apply @identifyMFEs_e@
to its subexpressions; otherwise we use @transformMFE@ to perform the
appropriate transformation.
> identifyMFEs_e cxt (level, e) =
> if (level == cxt || notMFECandidate e)
> then e'
> else transformMFE level e'
> where
> e' = identifyMFEs_e1 level e
> transformMFE level e = ELet nonRecursive [(("v",level), e)] (EVar "v")
@identifyMFEs_e1@ applies @identifyMFEs_e@ to the components of the expression.
Its definition is straightforward, and is given in the Appendix%
%~\ref{omitted}%
.
\subsection{Renaming and floating}
\label{rename}
\label{float}
The renaming pass is entirely straightforward, involving plumbing
a name supply in a similar manner to @collectSCs@.
The final pass, which floats let(rec) expressions out to the appropriate
level, is also fairly easy.
Complete definitions for @rename@ and @float@ are given in
the Appendix%
%~\ref{omitted}%
.
\section{Avoiding redundant full laziness}
\label{redundant}
Full laziness does not come for free. It has two main negative effects:
\begin{itemize}
\item
Multiple lambda abstractions, such as @\x->\y->E@ turn into one
supercombinator under the simple scheme, but two under the fully lazy
scheme. Two reductions instead of one are therefore required to apply it
to two arguments, which may well be more expensive.
\item
Lifting out MFEs removes subexpressions from their context, and thereby
reduces opportunities for a compiler to perform optimisations. Such
optimisations might be partially restored by an interprocedural analysis
which figured out the contexts again, but it is better still to avoid
creating the problem.
\end{itemize}
These points are elaborated by Fairbairn [.fairbairn redundant.]
and Goldberg.[.goldberg thesis.]
Furthermore, they point out that often no benefit arises from
lifting out {\em every} MFE from
{\em every} lambda abstraction. In particular,
\begin{itemize}
\item
If no partial applications of a multiple abstraction can be shared, then
nothing is gained by floating MFEs out to {\em between} the
nested abstractions.
\item
Very little is gained by lifting out an MFE that is not a
reducible expression.
No work is shared thereby, though there may be some saving in storage
because the closure need only be constructed once.
This is more than outweighed by the loss of compiler optimisations
caused by removing the expression from its context.
\item
Lifting out an MFE which is a constant expression (ie level 0),
thereby adding an extra parameter
to pass in, is inefficient. It would be better to make the constant
expression into a supercombinator.
\end{itemize}
These observations suggest some improvements to the fully lazy lambda
lifter, and they turn out to be quite easy to incorporate:
\begin{itemize}
\item
If a multiple abstraction is {\em not} separated into separate @ELam@
constructors by the @separateLam@ pass, then all the variables bound by
it will be given the {\em same} level number. It follows that no MFE
will be identified which is free in the inner abstraction but not the
outer one. This ensures that no MFEs will be floated out to between
two abstractions represented by a single @ELam@ constructor.
All that is required is to modify the @separateLams@ pass to keep in
a single @ELam@ constructor each multiple abstraction of which partial
applications cannot be shared. This sharing information is not trivial
to deduce, but at least we have an elegant way to use its results
by modifying only a small part of our algorithm.
This is one reason why we chose to allow @ELam@ constructors to
take a list of binders.
\item
@identifyMFEs@ uses a predicate @notMFECandidate@ to decide whether to
identify a particular subexpression as an MFE. This provides a convenient
place to add extra conditions to exclude from consideration expressions
which are not redexes.
This condition, too, is undecidable in general, but a good approximation
can be made in many cases; for example @(+ 3)@ is obviously not a redex.
\item
When identifying MFEs it is easy to spot those that are constant expressions,
because their level numbers are zero.
We can rather neatly ensure that it is turned
into a supercombinator by the subsequent
lambda-lifting pass,
by making it into a lambda abstraction {\em with an empty
argument list}.
This was the other reason why we decided to make the @ELam@ constructor take
a list of arguments.
The modification affects only @identifyMFEs_e@, thus:
1> identifyMFEs_e cxt (level, e) =
1> if (level == cxt || notMFECandidate e) then
1> e'
1> else if (level > 0) then
1> ELet nonRecursive [(("v",level), e')] (EVar "v")
1> else
1> ELam [] e'
1> where
1> e' = identifyMFEs_e1 level e
\end{itemize}
\section{Retrospective and comparison with other work}
It is interesting to compare our approach with Bird's
very nice paper
[.bird supercombinator compiler.] which addresses a similar problem.
Bird's objective is to give a formal development of an efficient
fully lazy lambda lifter, by successive transformation of an initial
specification.
The resulting algorithm is rather complex, and would be hard to write
down directly, thus fully justifying the effort of a formal development.
In contrast, we have expressed our algorithm as a
composition of a number of very simple phases, each of which can readily
be specified and
written down directly. The resulting program has a constant-factor
inefficiency, because it makes many traversals of the expression.
This is easily removed by folding together successive passes into
a single function, eliminating the intermediate data structure.
Unlike Bird's transformations, this is a straightforward process.
Our approach has the major advantage that is is {\em modular}.
This allows changes to be made more easily.
For example,
it would be a simple matter to replace the lambda lifter with one which
performed lambda-lifting in the way suggested by Johnsson
,[.johnsson lambda lifting.]
whereas doing the same for Bird's algorithm would be
a major exercise. Similarly, it proved rather easy to modify the
algorithm to be more selective about where full laziness is introduced%
%(Section \ref{redundant})%
.
The main disadvantage of our approach is that we are unable to take
advantage of one optimisation suggested by Hughes, namely ordering the
parameters to a supercombinator to reduce the number of MFEs.
The reason for this is that the optimisation absolutely requires that
lambda lifting be entwined with the process of MFE identification, while
we have carefully separated these activities!
Happily for us, the larger MFEs created by this optimisation
are always partial applications, which should probably {\em not} be
identified as MFEs because no work is shared thereby%
% (Section \ref{redundant})%
.
Even so, matters might not have fallen out so fortuitously, and
our separation of concerns has certainly made some sorts of transformation
rather difficult.
\section*{Acknowledgements}
We owe our thanks to John Robson and Kevin Hammond, and two anonymous
referees for their useful comments on a draft of this paper.
\section*{References}
.[]
\pagebreak % Prevents last reference from becoming detached
\appendix
\section{Appendix: Omitted code}
\label{omitted}
This appendix contains the definitions of functions omitted from the
main paper.
\subsection{@abstract@}
We begin with the function @abstract@.
% from Section~\ref{abstract}%
> abstract (_, AConst k) = EConst k
> abstract (_, AVar v) = EVar v
> abstract (_, AAp e1 e2) = EAp (abstract e1) (abstract e2)
> abstract (free, ALam args body) =
> foldl EAp sc (map EVar fvList)
> where
> fvList = setToList free
> sc = ELam (fvList ++ args) (abstract body)
> abstract (_, ALet isRec defns body) =
> ELet isRec [(name, abstract body) | (name, body) <- defns] (abstract body)
\subsection{@separateLams@}
Next comes the code for @separateLams@.
%from Section~\ref{separatelams}%
> separateLams (EConst k) = EConst k
> separateLams (EVar v) = EVar v
> separateLams (EAp e1 e2) = EAp (separateLams e1) (separateLams e2)
> separateLams (ELam args body) = foldr mkLam (separateLams body) args
> where
> mkLam arg body = ELam [arg] body
> separateLams (ELet isRec defns body) =
> ELet isRec [(name, separateLams rhs) | (name,rhs) <- defns]
> (separateLams body)
\subsection{@identifyMFEs_e1@}
@identifyMFEs_e1@ applies @identifyMFEs_e@ to the components of the expresion.
%(see Section~\ref{identifyMFEs})%
> identifyMFEs_e1 :: Level -> AnnExpr' (Name, Level) Level -> Expr (Name, Level)
> identifyMFEs_e1 level (AConst k) = EConst k
> identifyMFEs_e1 level (AVar v) = EVar v
> identifyMFEs_e1 level (AAp e1 e2) =
> EAp (identifyMFEs_e level e1) (identifyMFEs_e level e2)
When it encounters a binder it changes the ``context'' level
number carried down as its first argument.
> identifyMFEs_e1 level (ALam args body) =
> ELam args (identifyMFEs_e argLevel body)
> where
> (_, argLevel) = head args
>
> identifyMFEs_e1 level (ALet isRec defns body) =
> ELet isRec defns' body'
> where
> body' = identifyMFEs_e level body
> defns' = [ ((name,rhsLevel),identifyMFEs_e rhsLevel rhs)
> | ((name,rhsLevel),rhs) <- defns]
\subsection{@rename@}
The function @rename@ gives unique names
to the variables in an expression.
% from Section~\ref{rename}.
We need an auxiliary function @rename_e@ to do all
the work:
> rename e = e' where (_, e') = rename_e [] initialNameSupply e
> rename_e :: Assn Name Name -> NameSupply -> Expr (Name,a)
> -> (NameSupply, Expr (Name,a))
> rename_e env ns (EConst k) = (ns, EConst k)
> rename_e env ns (EVar v) = (ns, EVar (assLookup env v))
> rename_e env ns (EAp e1 e2) =
> (ns2, EAp e1' e2')
> where
> (ns1, e1') = rename_e env ns e1
> (ns2, e2') = rename_e env ns1 e2
> rename_e env ns (ELam args body) =
> (ns1, ELam args' body')
> where
> (ns1, args') = mapAccuml newBinder ns args
> (ns2, body') = rename_e (assocBinders args args' ++ env) ns1 body
> rename_e env ns (ELet isRec defns body) =
> (ns3, ELet isRec (zip binders' rhss') body')
> where
> (ns1, body') = rename_e env' ns body
> binders = bindersOf defns
> (ns2, binders') = mapAccuml newBinder ns1 binders
> env' = assocBinders binders binders' ++ env
> (ns3, rhss') = mapAccuml (rename_e rhsEnv) ns2 (rhssOf defns)
> rhsEnv | isRec = env'
> | not isRec = env
@newBinder@ is just like @newName@, but works over @(Name,a)@ binders.
> newBinder ns (name, info) =
> (ns1, (name', info)) where (ns1, name') = newName ns name
@assocBinders@ builds an assocation list between the names inside two lists
of @(Name,a)@ binders.
> assocBinders :: [(Name,a)] -> [(Name,a)] -> Assn Name Name
> assocBinders binders binders' = zip (map fst binders) (map fst binders')
\subsection{@float@}
The final pass floats let(rec) expressions out to the appropriate level%
% (Section~\ref{float})%
.
The main function has to return an expression together with a
list of definitions which should be floated outside the expression.
> float_e :: Expr (Name, Level) -> (FloatedDefns, Expression)
The top-level function @float@ uses @float_e@ to do the main work, but
if any definitions are floated out to the top level, @float@ had better
install them at this level, thus:
> float e = install floatedDefns e' where (floatedDefns, e') = float_e e
There are many possible representations for the @FloatedDefns@ type, and
we will choose a simple one, by representing the definitions being floated
as a list, each element of which represents a group of definitions,
identified by its level, and together with its @IsRec@ flag.
> type FloatedDefns = [(Level, IsRec, [Defn Name])]
Since the definitions in the list may depend on one another, we add the
following constraint: a definition group may depend only on
definition groups appearing {\em earlier} in the @FloatedDefns@ list.
It is now possible to define @install@, which wraps an expression
in a nested set of let(rec)s containing the specified definitions.
> install :: FloatedDefns -> Expression -> Expression
> install defnGroups e =
> foldr installGroup e defnGroups
> where
> installGroup (level, isRec, defns) e = ELet isRec defns e
We can now proceed to a definition of @float_e@. The cases for
variables, constants and applications are straightforward.
> float_e (EConst k) = ([], EConst k)
> float_e (EVar v) = ([], EVar v)
> float_e (EAp e1 e2) = (fd1 ++ fd2, EAp e1' e2')
> where
> (fd1, e1') = float_e e1
> (fd2, e2') = float_e e2
How far out should a definition be floated? There is more than
one possible choice, but here we choose to install a definition
just inside the innermost lambda which binds one its
free variables\footnote{
Recall
%from Section~\ref{levels}
that all variables bound by a single
@ELam@ construct are given the same level.}.
> float_e (ELam args body) =
> (outerLevelDefns, ELam args' (install thisLevelDefns body'))
> where
> args' = [arg | (arg,level) <- args]
> (_,thisLevel) = head args -- Extract level of abstraction
> (floatedDefns, body') = float_e body
> thisLevelDefns = filter groupIsThisLevel floatedDefns
> outerLevelDefns = filter (not.groupIsThisLevel) floatedDefns
> groupIsThisLevel (level,isRec,defns) = level >= thisLevel
The case for a let(rec) expression adds its definition group
to those
floated out from its body, and from its right-hand sides.
The latter must come first, since the new definition group may depend on them.
> float_e (ELet isRec defns body) =
> (rhsFloatDefns ++ [thisGroup] ++ bodyFloatDefns, body')
> where
> (bodyFloatDefns, body') = float_e body
> (rhsFloatDefns, defns') = mapAccuml float_defn [] defns
> thisGroup = (thisLevel, isRec, defns')
> (_,thisLevel) = head (bindersOf defns)
>
> float_defn floatedDefns ((name,level), rhs) =
> (rhsFloatDefns ++ floatedDefns, (name, rhs'))
> where
> (rhsFloatDefns, rhs') = float_e rhs
\subsection{@mapAccuml@}
Finally, here is the full definition of utility function @mapAccuml@.
% introduced in Section~\ref{mapaccum}
1> mapAccuml :: (b -> a -> (b, c)) -- Function of element of input list
1> -- and accumulator, returning new
1> -- accumulator and element of result list
1> -> b -- Initial accumulator
1> -> [a] -- Input list
1> -> (b, [c]) -- Final accumulator and result list
1>
1> mapAccuml f b [] = (b, [])
1> mapAccuml f b (x:xs) = (b2, x':xs') where (b1, x') = f b x
1> (b2, xs') = mapAccuml f b1 xs
%> readExpr :: Text a => String -> Expr a
%> readExpr s = read s
\section{Appendix: Interface to @Utilities@ module}
\label{utilities}
This is the text of the @interface@ for the @Utilities@ module.
This interface is imported by the
line @import Utilities@ in the @LambdaLift@ module.
The interface can be deduced by the compiler from the text of the
@Utilities@ module; indeed the interface below is exactly that produced
by the compiler, modulo some reordering and comments.
The first line introduces and names the interface.
1> interface Utilities where
Next we have declarations for the @Set@ type. Notice that
the data type @Set@ is given with an abbreviated @data@ declaration,
which omits the constructor(s). This makes the @Set@ type {\em abstract}
since the importing module can only build sets and take them apart using
the functions provided in the interface.
1> data Set a
1> setDifference :: (Ord a) => (Set a) -> (Set a) -> Set a
1> setIntersect :: (Ord a) => (Set a) -> (Set a) -> Set a
1> setUnion :: (Ord a) => (Set a) -> (Set a) -> Set a
1> setUnionList :: (Ord a) => [Set a] -> Set a
1> setToList :: (Set a) -> [a]
1> setSingleton :: a -> Set a
1> setEmpty :: Set a
1> setFromList :: (Ord a) => [a] -> Set a
Bags, association lists and the name supply follow similarly.
Notice that association lists are declared with a type synonym, so they
are not abstract.
1> data Bag a
1> bagUnion :: (Bag a) -> (Bag a) -> Bag a
1> bagInsert :: a -> (Bag a) -> Bag a
1> bagToList :: (Bag a) -> [a]
1> bagFromList :: [a] -> Bag a
1> bagSingleton :: a -> Bag a
1> bagEmpty :: Bag a
1> type Assn a b = [(a, b)]
1> assLookup :: (Eq a) => [(a, b)] -> a -> b
1> data NameSupply
1> initialNameSupply :: NameSupply
1> newName :: NameSupply -> [Char] -> (NameSupply, [Char])
\end{document}
|