I've been working on generating 6502 assembly from Clang/LLVM. This has been attempted many times by many different folks, so I'm wary of heaping another attempt on the pile, but I think the results I've gotten thus far are promising enough to share.
https://github.com/mysterymath/clang6502
TL;DR example:
Code: Select all
int main(void) {
const char *cur = "HELLO, WORLD!\n";
while (*cur) {
char c = *cur++;
asm volatile ("JSR\t$FFD2" : "+a"(c));
}
}
Code: Select all
.code
.global main
main:
LDX #0
LDA #72
LBB0__1:
;APP
JSR $FFD2
;NO_APP
LDA _2Estr+1,X
INX
CPX #14
BNE LBB0__1
LDA #0
LDX #0
RTS
.rodata
_2Estr:
.byt 72,69,76,76,79,44,32,87,79,82,76,68,33,10,0
The compiler is extremely incomplete. Because I've been tackling problems "riskiest first," the code generator does very little beyond what is necessary to get the milestones working. However, each milestone has been selected to showcase a difficult task in 6502 code generation, and I've honed each until LLVM generates roughly as good of assembly as I can. Once the hard problems are mostly squared away, I'll go back and fill out the code generator, which should be mostly mechanical.
Amongst the hard problems I've got at least partial solutions for:
- Interplay of hard and soft stacks. Each frame is presently allowed 4 bytes of hard stack, which it will place the most important values in. Hard stack saves/loads are optimized to PHA and PLA in some cases.
- Zero page usage. The zero page is treated as a large register file, then lowered to real memory accesses later. Values that aren't live across JSRs can often live their whole life in the zero page, even in recursive functions.
- 8-bit loop indices detection. The compiler rewrites loop induction variables to fit within 8 bits wherever possible. This allows using indexed addressing modes and INX for what are logically pointer induction variables.
- Whole-program destackification: LLVM already has a pass that takes global variables and moves them to the stack for non-recursive functions; for the 6502, we'd want to reverse the process to lower stack accesses to globals.
- Whole-program outlining: Instead of pre-designating certain "meta-operations" as JSR compiler intrinisics, LLVM supports outlining passes that essentially reverse inlining. That way, common code sequences can be automatically detected and code size reduced in a general fashion, without special casing, and in a way sensitive to the execution frequency of affected code paths.