‘if’ Expressions

Extending Kaleidoscope to support if/then/else is quite straightforward. It basically requires adding lexer support for this “new” concept to the lexer, parser, AST, and LLVM code emitter. This example is nice, because it shows how easy it is to “grow” a language over time, incrementally extending it as new ideas are discovered.

Before we get going on “how” we add this extension, let’s talk about “what” we want. The basic idea is that we want to be able to write this sort of thing:

  1. def fib(x)
  2. if x < 3 then
  3. 1
  4. else
  5. fib(x-1) + fib(x-2);

In Kaleidoscope, every construct is an expression: there are no statements. As such, the if/then/else expression needs to return a value like any other. Since we’re using a mostly functional form, we’ll have it evaluate its conditional, then return the ‘then’ or ‘else’ value based on how the condition was resolved. This is very similar to the C “?:” expression.

The semantics of the if/then/else expression is that it evaluates the condition to a boolean equality value: 0.0 is considered to be false and everything else is considered to be true. If the condition is true, the first subexpression is evaluated and returned, if the condition is false, the second subexpression is evaluated and returned. Since Kaleidoscope allows side-effects, this behavior is important to nail down.

Now that we know what we “want”, let’s break this down into its constituent pieces.

To represent the new expression we add a new AST node for it:

  1. data Expr
  2. ...
  3. | If Expr Expr Expr
  4. deriving (Eq, Ord, Show)

We also extend our lexer definition with the new reserved names.

  1. lexer :: Tok.TokenParser ()
  2. lexer = Tok.makeTokenParser style
  3. where
  4. ops = ["+","*","-","/",";",",","<"]
  5. names = ["def","extern","if","then","else"]
  6. style = emptyDef {
  7. Tok.commentLine = "#"
  8. , Tok.reservedOpNames = ops
  9. , Tok.reservedNames = names
  10. }

Now that we have the relevant tokens coming from the lexer and we have the AST node to build, our parsing logic is relatively straightforward. First we define a new parsing function:

  1. ifthen :: Parser Expr
  2. ifthen = do
  3. reserved "if"
  4. cond <- expr
  5. reserved "then"
  6. tr <- expr
  7. reserved "else"
  8. fl <- expr
  9. return $ If cond tr fl

Now that we have it parsing and building the AST, the final piece is adding LLVM code generation support. This is the most interesting part of the if/then/else example, because this is where it starts to introduce new concepts. All of the code above has been thoroughly described in previous chapters.

To motivate the code we want to produce, let’s take a look at a simple example. Consider:

  1. extern foo();
  2. extern bar();
  3. def baz(x) if x then foo() else bar();
  1. declare double @foo()
  2. declare double @bar()
  3. define double @baz(double %x) {
  4. entry:
  5. %ifcond = fcmp one double %x, 0.000000e+00
  6. br i1 %ifcond, label %then, label %else
  7. then: ; preds = %entry
  8. %calltmp = call double @foo()
  9. br label %ifcont
  10. else: ; preds = %entry
  11. %calltmp1 = call double @bar()
  12. br label %ifcont
  13. ifcont: ; preds = %else, %then
  14. %iftmp = phi double [ %calltmp, %then ], [ %calltmp1, %else ]
  15. ret double %iftmp
  16. }

To visualize the control flow graph, we can use a nifty feature of the LLVM opt tool. If we put this LLVM IR into “t.ll” and run

  1. $ llvm-as < t.ll | opt -analyze -view-cfg

A window will pop up and we’ll see this graph:

‘if’ Expressions - 图1

LLVM has many nice features for visualizing various graphs, but note that these are available only if your LLVM was built with Graphviz support (accomplished by having Graphviz and Ghostview installed when building LLVM).

Getting back to the generated code, it is fairly simple: the entry block evaluates the conditional expression (“x” in our case here) and compares the result to 0.0 with the fcmp one instruction (one is “Ordered and Not Equal”). Based on the result of this expression, the code jumps to either the “then” or “else” blocks, which contain the expressions for the true/false cases.

Once the then/else blocks are finished executing, they both branch back to the if.exit block to execute the code that happens after the if/then/else. In this case the only thing left to do is to return to the caller of the function. The question then becomes: how does the code know which expression to return?

The answer to this question involves an important SSA operation: the Phi operation. If you’re not familiar with SSA, the Wikipedia article is a good introduction and there are various other introductions to it available on your favorite search engine. The short version is that “execution” of the Phi operation requires “remembering” which block control came from. The Phi operation takes on the value corresponding to the input control block. In this case, if control comes in from the if.then block, it gets the value of calltmp. If control comes from the if.else block, it gets the value of calltmp1.

