Showing posts with label l. Show all posts
Showing posts with label l. Show all posts

2013/06/30

Structuring the compiler

Simple is difficult

In the implementation of the L compiler, the focus was put on the readability and simplicity of the source code. As often in computer science, it is difficult to make things simple: I had a complete, working prototype, with parser, typing, cps transformation and compilation to LLVM when I started this blog; I spent all this time cleaning, documenting, and especially simplifying the compiler, publishing the progress along the way.

I believe that the result is worth the work: a simpler compiler is easier to extend, both for myself and future contributors. The challenge will be to keep things easy as more features are added; this is possible only if we wait for things to be well structured before adding the non-essential features1; and if we do not hesitate to perform a local redesign occasionally.

Implementation of the toplevel of the compiler

The toplevel of the compiler, that link all the passes together, is a good example of the results of such a redesign. A design goal of this toplevel is to make each pass communicate with a minimal interface. I had written a first attempt, using a record updated functionally, with each field containing the internal state of one pass. The toplevel consisted in a giant function with nested loops – one loop per pass. This structure was inelegant for a number of reasons:

  • Even if the type of the state could be made abstract, the abstract type was still part of the interface. Moreover, the initial value for this type also had to be provided by the passes.
  • Updating the environment functionally introduced a lot of boilerplate code; even if imperative updates might have helped a little.
  • There was also some boilerplate code to handle the case where a single input could return multiple output (e.g. a datatype declaration in ML defines both a type and some constructors).
  • The nested loop pattern was not really readable.

After a while, I came up with a much simpler design using streams of definitions. The code for a toplevel is then dead-simple:

module Stream = Extensions.Stream

let process_file file =
   let parsetree_stream = Parser.make_stream file in
   let ast_stream = Astfromsexp.from_stream parsetree_stream in
   let cps_stream = Cpstransform.from_stream ast_stream in
   let converted_stream =
     Stream.iter_and_copy Cpsconvertclosures.in_definition cps_stream in
   let llvm_stream = Cpsllvm.from_stream converted_stream in
   Stream.iter (fun elt → Llvmexec.exec_nodef () elt) llvm_stream

The actual implementation also interposes optional printer to see the contents of the stream, which is very useful when debugging the compiler.

The stream interface make it easy to add new passes, completely remove the state from the interface, factorize the pattern when a pass can produce several elements at once, retain the minimal memory consumption of the nested loop design, and can be easily updated to a parallel pipelining implementation.

Interface to the passes and modular implementation

Interface of each pass

The interface of each pass is made minimal. Each pass only declares:

  • The type of the elements in the stream (e.g. tokens, typed AST definitions, CPS definitions…). The type is not abstract: it is used by the following pass to convert them to its own representation (e.g. the CPS pass use the AST type to perform CPS transformation, the LLVM pass use the CPS type to compile to LLVM).
  • A function mapping an input stream to an output stream.
  • Possibly, a function to print elements in the stream, used for debugging the compiler.

And that is all. The interface of each pass clear is minimal, which allows to study or change it independently.

Note: this way of structuring things imply that transformation from one pass to the next is done in the second pass. (This implies that the second pass depends on the first one).

An alternative is to expose functions to build elements in the second pass, and move the transformation code from the beginning of the second pass to the end of the first pass. In this case, the first pass depends on the second pass. One advantage is that the first pass does not have to expose the type of its internal format. I am considering using this structure for the parser, where a "parsetree" is produced, but whose type is not very interesting outside of the parsing phase; while the building functions for the AST are more interesting.

Modules inside each pass

Inside each pass, the code is structured around a hierarchy of modules and interfaces. The idea is that each interface progressively hides more details about the implementation of the pass, until the top where we get the minimal interface presented earlier. Intermediate levels show up when we manage to group a complex piece of code into a simple interface. For instance in the CPS pass, there are four intermediary modules:

  • CPStransform, performing AST to CPS transformation. Its implementation consist of 4 sub-modules, that include the compilation of pattern matching; its interface only consists of a single function.
  • CPSconvertclosure is a slightly less complex algorithm; its interface also consists in a single function.
  • CPSshrink will perform shrinking reductions and optimizations. I have not yet implemented the cleaned version of this module.
  • CPSbase provides general functions for creating and updating CPS terms; the interface of this module is more complex, but hides many details about the internal CPS data structure (which is quite complex), and prevents direct writes to this structure.

Structuring the code around hierarchical, modular interfaces and implementations thus allow intermediate levels of abstractions; when completing a task one is provided with just the needed information, the right degree of abstraction; this helps to focus. The hierarchy also decrease, and thus simplifies, the size of the interfaces. OCaml was perfect for this job, but C, to a lesser extent, also allows this kind of modular programming.

Comparison with object orientation

Note that this design is not at all object-oriented. I think that when providing a new abstract type, packing it with all the methods allowing to access or update objects of this type is an excellent idea. However by using only object-oriented interfaces, the tendency is to to present all methods of an object in a flat manner, and implement all of them in the same class and file. This is problematic when there are many methods (which should be grouped by themes), or that some methods are lengthy algorithms (the algorithm should be set in a separate file). Modules allow such grouping and separation of methods, while creating a new usual object-oriented classes would not work (because we operate on an object defined in another class). Of course there are object-oriented work-arounds; but I think that modules and interfaces with abstract types solve the problem more elegantly (while still allowing "type + methods"-style interfaces). I hope that the code for the L compiler, written in OCaml, shows my point.

Footnotes:

1 I have a huge and ever-growing list of extensions I would like to experiment or add to the L language

2013/05/28

Compiling pattern matching

I have finished implementing the compilation of pattern matching for L. L source code can now use ML-style patterns, as used in the match, let, and fun/function operators of OCaml. See http://caml.inria.fr/pub/docs/oreilly-book/html/book-ora016.html for an example in OCaml if you don't know about pattern matching.

Compilation of pattern matching happens together with the CPS transformation phase of the compiler, which translates the "AST" language (used in particular to perform type inference) to the "CPS" language (in which jumps and order of computation is made explicit). CPS transformation fixes the order of evaluation, so it makes sense to perform pattern compilation during this phase.

As the rest of the compiler, this module has been been written in literate programming style (or at least, it is heavily commented, so that it should be understandable by someone with no prior knowledge of the subject). I have extracted the header of the module at the bottom of this post.

The next step is to finish cleaning the rest of the CPS transformation (which is much easier); with this the entire backend of the compiler will have been published.

Module Cpstransform_rules

The Cpstransform_rules module handles the compilation of a set of rules. A rule is composed of a pattern, matched against a value; and an expression (the body of the rule), to be executed if the pattern succeeds in matching the value.

The match(expression){rules} expression matches an expression against a set of rules; its behaviour is to evaluate the expression once, and try to match the resulting value against the patterns in rules until one succeeds, in which case the corresponding body is executed.

The order of the rules is thus important: a value can be matched by the patterns of two rules, e.g. (5,6) is matched by (5,_) and (_,6); but only the body of the first rule is executed. This implies that two successive rules in a match can be reordered if, and only if, they are disjoint, i.e. they cannot match the same value.

But when allowed, reordering and factorizing the compilation of rules matching lead to more compact, faster code. Trying to produce the most efficient code for matching against a set of rules is a NP-complete problem (the complexity arise when compiling multiple independent patterns, for instance tuple patterns). Rather than attempting to solve this problem, L specifies how pattern matching is compiled, which allows the developper to visualize the costs of its pattern matching.

Pattern matching rewrites

The compiler maintains a list of patterns that remain to be matched for each rule, and a list of values against which each rule is matched. The list of the first pattern to be matched in each rules is the first column, and is matched against the first value. Several cases can occur (see in the book "The implementation of functional programming languages" by Simon Peyton Jones, the chapter 5: "Efficient compilation of pattern matching", by Philip Wadler):

There is no more column

When there are no remaining patterns to match, and no remaining values, it means that all the rules match. As pattern matching selects the first rule that matches, we execute the body of the first rule, and discard the other rules with a warning.

For instance in

match(){
 () → body1
 () → body2
}

body1 matches and is executed; body2 also matches, but is superseded by body1, and is just discarded.

The column contain only variables

In this case, in each rule the variable is bound to the value, and matching continues. For instance in

match(expr1,...){
 (a,...) → body1
 (b,...) → body2
}

a is bound to v1 in the first rule, and b in the second rule; where v1 is a CPS variable representing the result of computing expr1 in the condition of the match (computation in the condition is thus not repeated). Matching then proceeds, starting from the second column.

This rule can be extended to incorporate wildcard (_) patterns (where nothing is bound), and all irrefutable patterns. A pattern is irrefutable if it does not contain any variant.

For instance, consider

match((expr1,expr2),...){
 (a,...) → body1
 (_,...) → body2
 ((c,d),...) → body3
}

The column contains only irrefutable patterns. Let v1 be a CPS variable containing the evaluation of expr1, and v2 containing the evaluation of expr2. Then a is bound to (v1,v2), c to v1, and d to v2.

The column contains only calls to constructors

A constructor is a specific version of a variant type; for instance 3 is a constructor of Int, True a constructor of Bool, and Cons(1,Nil) a constructor of List<Int>.

Note that if the column contain a variant, then all the constructors that it contains are of the same type: this is necessary for the pattern matching to typecheck.

When two contiguous rules have different constructors at the same place, they cannot match the same value simultaneously: they are thus disjoint, and can be swapped. This allows to group the rules according to the constructor in their first column (the order of the rules within a group being preserved).

For instance,

match(expr1, expr2){
 (Cons(a,Cons(b,Nil)),Nil) → body1
 (Nil,Nil) → body2
 (Nil,c) → body3
 (Cons(d,e),_) → body4 
}

can be grouped as (preserving the order between rules 1 and 4, and 2 and 3) :

match(expr1, expr2){
 (Cons(a,Cons(b,Nil)),Nil) → body1
 (Cons(d,e),_) → body4
 (Nil,Nil) → body2
 (Nil,c) → body3
}

Then, the matching of contiguous rules with the same constructor can be factorized out, as follow:

let v1 = expr1 and v2 = expr2 in
match(v1){
 Cons(hd,tl) → match((hd,tl),v2){
     ((a,Cons(b,Nil)), Nil) → body1 
     ((d,e),_) → body4 
 }
 Nil → match(v2){
     Nil → body2 
     c → body3 
 }
}

Note that the L compiler matches the values in the constructor before matching the other columns of the tuple, as was exemplified in the Cons rules.

The construct of matching only the constructors of a single variant type can be transformed directly into the CPS case expression. It is generally compiled very efficiently into a jump table, and dispatching to any rule is done in constant time. (Note that the compiler may not generate a jump table if the list of constructors to check is sparse).

The first column contains both refutable and irrefutable patterns

If the first column contains both kind of patterns, the list of rules is split into groups such that the ordering between rules is preserved, and either all the rules in the group have their first pattern that is refutable, or they are all irrefutable.

For instance, the following match:

match((v1,v2),v3){
 (_,1) → 1
 ((a,b),2) → a+b+2
 ((3,_),3) → ...
 ((4,_),_) → ...
 ((_,5),_) → ...
 ((a,b),c) → a+b+c
}

is split into three groups, with respectively rules 1-2 (_ and (a,b) are both irrefutable patterns), rules 3-5, and rule 6. Then the groups are matched successively, the next group being matched only if no rule matched in the first one. This amount to performing the following transformation:

let c = ((v1,v2),v3) in
match(c){
 (_,1) → 1
 ((a,b),2) → a+b+2
 _ → match(c){
     ((3,_),3) → ...
     ((4,_),_) → ...
     ((_,5),_) → ...
     _ → match(c){
         ((a,b),c) → a+b+c
         }
     }
}

Note that rules 3,4,5 also need to be split and transformed further using this method.

Compiling pattern matching

Compilation order

