Conditions and stack trace

One of the things I don't want to see is stack traces. Whenever I saw this, I'd always be disappointed. There are basically 2 reasons: the first one is it usually means something went wrong, the second one is sometimes it doesn't show where the condition is actually raised. The first one can be considered a bug so I just need to snatch it. The second one is more for implementations issue.

Currently, Sagittarius shows stack trace whenever default exception handler is invoked, means no guard nor with-exception-handler. The stack trace is collected in the default exception handler. This is fine most of the time since I don't use them that much in my script. (well, I don't argue if it's good or not here.) However, sometimes I want a script fail safe or make sure it releases resources or so. Then I saw the fangs. As a simple example, you have the following script:
(guard (e (else (release-all) (raise e)))
Suppose open-something-and-fail-it may raise an error and you want to release all resources after the process. Now, you saw a stack trace, what would you expect to be shown? I would expect where the root cause is raised. Or at least I can see it. However because the stack trace is collected in the default exception handler, you don't see those information at all.

It was okay until I had written a server program which shows a stack trace whenever unhandled exception is raised. If you write this kind of script, then you want to know which one is the actual culprit, server itself or the application program. As long as I'm writing both, it's just a matter of good old days printf debug but this is pain in the ass. If the stack trace shows where this happened, I don't have to do this type of thing in some cases. So I've changed the timing of collecting stack traces.

The basic idea is whenever raise or raise-continuable is called with compound condition, then it appends stack trace object. Only one stack trace object can be appeared in the compound condition. So if the condition is re-raised, then new stack trace object is created and the old one is set to the new one as its cause. It's pretty much similar with Java's exception. Of course, there are bunch of differences.
  1. Java's exception collects stack trace when it's created
  2. java.lang.Throwable's cause property may have root cause while Sagittarius' only takes stack trace object.
  3. The stack trace object is implicitly added, means given condition is copied entirely while Java's exception can be the same.
The item #1 is the biggest one. On Java, if you throw an exception created before the throw syntax, then the stack trace indicates where it's created. This can be very inconvenient like this type of case:
public class Foo {
  private Exception exn = new Exception();
  public void foo() throws Exception {
    throw exn;
Don't ask me who would do this but the stack trace would be shown indicates the second line. (I didn't check the Java's spec, so this might depend on the implementation.)

The item #2 is because of implementation of condition. It flattens the given compound conditions if there are. For example, suppose you catch a condition and re-raise it with adding specific condition. The script would be like this:
;; suppose (fail) raises a compound condition
(guard (e (else (raise (condition (make-some-other-condition) e))))
In this case, the compound condition e will be flattened and merged into the new condition object. R6RS doesn't say how the condition procedure construct compound condition so it can decompose it by need.

The item #3 can be an issue. Suppose you want to re-use a condition and compare it by mean of eq? like this:
(let ((c (condition (make-error) (make-who-condition 'who))))
  (guard (e (else (eq? c e)))
    (raise c)))
;; -> #f
The raise procedure implicitly copy the given condition and adds stack trace object, thus the object wouldn't be the same one. I couldn't find anything mentioning this kind of thing on R6RS. So I think it's still R6RS compliant. However this might cause an issue in the future. (Using exception as a returning value or so?)

The reason why Sagittarius choose to collect stack trace  when condition is raised is because of the requirement of condition procedure. It's not so difficult to make condition procedure collect stack trace however this procedure must be able to take arbitrary number of conditions including compound conditions. So there is no way to specify which one is the actual root cause when multiple compound conditions are given. (Nobody do this? who knows!)

I'm kinda doubting that if I made a better choice. Especially item #3. But seeing irrelevant stack traces is much more painful. So I think I can live with this.


Shiro Kawai said...

This can be thought as the level of abstraction. Less abstract approach requires programmers to be more explicit on what's included in the condition object. More abstract approach may even make construction of condition object implicit.

Java took low abstraction approach (explicit condition object construction), except that it made inclusion of stack trace implicit. I think that's a bit of inconsistency, though it's probably for the convenience.

Regarding the level of abstraction, Scheme's exception system doesn't make lot of abstraction, too. Even "raise" does nothing special---merely invokes the current exception handler. If you want to be consistent, the solution could be to be explicit about the stack trace, that is, explicitly take the current stack trace and include it in the condition:

(raise (condition (message ...) (stack-trace (get-current-stack-trace))))

Then you can abstract it away. For example, raise/stack-trace procedure to compound the condition with stack trace; it may save the original condition in a slot so that you can later compare it with saved one, if you want.

The problem of this approach is that it's inconvenient that any code that don't use raise/stack-trace will lose the stack trace. So you can change a view. What if a stack trace is purely for the convenience of debugging? As if it's a source-code location info, or a REPL history, which is nothing to do with the program's semantics but just provides auxiliary info? If a condition object concerns program's semantics, then the stack trace info doesn't need to be a part of it. It rather exists in the debugging environment. E.g. "raise" can save it's dynamic state in some runtime.

kei said...

(define (raise c) (raise/stack-trace e)) is the psuedo code of the current approach. The reason why is indeed because of the convenience. (otherwise I need to re-write all code using raise)

Saving debugging environment sounds a better idea. Whenever "raise" is called, it piles up the current environment and it can be reached if needed. I just need to think when the piled environment needs to be released.

Post a Comment