At this point, you are probably starting to think “Oh no! This means my simple and elegant front-end will have to start generating SSA form in order to use LLVM!”. Fortunately, this is not the case, and we strongly advise not implementing an SSA construction algorithm in your front-end unless there is an amazingly good reason to do so. In practice, there are two sorts of values that float around in code written for your average imperative programming language that might need Phi nodes:

  • Code that involves user variables: x = 1; x = x + 1;
  • Values that are implicit in the structure of your AST, such as the Phi node in this case.

In Chapter 7 of this tutorial (“mutable variables”), we’ll talk about #1 in depth. For now, just believe and accept that you don’t need SSA construction to handle this case. For #2, you have the choice of using the techniques that we will describe for #1, or you can insert Phi nodes directly, if convenient. In this case, it is really really easy to generate the Phi node, so we choose to do it directly.

Okay, enough of the motivation and overview, let’s generate code!

In order to generate code for this, we implement the Codegen method for If node:

  1. cgen (S.If cond tr fl) = do
  2. ifthen <- addBlock "if.then"
  3. ifelse <- addBlock "if.else"
  4. ifexit <- addBlock "if.exit"
  5. -- %entry
  6. ------------------
  7. cond <- cgen cond
  8. test <- fcmp FP.ONE false cond
  9. cbr test ifthen ifelse -- Branch based on the condition
  10. -- if.then
  11. ------------------
  12. setBlock ifthen
  13. trval <- cgen tr -- Generate code for the true branch
  14. br ifexit -- Branch to the merge block
  15. ifthen <- getBlock
  16. -- if.else
  17. ------------------
  18. setBlock ifelse
  19. flval <- cgen fl -- Generate code for the false branch
  20. br ifexit -- Branch to the merge block
  21. ifelse <- getBlock
  22. -- if.exit
  23. ------------------
  24. setBlock ifexit
  25. phi double [(trval, ifthen), (flval, ifelse)]

We start by creating three blocks.

  1. ifthen <- addBlock "if.then"
  2. ifelse <- addBlock "if.else"
  3. ifexit <- addBlock "if.exit"

Next emit the expression for the condition, then compare that value to zero to get a truth value as a 1-bit (i.e. bool) value. We end this entry block by emitting the conditional branch that chooses between the two cases.

  1. test <- fcmp FP.ONE false cond
  2. cbr test ifthen ifelse -- Branch based on the condition

After the conditional branch is inserted, we move switch blocks to start inserting into the if.then block.

  1. setBlock ifthen

We recursively codegen the tr expression from the AST. To finish off the if.then block, we create an unconditional branch to the merge block. One interesting (and very important) aspect of the LLVM IR is that it requires all basic blocks to be “terminated” with a control flow instruction such as return or branch. This means that all control flow, including fallthroughs must be made explicit in the LLVM IR. If we violate this rule, the verifier will emit an error.

  1. trval <- cgen tr -- Generate code for the true branch
  2. br ifexit -- Branch to the merge block
  3. ifthen <- getBlock -- Get the current block

The final line here is quite subtle, but is very important. The basic issue is that when we create the Phi node in the merge block, we need to set up the block/value pairs that indicate how the Phi will work. Importantly, the Phi node expects to have an entry for each predecessor of the block in the CFG. Why then, are we getting the current block when we just set it 3 lines above? The problem is that theifthen expression may actually itself change the block that the Builder is emitting into if, for example, it contains a nested “if/then/else” expression. Because calling cgen recursively could arbitrarily change the notion of the current block, we are required to get an up-to-date value for code that will set up the Phi node.

  1. setBlock ifelse
  2. flval <- cgen fl -- Generate code for the false branch
  3. br ifexit -- Branch to the merge block
  4. ifelse <- getBlock

Code generation for the if.else block is basically identical to codegen for the if.then block.

  1. setBlock ifexit
  2. phi double [(trval, ifthen), (flval, ifelse)]

The first line changes the insertion point so that newly created code will go into the if.exit block. Once that is done, we need to create the Phi node and set up the block/value pairs for the Phi.

Finally, the cgen function returns the phi node as the value computed by the if/then/else expression. In our example above, this returned value will feed into the code for the top-level function, which will create the return instruction.

Overall, we now have the ability to execute conditional code in Kaleidoscope. With this extension, Kaleidoscope is a fairly complete language that can calculate a wide variety of numeric functions. Next up we’ll add another useful expression that is familiar from non-functional languages…