Just because all text is boring 🙂

The only really tricky thing in this chapter is dealing with the ternary , conditional expression ‘operator’. ie
int x = 2;
int y = x > 1 ? x : 42;
but the book walks through the recommended way of parsing this, and it works well.
Backend Generation
One really cool thing about this compiler design is how often things in a chapter ‘just work’ once we have done the TACKY generation phase. In fact we have not modified the TACKY grammar, and hence the x64 code generator, since chapter 4. Here is some sample TACKY for the features we added in this chapter, just FYI.
int main(void) {
int x = 4;
int y = 3;
int z = x + y;
if (x > 4)
y= 10;
else
y = 0;
return y == 42 ? x : 24;
}
produces
Copy Int(4) Variable("x$0")
Copy Int(3) Variable("y$1")
Binary Add Variable("x$0") Variable("y$1") Variable("temp.0")
Copy Variable("temp.0") Variable("z$2")
Binary GreaterThan Variable("x$0") Int(4) Variable("temp.1")
JumpIfZero Variable("temp.1") "_if_false_2"
Copy Int(10) Variable("y$1")
Jump "_if_end_3"
Label "_if_false_2"
Copy Int(0) Variable("y$1")
Label "_if_end_3"
Binary Equal Variable("y$1") Int(42) Variable("temp.4")
JumpIfZero Variable("temp.4") "_ternary_false_6"
Copy Variable("x$0") Variable("temp.7")
Jump "_ternary_true_5"
Label "_ternary_false_6"
Copy Int(24) Variable("temp.7")
Label "_ternary_true_5"
Return Variable("temp.7")
You can see the variable renaming scheme described in chapter 5 in action.
I mentioned at the start how the backend is a two step process. It produces an intermediate form that I christened ‘moira’, a fix up phase then runs over the moira code, this is then used to generate x64 code.
Here is the moira code for the same program
Mov(Immediate(4), Pseudo("x$0"))
Mov(Immediate(3), Pseudo("y$1"))
Mov(Pseudo("x$0"), Pseudo("temp.0"))
Binary(Add, Pseudo("y$1"), Pseudo("temp.0"))
Mov(Pseudo("temp.0"), Pseudo("z$2"))
Cmp(Immediate(4), Pseudo("x$0"))
Mov(Immediate(0), Pseudo("temp.1"))
SetCC(G, Pseudo("temp.1"))
Cmp(Immediate(0), Pseudo("temp.1"))
JmpCC(E, "_if_false_2")
Mov(Immediate(10), Pseudo("y$1"))
Jmp("_if_end_3")
Label("_if_false_2")
Mov(Immediate(0), Pseudo("y$1"))
Label("_if_end_3")
Cmp(Immediate(42), Pseudo("y$1"))
Mov(Immediate(0), Pseudo("temp.4"))
SetCC(E, Pseudo("temp.4"))
Cmp(Immediate(0), Pseudo("temp.4"))
JmpCC(E, "_ternary_false_6")
Mov(Pseudo("x$0"), Pseudo("temp.7"))
Jmp("_ternary_true_5")
Label("_ternary_false_6")
Mov(Immediate(24), Pseudo("temp.7"))
Label("_ternary_true_5")
Mov(Pseudo("temp.7"), Register(AX))
Ret
You can see how this closely follows the TACKY code but is aware of the x64 overall architecture, registers etc., plus it knows how to return a value to the OS via AX.
After the fix up phase the moira code looks like this
Mov(Immediate(4), Stack(4))
Mov(Immediate(3), Stack(8))
Mov(Stack(4), Register(R10))
Mov(Register(R10), Stack(12))
Mov(Stack(8), Register(R10))
Binary(Add, Register(R10), Stack(12))
Mov(Stack(12), Register(R10))
Mov(Register(R10), Stack(16))
Cmp(Immediate(4), Stack(4))
Mov(Immediate(0), Stack(20))
SetCC(G, Stack(20))
Cmp(Immediate(0), Stack(20))
JmpCC(E, "_if_false_2")
Mov(Immediate(10), Stack(8))
Jmp("_if_end_3")
Label("_if_false_2")
Mov(Immediate(0), Stack(8))
Label("_if_end_3")
Cmp(Immediate(42), Stack(8))
Mov(Immediate(0), Stack(24))
SetCC(E, Stack(24))
Cmp(Immediate(0), Stack(24))
JmpCC(E, "_ternary_false_6")
Mov(Stack(4), Register(R10))
Mov(Register(R10), Stack(28))
Jmp("_ternary_true_5")
Label("_ternary_false_6")
Mov(Immediate(24), Stack(28))
Label("_ternary_true_5")
Mov(Stack(28), Register(AX))
Ret
This has mapped variables and temporaries to the stack, and any instruction that had both operands as memory addresses was converted to use a register instead. (Follow the evolution of the code for int z = x + y;)
You can see that this is very close to x64 code.
Its interesting to look at the inefficiencies here, that line int z = x + y; ends up as
Binary Add Variable("x$0") Variable("y$1") Variable("temp.0")
Copy Variable("temp.0") Variable("z$2")
Why not just do
Binary Add Variable("x$0") Variable("y$1") Variable("z$2")
The reason is that the parser does not know where the result of the right hand expression is going to end up, so it puts it in a temporary, then the parser emits code to put that temporary into the variable z. Is that a by product of my single pass parse to TACKY, not sure, I am going to reread some earlier stuff in the book. If its not my fault then hopefully part III (optimizations) will clean it up.
Just to be complete here is the generated x64 code
INCLUDELIB LIBCMT
_TEXT SEGMENT
PUBLIC main
main PROC
push rbp
mov rbp, rsp
sub rsp, 28
mov DWORD PTR[rbp-4], 4
mov DWORD PTR[rbp-8], 3
mov r10d, DWORD PTR[rbp-4]
mov DWORD PTR[rbp-12], r10d
mov r10d, DWORD PTR[rbp-8]
add DWORD PTR[rbp-12], r10d
mov r10d, DWORD PTR[rbp-12]
mov DWORD PTR[rbp-16], r10d
cmp DWORD PTR[rbp-4], 4
mov DWORD PTR[rbp-20], 0
setg BYTE PTR[rbp-20]
cmp DWORD PTR[rbp-20], 0
je _if_false_2
mov DWORD PTR[rbp-8], 10
jmp _if_end_3
_if_false_2:
mov DWORD PTR[rbp-8], 0
_if_end_3:
cmp DWORD PTR[rbp-8], 42
mov DWORD PTR[rbp-24], 0
sete BYTE PTR[rbp-24]
cmp DWORD PTR[rbp-24], 0
je _ternary_false_6
mov r10d, DWORD PTR[rbp-4]
mov DWORD PTR[rbp-28], r10d
jmp _ternary_true_5
_ternary_false_6:
mov DWORD PTR[rbp-28], 24
_ternary_true_5:
mov eax, DWORD PTR[rbp-28]
mov rsp, rbp
pop rbp
ret
main ENDP
_TEXT ENDS
END
Note the DWORD and BYTE length specifiers
On to chapter 7
Leave a comment