Friday, May 29, 2026

CLRHack: handler-bind and handler-case

In the CLRHack compiler, handler-bind is a primitive form used to register condition handlers in the dynamic environment. It operates by managing a thread-local list of active handler objects, ensuring that condition signaling follows the standard Common Lisp search and execution rules.

Handling of handler-bind

When the compiler processes a handler-bind form, it generates CIL code that performs the following steps:

  1. Capture Previous State: It calls Lisp.HandlerControl::GetActiveHandlers() to retrieve the current list of active handlers and stores it in a frame-local variable.
  2. Construct New List: For each binding, it evaluates the condition type and the handler function (which is typically a closure). It instantiates a new [LispBase]Lisp.Handler object and conses it onto the current handler list.
  3. Install New State: It calls Lisp.HandlerControl::SetActiveHandlers(new_list) to update the dynamic environment for the current thread.
  4. Protected Execution: The body of the handler-bind is wrapped in a CIL .try block.
  5. Restoration: A finally block is emitted that calls SetActiveHandlers with the saved list. This ensures that handlers are properly uninstalled, regardless of whether the body completes normally, signals an error, or performs a non-local exit.

Lexical Non-Local Exits

Handlers in Common Lisp are executed in the dynamic environment of the signaller but have lexical access to the environment where they were defined. In CLRHack, if a handler function performs a non-local exit (such as a throw or return-from), the compiler utilizes its exception-based jump mechanism:

  • If the exit is a throw, it uses the standard CatchThrowException mechanism.
  • If the exit is a return-from to a block outside the handler closure, the compiler identifies this as a non-local exit during analyze-environment. It compiles the return-from into a throw of a BlockExitException, which is subsequently caught by the try/catch frame established by the target block.

Handler Search

The handler search is performed at runtime by the signal or error functions. These functions retrieve the active handlers list via HandlerControl.GetActiveHandlers() and iterate through them. For each handler, the runtime checks if the signaled condition is of the type (or a subtype of the type) the handler was registered for. If a match is found, the handler function is invoked. If the handler returns normally (declines), the search continues with the next applicable handler.

Dynamic Tags

The handler-bind implementation itself relies on the dynamic state of the thread-local activeHandlers list. However, when used in conjunction with handler-case, unique dynamic tags (typically fresh ListCell objects) are generated. These tags are used as the "target" for the throw performed by the handler, ensuring that the control flow returns exactly to the correct handler-case frame and doesn't conflict with other active handler or catch frames.

handler-case as an Extension of handler-bind

In CLRHack, handler-case is not a primitive but a macro that expands into a combination of block, catch, and handler-bind. It extends handler-bind by providing a mechanism to automatically exit the signaling context and execute a specific branch of code based on the condition caught.

The implementation details of the expansion are as follows:

  • Exit Block: The entire form is wrapped in a block with a unique exit tag to allow the normal path to return immediately upon completion of the protected expression.
  • Dynamic Setup: A unique dynamic tag is created for the catch frame. Local variables are established to store the captured condition and a unique ID identifying which clause was triggered.
  • The Binding: A handler-bind is generated where each handler function is a closure that, when called:
    1. Saves the signaled condition into the local condition-var.
    2. Sets the id-var to a unique GENSYM representing that specific clause.
    3. Performs a throw to the dynamic tag.
  • The Catch and Dispatch: A catch block surrounds the protected expression. If a handler performs the throw, the catch returns, and a cond statement (the dispatcher) checks the id-var. It then executes the body of the matching handler-case clause with the condition variable bound to the clause's parameter.

No comments: