Why is displaying a stack trace with values difficult

The problem as I understand it is when displaying a stack trace, finding actual values of arguments somewhere on the stack is hard. They may or may not actually be on the stack in a decipherable form. Let’s look at the function I defined in a PR yesterday:

function invmod(n::T) where {T<:BitInteger}
    isodd(n) || throw(DomainError(n, "Argument must be odd."))
    x = (3*n ⊻ 2) % T
    y = (1 - n*x) % T
    for _ = 1:trailing_zeros(2*sizeof(T))
        x *= y + true
        y *= y
    end
    return x
end

Suppose this is called in a context where we don’t care about n after the call to invmod(n) and the stack trace is thrown from somewhere after x and y have been initialized. At that point, there’s no reason to have n in memory anywhere—we no longer need it. So how can you recover the value of n to display it? The DWARF standard for debugging info is supposed to help address this, but it ends up being complex and brittle and it turns out that the JIT compiler that we use doesn’t really even try to emit DWARF code correctly.

Another thing to consider: what if you’ve modified an array argument by the time a stack trace needs to be shown? You can’t very well copy every array argument on the way in; so you’d end up displaying the modified contents of the array, which might end up being more confusing than helpful. Maybe that’s ok but it’s certainly a sharp edge that people would have to learn if we displayed values in stack traces.

The above example does suggest an idea to me though. If we forget about being in a debugger where you can set a breakpoint at an arbitrary line of code, then we could maybe improve this. Outside of a debugger we can only get a stack trace at an error point, so if we saved all arguments until error branches in functions, then we could display values in stack traces.

4 Likes