The TXR Lisp exception handling is based in C setjmp/longjmp. It has the same expressive power as Lisp conditions.

Well, once upon a time it used to be setjmp/longjmp; currently it's based on a re-implementation of a mechanism that is almost setjmp/longjmp in assembly language.

To use setjmp/longjmp for implementing sophisticated exception handling, you have to maintain your own unwind chain on the stack and unwind that.

Before invoking longjmp, you have to walk your own frames that are chained through the stack, and do all the clean-up yourself, up to that frame that holds the jmp_buf where you want to jump.

TXR correctly maintains the chain connectivity even under delimited continuation support, which works by copying sections of the C stack to and from a heap object.

When a delimited continuation is restored (involving copying it out of a heap into a new location on the stack), the frame linkage in the restored continuation is fixed up and hooked up. The continuation can throw an exception and unwind out through the caller that invoked it.

Here is the implementation of unwind-protect operator in the interpreter:

static val op_unwind_protect(val form, val env)
{
val prot_form = second(form);
val cleanup_forms = rest(rest(form));
val result = nil;

uw_simple_catch_begin;

result = eval(prot_form, env, prot_form);

uw_unwind {
eval_progn(cleanup_forms, env, cleanup_forms);
}

uw_catch_end;

return result;
}

The grotty jump saving and restoring stuff is hidden behind friendly-looking macros. The simple catch begin/end macros will create a frame with a particular type field. That type field tells the unwinder that it's supposed to stop there to do clean-up code. How that works is that the frame contains the saved jump buffer: a longjmp-like operation (that used to be longjmp once upon a time) restores control here, then the forms in the uw_unwind { } are executed and then the unwinding is resumed.

In the virtual machine, there is a uwprot instruction instead:

1> (disassemble (compile-toplevel '(unwind-protect (foo) (bar))))
** warning: (expr-1:1) unbound function foo
** warning: (expr-1:1) unbound function bar
data:
syms:
0: foo
1: bar
code:
0: 58000004 uwprot 4
1: 24000002 gcall t2 0
2: 00000000
3: 10000000 end nil
4: 24000003 gcall t3 1
5: 00000001
6: 10000000 end nil
7: 10000002 end t2
instruction count:
6
#<sys:vm-desc: 9432d90>

uwprot 4 means that the cleanup code is found at instruction address 4. uwprot registers a frame which references that code. What immediately follows the uwprot instruction is the protected code. This code is terminated by an end instruction. When the code hits the end instruction, control returns to the uwprot instruction, which then transfers control to instruction address 4. The cleanup code is also terminated by an end instruction. In the non-unwinding case, that just falls through to the next end instruction for ending this whole block and returning the value of register t2, which is the result of the (foo) call produced in gcall t2 0. In the unwinding case the end nil at 6 will allow control to return to the unwinder.

The function that the vm interpreter dispatches for uwprot is simplicity itself:

NOINLINE static void vm_uwprot(struct vm *vm, vm_word_t insn)
{
int saved_lev = vm->lev;
unsigned cleanup_ip = vm_insn_bigop(insn);

uw_simple_catch_begin;

vm_execute(vm);

uw_unwind {
vm->lev = saved_lev;
vm->ip = cleanup_ip;
vm_execute(vm);
}

uw_catch_end;
}

The VM context (frame level and instruction pointer) are saved very simply into local variables on the C stack. Well, what is saved is not the current instruction pointer but the one of the cleanup code, pulled from the instruction's operand. Then there is a simple catch frame which re-enters the vm, continuing with the next instruction. vm_execute will return when it hits that end instruction, passing control back to here. If an exception is thrown, then the uw_unwind block restores the VM context from the two variables and runs the cleanup code through to the end instruction, which also happens in the normal case.