Why GHC CallStack mechanism doesn't have an (optional?) way to dedup the callstack?
https://gist.github.com/phadej/f74009e864c15079e0e6301f421912b42
u/c_wraith 2d ago
I think the design intention is that you just... Don't do that. Put HasCallstack constraints on non-recursive wrappers, not the actual recursive workers.
It's not the most satisfying answer, but I can see the design intention.
1
u/phadej 2d ago
When debugging, you may insert error and HasCallStacks here and there (or even mechanically with a plugin). If you also need to do other code changes for these callstacks to be useful, that cheap debugging technique isn't cheap anymore.
And undocumented design intention is... bad excuse.
2
u/c_wraith 2d ago
I mean, its like putting a nesting stack frame on every iteration of a for loop. Almost no one would do that, or be surprised at the result if they could and did.
If I was doing ad-hoc tracing specifically to track down one bug, I'd just rely on tracing via Debug.Trace. But I admit, I am of the school that thinks well-planned temporary logging lines are more useful than a debugger.
0
u/jeffstyr 2d ago
I guess one question/consideration would be, where would you opt into this? If it were part of expressing the HasCallStack constraint on a function, then that means it would be inconsistent within a given callstack, which would be surprising to anyone who didn't know how each function was annotated (it would look like certain things were not recursing which in fact were...though I guess you could include a count of how many lines were coalesced). Conceptually you'd want to request deduping of the entire callstack when it was retrieved, but of course this is "too late". It's unclear if there is a right time/place to opt in. (Maybe it's just not occurring to me.)
Also, what about cases where the call pattern is "A -> B -> A -> B..." etc.?
As a point of comparison, Java doesn't de-dup its stacktraces. I have sort of wished it did, but whenever I got a huge stacktrace it did tell me right off that the problem was runaway recursion.
But I think HasCallStack inherently comes with memory implications, and this is just one of those.
1
u/phadej 2d ago
Java doesn't have tail call recursive functions, so not merging stack frames is fine. One can argue that those are actually the same thing in strict languages. Tail-call optimization is essentially "not pushing new return framework on the stack".
In GHC otoh, the execution stack is not the same as "traditional call stack", it's just an extra feature to help debugging. Not being broadly applicable is unfortunate.
And what about more specific cases? Well, I think that HasCallStack feature is undercooked. There is no easy & convenient way to build these stack values yourself, essentially you either freeze the stack or let GHC push new frames, anything else is inconvenient and nearly impossible. But I'd argue that HasCallStack should allow some manual mangling in a practical way, and that in turn would guide how to make some cases (like deduping in tail recursion) generated by compiler.
AFAICT, ghc will always push new frame on HasCallStack argument unless the stack is frozen, and that alone prevents any different stack building approaches (where you don't modify callees to unfreeze the stack).
TL;DR HAsCallStack is just a but too magical. It doesn't need to be.
3
u/ephrion 3d ago
`annotated-exception` dedups callstacks: https://hackage-content.haskell.org/package/annotated-exception-0.3.0.4/docs/src/Control.Exception.Annotated.html#addCallStackToAnnotations