Multiple Return Value Implementation in CLRHack
The CLRHack compiler implements Multiple Return Values (MRV) by extending the single-value limitation of the .NET Common Intermediate Language (CIL) stack through a thread-local side-channel. This allows Lisp forms to communicate multiple values (up to 64) across function boundaries.
1. The Side-Channel Storage
Because a CIL method can only return a single object on the stack, CLRHack utilizes a static class
[LispBase]Lisp.Values. This class contains [ThreadStatic] fields that act as a secondary communication
channel:
- Primary Value: Always resides on the CIL evaluation stack.
ReturnCount: Anint32field indicating the total number of values returned (including the primary one).Value1throughValue63: Object fields that store the second through sixty-fourth return values.
2. Producing Multiple Values (The Staging Logic)
To prevent corruption during evaluation, the values form uses a Stage-and-Commit strategy. This is
necessary because the side-channel is global to the thread; if a sub-expression inside a values form itself returns
multiple values, it would overwrite the global fields before the outer values form is finished.
The compilation process for (values form1 form2 ... formN) follows these steps:
- Evaluation: Each form is evaluated in order.
- Local Staging: The result of
form1is kept on the stack. The results ofform2throughformNare immediately stored into method-local variables (temporaries). This ensures that ifform3calls a function that returns multiple values, the result ofform2is safely tucked away in a local variable and cannot be overwritten. - Commitment: After all forms are evaluated, the compiler generates code to move the values from the local
temporaries into the global
Value1...ValueNfields. - Finalization: The
ReturnCountis set toN.
3. Preservation across Control Flow
Certain Lisp constructs must evaluate sub-forms without allowing those sub-forms to interfere with the return values of the primary form. This is handled by a Save-Restore pattern.
Multiple-Value-Prog1
The multiple-value-prog1 form evaluates its first form, then saves the entire side-channel state (the primary
value, the ReturnCount, and all ValueN fields) into local variables. It then evaluates the subsequent
forms. After they finish, it restores the side-channel state from its locals, ensuring the values of the first form are what the
caller receives.
Unwind-Protect
In unwind-protect, the protected form is evaluated and its primary result is stored in a local variable. Crucially,
the finally block (cleanup) must not destroy the side-channel state produced by the protected form. The compiler
generates code at the start of the finally block to save ReturnCount and Value1...63 into
locals. Once the cleanup forms complete, the state is restored from these locals before the method returns.
4. Nested Multiple Values (The Re-entrancy Problem)
The fundamental problem with a global side-channel is re-entrancy. If the compiler were to store form2 directly
into the global Value1 field, and then form3 involved a function call like (some-func), that
function might execute its own (values ...) logic. This would overwrite the global Value1 that was just
set for the outer form.
By enforcing the use of method-local temporaries during the production of values, CLRHack ensures that the global side-channel is only updated at the last possible moment ("atomically" relative to the Lisp expression), effectively shielding the return values from being corrupted by nested evaluations.