Proposal: offer a way to augment call stacks

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
3 messages Options
Reply | Threaded
Open this post in threaded view
|

Proposal: offer a way to augment call stacks

David Feuer
HasCallStack constraints give us a pretty good view of where we came from when an error occurs, but they don't always tell us enough about what was going on there at the time. I think we should add a small feature to the call stack interface to improve matters.

== Motivating example ==

Suppose we have

f :: Int -> Int
f x = g (h x)

If g throws an error because its argument is bad, we'd like to know how that happened. A HasCallStack constraint on g will reveal that g was called by f. Now we'd like to dig deeper. Was f supplied a bad argument? Did h calculate a bad value from it? If f is only called a few times, it's easy to work this out: just add a trace call to f reporting x. But if f is called thousands of times, that won't work at all.

== Proposed change ==

I'd like to add a function (of whatever name)

recordInCallStack
  :: forall (a :: TYPE rep)
     HasCallStack
  => String
  -> (HasCallStack => a)
  -> a

We could then modify the above program thus:

f :: Int -> Int
f x = recordInCallStack msg (g (h x))
  where
    msg = "x = " ++ show x

Now when g throws an error, we'll see the message f recorded in its proper position in the call stack.

== Implementation ==

I believe the cleanest approach would be to add a new constructor to CallStack (perhaps called Message):

Message :: String -> CallStack -> CallStack

Then recordInCallStack can look like

recordInCallStack
  :: forall (a :: TYPE rep)
     HasCallStack
  => String
  -> (HasCallStack => a)
  -> a
recordInCallStack s a =
    let ?callStack = Message s ?callStack
    in a

== Questions ==

For performance reasons, I believe recordInCallStack should be lazy in its string argument. But this means that we could encounter another error in the process of printing a call stack. How if at all should we attempt to recover in this case?

If we don't do anything special, I believe we may start printing a second call stack in the middle of the first. Kind of gross.

Another option is to catch all exceptions when evaluating the message. Then we could note that the message couldn't be printed but continue with the rest of the call stack.

David

_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries
Reply | Threaded
Open this post in threaded view
|

Re: Proposal: offer a way to augment call stacks

David Feuer
Faking up a way to add this functionality without modifying GHC or
base library code, I ran into several issues:

1. Lacking an appropriate CallStack constructor, I had to stuff extra
info into the function name field. This is pretty gross, and limits
display options.

2. There was no obvious way to deal with exceptions thrown evaluating
call stack notes themselves.

3. I wasn't able to find a reasonable way to prevent call stack
inference from adding a frame for the argument to my recordInCallStack
function. I ended up needing a horrible hack with unsafeCoerce.

For 1, I think we should add an appropriate constructor.

For 2, we need to discuss how to handle this situation. One option is
to catch the first exception, abort the process of printing the call
stack, and then rethrow the exception. Another option is to try to get
as much of the stack trace as possible before aborting; this gets a
bit hairy. I don't know what other options there might be. Most of
these options involve some modifications to the implementation of
error.

For 3, unless someone else works out a way I couldn't find, we have
two choices: add a GHC feature to suppress adding info at a particular
site, or have error strip out the extras at the end. The first is
harder; the second is a little less efficient.

On Tue, Mar 13, 2018 at 7:05 PM, David Feuer <[hidden email]> wrote:

> HasCallStack constraints give us a pretty good view of where we came from
> when an error occurs, but they don't always tell us enough about what was
> going on there at the time. I think we should add a small feature to the
> call stack interface to improve matters.
>
> == Motivating example ==
>
> Suppose we have
>
> f :: Int -> Int
> f x = g (h x)
>
> If g throws an error because its argument is bad, we'd like to know how that
> happened. A HasCallStack constraint on g will reveal that g was called by f.
> Now we'd like to dig deeper. Was f supplied a bad argument? Did h calculate
> a bad value from it? If f is only called a few times, it's easy to work this
> out: just add a trace call to f reporting x. But if f is called thousands of
> times, that won't work at all.
>
> == Proposed change ==
>
> I'd like to add a function (of whatever name)
>
> recordInCallStack
>   :: forall (a :: TYPE rep)
>      HasCallStack
>   => String
>   -> (HasCallStack => a)
>   -> a
>
> We could then modify the above program thus:
>
> f :: Int -> Int
> f x = recordInCallStack msg (g (h x))
>   where
>     msg = "x = " ++ show x
>
> Now when g throws an error, we'll see the message f recorded in its proper
> position in the call stack.
>
> == Implementation ==
>
> I believe the cleanest approach would be to add a new constructor to
> CallStack (perhaps called Message):
>
> Message :: String -> CallStack -> CallStack
>
> Then recordInCallStack can look like
>
> recordInCallStack
>   :: forall (a :: TYPE rep)
>      HasCallStack
>   => String
>   -> (HasCallStack => a)
>   -> a
> recordInCallStack s a =
>     let ?callStack = Message s ?callStack
>     in a
>
> == Questions ==
>
> For performance reasons, I believe recordInCallStack should be lazy in its
> string argument. But this means that we could encounter another error in the
> process of printing a call stack. How if at all should we attempt to recover
> in this case?
>
> If we don't do anything special, I believe we may start printing a second
> call stack in the middle of the first. Kind of gross.
>
> Another option is to catch all exceptions when evaluating the message. Then
> we could note that the message couldn't be printed but continue with the
> rest of the call stack.
>
> David
_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries
Reply | Threaded
Open this post in threaded view
|

Re: Proposal: offer a way to augment call stacks

Bardur Arantsson-2
On 2018-03-15 00:16, David Feuer wrote:
> Faking up a way to add this functionality without modifying GHC or
> base library code, I ran into several issues:
>
[--snip--]
> 2. There was no obvious way to deal with exceptions thrown evaluating
> call stack notes themselves.
>
[--snip--]
>
> For 2, we need to discuss how to handle this situation. One option is
> to catch the first exception, abort the process of printing the call
> stack, and then rethrow the exception. Another option is to try to get
> as much of the stack trace as possible before aborting; this gets a
> bit hairy. I don't know what other options there might be. Most of
> these options involve some modifications to the implementation of
> error.

When reading your initial proposal, I actually wondered about this --
could the model from Java's Throwable be used here?

Every Throwable (think Exception) in Java has an attached list of
so-called suppressed exceptions (plus stack traces). This allows one to
-- if not handle, exactly -- at least avoid losing exception information
when an exception is thrown during exception handling. Generally, one
exception is still regarded as the "primary" exception and is the only
one that's actually matched by "catch" clauses, but since Haskell is a
lot more flexible than Java wrt. catch, I don't think this necessarily
need to be a limitation for a Haskell implementation of the same
concept. Obviously, the "standard" catch mechanism would remain as-is,
regardless just to avoid breaking backward compatibility.

I have no idea what kind of performance impact this would have or how
hard it would be to do, but AFAICT it would "solve" your problem here
and it would also help in various cases of "nested" exception handling
where one is forced to through information away.

Regards,

_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries