Summary

Enable accurate caller location reporting during panic in {Option, Result}::{unwrap, expect} with the following changes:

  1. Support the #[track_caller] function attribute, which guarantees a function has access to the caller information.
  2. Add an intrinsic function 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();

Motivation

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.

Guide-level explanation

Let’s reimplement 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!

Track the caller

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.

Location type

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")
        }
    }
}

Propagation of tracker

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))
    })()
}

Why do we use implicit caller location

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:

  1. 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!

  2. Solutions that use default function arguments alongside normal arguments 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.

Reference-level explanation

Survey of panicking standard functions

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.

  1. 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.

    FunctionPanic condition
    Option::expectself is None
    Option::unwrapself is None
    Result::expect_errself is Ok
    Result::expectself is Err
    Result::unwrap_errself is Ok
    Result::unwrapself is Err
    [T]::index_mutrange out of bounds
    [T]::indexrange out of bounds
    BTreeMap::indexkey not found
    HashMap::indexkey not found
    str::index_mutrange out of bounds or off char boundary
    str::indexrange out of bounds or off char boundary
    VecDeque::index_mutindex out of bounds
    VecDeque::indexindex out of bounds
  2. 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.

    List of category 2 functions
    FunctionPanic condition
    std::env::argsnon UTF-8 values
    std::env::set_varinvalid key or value
    std::env::varsnon UTF-8 values
    std::thread::spawnOS failed to create the thread
    [T]::clone_from_sliceslice lengths differ
    [T]::copy_from_sliceslice lengths differ
    [T]::rotateindex out of bounds
    [T]::split_at_mutindex out of bounds
    [T]::swapindex out of bounds
    BinaryHeap::reserve_exactcapacity overflow
    BinaryHeap::reservecapacity overflow
    Duration::newarithmetic overflow
    HashMap::reservecapacity overflow
    HashSet::reservecapacity overflow
    i32::overflowing_divzero divisor
    i32::overflowing_remzero divisor
    i32::wrapping_divzero divisor
    i32::wrapping_remzero divisor
    Instance::duration_sincetime travel
    Instance::elapsedtime travel
    Iterator::countextremely long iterator
    Iterator::enumerateextremely long iterator
    Iterator::positionextremely long iterator
    Iterator::productarithmetic overflow in debug build
    Iterator::sumarithmetic overflow in debug build
    LinkedList::split_offindex out of bounds
    LocalKey::withTLS has been destroyed
    RawVec::double_in_placecapacity overflow
    RawVec::doublecapacity overflow
    RawVec::reserve_exactcapacity overflow
    RawVec::reserve_in_placecapacity overflow
    RawVec::reservecapacity overflow
    RawVec::shrink_to_fitgiven amount is larger than current capacity
    RawVec::with_capacitycapacity overflow
    RefCell::borrow_muta borrow or mutable borrow is active
    RefCell::borrowa mutable borrow is active
    str::split_at_mutrange out of bounds or off char boundary
    str::split_atrange out of bounds or off char boundary
    String::drainrange out of bounds or off char boundary
    String::insert_strindex out of bounds or off char boundary
    String::insertindex out of bounds or off char boundary
    String::removeindex out of bounds or off char boundary
    String::reserve_exactcapacity overflow
    String::reservecapacity overflow
    String::splicerange out of bounds or off char boundary
    String::split_offindex out of bounds or off char boundary
    String::truncateoff char boundary
    Vec::appendcapacity overflow
    Vec::drainrange out of bounds
    Vec::insertindex out of bounds
    Vec::pushcapacity overflow
    Vec::removeindex out of bounds
    Vec::reserve_exactcapacity overflow
    Vec::reservecapacity overflow
    Vec::splicerange out of bounds
    Vec::split_offindex out of bounds
    Vec::swap_removeindex out of bounds
    VecDeque::appendcapacity overflow
    VecDeque::drainrange out of bounds
    VecDeque::insertindex out of bounds
    VecDeque::reserve_exactcapacity overflow
    VecDeque::reservecapacity overflow
    VecDeque::split_offindex out of bounds
    VecDeque::swapindex out of bounds
    VecDeque::with_capacitycapacity overflow
  3. 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.

    List of category 3 functions
    FunctionPanic condition
    std::atomic::fenceusing invalid atomic ordering
    std::char::from_digitradix is outside 2 ..= 36
    std::env::remove_varinvalid key
    std::format!the fmt method returns Err
    std::panicking::set_hookcalled in panicking thread
    std::panicking::take_hookcalled in panicking thread
    [T]::chunks_mutchunk size == 0
    [T]::chunkschunk size == 0
    [T]::windowswindow size == 0
    AtomicUsize::compare_exchange_weakusing invalid atomic ordering
    AtomicUsize::compare_exchangeusing invalid atomic ordering
    AtomicUsize::loadusing invalid atomic ordering
    AtomicUsize::storeusing invalid atomic ordering
    BorrowRef::cloneborrow counter overflows, see issue 33880
    BTreeMap::range_mutend of range before start of range
    BTreeMap::rangeend of range before start of range
    char::encode_utf16dst buffer smaller than [u16; 2]
    char::encode_utf8dst buffer smaller than [u8; 4]
    char::is_digitradix is outside 2 ..= 36
    char::to_digitradix is outside 2 ..= 36
    compiler_fenceusing invalid atomic ordering
    Condvar::waitwaiting on multiple different mutexes
    Display::to_stringthe fmt method returns Err
    ExactSizeIterator::lensize_hint implemented incorrectly
    i32::from_str_radixradix is outside 2 ..= 36
    Iterator::step_bystep == 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.

Procedural attribute macro

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 DefIds 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.

Redirection (MIR inlining)

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.

Standard libraries

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.

“My fault” vs “Your fault”

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:

  • Definitely “my fault” (use actual location): debug_assert! and friends, unreachable!, unimplemented!
  • Probably “your fault” (propagate caller location): 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.

Location detail control

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

Drawbacks

Code bloat

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.

Narrow solution scope

#[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.

Confusing scoping rule

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].

Rationale and alternatives

Rationale

This RFC tries to abide by the following restrictions:

  1. 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.

  2. Source compatibility. Users should never need to modify existing source code to benefit from the improved precision.

  3. Debug-info independence. The precise caller location can still be reported even after stripping of debug information, which is very common on released software.

  4. 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 implementations 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.

Alternatives

🚲 Name of everything 🚲

  • Is #[track_caller] an accurate description?
  • Should we move std::panic::Location into core, or just use a 3-tuple to represent the location? Note that the former is advocated in RFC 2070.
  • Is Location::caller() properly named?

Using an ABI instead of an attribute

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.

Repurposing 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.

Inline MIR

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.

Default function arguments

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.

LanguageSyntax
Swiftfunc unwrap(file: String = #file, line: Int = #line) -> T
DT unwrap(string file = __FILE__, size_t line = __LINE__)
C# 5+T Unwrap([CallerFilePath] string file = "<n/a>", [CallerLineNumber] int line = 0)
Haskell with GHCunwrap :: (?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.

Semantic inlining

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:

  1. Inlining causes debugging to be difficult.
  2. It does not work with recursive functions.
  3. People do want to apply the attribute to long functions.
  4. The expected usage of “semantic inlining” and traditional inlining differ a lot, continue calling it inlining may confuse beginners.

Therefore the RFC is changed to the current form, and the inlining pass is now described as just an implementation detail.

Design-by-contract

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 + definition
#[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.

Non-viable alternatives

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.

Macros

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.

Backtrace

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 parameter

Introduced 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).

Unresolved questions

  • 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.