In the CLRHack compiler, restart-bind is a primitive form that manages the dynamic lifecycle of Common Lisp restarts by manipulating a thread-local stack of active restart objects.
Handling of restart-bind
When the compiler encounters a restart-bind form, it generates CIL code that performs the following steps:
- Capture Previous State: It calls
Lisp.RestartControl::GetActiveRestarts()to retrieve the current list of active restarts and stores it in a frame-local variable. - Construct New List: For each binding, it evaluates the restart name, handler function, and optional keyword
arguments (
:report-function,:interactive-function,:test-function). It then instantiates a new[LispBase]Lisp.Restartobject and conses it onto the existing list. - Install New State: It calls
Lisp.RestartControl::SetActiveRestarts(new_list)to update the dynamic environment. - Protected Execution: The body of the
restart-bindis wrapped in a CIL.tryblock. - Restoration: A
finallyblock is emitted that restores the previously saved restart list usingSetActiveRestarts, ensuring that restarts are properly uninstalled even if the body performs a non-local exit.
Lexical Non-Local Exits
The CLRHack compiler supports lexical non-local exits (e.g., return-from or go) through an
exception-based mechanism. During the analyze-environment pass, the compiler identifies if a return-from
target block is "non-local" (i.e., the return occurs within a nested closure). If so:
- The target block is wrapped in a
try/catchfor[LispBase]Lisp.BlockExitException. - The block is assigned a unique string ID.
- The return-from form is compiled into a
throwof aBlockExitException, which carries the target ID, the return value, and a captured array of multiple return values (retrieved viaLisp.Values::CaptureValues()). - The
catchhandler verifies the target ID. If it matches, it restores any captured multiple values and resumes normal execution; otherwise, it rethrows the exception.
Restart Search
The search for an applicable restart is handled at runtime by Lisp.RestartControl::FindRestart. It performs a
linear search through the current thread's activeRestarts list (stored in a [ThreadStatic] field). It can
accept either a symbol name or a Restart object itself. If a name is provided, the search respects shadowing,
returning the innermost (most recently bound) restart with that name.
Dynamic Tags
Dynamic tags are required for the catch and throw forms used in non-local control flow. In CLRHack, a
dynamic tag is simply a fresh object (typically a ListCell or a new System.Object) used as a unique
token. This ensures that a throw only matches the specific catch frame it was intended for, avoiding
collisions between different invocations of the same function or different restart-case blocks.
restart-case as an Extension of restart-bind
In CLRHack, restart-case is implemented as a macro that expands into a combination of block,
catch, and restart-bind. It extends the basic binding functionality by providing a built-in mechanism to
jump back to the site of the restart-case when a restart is invoked.
The implementation details are as follows:
- Exit Block: The entire expansion is wrapped in a
(block exit_tag ...)to allow normal completion of the expression. - Dynamic Tag: A unique dynamic tag is created (e.g.,
(let ((tag (list nil))) ...)). - Catch Frame: A
(catch tag ...)is established around therestart-bindand the expression. - Binding: The
restart-bindcreates restarts whose handler functions are closures. When invoked, these closures capture their arguments into local variables, set a unique clause ID, and thenthrowto the dynamic tag. - Dispatch: When the
throwis caught, therestart-casebody executes acondorcasestatement. This dispatcher checks the clause ID set by the handler and executes the corresponding forms provided in therestart-caseclause, eventually returning the result from theexit_tagblock.