Lexical Closures in CLRHack
CLRHack implements lexical closures by transforming dynamic Lisp environments into static CIL class structures. Since the .NET Common Language Runtime (CLR) does not have a native concept of "nesting" functions within the lexical scope of another function's local variables, the compiler employs Lambda Lifting and Explicit Closure Conversion.
1. Lambda Lifting
Every lambda expression (including those generated by flet and labels) is extracted from
its nesting site. The compiler generates a unique, standalone CIL class for each lambda. These classes inherit from the base
[LispBase]Lisp.Closure class.
2. The Closure Class Structure
The generated class acts as a container for both the code (the lambda body) and the environment (the captured variables). It consists of:
- Environment Fields: For every "free variable" (a variable referenced in the lambda but defined in an outer scope), the compiler adds a public field to the class.
- The Constructor: A constructor is generated that accepts the values (or references) of these free variables and stores them in the class fields.
- The Invoke Methods: The class overrides the virtual
Invokemethods of the baseClosureclass. The body of the Lisp lambda is compiled into these methods.
3. Environment Capture
At the point in the code where the lambda is defined, the compiler emits a newobj instruction. It
passes the current values of the required local variables into the closure's constructor. This "closes" over the variables,
creating a persistent instance of the environment that lives on the heap.
; Lisp Source
(let ((factor 10))
(lambda (x) (* x factor)))
; Conceptual CIL Transformation
.class private Lambda_1 extends [LispBase]Lisp.Closure {
.field public object factor_captured
.method public hidebysig specialname rtspecialname void .ctor(object f) {
ldarg.0
ldarg.1
stfld object Lambda_1::factor_captured
ret
}
.method public virtual object Invoke(object x) {
ldarg.0
ldfld object Lambda_1::factor_captured
ldarg.1
; ... multiplication logic ...
}
}
4. Shared Mutability via ValueCells
Common Lisp requires that if an outer variable is mutated (via setq), all closures capturing that variable must see
the change. To support this, CLRHack uses Indirection Cells:
- If the compiler detects that a captured variable is mutated, it "boxes" that variable.
- Instead of storing a raw value (like an integer) on the stack, it creates a
[LispBase]Lisp.ValueCellobject. - The closure captures the reference to this
ValueCell. - Both the parent function and the closure access the variable by reading from or writing to the
Valuefield of the cell. This ensures that all parties share the same mutable state.
5. Invocation
When a closure is invoked (e.g., via funcall), the Invoke method is called. Inside this method, the
this pointer (ldarg.0 in CIL) provides the code with access to the captured environment fields. This
allows the lifted function to behave as if it were still sitting inside its original lexical scope.