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.

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, will begin with a CLD (the state of the decimal flag is undefined upon interrupt), and will return with RTI instead of RTS. 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.

Manual Interrupt Sequence
Some operating systems may impose unusual requirements on interrupt handlers. They may, for example, push certain registers before calling the handler, but expect the handler to pop them before returning. no_isr routines allow implementing this kind of custom interrupt prologue and epilogue. However, doing this safely requires saving and restoring anything that might be in-use by the compiler.

Below is a sample routine that includes the sum total of all pushes and pops needed by the compiler. If it can be proven that the entire, transitively called interrupt handler cannot use certain locations, then they can be elided. This is typically only possible if the interrupt handler is either written entirely in assembly.

The callee-saved registers must be preserved by any function callable by C; interrupt handlers are no different. Accordingly, callee-saved registers aren't included in the code below. In this example, the JSR to "body" is expected to preserve them.

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]]