Project Update!
Well, it's been another month! There's two interesting developments.
Continuous End-To-End TestsFirst, the boring one. We now have continous nightly runs of LLVM's end-to-end test suite running and passing at each optimization level and allowed number of imaginary pointer registers (from 5 to 127).
Interrupt HandlingNext, the interesting one. It's come to my attention that interrupt handling is actually really important to programming on 6502 machines. I'd previously been attempting to defer handling this past our first release, but it didn't seem likely that we'd be able to shoehorn-in a good solution after the fact. So I've spent much of my time finding a way to incorporate good interrupt handling into llvm-mos.
There are two chief obstacles: the large number of zero page registers, and static stack allocation.
Zero Page RegistersFirst, it's just not practical to save the entire zero page when an interrupt happens. But if most of the registers were caller-saved, we'd have to do so if the interrupt handler called even a single C function. But we don't want to make most of the zero page registers callee-saved, since it makes it that much harder for the compiler to actually use them. And if a program doesn't involve interrupt handlers, then most of the registers can be caller-saved without issue.
So, if a program has any interrupt handlers that can call C, we require the C function entry points to bear one of two annotations: "interrupt" or "interrupt_norecurse." I'll get to the distinction between the two in a moment. Having such a function anywhere in the program will cause the calling convention to globally change. All but the first 5 zero page pointers become callee-saved, which allows the interrupt handler to only need to save 2 of them. This along with A, X, Y, and a handful of compiler temporaries leads to an okay-ish interrupt save/restore sequence.
Static Stack AllocationSecond, interrupt handling really futzes with static stack allocation, since interrupt handlers can come along and call anything at any time. Worse, an interrupt handler could itself be interrupted by the same interrupt handler, making everything possibly callable by that handler possibly recursive.
Accordingly, if a function is marked "interrupt", we walk the call graph, find anything that might possibly be called by it, and mark it possibly recursive.
However, in practice, a lot of interrupt handlers can be guaranteed non-recursive, that is, once an invocation of the handler becomes active, you can turn off the source of interrupts until it finishes. This is what the "interrupt_norecurse" annotation is for. If a function can be called by two *different* interrupt_norecurse, or by an interrupt_norecurse function and main, then it's marked as possibly recursive. Judicious use of this annotation should allow interrupt handlers to use static stack a lot of the time.
If you don't want to have C handle all of the save, restore, and RTI logic, you can also mark a function as "no_isr", which keeps the above semantics for static stacks, but makes the function use the original calling convention.
It's quite a complicated set of concepts, but it should allow writing interrupt handlers without completely disabling optimization opportunities like static stacks and zero page register function parameters. Eventually, I'm hoping to improve the zero-page register usage of interrupt functions by automatically promoting their static stacks to the zero page. This would automate the common practice of reserving zero-page registers for interrupt handler usage, ensuring that they don't conflict with main program functionality. We can even pass arguments directly via these locations when the callee is statically known to the caller.
A more thorough discussion is at
https://llvm-mos.org/wiki/C_interrupts, and as always, I'm available to answer questions about the approach.
Until next time!