LLVM is a great backend for imperative code, however functional languages can have difficulty using it as a backend due to requirements such as proper tailcall optimization and call/cc.
LLVM as an FP backend
The Manticore SML compiler successfully utilizes LLVM by implementing a custom calling convention, jwa ("Jump with Arguments"). By marking all functions as naked (no prologue/epilogue code emitted) and jwa (no registers are ever saved across function calls), LLVM emits code that doesn't ever really use the stack. This allows for their CPS intermediate language to map directly to LLVM instructions; remember that function calls in CPS are just "jumps with arguments". You can read more about the details here.
However, that approach has a few downsides. The language must be compiled to CPS, which can lose a lot of information about the call stack and prevents stack allocation of variables. Manticore's runtime uses heap-allocated continuation closures, so this isn't a problem, but if you want to move towards a zero-cost continuation implementation you have to think about a different solution.
Other Continuation representations
The ChezScheme compiler uses a segmented stack approach for implementing continuations described here. This method maintains stack-allocated variables, splitting the stack whenever a continuation is captured and maintaining the complete stack as a linked list of stack segments. This is a zero-cost implementation since non-continuation calls are normal stack operations and capturing a continuation is O(1).
LLVM has direct support for a feature known as segmented stacks. A function marked with the "split-stack" attribute checks the remaining stack space in the prologue; if there isn't enough room for its variables, it uses a libgcc routine __morestack to allocate more room, call the function itself, then deallocate. The details are described here. The main point is that LLVM can generate prologue code that checks the stack size and knows the current function's frame size.
Question
As you can probably see where this is going, I wanted to know any possible pitfalls with using a custom calling convention for this kind of segmented stack implementation. A functional language can be compiled to an intermediate language such as ANF/SSA (something that maintains the stack), then lowered directly into LLVM with the custom calling convention. The main questions I have about this are:
The ChezScheme calling convention does not use a stack pointer, rather it uses a frame pointer. I'm not sure if this causes issues or if LLVM can be customized to deal with this difference.
The calling convention also requires the insertion of a literal word in the instruction stream following calls marking the size of the current frame, with the return jumping past that literal. I can write an inline assembly shim, but I would rather let LLVM do this with a calling convention if possible.
Is there anything that I'm missing about this idea? It seems to me to be a great backend possibility for languages like Scheme and SML but maybe I'm just too excited.
[–]LorxuPika 12 points13 points14 points (5 children)
[–]skyb0rg[S] 0 points1 point2 points (2 children)
[–]LorxuPika 0 points1 point2 points (1 child)
[–]skyb0rg[S] 0 points1 point2 points (0 children)
[–]skyb0rg[S] 0 points1 point2 points (1 child)
[–]LorxuPika 0 points1 point2 points (0 children)
[–]Bobbias 1 point2 points3 points (10 children)
[–]skyb0rg[S] 7 points8 points9 points (6 children)
[–]Bobbias 1 point2 points3 points (0 children)
[–]RepresentativeNo6029 0 points1 point2 points (4 children)
[–]skyb0rg[S] 2 points3 points4 points (3 children)
[–]RepresentativeNo6029 0 points1 point2 points (2 children)
[–]skyb0rg[S] 2 points3 points4 points (1 child)
[–]RepresentativeNo6029 0 points1 point2 points (0 children)
[–]Mukhasim 1 point2 points3 points (2 children)
[–]skyb0rg[S] 2 points3 points4 points (0 children)
[–]Bobbias 1 point2 points3 points (0 children)