The order in which checking is made for a set of patterns is a choice, done by the compiler. L chooses to match the tuples from left to right, and the contents of the constructor as soon as they are matched; and to split rules according to the refutability of the pattern in their first remaining column. This choice may not be optimal in every case (but minimizing the number of matches is a NP-hard problem), but allows for a simple, visual analysis of the cost of pattern matching. The user is free to rearrange the set of patterns to improve performance (possibly guided by compiler hints).

Element retrieval

At the beginning of a match, all the components, needed by at least one rule, that can be retrieved (i.e. components in a tuple etc., but not those that are under a specific constructor) are retrieved. When a constructor is matched, all the components that can be retrieved that were under this constructor are retrieved. This behaviour produces the most compact code (avoid duplicating retrieval of elements in the compilation of several rules), but maybe not the most efficient (sometimes elements are retrieved that are not used). Optimizations, such as shrinking reductions, are allowed to move down or even duplicate code performing retrieval of elements into the case.

CPS

Compilation of pattern matching is done during the CPS transformation, which transforms source code from the AST language to the CPS language. There are several reasons for that:

  • The CPS transformation of expression fixes the order of their evaluation; compiling pattern matching fixes the order in which patterns are matched. So it makes sense to do both at the same time, to have a single pass that fixes all the order of evaluation.

    As a side note, it makes sense to keep pattern matching in the AST language, because patterns are easy to type, and any typing error can be more easily returned to the user.

  • The CPS language provides continuations, which allows to express explicit join points in the control flow, something not possible in the AST language (without creating new functions). These joint points are necessary notably to factorize the compilation of pattern matching (this problem is similar to compilation of boolean expressions with the short-circuit operators && and ||). For instance, compiling:

    match(v){ (4,5) → 1; _ → 2 }

    gives:

    let k_not4_5() = { kreturn(2) }
    match(#0(v)){
     4 → match(#1(v)){
       5 → kreturn(1)
       _ → k_not4_5()
     }
     _ → k_not4_5()
    }

    Matching against (4,5) can fail at two different steps, and the action to perform in these two cases are the same, so they should be factorized using the same continuation.

    The L compiler does not yet allow it, but "or-patterns" (i.e. in match(l){ Cons(NilCons(_,Nil)) → 0 _ → 1 }) also need join points. Finally, there is also a joint point (in expr_env.context) to which the value of the bodies in each rule is returned.

All the functions that involve building CPS code are themselves in CPS style; see the Cps_transform_expression module for an explanation.

A complete example

Here is a (contrived) exemple of a complete pattern matching:

This pattern is compiled as follows. We begin by creating a join continuation, which is where the result of the match is returned. This allows to factorize the following computation (the addition to 17 in our case).

let kfinal(x) = { let x17 = x + 17 in halt(x17) }

Then, the condition of the match e is evaluated, and its result stored in a temporary value.

let v = ... eval e ...

Then, analysis of the patterns show that v contains a tuple.

let v.0 = #0(v)
let v.1 = #1(v)

Analysis of the patterns also show that v.0 contain a tuple. v.1 is a variant type, so its elements cannot be retrieved yet.

let v.0.0 = #0(v.0)
let v.0.1 = #1(v.0)

We begin by analysis the whole pattern (i.e. column c0). All the rules are refutable, except the last one, so we split them into two contiguous blocks bi and bii; bii is executed if matching against all the rules in bi fail.

decl kb_ii

All the rules in bi are tuples, so we inspect them from left to right (i.e. we begin by column c1, then proceed with c4). Analysis of column c1 yields three contiguous blocks: the patterns in column c1 are all irrefutable for block bi.a, refutable for block bi.b, and irrefutable again for block bi.c.

decl kb_i.b, kb_i.c

As the patterns of column c1 in rules in bi.a are all irrefutable, we just have to associate the variable x to v.0.0 and a to v.0.1 for the translation of the body of the rules. (x and a are unused in the rules of the example).

We can then proceed with the analysis of column c2 (still in block bi.a). It is a variant, so we can regroup the rules according to the constructor, and perform a simple case analysis.

decl kcons
match(v.1){
 Nil → { kfinal(2) }
 Cons(x) → { kcons(x) } 
}

For the Nil constructor, we are already done. For Cons, we have to discriminate against the patterns inside the Cons. But first, we analyze these patterns to retrieve all the elements that are needed:

let kcons(x) = {
 let x.0 = #0(x)
 let x.1 = #1(x)
 let x.0.0 = #0(x.0)
 let x.0.1 = #1(x.0)

There are two contiguous blocks: one with rule 1 and 3 (since rule 2 has been regrouped with the Nil), and one with rule 4. We begin with the 1-3 block:

 decl knext
 match(x.0.0){
   1 → { kfinal(1)}
   3 → { kfinal(3)}
   _ → { knext()}
 }

If matching against the 1-3 block fails, we match against rule 4. If this fails, then matching against all the rules in bi.a failed, and we try to match against the rules in bi.b.

 let knext() = {
   match(x.0.1){
     4 → { kfinal(4)}
     _ → { kb_i.b()}
   }
 }
}

The rest of the matching is very similar. In bi.b.1, the matching against rules 5 and 7 is factorized, because there is a common constructor. (Note that there is no factorization on Cons between rules 4 and 5, because they are in different blocks). Then blocks bi.b.2, bi.b.3, bi.c are tried successively. In bi.c, the test of Cons is factorized, but not the test for Nil, because testing Nil is done after testing 10.

Finally, the pattern matching always succeeds since rule 12 is irrefutable, so there is no need to introduce code that perform match_failure in case nothing succeeds in matching.

Note that the presentation would have been clearer if the patterns had been regrouped differently; in particular, grouping rules who share a constructor matched as the same time (e.g. exchanging rules 1 and 2, and rule 5 and 6) would improve the presentation.

2012/12/30

A framework for CPS transformation (and a Github account)

