|
| 1 | +# Implicit Caller Location |
| 2 | + |
| 3 | +Approved in [RFC 2091], this feature enables the accurate reporting of caller location during panics |
| 4 | +initiated from functions like `Option::unwrap`, `Result::expect`, and `Index::index`. This feature |
| 5 | +adds the [`#[track_caller]`][attr-reference] attribute for functions, the |
| 6 | +[`caller_location`][intrinsic] intrinsic, and the stabilization-friendly |
| 7 | +[`core::panic::Location::caller`][wrapper] wrapper. |
| 8 | + |
| 9 | +## Motivating Example |
| 10 | + |
| 11 | +Take this example program: |
| 12 | + |
| 13 | +```rust |
| 14 | +fn main() { |
| 15 | + let foo: Option<()> = None; |
| 16 | + foo.unwrap(); // this should produce a useful panic message! |
| 17 | +} |
| 18 | +``` |
| 19 | + |
| 20 | +Prior to Rust 1.42, panics like this `unwrap()` printed a location in libcore: |
| 21 | + |
| 22 | +``` |
| 23 | +$ rustc +1.41.0 example.rs; example.exe |
| 24 | +thread 'main' panicked at 'called `Option::unwrap()` on a `None` value',...core\macros\mod.rs:15:40 |
| 25 | +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. |
| 26 | +``` |
| 27 | + |
| 28 | +As of 1.42, we get a much more helpful message: |
| 29 | + |
| 30 | +``` |
| 31 | +$ rustc +1.42.0 example.rs; example.exe |
| 32 | +thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', example.rs:3:5 |
| 33 | +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace |
| 34 | +``` |
| 35 | + |
| 36 | +These error messages are achieved through a combination of changes to `panic!` internals to make use |
| 37 | +of `core::panic::Location::caller` and a number of `#[track_caller]` annotations in the standard |
| 38 | +library which propagate caller information. |
| 39 | + |
| 40 | +## Reading Caller Location |
| 41 | + |
| 42 | +Previously, `panic!` made use of the `file!()`, `line!()`, and `column!()` macros to construct a |
| 43 | +[`Location`] pointing to where the panic occurred. These macros couldn't be given an overridden |
| 44 | +location, so functions which intentionally invoked `panic!` couldn't provide their own location, |
| 45 | +hiding the actual source of error. |
| 46 | + |
| 47 | +Internally, `panic!()` now calls [`core::panid::Location::caller()`][wrapper] to find out where it |
| 48 | +was expanded. This function is itself annotated with `#[track_caller]` and wraps the |
| 49 | +[`caller_location`][intrinsic] compiler intrinsic implemented by rustc. This intrinsic is easiest |
| 50 | +explained in terms of how it works in a `const` context. |
| 51 | + |
| 52 | +## Caller Location in `const` |
| 53 | + |
| 54 | +There are two main phases to returning the caller location in a const context: walking up the stack |
| 55 | +to find it and allocating a const value to return. |
| 56 | + |
| 57 | +In a const context we "walk up the stack" from where the intrinsic is invoked, stopping when we |
| 58 | +reach the first function call in the stack which does *not* have the attribute. This walk is in |
| 59 | +[`InterpCx::find_closest_untracked_caller_location()`][const-find-closest] which returns |
| 60 | +`Option<Span>`. |
| 61 | + |
| 62 | +If the caller of the current function is untracked, it returns `None`. We use the span for the |
| 63 | +intrinsic's callsite in this case. |
| 64 | + |
| 65 | +Otherwise it searches by iterating in reverse over [`Frame`][const-frame]s in the [InterpCx::stack], |
| 66 | +calling [`InstanceDef::requires_caller_location`][requires-location] on the |
| 67 | +[`Frame::instance`][frame-instance]'s def until it finds a `false` return. It then returns the span |
| 68 | +of the prior still-tracked frame which is the "topmost" tracked function. |
| 69 | + |
| 70 | +We use the same code in both contexts to allocate a static value for each `Location`. This is |
| 71 | +performed by the [`TyCtxt::const_caller_location()`][const-location-query] query. Internally this |
| 72 | +calls [`InterpCx::alloc_caller_location()`][alloc-location] and results in a unique |
| 73 | +[memory kind][location-memory-kind]. The SSA codegen backend is able to emit code for these same |
| 74 | +values. |
| 75 | + |
| 76 | +Once our location has been allocated in static memory we return a pointer to it. |
| 77 | + |
| 78 | +## Generating code for `#[track_caller]` callees |
| 79 | + |
| 80 | +To generate efficient code for a tracked function and its callers we need to provide the same |
| 81 | +behavior from the intrinsic's point of view without having a stack to walk up at runtime. We invert |
| 82 | +the approach: as we grow the stack down we pass an additional argument to calls of tracked functions |
| 83 | +rather than walking up the stack when the intrinsic is called. That additional argument can be |
| 84 | +returned wherever the caller location is queried. |
| 85 | + |
| 86 | +The argument we append is of type `&'static core::panic::Location<'staic>`. A reference was chosen |
| 87 | +to avoid unnecessary copying because a pointer is a third the size of |
| 88 | +`std::mem::size_of::<core::panic::Location>() == 24` at time of writing. |
| 89 | + |
| 90 | +When generating a call to a function which is tracked, we pass the location argument the value of |
| 91 | +[`FunctionCx::get_caller_location`][fcx-get]. |
| 92 | + |
| 93 | +If the calling function is tracked, `get_caller_location` returns the local in |
| 94 | +[`FunctionCx::caller_location`][fcx-location] which was populated by the current caller's caller. |
| 95 | +In these cases the intrinsic "returns" a reference which was actually provided in an argument to its |
| 96 | +caller. |
| 97 | + |
| 98 | +If the calling function is not tracked, `get_caller_location` allocates a `Location` static from |
| 99 | +the current `Span` and returns a reference to that. |
| 100 | + |
| 101 | +By chaining together the `caller_location` fields of multiple `FunctionCx`s as we grow the bottom of |
| 102 | +the stack, we achieve the same behavior as a loop starting from the bottom without imposing |
| 103 | +bookkeeping requirements on *all* function calls. |
| 104 | + |
| 105 | +### Codegen examples |
| 106 | + |
| 107 | +What does this transformation look like in practice? Take this example which uses the new feature: |
| 108 | + |
| 109 | +```rust |
| 110 | +#![feature(track_caller)] |
| 111 | +use std::panic::Location; |
| 112 | + |
| 113 | +#[track_caller] |
| 114 | +fn print_caller() { |
| 115 | + println!("called from {}", Location::caller()); |
| 116 | +} |
| 117 | + |
| 118 | +fn main() { |
| 119 | + print_caller(); |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +Here `print_caller()` appears to take no arguments, but we compile it to something like this: |
| 124 | + |
| 125 | +```rust |
| 126 | +#![feature(panic_internals)] |
| 127 | +use std::panic::Location; |
| 128 | + |
| 129 | +fn print_caller(caller: &Location) { |
| 130 | + println!("called from {}", caller); |
| 131 | +} |
| 132 | + |
| 133 | +fn main() { |
| 134 | + print_caller(&Location::internal_constructor(file!(), line!(), column!())); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +### Dynamic Dispatch |
| 139 | + |
| 140 | +In codegen contexts we have to modify the callee ABI to pass this information down the stack, but |
| 141 | +the attribute expressly does *not* modify the type of the function. The ABI change must be |
| 142 | +transparent to type checking and remain sound in all uses. |
| 143 | + |
| 144 | +Direct calls to tracked functions will always know the full codegen flags for the callee and can |
| 145 | +generate appropriate code. Indirect callers won't have this information and it's not encoded in |
| 146 | +the type of the function pointer they call, so we generate a [`ReifyShim`] around the function |
| 147 | +whenever taking a pointer to it. This shim isn't able to report the actual location of the indirect |
| 148 | +call (the function's definition site is reported instead), but it prevents miscompilation and is |
| 149 | +probably the best we can do without modifying fully-stabilized type signatures. |
| 150 | + |
| 151 | +> *Note:* We always emit a [`ReifyShim`] when taking a pointer to a tracked function. While the |
| 152 | +> constraint here is imposed by codegen contexts, we don't know during MIR construction of the shim |
| 153 | +> whether we'll be called in a const context (safe to ignore shim) or in a codegen context (unsafe |
| 154 | +> to ignore shim). |
| 155 | +
|
| 156 | +## The Attribute |
| 157 | + |
| 158 | +The `#[track_caller]` attribute is checked alongside other codegen attrs to ensure the function: |
| 159 | + |
| 160 | +* is not a foreign import (e.g. in an `extern {...}` block) |
| 161 | +* has `"Rust"` ABI (as opposed to `"C"`, etc...) |
| 162 | +* is not a closure |
| 163 | +* is not `#[naked]` |
| 164 | + |
| 165 | +If the use is valid, we set [`CodegenFnAttrsFlags::TRACK_CALLER`][attrs-flags]. This flag influences |
| 166 | +the return value of [`InstanceDef::requires_caller_location`][requires-location] which is in turn |
| 167 | +used in both const and codegen contexts to ensure correct propagation. |
| 168 | + |
| 169 | +### Traits |
| 170 | + |
| 171 | +When applied to a trait method prototype, the attribute takes effect on all implementations of the |
| 172 | +trait method. When applied to a default trait method implementation, the attribute takes effect on |
| 173 | +that implementation *and* any overrides. It is valid to apply the attribute to a regular |
| 174 | +implementation of a trait method, regardless of whether the defining trait does. It is a no-op to |
| 175 | +apply the attribute to trait methods with the attribute in the trait definition, but a valid one. |
| 176 | + |
| 177 | +Example: |
| 178 | + |
| 179 | +```rust |
| 180 | +#![feature(track_caller)] |
| 181 | + |
| 182 | +macro_rules! assert_tracked { |
| 183 | + () => {{ |
| 184 | + let location = std::panic::Location::caller(); |
| 185 | + assert_eq!(location.file(), file!()); |
| 186 | + assert_ne!(location.line(), line!(), "line should be outside this fn"); |
| 187 | + println!("called at {}", location); |
| 188 | + }}; |
| 189 | +} |
| 190 | + |
| 191 | +trait TrackedFourWays { |
| 192 | + /// All implementations inherit `#[track_caller]`. |
| 193 | + #[track_caller] |
| 194 | + fn blanket_tracked(); |
| 195 | + |
| 196 | + /// Implementors can annotate themselves. |
| 197 | + fn local_tracked(); |
| 198 | + |
| 199 | + /// This implementation is tracked (overrides are too). |
| 200 | + #[track_caller] |
| 201 | + fn default_tracked() { |
| 202 | + assert_tracked!(); |
| 203 | + } |
| 204 | + |
| 205 | + /// Overrides of this implementation are tracked (it is too). |
| 206 | + #[track_caller] |
| 207 | + fn default_tracked_to_override() { |
| 208 | + assert_tracked!(); |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +/// This impl uses the default impl for `default_tracked` and provides its own for |
| 213 | +/// `default_tracked_to_override`. |
| 214 | +impl TrackedFourWays for () { |
| 215 | + fn blanket_tracked() { |
| 216 | + assert_tracked!(); |
| 217 | + } |
| 218 | + |
| 219 | + #[track_caller] |
| 220 | + fn local_tracked() { |
| 221 | + assert_tracked!(); |
| 222 | + } |
| 223 | + |
| 224 | + fn default_tracked_to_override() { |
| 225 | + assert_tracked!(); |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +fn main() { |
| 230 | + <() as TrackedFourWays>::blanket_tracked(); |
| 231 | + <() as TrackedFourWays>::default_tracked(); |
| 232 | + <() as TrackedFourWays>::default_tracked_to_override(); |
| 233 | + <() as TrackedFourWays>::local_tracked(); |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +## Background/History |
| 238 | + |
| 239 | +Broadly speaking, this feature's goal is to improve common Rust error messages without breaking |
| 240 | +stability guarantees, requiring modifications to end-user source, relying on platform-specific |
| 241 | +debug-info, or preventing user-defined types from having the same error-reporting benefits. |
| 242 | + |
| 243 | +Improving the output of these panics has been a goal of proposals since at least mid-2016 (see |
| 244 | +[non-viable alternatives] in the approved RFC for details). It took two more years until RFC 2091 |
| 245 | +was approved, much of its [rationale] for this feature's design having been discovered through the |
| 246 | +discussion around several earlier proposals. |
| 247 | + |
| 248 | +The design in the original RFC limited itself to implementations that could be done inside the |
| 249 | +compiler at the time without significant refactoring. However in the year and a half between the |
| 250 | +approval of the RFC and the actual implementation work, a [revised design] was proposed and written |
| 251 | +up on the tracking issue. During the course of implementing that, it was also discovered that an |
| 252 | +implementation was possible without modifying the number of arguments in a function's MIR, which |
| 253 | +would simplify later stages and unlock use in traits. |
| 254 | + |
| 255 | +Because the RFC's implementation strategy could not readily support traits, the semantics were not |
| 256 | +originally specified. They have since been implemented following the path which seemed most correct |
| 257 | +to the author and reviewers. |
| 258 | + |
| 259 | +[RFC 2091]: https://github.com/rust-lang/rfcs/blob/master/text/2091-inline-semantic.md |
| 260 | +[attr-reference]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-track_caller-attribute |
| 261 | +[intrinsic]: https://doc.rust-lang.org/nightly/core/intrinsics/fn.caller_location.html |
| 262 | +[wrapper]: https://doc.rust-lang.org/nightly/core/panic/struct.Location.html#method.caller |
| 263 | +[non-viable alternatives]: https://github.com/rust-lang/rfcs/blob/master/text/2091-inline-semantic.md#non-viable-alternatives |
| 264 | +[rationale]: https://github.com/rust-lang/rfcs/blob/master/text/2091-inline-semantic.md#rationale |
| 265 | +[revised design]: https://github.com/rust-lang/rust/issues/47809#issuecomment-443538059 |
| 266 | +[attrs-flags]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc/middle/codegen_fn_attrs/struct.CodegenFnAttrFlags.html#associatedconstant.TRACK_CALLER |
| 267 | +[`ReifyShim`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc/ty/enum.InstanceDef.html#variant.ReifyShim |
| 268 | +[`Location`]: https://doc.rust-lang.org/core/panic/struct.Location.html |
| 269 | +[const-find-closest]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/interpret/struct.InterpCx.html#method.find_closest_untracked_caller_location |
| 270 | +[requires-location]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc/ty/enum.InstanceDef.html#method.requires_caller_location |
| 271 | +[alloc-location]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/interpret/struct.InterpCx.html#method.alloc_caller_location |
| 272 | +[fcx-location]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_codegen_ssa/mir/struct.FunctionCx.html#structfield.caller_location |
| 273 | +[const-location-query]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc/ty/struct.TyCtxt.html#method.const_caller_location |
| 274 | +[location-memory-kind]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/interpret/enum.MemoryKind.html#variant.CallerLocation |
| 275 | +[const-frame]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/interpret/struct.Frame.html |
| 276 | +[InterpCx::stack]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/interpret/struct.InterpCx.html#structfield.stack |
| 277 | +[fcx-get]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_codegen_ssa/mir/struct.FunctionCx.html#method.get_caller_location |
| 278 | +[frame-instance]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/interpret/struct.Frame.html#structfield.instance |
0 commit comments