Chapter 7 Control Flow: If/Else
Where is this going: we want to add basic control flow to our language, starting with conditionals (if/else).
We need to learn about labels and jumps at the assembly level, and some background about how the CPU chooses the next instruction.
We touch on the idea of continuations, as a way of describing “what to do next”: the point in the code where execution resumes after an if/else is the continuation of the statement.
7.1 Language Definition
7.1.1 Syntax
<op> ::= '+' | '-' | '*' | '/'
<bop> ::= '==' | '!=' | '<' | '>' | '<=' | '>='
<atom> ::= <number>
| '('<simp>')'
| <ident>
| '{'<exp>'}'
<uatom> ::= [<op>]<atom>
<cond> ::= <simp><bop><simp>
<simp> ::= <uatom>[<op><uatom>]*
| 'if' '('<cond>')' <simp> 'else' <simp>
<exp> ::= <simp>
| 'val' <ident> '=' <simp>';' <exp>
7.1.2 AST
case class Cond(op: String, lop: Exp, rop: Exp)
case class If(cond: Cond, tBranch: Exp, eBranch: Exp) extends Exp
7.1.3 Semantics
type Val = Int
def evalCond(op: String)(v: Val, w: Val) = op match {
case "==" => v == w
// ...
}
def eval(exp: Exp)(env: Env): Val = exp match {
case If(Cond(op, l, r), tBranch, eBranch) =>
if (evalCond(op)(eval(l)(env), eval(r)(env)))
eval(tBranch)(env)
else
eval(eBranch)(env)
}
Example
eval(Let("x", 1, // Omitted Lit
If(Cond(">", Ref("x"), 0), Prim("+", 2, Ref("x")), 0)))(Map())
7.2 Code Generation
7.2.1 Machine model
The CPU has a program counter register (PC) that specifies where to load the next instruction from. The ISA provides jump or branch instructions that modify the program counter, e.g. specifying “add 16” to the PC.
At the essembly level, we don’t have to deal with numeric offsets for jumps explicitly. Instead, we can define labels in the assembly code. The assembler, which compiles source-level assembly code to binary machine code, and the linker, which merges separately compiled modules, will figure out the correct jump offset in the generated binary executable.
Hardware architectures also support indirect jumps, where an absolute target address is provided in a register, and conditional jumps (these are often called branches), which are only executed if a given condition is true. A conditional jump based on comparing two values, for example, is typically executed in two steps. First, the two values are compared, and the result is stored in a special flags register, e.g. setting a bit that indicates one of the value was smaller that the other one. Then, a conditional jump instruction is executed that inspects the desired bit in the flags register, and either does nothing or updates the PC to continue execution at the specified target address based on the outcome of the test.
Todo: X86 Example here
Todo: Scala Example here
In Scala, we can simulate jumps using functions that do not
return. These have return type Nothing
.
Example:
def main(): Nothing = {
if (cond) thenBranch() else elseBranch()
}
def thenBranch(): Nothing = {
...
endIf()
}
def elseBranch(): Nothing = {
...
endIf()
}
def endIf(): Nothing = {
...
exit() // provided by runtime
}
Note: function calls like this still consume stack space in Scala, so they aren’t real jumps.
The function endIf
is called the continuation of
the if/else statement, because this is where execution
continues afterwards. Following this terminology,
endIf
is also the continuation of thenBranch
and
elseBranch
.
7.2.2 Stack-Based Interpreter
Focus on handling of flags register first.
val memory = new Array[Int](MEM_SIZE)
var flag = 0
def evalCond(op: String) = op match {
case "==" => flag == 0
// ...
}
def eval(exp: Exp, sp: Int)(env: Env): Unit = exp match {
case If(Cond(op, l, r), tBranch, eBranch) =>
flag = eval(l, sp)(env) - eval(r, sp + 1)(env)
if (evalCond(op))
eval(tBranch, sp)(env)
else
eval(eBranch, sp)(env)
}
eval(Let("x", 1, // Omitted Lit
If(Cond(">", Ref("x"), 0), Prim("+", 2, Ref("x")), 0)), 0)(Map())
7.2.3 Stack-Based Compiler
def emitCode(exp: Exp): Unit = {
emitln("val memory = new Array[Int](1000)")
emitln("var flag = 0")
trans(exp, 0)(Env())
emitln(s"memory(0)")
}
def trans(exp: Exp, sp: Int)(env: Env) = exp match {
case If(Cond(op, l, r), tBranch, eBranch) =>
trans(l, sp)(env); trans(r, sp+1)(env)
emitln(s"flag = (memory($sp) - memory(${sp+1}))") // Set flag value
emitln(s"if (flag $op 0) {")
trans(tBranch, sp)(env)
emitln("} else {")
trans(eBranch, sp)(env)
emitln("}")
}
Example:
emitCode(Let("x", 1, // Omitted Lit
If(Cond(">", Ref("x"), 0), Prim("+", 2, Ref("x")), 0)))
val memory = new Array[Int](1000); var flag = 0
memory(0) = 1
memory(1) = memory(0); memory(2) = 0
flag = (memory(1) - memory(2))
if (flag > 0) {
memory(1) = 2
memory(2) = memory(0)
memory(1) += memory(2)
} else {
memory(1) = 0
}
memory(0) = memory(1)
memory(0)
Note: this isn’t the full deal. Need labels
7.2.4 Stack-Based Compiler with Labels and Jumps
val memory = new Array[Int](1000); var flag = 0
def main() {
memory(0) = 1
memory(1) = memory(0); memory(2) = 0
flag = (memory(1) - memory(2))
if (flag > 0) thenBranch() else elseBranch()
}
def thenBranch() = {
memory(1) = 2
memory(2) = memory(0)
memory(1) += memory(2)
endIf()
}
def elseBranch() = {
memory(1) = 0
endIf()
}
def endIf() = {
memory(0) = memory(1)
exit()
}
Exercise: implement compiler that emits this
Exercise (hard, need background): derive this compiler from an interpreter. Hint: continuation passing style (CPS)
7.2.5 x86 Flags And Jumps
The x86 processor is using flags to handle comparison.
cmpq %rbx, %rax # compare %rax to rbx and set the flags accordingly
Several instructions can be used for jump:
je labelA # jump equals
jne labelA # jump not equals
jg labelA # jump greater
jge labelA # jump greater or equals
jl labelA # jump less
jle labelA # jump less or equals
jmp labelA # always jump
Example:
movq $0, %rax
movq $1, %rbx
cmpq %rbx, %rax # operands inverted (%rax > %rbx)!!
jg greater
movq $1, %rax
greater:
ret # what value is in %rax?
Returns 1, because 0 is not greater than 1 so the jump doesn’t happen.
7.2.6 Compiling Ifs
trans(If(Cond(op, l, r), tBranch, eBranch), 0)(Map())
In order to compile the if statement, we are going to follow this idea:
... # code for l and r
cmpq <r>, <l>
j<op> if_then # the jump operation depends on 'op'
... # code for else branch
jmp if_end # unconditional jump
if_then: # label for the beginning of the then branch
.. # code for then branch
if_end:
... # code for the rest
Example:
trans(If(Cond("==", 1, 0), 2, 3), 0)(Map())
# begin code generated
movq $1, %rbx # generate code that compute l, stored in %rbx
movq $0, %rcx # generate code that compute r, stored in %rcx
cmpq %rcx, %rbx
je if1_then
movq $3, %rbx # generate code for eBranch, store result in %rbx
jmp if1_end
if1_then:
movq $2, %rbx # generate code for tBranch, store result in %rbx
if1_end:
# end code generated
movq %rbx, %rax
ret
7.3 Parsing
Todo: Simple, left as exercise.
7.4 Where Are We?
We added IF statements to our language and discussed the syntax, semantics and simple implementation.