I have implemented a framework for efficient transformation of CPS code. The code is too big to be presented in a blog post; I have set up a github account to put it (https://github.com/mlemerre/l-lang/). It has been working for several months, but I have spent a long time to improve its structure, write commented interfaces, and document it to make it easily readable, as I have done with the previous modules. Do not hesitate to tell me about any comments you may have, on the code or documentation.

The code is based on the paper "Compiling with continuations, continued" by Andrew Kennedy (which is very well written and easy to read), itself inspired by "Shrinking lambda expressions in linear time", by Andrew W. Appel and Trevor Jim.

The CPS representation in Andrew Kennedy's paper provides many interesting features:

  • It is efficiently compilable and can use a stack; see the translation of this representation to the SSA form of LLVM.
  • The representation separates continuation from normal functions; this ensures that continuations do not require heap allocations and are compiled into jumps. This allows to express control flow, and control flow optimizations, in the representation.
  • Appel and Jim, and Kennedy have developed a representation that allows efficient (in-place) rewrite of terms while maintaining the links between variables, their occurrences, and their binding sites. This allows to implement shrinking reductions (and other transformations, such as closure conversion) in linear time.

    Shrinking reductions rules are very easy to understand, and can be used as a basis for expressing guaranteed optimizations. For instance, it should be easy to state that functions used only once are inlined, that tuples used only to pass information locally are not heap-allocated, etc.

The modules I have implemented provide means to access, print, or change terms in the CPS intermediary language. The main modules, are represented on the figure below.

The Base module is the entry point for accessing the CPS representation, and the first module if trying to understand my code. It provides read-only direct access to the CPS representation, and to the links between variables, occurrences, and their binding sites. It also gives access to the other modules that implement the CPS manipulation framework:

  • Ast: Really a part of Base representation, it provides access to the CPS representation using simple algebraic datatypes of syntax tree.
  • Check: Allows checking for some invariants of terms in the CPS representation. Some information in the representation is redundant, which allows fast access; this module checks that redundant information is in sync. For instance, if a term contains a variable, it checks that the variable's uplink also points to that term.
  • Build: Provides functions that allows to create new terms in the CPS representation, without worrying about the complexities of the representation. The API of this module is based on the idea of higher-order abstract syntax, i.e. where binding creations in the destination language (L) language correspond to creation of bindings in the source language (OCaml).
  • Traverse: Allows folding and iteration on the terms, variables, and occurrences of the CPS representation. Using this module allows, in particular, code to be independent of future changes to the CPS data structures, which will be gradually improved.
  • Change: Provides high-level functions to change CPS terms. This is how transformation passes modify terms in the CPS representation.
  • Print: Provides a human-readable representation of the CPS form. I have tried to come up with a representation that is "easy" to read; the representation looks more like SSA and/or assembly than classical lambda-calculus (which make it much easier to read on huge terms). This textual representation should also be easily parsable (although I did not write the parser).
  • Def: Implements and provides accessors and a first level of abstraction to the CPS data structures. It relies on Var, which implements the relationships between variables and their occurrences (itself based on the Union_find data structure described earlier on this blog).

The other modules on the figure are Union_find and Unique, which are "support" modules; and Closure_conversion and Shrinking_reductions, which are transformation passes on the CPS representation. These two passes are not yet in the github repository.

I have a working closure conversion that gives me a complete basic working compiler for the L programming language, based on this CPS framework. I plan to document it and upload it to github very soon. I also have implemented some basic shrinking reductions.

Next I will concentrate on the parser and the L abstract syntax tree, and I will be will be covering the syntax and semantics of L in a future blog post. But here is already an excerpt of test L code (that uses first-class functions) that can be compiled to LLVM:

assert( { let true = { (x,y) -> x }
          let false = { (x,y) -> y }
          let pair = { (first,second) -> boolean -> boolean( first, second) }
          let first = { p -> p( true)}
          let second = { p -> p( false)}
          let p = pair( 7, 5)
          second( p) * (first( p) + second( p)) } == 60)

2012/08/25

CPS to LLVM SSA conversion in literate programming

My L compiler's toolchain is now complete, in that every necessary transformation pass is here. The various passes are parsing, macro-expansion, type checking and inference, CPS transformation, closure conversion, and compilation to LLVM instructions.

Most of the passes are still simple, and a lot of work remains to obtain something usable. For instance I do not propagate informations about locations, so typing error does not explain where the error is. All values, including integers, are boxed, allocated with malloc and never freed; and L code cannot call external C functions. The CPS transformations are not very efficient, and do not carry type informations. These are the points I am going to improve next.

However having a complete toolchain is nice: it gives a complete overview so now I know how changing a pass can benefit to both the above and below layers.

The nice thing about the passes being simple is that they are easy to understand, so this is a good opportunity to publish the code. To further improve the comprehension, I have decided for the last pass I wrote, which is the transformation from CPS to LLVM, to give a try at literate programming. It basically consists in writing your code in the manner of a text book.

There is a nice tool to do literate programming in ocaml, named ocamlweb. It allows to write the literate parts in standard comments, so that the Ocaml files can either be compiled or transformed into a document. The default HTML output of ocamlweb (based on HEVEA is not very nice however, but some configuration allows to improve it. Here is is mine, that I put in a file heveaprefix.tex. This file changes the HTML output of the code parts of OcamlWeb to look like the HTML output of source code in Emacs Org-mode (to maintain consistency with this blog).

%% Note: The colors code are those of Emacs org-mode output (which I
%% think just put those of Emacs).

%% This makes \url links as clickables urls.
\input{urlhref.hva}

%% Big code blocks.
\renewcommand{\ocwbegincode}{%
\begin{rawhtml}
<div style="border: solid 1px gray; background:#eeeeee;
            margin: 0.5em 1em 0.5em 1em;
            padding: 0.5em 1em 0.5em 1em;
            font-family:mono"><code>
\end{rawhtml}}

\renewcommand{\ocwendcode}{\begin{rawhtml}</code></div>\end{rawhtml}}

%% Inline code blocks inside comments (given with [])
\renewcommand{\ocwbegindcode}{\begin{rawhtml}<code>\end{rawhtml}}
\renewcommand{\ocwenddcode}{\begin{rawhtml}</code>\end{rawhtml}}

%% Keywords. We distinguish some keywords (those that ``create''
%% something, and begin, in blue). We rely on HEVEA native support for
%% the ifthen package.
\newcommand{\spanpurple}[1]{%
\begin{rawhtml}<span style="color: #a020f0; ">\end{rawhtml}#1%
\begin{rawhtml}</span>\end{rawhtml}}

\newcommand{\spanred}[1]{%
\begin{rawhtml}<span style="color: #a52a2a; ">\end{rawhtml}#1%
\begin{rawhtml}</span>\end{rawhtml}}

\newcommand{\spanboldblue}[1]{%
\begin{rawhtml}<span style="color: #0000ff; font-weight: bold;">\end{rawhtml}#1%
\begin{rawhtml}</span>\end{rawhtml}}

\renewcommand{\ocwkw}[1]{%
\ifthenelse{\equal{#1}{let}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{and}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{rec}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{in}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{type}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{of}}{\spanred{#1}}{%
\ifthenelse{\equal{#1}{open}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{struct}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{sig}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{functor}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{module}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{val}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{begin}}{\spanboldblue{#1}}{%
\ifthenelse{\equal{#1}{end}}{\spanboldblue{#1}}{%
\spanpurple{#1}}}}}}}}}}}}}}}}


%% Ids that begin in lower case. The textrm command (note: Hevea does
%% not know about mathrm) allows non-italic typesetting. We also
%% consider failwith as a keyword (even if it is a function that calls
%% raise).
\renewcommand{\ocwlowerid}[1]{%
\ifthenelse{\equal{#1}{failwith}}{\spanpurple{\textrm{#1}}}{%
\textrm{#1}}}

%% Ids that begin in upper case.
\newcommand{\spangreen}[1]{%
\begin{rawhtml}<span style="color: #228b22; ">\end{rawhtml}#1%
\begin{rawhtml}</span>\end{rawhtml}}

\renewcommand{\ocwupperid}[1]{\spangreen{\textrm{#1}}}

%% Comments are type set in red, with the leading (* and closing *).
\renewcommand{\ocwbc}{\begin{rawhtml}<span style="color: #b22222">(&#X2217; \end{rawhtml}}
\renewcommand{\ocwec}{\begin{rawhtml} &#X2217;)</span>\end{rawhtml}}

%% Strings are typeset in brown.
\newcommand{\spanbrown}[1]{%
\begin{rawhtml}<span style="color: #8b2252; ">\end{rawhtml}#1%
\begin{rawhtml}</span>\end{rawhtml}}
\renewcommand{\ocwstring}[1]{\spanbrown{\textrm{#1}}}

%% Base types and type variables are in green.
\renewcommand{\ocwbt}[1]{\spangreen{\textrm{#1}}}
\renewcommand{\ocwtv}[1]{\spangreen{#1e}}

The compilation command I use to perform the Ocaml to HTML transformation is:

ocamlweb -p "\usepackage{hevea}\usepackage{url}" --no-index heveaprefix.tex cps/cpsbase.ml llvm/cpsllvm.mli llvm/cpsllvm.ml > web/cpsllvm.tex \ 
&& cd web && hevea -I /usr/share/texmf/tex/latex/misc ocamlweb.sty cpsllvm.tex 

Below is the result (no need to explain it since this is literate programming! :))

Update: Apparently editing the post with blogger's editor mixes up the HTML, but this should be fixed now.

Module Cpsbase

1.  These definitions originates from the "compiling with continuations, continued" paper, by Andrew Kennedy (we currently use the simplified, non-graph version).

CPS (for continuation passing style) puts constraints on functional programs so that a function f never returns; instead it is passed a continuation k, which is a function that represents what is executed on f has finished its execution. So instead of returning a value x, f "returns" by calling k(x). CPS style makes returning from functions, and more generally control flow, explicit, at the expense or more verbosity.

This file presents a particular representation of CPS terms that separates continuations, calling a continuations, variables holding continations from respectively normal functions, normal function calls, and normal variables. This distinction allows to compile the CPS program using a stack (see the Cpsllvm module for an implementation of that).

The representation also forces all values (including constants such as integers) to be held in variables, which simplify later transformation algorithms.


2.  We define variables and continuation variables a unique, to avoid any need for alpha conversion.

module UniqueCPSVarId = Unique.Make(struct end)

module UniqueCPSContVarId = Unique.Make(struct end)

type var = Var of UniqueCPSVarId.t
type contvar = ContVar of UniqueCPSContVarId.t

Many algorithms use sets and maps of variables and continuation variables.
module VarMap = Map.Make(struct
   type t = var
   let compare = compare
end)

module VarSet = Set.Make(struct
   type t = var
   let compare = compare
end)

module ContVarMap = Map.Make(struct
   type t = contvar
   let compare = compare
end)

module ContVarSet = Set.Make(struct
   type t = contvar
   let compare = compare
end)

3.  Values are primitive objects, held in continuation variables.
type value = 
   ∣ Void 
   ∣ Constant of Constant.t
   ∣ Tuple of var list 
   ∣ Lambda of contvar × var × term
4.  The representation of CPS terms separates continuations from usual functions. The various terms are:

  • let x = value; body creates a binding to a primitive value, or to the result of a primitive operation (to be used in body)
  • let k(x) = t; body creates a binding to a continuation k. x is bound in t, but not in body. The k continuation variable is bound both in body and t (this allows loops).
  • k(x) calls the continuation k with x. It can be seen as a "jump with argument x"
  • v(k,x) calls the function v, k being the return continuation, and x a parameter. v does not return; instead it will call k with the "return value" as a parameter.
  • halt(x) is used only as a base case, to stop induction. Its semantics is that it returns the value x, which is the result of the computation, to the caller.


and term = 
   ∣ Let_value of var × value × term
   ∣ Let_primop of var × primitive_operation × term
   ∣ Let_cont of contvar × var × term × term
   ∣ Apply_cont of contvar × var
   ∣ Apply of var × contvar × var
   ∣ Halt of var
5.  Primitive operations return a value. The various operations do not take values as parameters (even constants such as int), only variables: the representation forces all values to be bound in a variable. This allows a uniform treatment that helps transformation passes.

The various operations are:

  • x[i] get the ith element out of x. x is a variable bound to a tuple.
  • x1 op x2 applies binary op to two arguments.

Note that there are no primitive that would allow to write let x = y, where y is a variable; thus there cannot be two variables that directly share the same value.


and primitive_operation = 
   ∣ Projection of var × int
   ∣ Integer_binary_op of Constant.integer_binary_op × var × var

Interface for module Cpsllvm

6.  This module translates CPS representation to the LLVM IR. CPS terms must observe that

  • Functions do not have free (unbound) variables or continuation variables (use closure conversion to get rid of free variables in functions)
  • Constants functions (such as +,−) have been η-expanded, and translated to the use of CPS primitive operations.


7.  All translations are done using Llvm.global_context(), and in a single Llvm module named the_module.

val the_module : Llvm.llmodule
8.  build_nodef name expr builds an expr, an expression in CPS form that is not part of a function, (for instance if it was typed in the interactive prompt). It is translated to a Llvm function that take no argument, named name.
val build_nodef : string → Cpsbase.term → Llvm.llvalue

Module Cpsllvm

9.  This module translates a term written in CPS representation to LLVM instructions in SSA form.

The CPS representations stems from the paper "Compiling with continuations, continued" by Andrew Kennedy. In particular this representation separates continuations from standard lambda functions, which allows calling and returning from functions using the normal stack.

This module assumes that functions have no free variables (or continuation variables). Closure conversion removes free variables from functions. Free continuation variables should never happen when translating normal terms to CPS.

The module also assumes that the CPS values do not refer to primitive operations, such as +,-,*,/. Previous passes must transform calls to primitive operations to let x = primitive(args); and η-expand primitive operations passed as functions (e.g. let x = f() must have been transformed).

To keep things simple in this first version, no external functions is called (only lambdas defined in the body of the expression, and primitive operations, can be called).

In addition, all data is boxed, allocated using malloc (and never freed; this could be improved by using libgc). Unboxed data would requires to carry typing information in the CPS terms.
10.  To get an overview of the translation algorithm, the best is to understand how the CPS concepts are mapped to the SSA concepts. In the following, we denote by [x] the translation of x.

  • Lambda are translated to LLVM functions with one argument and one return value.
  • Other values (i.e. int, floats, and tuples) are all translated boxed. Thus they all have a single llvm type, which is i8 *.
  • A CPS variable x is mapped to a SSA variables (of type Llvm.llvalue). CPS variables are introduced as arguments to lambda and continuations, and in the let x = ... form.
  • A CPS continuation variable k introduced by λ k. x. t corresponds to the return from the lambda. A call k(y) to this continuation with a value y is translated to a "ret" instruction returning the translation of y.
  • A CPS continuation variable k introduced by let k(x) = t1; t2 is mapped to the SSA basic block [t1] (of type Llvm.basicblock). The x formal argument of k corresponds to a phi node at the start of [t1]. A call k( y to this continuation with a value y is translated to a "jmp" instruction to the basic block [t1], that binds [y] to the phi node at the start of [t1].
  • A call f( k, x) of a regular (non-continuation) function f with first argument being a continuation variable argument k and second argument being a variable v is translated to a call to [f] with argument [x], followed by the translation of k( r), with r being the value returned by the call to f. This is because after calling a function in the LLVM SA, the control is returned to the following instruction. LLVM optimization passes like simplifycfg can optimize this if needed. Note: this allows tail call optimizations http://llvm.org/docs/CodeGenerator.html#tail-calls to take place.
  • Primitive operations, such as let x = primitive(args) are translated to the corresponding LLVM operations.

Note that the SSA representation are well-formed only if "the definition of a variable %x does not dominate all of its uses" (http://llvm.org/docs/LangRef.html#introduction). The translation from a CPS term (without free variables) ensures that.
11.  Here is a simplified example of how the translation from CPS to SSA works.

The CPS code:

  let v = 3;
  let k(x) = k(2+x);
  k(11)  

Is translated to SSA (ignoring boxing):

  entry: 
    v = 3
    n_ = 11
    jmp k

  k:
    x = phi (entry n_) (k o_)
    m_ = 2 
    o_ = m_ + x
    jmp k 

This shows how k is translated to a separate basic block, and the argument x to a phi node connected to all the uses of k.


12.  If one encounters segmentation faults when changing the LLVM related code, this may be caused by:

  • Calling Llvm.build_call on a value which does not have the function lltype, or Llvm.build_gep with operations that do not correspond to the lltype of the value.
  • Calling build_phi with an empty list of "incoming".
  • Calling ExecutionEngine.create the_module before calling Llvm_executionengine.initialize_native_target() can also segfault.

Using valgrind or gdb allows to quickly locate the problematic Ocaml Llvm binding.


let context = Llvm.global_context()

let the_module = Llvm.create_module context "my jitted module"

let void_type = Llvm.void_type context

let i32_type = Llvm.i32_type context

let i32star_type = Llvm.pointer_type i32_type

let anystar_type = Llvm.pointer_type (Llvm.i8_type context)

open Cpsbase

Creating and accessing memory objects


13.  These helper functions create or read-from memory object. Currently LLVM compiles using a very simple strategy: every value is boxed (including integers and floats). This simplifies compilation a lot: every value we create has type void *, and we cast the type from void * according to how we use it.

LLVM does not (yet?) know how to replace heap allocations with stack allocations, so we should do that (using an escape analysis). But LLVM has passes that allow promotion of stack allocations to register ("mem2reg" and "scalarrepl"), so once this is done (plus passing and returning arguments in registers), many values should be unboxed by the compiler (and this would not be that inefficient). Additional performances could then be obtained by monomorphizing the code.
14.  Store llvalue in heap-allocated memory.

let build_box llvalue name builder = 
   let lltype = Llvm.type_of llvalue in
   let pointer = Llvm.build_malloc lltype name builder in
   ignore(Llvm.build_store llvalue pointer builder);
   Llvm.build_bitcast pointer anystar_type (name ^ "box") builder
15.  Unbox a llvalue of type lltype.
let build_unbox llvalue lltype name builder = 
   let typeptr = Llvm.pointer_type lltype in
   let castedptr = Llvm.build_bitcast llvalue typeptr (name ^ "castedptr") builder in
   Llvm.build_load castedptr (name ^ "unbox") builder
16.  A n-tuple is allocated as an array of n anystar_type. Each element of the array contains the llvalue in l.
let build_tuple l builder = 
   let length = List.length l in
   let array_type = Llvm.array_type anystar_type length in 
   let pointer = Llvm.build_malloc array_type "tuple" builder in

   let f () (int,elem) = 
     (∗ Note: the first 0 is because pointer is not the start of the array, but a pointer to the start of the array, that must thus be dereferenced. ∗)
     let path = [| (Llvm.const_int i32_type 0); (Llvm.const_int i32_type int) |] in
     let gep_ptr = Llvm.build_gep pointer path "gep" builder in
     ignore(Llvm.build_store elem gep_ptr builder) in

   Utils.Int.fold_with_list f () (0,l);
   Llvm.build_bitcast pointer anystar_type ("tuplecast") builder

17.  Retrieve an element from a tuple.
let build_letproj pointer i builder = 
   let stringi = (string_of_int i) in 
   (∗ First we compute an acceptable LLvm type, and cast the pointer to that type (failure to do that makes Llvm.build_gep segfault). As we try to access the ith element, we assume we are accessing an array of size i+1. ∗)
   let array_type = Llvm.array_type anystar_type (i+1) in 
   let arraystar_type = Llvm.pointer_type array_type in
   let cast_pointer = Llvm.build_bitcast pointer arraystar_type ("castptr") builder in
   let gep_ptr = Llvm.build_gep cast_pointer [| (Llvm.const_int i32_type 0);
                                                 (Llvm.const_int i32_type i) |] 
     ("gep" ^ stringi) builder in 
   let result = Llvm.build_load gep_ptr ("builder" ^ stringi) builder in
   result 
18.  Apply primitive operations.
let build_integer_binary_op op a b builder = 
   let build_fn = match op with
     ∣ Constant.IAdd → Llvm.build_add
     ∣ Constant.ISub → Llvm.build_sub
     ∣ Constant.IMul → Llvm.build_mul
     ∣ Constant.IDiv → Llvm.build_udiv in
   let a_unbox = (build_unbox a i32_type "a" builder) in
   let b_unbox = (build_unbox b i32_type "b" builder) in
   let res = build_fn a_unbox b_unbox "bop" builder in
   build_box res "res" builder
19.  Build a call instruction, casting caller to a function pointer.
let build_call caller callee builder =
   let function_type = Llvm.pointer_type (Llvm.function_type anystar_type [| anystar_type |]) in
   let casted_caller = Llvm.build_bitcast caller function_type "function" builder in 
   let retval = Llvm.build_call casted_caller [| callee |] "retval" builder in
   retval

Creating and accessing basic blocks


20.  This special value is used to ensure, via the type checker, that compilation to LLVM never leaves a basic-block halfly built. LLVM basic blocks should all end with a terminator instruction; whenever one is inserted, the function should return End_of_block. When building non-terminator instructions, the code must continue building the basic block.

type termination = End_of_block
21.  This creates a new basic block in the current function.

Note that LLVM basic blocks are associated to a parent function, that we need to retrieve to create a new basic block.

let new_block builder = 
   let current_bb = Llvm.insertion_block builder in
   let the_function = Llvm.block_parent current_bb in
   let new_bb = Llvm.append_block context "k" the_function in
   new_bb
22.  Returns Some(phi) if the block already begins with a phi instruction, or None otherwise.
let begin_with_phi_node basic_block = 
   let pos = Llvm.instr_begin basic_block in
   match pos with
     ∣ Llvm.At_end(_) → None
     ∣ Llvm.Before(inst) → 
       (match Llvm.instr_opcode inst with
         ∣ Llvm.Opcode.PHI → Some(inst)
         ∣ _ → None)
23.  This builds a jmp instruction to destination_block, also passing the v value. This is achieved by setting v as an incoming value for the phi instruction that begins destination_block. If destination_block does not start with a phi node, then it is the first time that destination_block is called, and we create this phi node.
let build_jmp_to_and_add_incoming destination_block v builder =

   let add_incoming_to_block basic_block (value,curblock) = 
     match begin_with_phi_node basic_block with
       ∣ Some(phi) → Llvm.add_incoming (value,curblock) phi
       ∣ None → 
         (∗ Temporarily create a builder to build the phi instruction. ∗)
         let builder = Llvm.builder_at context (Llvm.instr_begin basic_block) in
         ignore(Llvm.build_phi [value,curblock] "phi" builder) in

   let current_basic_block = Llvm.insertion_block builder in
   add_incoming_to_block destination_block (v, current_basic_block);

   ignore(Llvm.build_br destination_block builder);
   End_of_block

24.  We use the following sum type to establish a distinction between:

  • continuation variables bound with lambda: calling them returns from the function, and the parameter x of the call k( x) is returned;
  • and continuation variables bound with letcont: calling them jumps to the corresponding basic block, and the parameter x of the call k( x) is passed to the phi node starting this basic block.

The CPS→LLVM translation maps continuation variables to dest_types.


type dest_type = 
   ∣ Ret 
   ∣ Jmp_to of Llvm.llbasicblock

Build a call to a continuation k x.
let build_applycont k x builder = 
   match k with
     ∣ Ret → ignore(Llvm.build_ret x builder); End_of_block
     ∣ Jmp_to(destination) → build_jmp_to_and_add_incoming destination x builder

Main CPS term translation


It is important for LLVM that function names are unique.

module UniqueFunctionId = Unique.Make(struct end)
25.  This function builds the CPS term cps, in the current block pointed to by builder. varmap maps CPS variables to LLVM llvalues. contvarmap maps CPS continuation variables to values of type contvar_type.

All the free variables or continuation variables in cps must be in contvarmap or in varmap. cps can contain lambda, but they must not contain any free variables or free continuation variables (even the one in varmap and contvarmap). Closure conversion deals with this. Note: previously-defined global variables are not considered free.

let rec build_term cps (contvarmap, varmap) builder =
26.  Helper functions to retrieve/add values from/to maps.
   let lookup_var x = 
     try VarMap.find x varmap 
     with _ → failwith "in lookup" in

   let lookup_contvar k = 
     try ContVarMap.find k contvarmap 
     with _ → failwith "in contvar lookup" in

   let add_to_varmap var value = VarMap.add var value varmap in
   let add_to_contvarmap contvar block = ContVarMap.add contvar (Jmp_to block) contvarmap in

27.  Converting the term is done by inductive decomposition. There are three kind of cases:

  • those that only build new values (letvalue, letproj, letprimop...) in the current basic block
  • those that return a value and end a basic block (apply, applycont, and halt)
  • the one that build a new basic blocks (letcont).

To keep the implementation simple, all values are boxed (i.e. put in the heap and accessed through a pointer), and of llvm type "i8 *". Pointer conversions are done according to the use of the value.

   match cps with
28.  These cases build a new value, then continue building the basic block.
     ∣ Let_value(x, value, body) → 
       let newllvalue = 
         (match value with 
           ∣ Constant(Constant.Int i) →
             let llvalue = Llvm.const_int i32_type i in
             build_box llvalue ("int" ^ string_of_int i) builder

           ∣ Tuple(l) →
             let llvalues = List.map lookup_var l in
             build_tuple llvalues builder

           This build a new function, with private linkage (since that it can be used only by the current term), which allows llvm optimizations.

Note that build_function will use a new builder, so the lambda can be built in parallel with the current function. Also it will use new variables and continuation variable maps (with only the x parameter), so the lambda expression must not contain any free variables.

           ∣ Lambda(k,x,body) → 
             let f = build_function "lambda" k x body in
             Llvm.set_linkage Llvm.Linkage.Private f;
             Llvm.build_bitcast f anystar_type "lambdacast" builder

           Expressions such as let x = primitive] should have been translated into something like let x = (a,b) -> primitiveop( a,b) ] in previous compilation stage, so should fail here.
           ∣ Constant(c) → 
             assertConstant.is_function c);
             failwith "ICE: primitive operations as value in LLVM translation."
         )
       in build_term body (contvarmap, (add_to_varmap x newllvalue)) builder

     Primitive operations are similar to letvalue.
     ∣ Let_primop(x,prim,body) → 
       let result = (match prim with 
         ∣ Integer_binary_op(op,xa,xb) → 
           build_integer_binary_op op (lookup_var xa) (lookup_var xb) builder
         ∣ Projection(x,i) → build_letproj (lookup_var x) i builder
       ) in
       build_term body (contvarmap, (add_to_varmap x result)) builder
29.  Building new basic blocks. The algorithm first creates an empty basic block, bound to [k], then build [body], then build [term] (if [k] is really called), binding [x] to the phi node.

The tricky part is that the llvm bindings do not allow to create an "empty" phi node (even if it would, in future implementations which would not box everything we would still have to know the llvm type of the phi node, and that llvm type is not known until we have processed the jumps to that node). So it is the calls to k that create or change the phi node; no phi node means [k] is never called.

Doing the operations in this order ensures that calls to [k] are processed before [k] is built.

     ∣ Let_cont(k,x,term,body) → 
       let new_bb = new_block builder in
       let newcvm = add_to_contvarmap k new_bb in
       let End_of_block = build_term body (newcvm, varmap) builder in
       Llvm.position_at_end new_bb builder;
       (match begin_with_phi_node new_bb with
         ∣ None → End_of_block
         ∣ Some(phi) → build_term term (newcvm, (add_to_varmap x phi)) builder)
30.  Cases that change or create basic blocks.
     Depending on k, applycont either returns or jumps to k.
     ∣ Apply_cont(k,x) → 
       build_applycont (lookup_contvar k) (lookup_var x) builder

     The CPS semantics state that caller should return to k, but LLVM SSA does not require that calls end basic blocks. So we just build a call instruction, and then a call to [k]. LLVM optimizations will eliminate the superfluous jump if needed.
     ∣ Apply(caller,k,callee) → 
       let retval = build_call (lookup_var caller) (lookup_var callee) builder in
       build_applycont (lookup_contvar k) retval builder

     ∣ Halt(x) → ignore(Llvm.build_ret (lookup_var x) builder); End_of_block

Expression built out of a definition are put in a "void -> void" function.
and build_nodef name cpsbody = 
   prepare_build name cpsbody None

and build_function name contparam param cpsbody =
   prepare_build name cpsbody (Some (contparam,param))

Build the function around the main term cpsbody, possibly taking some parameters k and x.
and prepare_build name cpsbody param = 
   let params_type = match param with None → [| |] ∣ _ → [| anystar_type |] in
   let function_type = Llvm.function_type anystar_type params_type in
   (∗ Note: it is important for LLVM that function names are unique. ∗)
   let funname = name ^ "#" ^ (UniqueFunctionId.to_string (UniqueFunctionId.fresh())) in
   let the_function = Llvm.declare_function funname function_type the_module in
   let bb = Llvm.append_block context "entry" the_function in
   (∗ Note that we use a new builder. We could even build the functions in parallel. ∗)
   let builder = Llvm.builder context in
   Llvm.position_at_end bb builder;
   try 
     let initial_varmaps = 
       match param with 
         ∣ None → (ContVarMap.empty, VarMap.empty)
         ∣ Some(k,x) → (ContVarMap.singleton k Ret,
                         VarMap.singleton x (Llvm.param the_function 0)) in

     ignore(build_term cpsbody initial_varmaps builder);
     (∗ Prints the textual representation of the function to stderr. ∗)
     Llvm.dump_value the_function;
     (∗ Validate the code we just generated. ∗)
     Llvm_analysis.assert_valid_function the_function;
     the_function
   (∗ Normally, no exception should be thrown, be we never know. ∗)
   with e → Llvm.delete_function the_function; raise e