Last time I mentioned Tec30, a ridiculously tiny softcore CPU I designed. I stabilized the ISA and now TEC30X7 takes 160 LUTs bare. Now that was done, I wanted to port a compiler, namely LLVM, to it. In the end I decided NOT to mess with the giants, because LLVM is optimized for totally different kinds of CPU architectures. In the end, I chose SDCC for the porting work. Here is a summary of what happened, how, and hopefully it can act as a guide to a future SDCC backend developer who is either very brave or has made similarly questionable life choices.
SDCC is a very small project compared to LLVM. The porting process consists of creating a subfolder for your backend,
and creating a bunch of files, each doing something very specific. You have your own backend's main.c,
ralloc.c, gen.c and so on. The fact I could design register allocation was great: my machine
is very unusual in that it has no traditional registers. The 0-page in memory is used as a register file and that means
I have absolutely no alignment issues: I can pack single byte, double, triple, quadruple byte values without aligning to a fixed width.
I can use every single byte freely which is not the case for x86 for example: you have eax and rax, but you don't have eax_hi and eax_lo.
The porting process was kind of straightforward, and I gradually implemented many features as well as testing them.
But of course, tests are written by the programmer, so you never know what bugs may be lurking in the code. Especially when the programmer is me.
This being said, here is how my current port diverges from ISO C:
I compiled the following simple C program:
int add(int a, int b) {
return a + b;
}
.module file
_add_PARM_1:
.ds 2
_add_PARM_2:
.ds 2
_L2:
add:
mov 11, _add_PARM_2+1
mov 10, _add_PARM_1+1
tst 0, 0, #0
add f1, 10, 11
mov 11, _add_PARM_2
mov 10, _add_PARM_1
add f0, 10, 11
tst 0, 0, #0
add ret_hi, f0, zero
tst 0, 0, #0
add ret_lo, f1, zero
ret
_L1:
Let's briefly explain what's going on. Tec30 starts by fetching the arguments from RAM, and moving them to "11" and "10", as you can see.
Those are addresses in the 0-page. Essentially, it's the equivalent of performing an ld instruction on more
conventional load-store architectures.
Now that the arguments are in the 0-page, the compiler clears carry with a tst 0, 0, #0 instruction.
This is because the Tec30's only add opcode sums the carry flag, performing an add with carry.
The compiler then performs the addition of the two arguments, storing the result in "f1" and "f0", which are also addresses in the 0-page.
This is inefficiency number 1: the compiler could have easily stored the results in the ret_hi and ret_lo registers.
The compiler then clears carry one more time, adds the remaining bytes, and moves the result to the return registers.
What seems like a simple inefficiency causes the code to get a lot fatter than necessary, and there's another
subtlety here: the broken sums.
If Tec30 uses adds to move values around, the compiler clears carry first EVERY SINGLE TIME.
That's twice as wasteful as simply having "one more add".
Not to mention that Tec30 takes 3 cycles to perform any ALU operation. That means a single generated mov between
0-page addresses takes 6 cycles. Absolutely insane, but at least the inefficiency is consistent.
This is how the code should have rather been:
mov 11, _add_PARM_2+1
mov 10, _add_PARM_1+1
tst 0, 0, #0
add ret_lo, 10, 11 ; carry chain continues below
mov 11, _add_PARM_2
mov 10, _add_PARM_1
add ret_hi, 10, 11
ret