Aside from restarts, the core difference between the Common Lisp condition system and exceptions in most other languages is that the exception handler can run before the stack is unwound.
Having the call-stack available at the time the handler runs can be very useful; many modern dynamic languages will save some of that information in the exception object. It's still surprising to me that so very few languages have copied this feature (or even independently discovered it).
the exception handler can run before the stack is unwound.
Old basic had an error handling model that is exceptionally elegant in some cases: "on error goto ...", with an an indication (errline, I think?) of where the exception happened, and then you could "resume next" to continue (perhaps after fixing the error condition, if the error handler is somewhere else) or directly restart at an earlier point if that makes sense.
It's simple, a little goto-ish, and has abuse potential - but if actually used, tended to end with robust code where the "usual path" was the simplified "that's what we usually do", and all the "in case something was unexpected" are listed independently. C code using goto for error handling (e.g. in the linux kernel) has similar properties.
try/catch, in all languages it exists, does not let you restart in the middle unless all the error handling code is in-line (which is comparatively disgusting IMO); lisp conditions do, and more - but they only exist in Lisp.
I wouldn't call it robust. It was way too easy to do it wrong by forgetting some statement that could fail, and then your RESUME ended up resuming something you didn't anticipate, with a potentially invalid program state.
Not really, though it does share the property that it executes user code before unwinding the stack (the exception filters), and that the user code can choose to mark the exception as handled and allow the program to continue without unwinding the stack.
However, there is no Restart system, allowing the code that raises an exception to also declare how it can be handled. And there are no alternative exception handling strategies than searching the stack and unwinding in case a suitable handler is not found. Unwinding is not a property of Conditions in general CL, it is just one of the strategies that can be used when signalling a condition.
But in Window's SEH, you can fix up a page fault and re-start the offending instruction. You have access to the detailed machine state, like all the registers and the bad address where an invalid access took place and such.
I think you meant to say that the "condition handler can run before the stack is unwound." (whereas exception handlers in Java, C++, Python unwind the stack while searching for the handler)
(I agree it's a nitpick, but since your comment is likely directed to people who do not know the condition system, it's better to be precise.)
Unwinding the stack isn't necessary to find the exception handler. The language runtime code that finds the correct exception handler could just as easily walk the return pointers stored on the stack without modifying the stack pointer, frame pointer, or current return pointer. The reasons for unwinding the stack are (1) avoiding running out of stack space, particularly if the exception handler might itself throw (2) simplifying/optimizing the very common case of wanting the exception handler to unwind the stack.
It's vastly simpler in languages where you have resources that must be freed in the correct order (C++ destructors, Java monitors being released, etc.) to ensure correctness. It's difficult to correctly reason about mutating sate to recover from an error while other pieces of code might actually be holding the mutexes/monitors keeping that data safe.
It's an optimization, because you already have to do much of the work of stack unwinding (in the absence of destructors, monitor releasing, and the like) in order to find the correct exception handler.
In .NET, at least, the stack is not unwound when searching for the exception handler. It's walked to determine the handler, and then unwound before running that handler.
(This is particularly visible when debugging, because in the debugger, you still see the original stack at which the exception occurred when it's reported as unhandled - but it had to walk the stack and observe that there's no matching handler to determine that it's unhandled!)
This has an interesting side effect - since .NET catch-blocks can have filters, which are just predicates. Those predicates have to be executed while searching for the handler. Thus, they can observe the original stack before unwinding.
Win32 SEH is similar, wrt filter expressions in __except blocks.
I'm pretty sure that most C++ implementations are similar, except that they don't have filters, and so the two-phase process is not observable from within the application itself (but is observable under debugger).
Python, on the other hand, really does unwind the frames to find the handler - every function only looks at its own handlers, and if it can't find one, then it re-raises the exception to the caller. Thus, you can't readily tell if an exception is handled or unhandled in Python at the point where it occurs.
For .NET, this behavior is actually visible in VB.NET if I remember correctly, it exposes user-specified exception filters instead of only relying on types like C#.
Not sure why that was considered useful in Visual Basic, of all languages :).
It's visible in both languages these days, since both have exception filters. The difference is that VB had them from the very beginning, and C# only got them 5 years ago in version 6.
It's still surprising to me that so very few languages have copied this feature (or even independently discovered it).
I have often wondered why this is. It seems like Lisps did so many things right and here we are in These Modern Times waiting for people to rediscover "the secret is to bang the rocks together, guys".
Are these features just hard to implement in some platforms/languages? Why? Have they been de-prioritized generally until the zeitgeist decides otherwise?
I think this particular one is fairly straightforward:
1. It's a non-obvious solution (Lisp had (and in some cases, still has) other methods of doing what other languages do with exceptions before the current system
2. It was hard to implement in popular languages; any language without closures is unlikely to have this.
3. More dynamic languages that could implement these fairly easily inherited their exception system from less dynamic languages, with some other features tacked-on, or got the dynamic features after the exception system was already created.
4. Situations in which you aren't going to restart are already (mostly?) solved by the "shove the call-stack into the exception object" which significantly reduces pain compared to e.g. C++ exceptions.
Gabriel's "Worse is Better" is still relevant and explains a lot (if not everything).
Look at Rust removing CL-style conditions for a good example and Rust is by no means a popular language or a language that makes technical sacrifices in the interest of popular appeal (unlike Python and Javascript).
Expanding a bit on what you're saying, and repeating a bit from another of my comments in this thread, it's vastly easier to reason about exception/condition handling when all of the resources that need to be cleaned up (especially releasing mutexes/monitors) between the exception handler and the exception thrower have been cleaned up.
Making stack undwinding the decision of the exception/condition handler is strictly more powerful, but it greatly increases the number of corner cases that need to be considered when writing correct handlers. Just as a small subset of the issues, consider some data structure for which a mutex must be held in order to correctly recover. Assuming the mutex isn't held at the time the handler is installed, you need to consider 3 cases: (1) the mutex isn't currently held (so the handler must acquire it and remember to release the mutex before resuming normal execution), (2) the mutex is held by some active call frame sitting between the thrower and the handler (in most cases, it would then be safe to modify the data structure, but the handler absolutely must not release the mutex), and (3) the mutex is held by another thread (it's probably not possible to recover in this case).
Standard exception handling is non-local control flow, and that can make some situations difficult to reason about. Exception/condition handling where stack unwinding is optional is non-local control flow on steroids, engaged in 3D Chess Boxing.
That is true but I'd rather have the strategy in my toolbox and the choice to deploy it when needed rather than the language implementor making that decision for me.
That's a fundamental difference between Common Lisp and other more popular languages. CL tries to give you all the tools and trusts you to use them responsibly. Other, more opinionated languages, limit the problem solving approaches they offer in the interest of (popularity|performance|implementation simplicity|personal philosophy).
The CL philosophy makes a lot of sense if you examine its background and the culture that birthed it: a language designed to solve hard problems that did not have well-defined solutions.
Comments
Aside from restarts, the core difference between the Common Lisp condition system and exceptions in most other languages is that the exception handler can run before the stack is unwound.
Having the call-stack available at the time the handler runs can be very useful; many modern dynamic languages will save some of that information in the exception object. It's still surprising to me that so very few languages have copied this feature (or even independently discovered it).
Old basic had an error handling model that is exceptionally elegant in some cases: "on error goto ...", with an an indication (errline, I think?) of where the exception happened, and then you could "resume next" to continue (perhaps after fixing the error condition, if the error handler is somewhere else) or directly restart at an earlier point if that makes sense.
It's simple, a little goto-ish, and has abuse potential - but if actually used, tended to end with robust code where the "usual path" was the simplified "that's what we usually do", and all the "in case something was unexpected" are listed independently. C code using goto for error handling (e.g. in the linux kernel) has similar properties.
try/catch, in all languages it exists, does not let you restart in the middle unless all the error handling code is in-line (which is comparatively disgusting IMO); lisp conditions do, and more - but they only exist in Lisp.
I wouldn't call it robust. It was way too easy to do it wrong by forgetting some statement that could fail, and then your RESUME ended up resuming something you didn't anticipate, with a potentially invalid program state.
Windows's SEH is essentially simplified CL condition system.
Not really, though it does share the property that it executes user code before unwinding the stack (the exception filters), and that the user code can choose to mark the exception as handled and allow the program to continue without unwinding the stack.
However, there is no Restart system, allowing the code that raises an exception to also declare how it can be handled. And there are no alternative exception handling strategies than searching the stack and unwinding in case a suitable handler is not found. Unwinding is not a property of Conditions in general CL, it is just one of the strategies that can be used when signalling a condition.
But in Window's SEH, you can fix up a page fault and re-start the offending instruction. You have access to the detailed machine state, like all the registers and the bad address where an invalid access took place and such.
Lisp conditions seem to take quite a lot from PL/I conditions.
I think you meant to say that the "condition handler can run before the stack is unwound." (whereas exception handlers in Java, C++, Python unwind the stack while searching for the handler)
(I agree it's a nitpick, but since your comment is likely directed to people who do not know the condition system, it's better to be precise.)
Oh, that's why they need to unwind the stack!? These things are what some take for granted and others don't even know how to ask. Thanks.
Unwinding the stack isn't necessary to find the exception handler. The language runtime code that finds the correct exception handler could just as easily walk the return pointers stored on the stack without modifying the stack pointer, frame pointer, or current return pointer. The reasons for unwinding the stack are (1) avoiding running out of stack space, particularly if the exception handler might itself throw (2) simplifying/optimizing the very common case of wanting the exception handler to unwind the stack.
It's vastly simpler in languages where you have resources that must be freed in the correct order (C++ destructors, Java monitors being released, etc.) to ensure correctness. It's difficult to correctly reason about mutating sate to recover from an error while other pieces of code might actually be holding the mutexes/monitors keeping that data safe.
It's an optimization, because you already have to do much of the work of stack unwinding (in the absence of destructors, monitor releasing, and the like) in order to find the correct exception handler.
In .NET, at least, the stack is not unwound when searching for the exception handler. It's walked to determine the handler, and then unwound before running that handler.
(This is particularly visible when debugging, because in the debugger, you still see the original stack at which the exception occurred when it's reported as unhandled - but it had to walk the stack and observe that there's no matching handler to determine that it's unhandled!)
This has an interesting side effect - since .NET catch-blocks can have filters, which are just predicates. Those predicates have to be executed while searching for the handler. Thus, they can observe the original stack before unwinding.
Win32 SEH is similar, wrt filter expressions in __except blocks.
I'm pretty sure that most C++ implementations are similar, except that they don't have filters, and so the two-phase process is not observable from within the application itself (but is observable under debugger).
Python, on the other hand, really does unwind the frames to find the handler - every function only looks at its own handlers, and if it can't find one, then it re-raises the exception to the caller. Thus, you can't readily tell if an exception is handled or unhandled in Python at the point where it occurs.
For .NET, this behavior is actually visible in VB.NET if I remember correctly, it exposes user-specified exception filters instead of only relying on types like C#.
Not sure why that was considered useful in Visual Basic, of all languages :).
It's visible in both languages these days, since both have exception filters. The difference is that VB had them from the very beginning, and C# only got them 5 years ago in version 6.
Oh, I haven't followed C# for quite some time, didn't know they had gotten them.
I have often wondered why this is. It seems like Lisps did so many things right and here we are in These Modern Times waiting for people to rediscover "the secret is to bang the rocks together, guys".
Are these features just hard to implement in some platforms/languages? Why? Have they been de-prioritized generally until the zeitgeist decides otherwise?
I think this particular one is fairly straightforward:
1. It's a non-obvious solution (Lisp had (and in some cases, still has) other methods of doing what other languages do with exceptions before the current system
2. It was hard to implement in popular languages; any language without closures is unlikely to have this.
3. More dynamic languages that could implement these fairly easily inherited their exception system from less dynamic languages, with some other features tacked-on, or got the dynamic features after the exception system was already created.
4. Situations in which you aren't going to restart are already (mostly?) solved by the "shove the call-stack into the exception object" which significantly reduces pain compared to e.g. C++ exceptions.
Re 2: the condition system is easy to implement, try/catch implementation includes all needed building blocks.
Gabriel's "Worse is Better" is still relevant and explains a lot (if not everything).
Look at Rust removing CL-style conditions for a good example and Rust is by no means a popular language or a language that makes technical sacrifices in the interest of popular appeal (unlike Python and Javascript).
Expanding a bit on what you're saying, and repeating a bit from another of my comments in this thread, it's vastly easier to reason about exception/condition handling when all of the resources that need to be cleaned up (especially releasing mutexes/monitors) between the exception handler and the exception thrower have been cleaned up.
Making stack undwinding the decision of the exception/condition handler is strictly more powerful, but it greatly increases the number of corner cases that need to be considered when writing correct handlers. Just as a small subset of the issues, consider some data structure for which a mutex must be held in order to correctly recover. Assuming the mutex isn't held at the time the handler is installed, you need to consider 3 cases: (1) the mutex isn't currently held (so the handler must acquire it and remember to release the mutex before resuming normal execution), (2) the mutex is held by some active call frame sitting between the thrower and the handler (in most cases, it would then be safe to modify the data structure, but the handler absolutely must not release the mutex), and (3) the mutex is held by another thread (it's probably not possible to recover in this case).
Standard exception handling is non-local control flow, and that can make some situations difficult to reason about. Exception/condition handling where stack unwinding is optional is non-local control flow on steroids, engaged in 3D Chess Boxing.
That is true but I'd rather have the strategy in my toolbox and the choice to deploy it when needed rather than the language implementor making that decision for me.
That's a fundamental difference between Common Lisp and other more popular languages. CL tries to give you all the tools and trusts you to use them responsibly. Other, more opinionated languages, limit the problem solving approaches they offer in the interest of (popularity|performance|implementation simplicity|personal philosophy).
The CL philosophy makes a lot of sense if you examine its background and the culture that birthed it: a language designed to solve hard problems that did not have well-defined solutions.
Lua has this with `xpcall`.