wryun / es-shell

es: a shell with higher-order functions
http://wryun.github.io/es-shell/
Other
313 stars 26 forks source link

Exceptions thrown from settor variables called during `varpop()` cause assertion errors #87

Closed jpco closed 8 months ago

jpco commented 9 months ago

Minimal example:

; es -c 'local (var = !) {set-var = {throw something}}'
except.c:25: assertion failed (handler != ((void *)0))
IOT instruction--core dumped

This seems to be because things get stuck in an infinite loop.

  1. when exiting the dynamic scope, var gets varpopped
  2. varpop invokes set-var
  3. set-var invokes throw something
  4. throw() causes var to get varpopped

which repeats the loop until something breaks. Right now what breaks is that each time throw() is called, it takes another exception handler off of the handler stack until none are left, and then the next throw() calls assert(handler != NULL) which fails.

There's a similar infinite loop if you just directly set a variable inside its own settor function

; es -c 'local (var = !) {set-var = {var = ()}}'
except.c:25: assertion failed (handler != ((void *)0))
IOT instruction--core dumped

But in this case at least it's more obvious that you're doing something wrong. I think in this case you loop until you hit the point where the shell throws the max-eval-depth exception, and then you hit the same throw() infinite loop in the first example.

An infinite loop or exception during the varpush causes more reasonable behavior:

; # totally normal behavior
; es -c 'set-var = {throw something}; local (var = !) {}'
uncaught exception: something

; # throws an exception, but at least it's a reasonable one
; es -c 'set-var = {var = ()}; local (var = !) {}'
max-eval-depth exceeded

The immediately obvious fix to try for the assertion failures is to move the tophandler = handler->up line in throw() down below the varpop()s. This half-works, in that it stops the assertion failures from happening, but it doesn't stop the infinite loop from blowing the stack. Worse is that when the infinite loop causes max-eval-depth to get thrown, handling that exception just gets caught up in everything (because it's calling throw(), which is calling varpop(), etc...), and then the "you blew your stack" segfault happens despite the shell's best attempts at protecting itself!

My preferred alternative is for varpop to some exception-handling itself -- catch any exceptions from the settor function, finish popping the var, and then re-throw. That seems to produce the preferred behavior in each case here.