Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
stack traces for errors #651
This proposal depends on having failable functions instead of error union types (see #632).
We would like to get rid of the
Well, kind of. You get the stack trace of where you tried to unwrap the error, but not the stack trace of where the error was created and how it bubbled up the call stack.
This proposal describes a way to get access to this stack trace.
Consider this code. I will use the new syntax from proposal #632, where
Here, when we run the program, we're going to see either a successful exit code, or "ItBroke" and a failure exit code.
The problem is that if you get "ItBroke" that doesn't tell you where exactly the error originated from.
Here's what I propose:
A function that can fail has a secret parameter passed to it which is a pointer to a stack trace node, which looks something like this:
When a failable function returns, it sets the
A function that calls a failable function has a
As a result, at every callsite of a failable function, we have access to a full callstack of where the
We need language support to enable capturing the stack trace data in a way that can't accidentally lead to calling a function and accessing invalid memory.
Consider the example above again, but with a different
Alright, this proposal can't work as is. Because what are we going to do with a linked list of instruction pointers that becomes invalid memory if we do so much as call
So here's how I'll alter the proposal to make it still work. We have a fixed size number of stack frames,
Once again we pass a pointer to this struct as a secret parameter to failable functions, and just before returning, we do:
This assumes that the deepest stack entries are the most significant. It is compatible with tail calls. Now our example code can look like:
31 was chosen because 31 * 8 + 8 = 256 bytes on a 64 bit system, or 128 bytes on a 32 bit system, is a pretty reasonable amount of bytes to put on a stack. Note that it's not for every call, but 256 bytes in the stack frame of the first non-failable function to call a failable function.
We could potentially analyze the call graph, in a similar way that we want to do for #157, to find out the maximum number of function calls - tail calls included - that contain consecutive failable functions. This would give us our global value to use for the
Another possible modification to this proposal, is that we could enable this for every function. Then at any given point, you can use
This would prevent a lot of optimizations and should probably only be allowed in Debug mode. Functions with safety disabled should probably not contain the code to add themselves to this return trace. They would be mysteriously missing.
Doing this for failable functions, however, should probably be allowed in at least Debug and ReleaseSafe mode. For ReleaseFast mode, maybe this is still OK. But in any mode, we should omit the code if the stack trace object is never captured.
I realized that all of these have only a maximum stack depth of 2, but hopefully it still illustrates the point.
I had once implemented stack trace support for Win32 C applications.
C pseudocode how it could be implemented:
Disadvantages of this mechanism:
This stack traces mechanism would allow to implement builtin
Consider this example:
Say we get to
The return-point stack trace, when we come back from
If we return at example 1, then the return-point stack trace gets a new address appended which is the address of the return instruction at example 1. It gets truncated in the sense that a ring buffer truncates data, if and only if there is not enough pre-allocated items in the stack trace to handle the new entry. Why do you say this make the least amount of sense? This is exactly what a return-point trace is. It answers the question, "how did I get back to this location?"
When we call
Example 3 is exactly the same as example 1, because modifications to the stack trace data only happen in these two places:
Ok, I was thinking about "appending" backwards. This makes a little more sense.
Here's my interpretation of what we'd get from example 1:
It's a little confusing that we get a trace of something inside
I suppose that's not in the trace, because it's not a stack trace but rather a return trace.
Ok, let's do example 2:
Here we can see the trace jumping all over the place.
Let's say in example 3 that logError() caught and ignored some error:
It's pretty mysterious how
Maybe this is ok? It's more like a log of where in the source code errors happened rather than any kind of stack trace. Whenever a function returns an error, it adds an entry to the log. The log won't necessarily be a coherent history of control flow, but you get some clues about errors that happen. Is that the idea?
All of these examples accurately describe the proposal, and yes I agree with the conclusion that this is the idea.
In a lot of cases you have a single return error.Something followed by a chain of %returns. For this case it will be perfectly clear how the error originated. This will make it more attractive to use %return (or the proposed