Mutable Variables
Now that we know the sort of problem we want to tackle, let’s see what this looks like in the context of our little Kaleidoscope language. We’re going to add two features:
- The ability to mutate variables with the ‘=’ operator.
- The ability to define new variables.
While the first item is really what this is about, we only have variables for incoming arguments as well as for induction variables, and redefining those only goes so far :). Also, the ability to define new variables is a useful thing regardless of whether we will be mutating them. Here’s a motivating example that shows how we could use these:
# Define ':' for sequencing: as a low-precedence operator that ignores operands
# and just returns the RHS.
def binary : 1 (x y) y;
# Recursive fib, we could do this before.
def fib(x)
if (x < 3) then
1
else
fib(x-1)+fib(x-2);
# Iterative fib.
def fibi(x)
var a = 1, b = 1, c = 0 in
(for i = 3, i < x in
c = (a + b) :
a = b :
b = c) :
b;
# Call it.
fibi(10);
At this point in Kaleidoscope’s development, it only supports variables for two things: incoming arguments to functions and the induction variable of ‘for’ loops. For consistency, we’ll allow mutation of these variables in addition to other user-defined variables. This means that these will both need memory locations.
We introduce a new var
syntax which behaves much like the let
notation in Haskell. We will let the user define a sequence of new variable names and inject these new variables into the symbol table.
data Expr
...
| Let Name Expr Expr
deriving (Eq, Ord, Show)
The parser for it will allow for multiple declarations on a single line and right fold the AST node bodies, allowing us to use variables declared earlier in the list in subsequent declarations (i.e. var x = 3, y = x + 1
).
letins :: Parser Expr
letins = do
reserved "var"
defs <- commaSep $ do
var <- identifier
reservedOp "="
val <- expr
return (var, val)
reserved "in"
body <- expr
return $ foldr (uncurry Let) body defs
The code generation for this new syntax is very straight forward, we simply allocate a new reference and assign it to the name given then return the assigned value.
cgen (S.Let a b c) = do
i <- alloca double
val <- cgen b
store i val
assign a i
cgen c
We can test out this new functionality. Note that code below is unoptimized and involves several extranous instructions that would normally be optimized away by mem2reg.
ready> def main(x) var y = x + 1 in y;
; ModuleID = 'my cool jit'
define double @main(double %x) {
entry:
%0 = alloca double
store double %x, double* %0
%1 = alloca double
%2 = load double* %0
%3 = fadd double %2, 1.000000e+00
store double %3, double* %1
%4 = load double* %1
ret double %4
}
Evaluated to: 1.0