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))) (open-something-and-fail-it) (release-all))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.- Java's exception collects stack trace when it's created
java.lang.Throwable
's cause property may have root cause while Sagittarius' only takes stack trace object.- The stack trace object is implicitly added, means given condition is copied entirely while Java's exception can be the same.
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)))) (fail))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))) ;; -> #fThe
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.
2 comments:
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.
(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