Skip to content

Commit b3e1428

Browse files
committed
Implicit Caller Location section.
1 parent 3cc54f2 commit b3e1428

File tree

2 files changed

+279
-0
lines changed

2 files changed

+279
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
- [Updating LLVM](./backend/updating-llvm.md)
110110
- [Debugging LLVM](./backend/debugging.md)
111111
- [Backend Agnostic Codegen](./backend/backend-agnostic.md)
112+
- [Implicit Caller Location](./codegen/implicit-caller-location.md)
112113
- [Profile-guided Optimization](./profile-guided-optimization.md)
113114
- [Sanitizers Support](./sanitizers.md)
114115
- [Debugging Support in Rust Compiler](./debugging-support-in-rustc.md)
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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

Comments
 (0)