Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

C's longjmp doesn't cleanly unwind the stack. Common Lisp's GO, just like all CL control flow operators, cleanly unwind the stack, allowing cleanup forms established by UNWIND-PROTECT to be executed. That's the key difference.

The book contains an implementation of dynamic variables in C, though, which uses a GCC cleanup extension; the example code contains the contributed code examples for that showing various means of implementing dynavars in C.

Promises are asynchronous while signaling and condition handling is fully synchronous; I don't know what kind of parallel one can draw here. If anything, a promise's .catch(...) method may act like a CL HANDLER-CASE; the promise becomes resolved, just a different code block is executed in case of a failure.



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.


`UNWIND-PROTECT` thats cool!

What I meant with the promises was, If you could pass three closures ... like success, error, restart could you get some kinda condition system ?


That's not enough. The condition system decouples the act of signaling a condition from the act of deciding how to handle it. This means that the promise would need to reach out to its dynamic environment to figure out what it would need to do in case of errors and restarts.

And that also only handles the case of "help I'm ded get me out of here". What about signals that do not expect to be handled, and instead are used to transfer information higher up the stack by invoking a handler function specified in the dynamic environment? That's a valid use of the condition system, as outlined in the book.


It's notable that the Worlogog::Incident and Worlogog::Restart perl modules on CPAN provide a condition system whose unwinding is implemented by Return::MultiLevel which contains a fallback pure perl implementation that does use goto-to-outer-function's-label (with gensym-ed label names for uniqueness).

Works for perl because while we don't (yet, somebody's working on one) have an unwind-protect like primitive, perl's refcounting system provides timely destruction so you can use a scope guard object stuffed into a lexical on the stack whose DESTROY method does whatever unwind cleanup you need.

Ironically, the main reason I'm not using this so much at the moment is that it isn't compatible with the suspend/resume code around promises provided by Future::AsyncAwait and I'm heavily using that in my current code, but at the point where I need both I'll probably attempt to nerd snipe one of the relevant authors into helping me figure it out.

(EDIT: Aaaaactually, I think I might already know how to make them work together, naturally an idea popped into my head just after I hit the post button ... using Syntax::Keyword::Dynamically and capturing a relevant future higher up the chain of calls should allow me to return-to-there cleanly, then I "just" need to cancel the intermediate futures to simulate unwinding ... but I'll have to try it to find out)


Yes. Destructors are an equivalent of UNWIND-PROTECT, just like Java's finally{...} block of the try/catch/finally trio.

Thank you for mentioning Worlogog, I'll include that in the book. How should I credit you?


Not quite an equivalent in perl because there are limitations like exceptions being discarded if you're unwinding because of an exception being thrown, but it's entirely possible to make things go if you know what you're doing ;)

"Matt S. Trout (mst)" is good - I'm probably better known by my IRC nick than my full name in geek circles ;)


I'll keep that in mind, and I'll add that link to the book's contents, and I'll add you to my book's Hall of Fame. Thanks.


The author of Future::AsyncAwait is working on a patch to core to provide LEAVE {} blocks which will be a more full unwind-protect solution. Note of course the destructor limitation doesn't affect Worlogog use so much since you're -not- throwing exceptions, but once you're into mixed condition and exception based code of course things that to get more "fun".


> once you're into mixed condition and exception based code of course things that to get more "fun".

I imagine that is the reality of everyone who tries to implement a condition system in a language where the default behavior is to immediately unwind the stack by throwing some sort of an exception.


Yes. But as the only perler I've seen in this thread I wanted to do my best to be honest about the risks.

If I were to want to continue discussing this, do you happen to kick around anywhere on IRC that would be suitable?


#lisp on Freenode sounds okay - my nickname is phoe.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: