lang (debugging | panic | attributes)
track_caller
Enable accurate caller location reporting during panic in {Option, Result}::{unwrap, expect}
with
the following changes:
#[track_caller]
function attribute, which guarantees a function has access to the
caller information.caller_location()
(safe wrapper: Location::caller()
) to retrieve
the caller's source location.Example:
#![feature(track_caller)]
use std::panic::Location;
#[track_caller]
fn unwrap(self) -> T {
panic!("{}: oh no", Location::caller());
}
let n: Option<u32> = None;
let m = n.unwrap();
It is well-known that the error message reported by unwrap()
is useless:
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', /checkout/src/libcore/option.rs:335
note: Run with `RUST_BACKTRACE=1` for a backtrace.
There have been numerous discussions (a, b, c) that want unwrap()
and friends to provide
better information to locate the panic. RFC 1669 attempted to address this by
introducing the unwrap!(x)
macro to the standard library, but it was closed since the x.unwrap()
convention is too entrenched.
This RFC introduces line numbers into unwrap()
without requiring users to adapt a new
idiom, i.e. the user should be able to see the precise location without changing any source
code.
unwrap()
unwrap()
and expect()
are two methods on Option
and Result
that are commonly used when you
are absolutely sure they contain a successful value and you want to extract it.
// 1.rs
use std::env::args;
fn main() {
println!("args[1] = {}", args().nth(1).unwrap());
println!("args[2] = {}", args().nth(2).unwrap());
println!("args[3] = {}", args().nth(3).unwrap());
}
If the assumption is wrong, they will panic and tell you that an error is unexpected.
$ ./1
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', 1.rs:4:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./1 arg1
args[1] = arg1
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', 1.rs:5:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./1 arg1 arg2
args[1] = arg1
args[2] = arg2
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', 1.rs:6:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./1 arg1 arg2 arg3
args[1] = arg1
args[2] = arg2
args[3] = arg3
Let's say you are unhappy with these built-in functions, e.g. you want to provide an alternative error message:
// 2.rs
use std::env::args;
pub fn my_unwrap<T>(input: Option<T>) -> T {
match input {
Some(t) => t,
None => panic!("nothing to see here, move along"),
}
}
fn main() {
println!("args[1] = {}", my_unwrap(args().nth(1)));
println!("args[2] = {}", my_unwrap(args().nth(2)));
println!("args[3] = {}", my_unwrap(args().nth(3)));
}
This trivial implementation, however, will only report the panic that happens inside my_unwrap
. This is
pretty useless since it is the caller of my_unwrap
that made the wrong assumption!
$ ./2
thread 'main' panicked at 'nothing to see here, move along', 2.rs:5:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./2 arg1
args[1] = arg1
thread 'main' panicked at 'nothing to see here, move along', 2.rs:5:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./2 arg1 arg2
args[1] = arg1
args[2] = arg2
thread 'main' panicked at 'nothing to see here, move along', 2.rs:5:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./2 arg1 arg2 arg3
args[1] = arg1
args[2] = arg2
args[3] = arg3
The trivial solution would require the user to provide file!()
, line!()
and column!()
. A
slightly more ergonomic solution would be changing my_unwrap
into a macro, allowing these constants to
be automatically provided.
pub fn my_unwrap_at_source_location<T>(input: Option<T>, file: &str, line: u32, column: u32) -> T {
match input {
Some(t) => t,
None => panic!("nothing to see at {}:{}:{}, move along", file, line, column),
}
}
macro_rules! my_unwrap {
($input:expr) => {
my_unwrap_at_source_location($input, file!(), line!(), column!())
}
}
println!("args[1] = {}", my_unwrap!(args().nth(1)));
// ^ tell user to add an `!`.
...
What if you have already published the my_unwrap
crate that has thousands of users, and you
want to maintain API stability? Before Rust 1.XX, the builtin unwrap()
had the same problem!
The reason the my_unwrap!
macro works is because it copy-and-pastes the entire content of its macro
definition every time it is used.
println!("args[1] = {}", my_unwrap!(args().nth(1)));
println!("args[2] = {}", my_unwrap!(args().nth(2)));
...
// is equivalent to:
println!("args[1] = {}", my_unwrap(args().nth(1), file!(), line!(), column!()));
println!("args[1] = {}", my_unwrap(args().nth(2), file!(), line!(), column!()));
...
What if we could instruct the compiler to automatically fill in the file, line, and column?
Rust 1.YY introduced the #[track_caller]
attribute for exactly this reason:
// 3.rs
#![feature(track_caller)]
use std::env::args;
#[track_caller] // <-- Just add this!
pub fn my_unwrap<T>(input: Option<T>) -> T {
match input {
Some(t) => t,
None => panic!("nothing to see here, move along"),
}
}
fn main() {
println!("args[1] = {}", my_unwrap(args().nth(1)));
println!("args[2] = {}", my_unwrap(args().nth(2)));
println!("args[3] = {}", my_unwrap(args().nth(3)));
}
Now we have truly reproduced how the built-in unwrap()
is implemented.
$ ./3
thread 'main' panicked at 'nothing to see here, move along', 3.rs:12:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./3 arg1
args[1] = arg1
thread 'main' panicked at 'nothing to see here, move along', 3.rs:13:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./3 arg1 arg2
args[1] = arg1
args[2] = arg2
thread 'main' panicked at 'nothing to see here, move along', 3.rs:14:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$ ./3 arg1 arg2 arg3
args[1] = arg1
args[2] = arg2
args[3] = arg3
#[track_caller]
is an automated version of what you've seen in the last section. The attribute
copies my_unwrap
to a new function my_unwrap_at_source_location
which accepts the caller's
location as an additional argument. The attribute also instructs the compiler to replace
my_unwrap(x)
with my_unwrap_at_source_location(x, file!(), line!(), column!())
(sort of)
whenever it sees it. This allows us to maintain the stability guarantee while allowing the user to
get the new behavior with just one recompile.
Let's enhance my_unwrap
to also log a message to the log file before panicking. We would need to
get the caller's location as a value. This is supported using the method Location::caller()
:
use std::panic::Location;
#[track_caller]
pub fn my_unwrap<T>(input: Option<T>) -> T {
match input {
Some(t) => t,
None => {
let location = Location::caller();
println!("unwrapping a None from {}:{}", location.file(), location.line());
panic!("nothing to see here, move along")
}
}
}
When your #[track_caller]
function calls another #[track_caller]
function, the caller location
will be propagated downwards:
use std::panic::Location;
#[track_caller]
pub fn my_get_index<T>(input: &[T], index: usize) -> &T {
my_unwrap(input.get(index)) // line 4
}
indirectly_unwrap(None); // line 6
When you run this, the panic will refer to line 6, the original caller, instead of line 4 where
my_get_index
calls my_unwrap
. When a library function is marked #[track_caller]
, it is
expected the function is short, and does not have any logic errors. This allows us to always track
the caller on failure.
If a panic that refers to the local location is actually needed, you may workaround by wrapping the code in a closure which cannot track the caller:
#[track_caller]
pub fn my_get_index<T>(input: &[T], index: usize) -> &T {
(|| {
my_unwrap(input.get(index))
})()
}
If you are learning Rust alongside other languages, you may wonder why Rust obtains the caller information in such a strange way. There are two restrictions that force us to adopt this solution:
Programmatic access to the stack backtrace is often used in interpreted or runtime-heavy languages like Python and Java. However, the stack backtrace is not suitable as the only solution for systems languages like Rust because optimization often collapses multiple levels of function calls. In some embedded systems, the backtrace may even be unavailable!
Solutions that use default function arguments alongside normal arguments are are often used in languages that do not perform inference higher than statement level, e.g. Swift and C#. Rust does not (yet) support default function arguments or function overloading because they interfere with type inference, so such solutions are ruled out.
Many standard functions may panic. These are divided into three categories depending on whether they should receive caller information despite the inlining cost associated with it.
The list of functions is not exhaustive. Only those with a "Panics" section in the documentation are included.
Must have. These functions are designed to generate a panic, or used so often that indicating a panic happening from them often gives no useful information.
| Function | Panic condition |
|:---------|:----------------|
| Option::expect
| self is None |
| Option::unwrap
| self is None |
| Result::expect_err
| self is Ok |
| Result::expect
| self is Err |
| Result::unwrap_err
| self is Ok |
| Result::unwrap
| self is Err |
| [T]::index_mut
| range out of bounds |
| [T]::index
| range out of bounds |
| BTreeMap::index
| key not found |
| HashMap::index
| key not found |
| str::index_mut
| range out of bounds or off char boundary |
| str::index
| range out of bounds or off char boundary |
| VecDeque::index_mut
| index out of bounds |
| VecDeque::index
| index out of bounds |
Nice to have. These functions are not commonly used, or the panicking condition is pretty rare. Often the panic information contains enough clue to fix the error without a backtrace. Inlining them would bloat the binary size without much benefit.
| Function | Panic condition |
|:---------|:----------------|
| std::env::args
| non UTF-8 values |
| std::env::set_var
| invalid key or value |
| std::env::vars
| non UTF-8 values |
| std::thread::spawn
| OS failed to create the thread |
| [T]::clone_from_slice
| slice lengths differ |
| [T]::copy_from_slice
| slice lengths differ |
| [T]::rotate
| index out of bounds |
| [T]::split_at_mut
| index out of bounds |
| [T]::swap
| index out of bounds
| BinaryHeap::reserve_exact
| capacity overflow |
| BinaryHeap::reserve
| capacity overflow |
| Duration::new
| arithmetic overflow |
| HashMap::reserve
| capacity overflow |
| HashSet::reserve
| capacity overflow |
| i32::overflowing_div
| zero divisor |
| i32::overflowing_rem
| zero divisor |
| i32::wrapping_div
| zero divisor |
| i32::wrapping_rem
| zero divisor |
| Instance::duration_since
| time travel |
| Instance::elapsed
| time travel |
| Iterator::count
| extremely long iterator |
| Iterator::enumerate
| extremely long iterator |
| Iterator::position
| extremely long iterator |
| Iterator::product
| arithmetic overflow in debug build |
| Iterator::sum
| arithmetic overflow in debug build |
| LinkedList::split_off
| index out of bounds |
| LocalKey::with
| TLS has been destroyed |
| RawVec::double_in_place
| capacity overflow |
| RawVec::double
| capacity overflow |
| RawVec::reserve_exact
| capacity overflow |
| RawVec::reserve_in_place
| capacity overflow |
| RawVec::reserve
| capacity overflow |
| RawVec::shrink_to_fit
| given amount is larger than current capacity |
| RawVec::with_capacity
| capacity overflow |
| RefCell::borrow_mut
| a borrow or mutable borrow is active |
| RefCell::borrow
| a mutable borrow is active |
| str::split_at_mut
| range out of bounds or off char boundary |
| str::split_at
| range out of bounds or off char boundary |
| String::drain
| range out of bounds or off char boundary |
| String::insert_str
| index out of bounds or off char boundary |
| String::insert
| index out of bounds or off char boundary |
| String::remove
| index out of bounds or off char boundary |
| String::reserve_exact
| capacity overflow |
| String::reserve
| capacity overflow |
| String::splice
| range out of bounds or off char boundary |
| String::split_off
| index out of bounds or off char boundary |
| String::truncate
| off char boundary |
| Vec::append
| capacity overflow |
| Vec::drain
| range out of bounds |
| Vec::insert
| index out of bounds |
| Vec::push
| capacity overflow |
| Vec::remove
| index out of bounds |
| Vec::reserve_exact
| capacity overflow |
| Vec::reserve
| capacity overflow |
| Vec::splice
| range out of bounds |
| Vec::split_off
| index out of bounds |
| Vec::swap_remove
| index out of bounds |
| VecDeque::append
| capacity overflow |
| VecDeque::drain
| range out of bounds |
| VecDeque::insert
| index out of bounds |
| VecDeque::reserve_exact
| capacity overflow |
| VecDeque::reserve
| capacity overflow |
| VecDeque::split_off
| index out of bounds |
| VecDeque::swap
| index out of bounds |
| VecDeque::with_capacity
| capacity overflow |
Not needed. Panics from these indicate silly programmer error and the panic itself has enough clue to let programmers figure out where the error comes from.
| Function | Panic condition |
|:---------|:----------------|
| std::atomic::fence
| using invalid atomic ordering |
| std::char::from_digit
| radix is outside 2 ..= 36
|
| std::env::remove_var
| invalid key |
| std::format!
| the fmt
method returns Err |
| std::panicking::set_hook
| called in panicking thread |
| std::panicking::take_hook
| called in panicking thread |
| [T]::chunks_mut
| chunk size == 0 |
| [T]::chunks
| chunk size == 0 |
| [T]::windows
| window size == 0 |
| AtomicUsize::compare_exchange_weak
| using invalid atomic ordering |
| AtomicUsize::compare_exchange
| using invalid atomic ordering |
| AtomicUsize::load
| using invalid atomic ordering |
| AtomicUsize::store
| using invalid atomic ordering |
| BorrowRef::clone
| borrow counter overflows, see issue 33880 |
| BTreeMap::range_mut
| end of range before start of range |
| BTreeMap::range
| end of range before start of range |
| char::encode_utf16
| dst buffer smaller than [u16; 2]
|
| char::encode_utf8
| dst buffer smaller than [u8; 4]
|
| char::is_digit
| radix is outside 2 ..= 36
|
| char::to_digit
| radix is outside 2 ..= 36
|
| compiler_fence
| using invalid atomic ordering |
| Condvar::wait
| waiting on multiple different mutexes |
| Display::to_string
| the fmt
method returns Err |
| ExactSizeIterator::len
| size_hint implemented incorrectly |
| i32::from_str_radix
| radix is outside 2 ..= 36
|
| Iterator::step_by
| step == 0 |
This RFC only advocates adding the #[track_caller]
attribute to the unwrap
and expect
functions. The index
and index_mut
functions should also have it if possible, but this is
currently postponed as it is not investigated yet how to insert the transformation after
monomorphization.
The #[track_caller]
attribute will modify a function at the AST and MIR levels without touching
the type-checking (HIR level) or the low-level LLVM passes.
It will first wrap the body of the function in a closure, and then call it:
#[track_caller]
fn foo<C>(x: A, y: B, z: C) -> R {
bar(x, y)
}
// will become:
#[rustc_implicit_caller_location]
#[inline]
fn foo<C>(x: A, y: B, z: C) -> R {
std::ops::FnOnce::call_once(move |__location| {
bar(x, y)
}, (unsafe { std::intrinsics::caller_location() },))
}
This is to split the function into two: the function foo
itself, and the closure
foo::{{closure}}
in it. (Technically: it is the simplest way to create two DefId
s at the HIR
level as far as I know.)
The function signature of foo
remains unchanged, so typechecking can proceed normally. The
attribute will be replaced by #[rustc_implicit_caller_location]
to let the compiler internals
continue to treat it specially. #[inline]
is added so external crates can see through foo
to
find foo::{{closure}}
.
The closure foo::{{closure}}
is a proper function so that the compiler can write calls directly to
foo::{{closure}}
, skipping foo
. Multiple calls to foo
from different locations can be done via
calling foo::{{closure}}
directly, instead of copying the function body every time which would
bloat the binary size.
The intrinsic caller_location()
is a placeholder which will be replaced by the actual caller
location when one calls foo::{{closure}}
directly.
Currently the foo::{{closure}}
cannot inherit attributes defined on the main function. To prevent
problems regarding ABI, using #[naked]
or extern "ABI"
together with
#[rustc_implicit_caller_location]
should raise an error.
After all type-checking and validation is done, we can now inject the caller location. This is done
by redirecting all calls to foo
to foo::{{closure}}
.
_r = call foo(_1, _2, _3) -> 'bb1;
// will become:
_c = call std::intrinsics::caller_location() -> 'bbt;
'bbt:
_r = call foo::{{closure}} (&[closure: x: _1, y: _2], _c) -> 'bb1;
We will further replace the caller_location()
intrinsic according to where foo
is called.
If it is called from an ordinary function, it would be replaced by the callsite's location:
// for ordinary functions,
_c = call std::intrinsics::caller_location() -> 'bbt;
// will become:
_c = Location { file: file!(), line: line!(), column: column!() };
goto -> 'bbt;
If it is called from an #[rustc_implicit_caller_location]
's closure e.g. foo::{{closure}}
, the
intrinsic will be replaced by the closure argument __location
instead, so that the caller location
can propagate directly
// for #[rustc_implicit_caller_location] closures,
_c = call std::intrinsics::caller_location() -> 'bbt;
// will become:
_c = __location;
goto -> 'bbt;
These steps are very similar to inlining, and thus the first proof-of-concept is implemented directly as a variant of the MIR inliner (but a separate pass). This also means the redirection pass currently suffers from all disadvantages of the MIR inliner, namely:
Locations will not be propagated into diverging functions (fn() -> !
), since inlining them is
not supported yet.
MIR passes are run before monomorphization, meaning #[track_caller]
currently cannot be
used on trait items:
trait Trait {
fn unwrap(&self);
}
impl Trait for u64 {
#[track_caller] //~ ERROR: `#[track_caller]` is not supported for trait items yet.
fn unwrap(&self) {}
}
To support trait items, the redirection pass must be run as post-monomorphized MIR pass (which does
not exist yet), or converted to queries provided after resolve, or a custom LLVM inlining pass which
can extract the caller's source location. This prevents the Index
trait from having
#[track_caller]
yet.
We cannot hack the impl resolution method into pre-monomorphization MIR pass because of deeply nested functions like
f1::<u32>();
fn f1<T: Trait>() { f2::<T>(); }
fn f2<T: Trait>() { f3::<T>(); }
fn f3<T: Trait>() { f4::<T>(); }
...
fn f100<T: Trait>() {
T::unwrap(); // No one will know T is u32 before monomophization.
}
Currently the redirection pass always runs before the inlining pass. If the redirection pass is run
after the normal MIR inlining pass, the normal MIR inliner must treat
#[rustc_implicit_caller_location]
as #[inline(never)]
.
The closure foo::{{closure}}
must never be inlined before the redirection pass.
When #[rustc_implicit_caller_location]
functions are called dynamically, no inlining will occur,
and thus it cannot take the location of the caller. Currently this will report where the function is
declared. Taking the address of such functions must be allowed due to backward compatibility. (If
a post-monomorphized MIR pass exists, methods via trait objects would be another case of calling
#[rustc_implicit_caller_location]
functions without caller location.)
let f: fn(Option<u32>) -> u32 = Option::unwrap;
let g: fn(Option<u32>) -> u32 = Option::unwrap;
assert!(f == g); // This must remain `true`.
f(None);
g(None); // The effect of these two calls must be the same.
The caller_location()
intrinsic returns the Location
structure which encodes the file, line and
column of the callsite. This shares the same structure as the existing type std::panic::Location
.
Therefore, the type is promoted to a lang-item, and moved into core::panicking::Location
. It is
re-exported from libstd
.
Thanks to how #[track_caller]
is implemented, we could provide a safe wrapper around the
caller_location()
intrinsic:
impl<'a> Location<'a> {
#[track_caller]
pub fn caller() -> Location<'static> {
unsafe {
::intrinsics::caller_location()
}
}
}
The panic!
macro is modified to use Location::caller()
(or the intrinsic directly) so it can
report the caller location inside #[track_caller]
.
macro_rules! panic {
($msg:expr) => {
let loc = $crate::panicking::Location::caller();
$crate::panicking::panic(&($msg, loc.file(), loc.line(), loc.column()))
};
...
}
Actually this is now more natural for core::panicking::panic_fmt
to take Location
directly
instead of tuples, so one should consider changing their signature, but this is out-of-scope for
this RFC.
panic!
is often used outside of #[track_caller]
functions. In those cases, the
caller_location()
intrinsic will pass unchanged through all MIR passes into trans. As a fallback,
the intrinsic will expand to Location { file: file!(), line: line!(), col: column!() }
during
trans.
In a #[track_caller]
function, we expect all panics being attributed to the caller (thus the
attribute name). However, sometimes the code panics not due to the caller, but the implementation
itself. It may be important to distinguish between "my fault" (implementation error) and
"your fault" (caller violating API requirement). As an example,
use std::collections::HashMap;
use std::hash::Hash;
fn count_slices<T: Hash + Eq>(array: &[T], window: usize) -> HashMap<&[T], usize> {
if !(0 < window && window <= array.len()) {
panic!("invalid window size");
// ^ triggering this panic is "your fault"
}
let mut result = HashMap::new();
for w in array.windows(window) {
if let Some(r) = result.get_mut(w) {
*r += 1;
} else {
panic!("why??");
// ^ triggering this panic is "my fault"
// (yes this code is wrong and entry API should be used)
}
}
result
}
One simple solution is to separate the "my fault" panic and "your fault" panic into two, but since declarative macro 1.0 is insta-stable, this RFC would prefer to postpone introducing any new public macros until "Macros 2.0" lands, where stability and scoping are better handled.
For comparison, the Swift language does
distinguish between the two kinds of panics semantically. The "your fault" ones are
called precondition
, while the "my fault" ones are called assert
, though they don't deal with
caller location, and practically they are equivalent to Rust's assert!
and debug_assert!
.
Nevertheless, this also suggests we can still separate existing panicking macros into the "my fault"
and "your fault" camps accordingly:
debug_assert!
and friends, unreachable!
,
unimplemented!
assert!
and friends, panic!
The question is, should calling unwrap()
, expect()
and x[y]
(index()
) be "my fault" or "your
fault"? Let's consider existing implementation of index()
methods:
// Vec::index
fn index(&self, index: usize) -> &T {
&(**self)[index]
}
// BTreeMap::index
fn index(&self, key: &Q) -> &V {
self.get(key).expect("no entry found for key")
}
// Wtf8::index
fn index(&self, range: ops::RangeFrom<usize>) -> &Wtf8 {
// is_code_point_boundary checks that the index is in [0, .len()]
if is_code_point_boundary(self, range.start) {
unsafe { slice_unchecked(self, range.start, self.len()) }
} else {
slice_error_fail(self, range.start, self.len())
}
}
If they all get #[track_caller]
, the x[y]
, expect()
and slice_error_fail()
should all report
"your fault", i.e. caller location should be propagated downstream. It does mean that the current
default of caller-location-propagation-by-default is more common. This also means "my fault"
happening during development may become harder to spot. This can be solved using RUST_BACKTRACE=1
,
or workaround by splitting into two functions:
use std::collections::HashMap;
use std::hash::Hash;
#[track_caller]
fn count_slices<T: Hash + Eq>(array: &[T], window: usize) -> HashMap<&[T], usize> {
if !(0 < window && window <= array.len()) {
panic!("invalid window size"); // <-- your fault
}
(|| {
let mut result = HashMap::new();
for w in array.windows(window) {
if let Some(r) = result.get_mut(w) {
*r += 1;
} else {
panic!("why??"); // <-- my fault (caller propagation can't go into closures)
}
}
result
})()
}
Anyway, treating everything as "your fault" will encourage that #[track_caller]
functions should
be short, which goes in line with the "must have" list in
the RFC. Thus the RFC will remain advocating for propagating caller location implicitly.
An unstable flag -Z location-detail
is added to rustc
to control how much factual detail will
be emitted when using caller_location()
. The user can toggle file
, line
and column
separately,
e.g. when compiling with:
rustc -Zlocation-detail=line
only the line number will be real. The file and column will always be a dummy value like
thread 'main' panicked at 'error message', <redacted>:192:0
Previously, all calls to unwrap()
and expect()
referred to the same location. Therefore, the
panicking branch will only needed to reuse a pointer to a single global tuple.
After this RFC is implemented, the panicking branch will need to allocate space to store the varying caller location,
so the number of instructions per unwrap()
/expect()
will increase.
The optimizer will lose the opportunity to consolidate all jumps to the panicking branch. Before
this RFC, LLVM would optimize a.unwrap() + b.unwrap()
, to something like
if (a.tag != SOME || b.tag != SOME) {
panic(&("called `Option::unwrap()` on a `None` value", "src/libcore/option.rs", 335, 20));
}
a.value_of_some + b.value_of_some
After this RFC, LLVM can only lower this to
if (a.tag != SOME) {
panic(&("called `Option::unwrap()` on a `None` value", "1.rs", 1, 1));
}
if (b.tag != SOME) {
panic(&("called `Option::unwrap()` on a `None` value", "1.rs", 1, 14));
}
a.value_of_some + b.value_of_some
One can use -Z location-detail
to get the old optimization behavior.
#[track_caller]
is only useful in solving the "get caller location" problem. Introducing an
entirely new feature just for this problem seems wasteful.
Default function arguments is another possible solution for this problem but with much wider application.
Consts, statics and closures are separate MIR items, meaning the following marked places will not get caller locations:
#[track_caller]
fn foo() {
static S: Location = Location::caller(); // will get actual location instead
let f = || Location::caller(); // will get actual location instead
Location::caller(); // this one will get caller location
}
This is confusing, but if we don't support this, we will need two panic!
macros which is not a
better solution.
Clippy could provide a lint against using Location::caller()
outside of #[track_caller]
.
This RFC tries to abide by the following restrictions:
Precise caller location. Standard library functions which commonly panic will report the
source location as where the user called them. The source location should never point inside the
standard library. Examples of these functions include Option::unwrap
and HashMap::index
.
Source compatibility. Users should never need to modify existing source code to benefit from the improved precision.
Debug-info independence. The precise caller location can still be reported even after stripping of debug information, which is very common on released software.
Interface independence. The implementation of a trait should be able to decide whether to
accepts the caller information; it shouldn't require the trait itself to enforce it. It
should not affect the signature of the function. This is an extension of rule 2, since the
Index
trait is involved in HashMap::index
. The stability of Index
must be upheld, e.g. it
should remain object-safe, and existing implementions should not be forced to accept the caller
location.
Restriction 4 "interface independence" is currently not implemented due to lack of
post-monomorphized MIR pass, but implementing #[track_caller]
as a language feature follows this
restriction.
#[track_caller]
an accurate description?std::panic::Location
into core
, or just use a 3-tuple to represent the
location? Note that the former is advocated in RFC 2070.Location::caller()
properly named?pub extern "implicit-caller-location" fn my_unwrap() {
panic!("oh no");
}
Compared with attributes, an ABI is a more natural way to tell the post-typechecking steps about
implicit parameters, pioneered by the extern "rust-call"
ABI. However, creating a new ABI will
change the type of the function as well, causing the following statement to fail:
let f: fn(Option<u32>) -> u32 = Option::unwrap;
//~^ ERROR: [E0308]: mismatched types
Making this pass will require supporting implicitly coercing extern "implicit-caller-location" fn
pointer to a normal function pointer. Also, an ABI is not powerful enough to implicitly insert a
parameter, making it less competitive than just using an attribute.
file!()
, line!()
, column!()
We could change the meaning of file!()
, line!()
and column!()
so they are only converted to
real constants after redirection (a MIR or trans pass) instead of early during macro expansion (an
AST pass). Inside #[track_caller]
functions, these macros behave as this RFC's
caller_location()
. The drawback is using these macro will have different values at compile time
(e.g. inside include!(file!())
) vs. runtime.
Introduced as an alternative to RFC 1669, instead of the caller_location()
intrinsic,
we could provide a full-fledged inline MIR macro mir!
similar to the inline assembler:
#[track_caller]
fn unwrap(self) -> T {
let file: &'static str;
let line: u32;
let column: u32;
unsafe {
mir! {
StorageLive(file);
file = const $CallerFile;
StorageLive(line);
line = const $CallerLine;
StorageLive(column);
column = const $CallerColumn;
goto -> 'c;
}
}
'c: {
panic!("{}:{}:{}: oh no", file, line, column);
}
}
The problem of mir!
in this context is trying to kill a fly with a sledgehammer. mir!
is a very
generic mechanism which requires stabilizing the MIR syntax and considering the interaction with
the surrounding code. Besides, #[track_caller]
itself still exists and the magic constants
$CallerFile
etc are still magical.
Assume this is solved by implementing RFC issue 323.
fn unwrap(file: &'static str = file!(), line: u32 = line!(), column: u32 = column!()) -> T {
panic!("{}:{}:{}: oh no", file, line, column);
}
Default arguments was a serious contender to the better-caller-location problem as this is usually how other languages solve it.
Language | Syntax |
---|---|
Swift | func unwrap(file: String = #file, line: Int = #line) -> T |
D | T unwrap(string file = __FILE__, size_t line = __LINE__) |
C# 5+ | T Unwrap([CallerFilePath] string file = "<n/a>", [CallerLineNumber] int line = 0) |
Haskell with GHC | unwrap :: (?callstack :: CallStack) => Maybe t -> t |
C++ with GCC 4.8+ | T unwrap(const char* file = __builtin_FILE(), int line = __builtin_LINE()) |
A naive solution will violate restriction 4 "interface independence": adding the file, line, column
arguments to index()
will change its signature. This can be resolved if this is taken into
account.
impl<'a, K, Q, V> Index<&'a Q> for BTreeMap<K, V>
where
K: Ord + Borrow<Q>,
Q: Ord + ?Sized,
{
type Output = V;
// This should satisfy the trait even if the trait specifies
// `fn index(&self, idx: Idx) -> &Self::Output`
#[inline]
fn index(&self, key: &Q, file: &'static str = file!(), line: u32 = line!(), column: u32 = column!()) -> &V {
self.get(key).expect("no entry found for key", file, line, column)
}
}
This can be resolved if the future default argument proposal takes this into account. But again, this feature itself is going to be large and controversial.
Treat #[track_caller]
as the same as a very forceful #[inline(always)]
. This eliminates the
procedural macro pass. This was the approach suggested in the first edition of this RFC, since the
target functions (unwrap
, expect
, index
) are just a few lines long. However, it experienced
push-back from the community as:
Therefore the RFC is changed to the current form, and the inlining pass is now described as just an implementation detail.
This is inspired when investigating the difference in "my fault" vs "your fault". We incorporate ideas from design-by-contract (DbC) by specifying that "your fault" is a kind of contract violation. Preconditions are listed as part of the function signature, e.g.
// declaration
extern {
#[precondition(fd >= 0, "invalid file descriptor {}", fd)]
fn close_fd(fd: c_int);
}
// declaration + defintion
#[precondition(option.is_some(), "Trying to unwrap None")]
fn unwrap<T>(option: Option<T>) -> T {
match option {
Some(t) => t,
None => unsafe { std::mem::unchecked_unreachable() },
}
}
Code that appears in the #[precondition]
attribute should be copied to caller site, so when the
precondition is violated, they can get the caller's location.
Specialization should be treated like subtyping, where preconditions can be weakened:
trait Foo {
#[precondition(condition_1)]
fn foo();
}
impl<T: Debug> Foo for T {
#[precondition(condition_2a)]
#[precondition(condition_2b)]
default fn foo() { ... }
}
impl Foo for u32 {
#[precondition(condition_3)]
fn foo() { ... }
}
assert!(condition_3 || (condition_2a && condition_2b) || condition_1);
// ^ automatically inserted when the following is called...
<u32 as Foo>::foo();
Before Rust 1.0, there was the hoare
compiler plugin which introduces DbC using the similar
syntax. However, the conditions are expanded inside the function, so the assertions will not fail
with the caller's location. A proper solution will be similar to what this RFC proposes.
Many alternatives have been proposed before but failed to satisfy the restrictions laid out in the Rationale subsection, thus should not be considered viable alternatives within this RFC, at least at the time being.
The unwrap!()
macro introduced in RFC 1669 allows the user to write unwrap!(x)
instead of
x.unwrap()
.
A similar solution is introducing a loc!()
macro that expands to
concat!(file!(), ":", line!(), ":", column!())
, so user writes x.expect(loc!())
instead of
x.unwrap()
.
There is even the better_unwrap
crate that
automatically rewrites all unwrap()
and expect()
inside a module to provide the caller location
through a procedural attribute.
All of these are non-viable since they require the user to actively change their source code, thus
violating restriction 2 "source compatibility", unless we are willing to drop the .!
from
macros
All pre-typeck rewrites are prone to false-positive failures affecting unrelated types that have an
unwrap()
method. Post-typeck rewrites are no different from this RFC.
When given debug information (DWARF section/file on Linux, *.pdb
file on Windows, *.dSYM
folder
on macOS), the program is able to obtain the source code location for each address. This solution is
often used in runtime-heavy languages like Python, Java and Go.
For Rust, however:
The debug information is usually not provided in release mode.
In particular, cargo
defaults to disabling debug symbols in release mode (this default can
certainly be changed). rustc
itself is tested in CI and distributed in release mode, so
getting a usable location in release mode is a real concern (see also RFC 1417 for why it was
disabled in the official distribution in the first place).
Even if this is generated, the debug symbols are generally not distributed to end-users, which means the error reports will only contain numerical addresses. This can be seen as a benefit, as the implementation detail won't be exposed, but how to submit/analyze an error report would be out-of-scope for this RFC.
There are multiple issues preventing us from relying on debug info nowadays.
Issues 24346 (Backtrace does not include file and line number on non-Linux platforms) and 42295 (Slow backtrace on panic) and are still not entirely fixed. Even after the debuginfo is properly handled, if we decide not to expose the whole the full stacktrace, we may still need to reopen pull request 40264 (Ignore more frames on backtrace unwinding).
These signal that debuginfo support is not reliable enough if we want to solve the unwrap/expect issue now.
These drawbacks are the main reason why restriction 3 "debug-info independence" is added to the motivation.
(A debuginfo-based stack trace proposal can be found at RFC 2154.)
SourceContext
generic parameterIntroduced as an alternative in RFC 1669, inspired by GHC's implicit parameter:
fn unwrap<C: SourceContext = CallerSourceContext>(self) -> T {
panic!("{}: oh no", C::default());
}
The CallerSourceContext
lang item will instruct the compiler to create a new type implementing
SourceContext
whenever unwrap()
is instantiated.
Unfortunately this violates restriction 4 "interface independence". This solution cannot apply to
HashMap::index
as this will require a change of the method signature of index()
which has been
stabilized. Methods applying this solution will also lose object-safety.
The same drawback exists if we base the solution on RFC 2000 (const generics).
If we want to support adding #[track_caller]
to trait methods, the redirection
pass/query/whatever should be placed after monomorphization, not before. Currently the RFC
simply prohibit applying #[track_caller]
to trait methods as a future-proofing measure.
Diverging functions should be supported.
The closure foo::{{closure}}
should inherit most attributes applied to the function foo
, in
particular #[inline]
, #[cold]
, #[naked]
and also the ABI. Currently a procedural macro
won't see any of these, nor would there be anyway to apply these attributes to a closure.
Therefore, #[rustc_implicit_caller_location]
currently will reject #[naked]
and ABI, and
leaving #[inline]
and #[cold]
mean no-op. There is no semantic reason why these cannot be
used though.