C interrupts

Normal C Interrupt Handling
The techniques llvm-mos uses for interrupt handling are somewhat unusual. To understand why, it's useful to start with the normal way C compilers deal with interrupts.

Generally, C calling conventions divide registers into two classes: caller-saved and callee-saved. Functions are free to overwrite caller-saved registers, and callers of those functions need to be aware of and correctly handle this possibility. Functions must preserve the values of callee-saved registers, and their callers are allowed to count on this.

Interrupt handlers necessarily break with this convention; a function can't know when and how it's going to be interrupted, so there's no way to "deal with" the interrupt handler overwriting caller-saved registers. So interrupt handlers need to treat all registers as if they were callee-saved.

Usually, a target has at most around 32 registers, more-or-less evenly split between caller-saved and callee-saved. So an interrupt handler can reasonably save all the caller-saved registers (it would implicitly also save the callee-saved registers if it uses them, just by virtue of being a C function). Note that if it calls any other function, it needs to save all of the caller-saved registers, since it can't know which the callee will overwrite.

Challenges
The nature of the 6502 presents some challenges to using this model.

Large "Register" File
llvm-mos treats the zero page as registers, and these locations participate in the C calling convention. However, there are rather a lot more than 32 on most platforms; anywhere from 60 to 255. Saving and restoring all of them presents a considerable burden, especially in the context of an interrupt handler, which needs to be fast.

We could make all but a few of these registers callee-saved, but this would lead to the compiler quite rarely using them; it wouldn't often be worth it to save and restore them. If we made most of them caller saved, then interrupt handlers (that call a single C function) become impossibly slow.

If there are no interrupt handlers in a program, the answer is obvious: make all but a few caller-saved, and the compiler can take advantage of as many as it desires. But if there's even a single interrupt anywhere in the program, we'd need the opposite convention: all but a few are callee-saved.

Static Stack Allocation
The indexed addressing modes on the 6502 are quite slow, which gives incentive to avoid the normal C stack implementation. For llvm-mos, we opted to perform a call graph analysis and allocate the stack frames of non-recursive functions statically. This is safe, since via a conservative analysis we can prove that certain functions cannot have more than one invocation active at a time.

However, interrupts can be active at the same time as any other function, potentially including themselves. The static stack analysis will need to be made aware of interrupts somehow, which means that programmers will need to annotate which functions have this "can appear out of nowhere" property.

Interrupt Annotations
To solve the above problems, we introduce three new function attributes "interrupt", "interrupt_norecurse", and "no_isr".

"interrupt" Attribute
This attribute isn't actually MOS-specific, but we do ascribe to it some additional semantics. Any function bearing this attribute will treat all registers (except flags) as callee-saved and will return with RTI instead of RTS. A handful of Additionally, any such a function will be marked as possibly recursive for the purpose of static stack allocation. This will in turn cause any function that might be called by such a function to be forced to use the dynamic soft stack.

"interrupt_norecurse" Attribute
This attribute behaves identically to "interrupt", with one exception. When performing the static stack analysis, functions marked with this attribute will not be automatically considered recursive. Instead, any functions that might possibly called by an interrupt_norecurse function and main or two different interrupt_norecurse functions will be considered possibly recursive. Another way of looking at it is that interrupt_norecurse functions correspond to different sources of interrupts, and the model is one where interrupts from that source are disabled until they finish processing. Judicious use of interrupt_norecurse allows interrupt handlers to benefit from static stack allocation, leaving the "interrupt" attribute for interrupt handlers that can interrupt even themselves.

"no_isr" Attribute
This attribute can be added to an interrupt or interrupt_norecurse function to cause it to return with RTS and not perform any of the additional saving that interrupt handlers usually do. All effects on static stack analysis and calling conventions (see below) still occur. The intent is to allow interrupt handlers to be implemented in assembly and call into C with the normal calling convention. The interrupt attributes would still be necessary in that case for program correctness.

Program-Wide Calling Convention Modification
If an "interrupt" or "interrupt_norecurse" attribute is used anywhere within the program, then the calling convention of the entire program will change. All zero-page pointers past the first 5 become callee saved instead of caller-saved.

Undefined Behavior
It shall be undefined behavior for any mechanism external to a C module to asynchronously call a C function that does not bear either the "interrupt" or "interrupt_norecurse" attributes. It shall also be undefined behavior for any mechanism external to a C module to asynchronously call an "interrupt_norecurse" function while another invocation of that same function is still active.

[[Category:C]]