Line info for `unwrap`/`expect`


#1

This thread is to continue a discussion about how we could enable panicking with accurate line info from unwrap/expect. Currently, when you unwrap a None or Err you get something along the lines of

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', ../src/libcore/option.rs:325

It would be good if this could instead print the actual location where the unwrap call occurred rather than some random location inside libcore.

So far, the suggestions for how to implement this include:

  1. Replace unwrap/expect with an unwrap! macro which internally uses line!(), file!() etc.
let x = Err(23);
let y = unwrap!(x, "oh no! x == {:?}", x);

PROS:

  • Doesn't require extending the language in any way. Just an addition to the standard library.
  • Let's you add a format string and args to the panic message.
CONS:
  • Would mean deprecating the already established conventions around the use of `unwrap`/`expect`
  • The macro call appears at the front of an expression rather than in the method position.
  1. Make the inlined location of an inlined function available through MIR or a macro or something, make unwrap/expect inline.
#[inline(always)]
fn unwrap(self) -> T {
    ...
    let line: u32;
    let col: u32;
    unsafe {
        mir!(line = Inline(Line); col = Inline(Col));
    };
    panic!("panicking at line == {}, col == {}", line, col);
}

PROS

  • Allows existing code to work, unchanged.
CONS
  • Seems kinda weird and hacky, at least to me.
  1. Add a type parameter to unwrap/expect which defaults to a magic lang item which gets filled in with a different type representing the call location at every place the method is called.
impl<T> Option<T> {
    fn unwrap<C: SourceContext = CallerSourceContext>(self) -> T {
        match self {
            Some(t) => t,
            None => {
                panic!("unwrap on empty Option at location: {}", C::default());
            }
        }
    }
}

PROS

  • Allows exiting code to work, unchanged.
  • Exposes the API for getting call location and makes it available to non-inlined functions.
CONS
  • Pollutes the type of `unwrap`/`expect`
  • Also kinda weird and hacky.

Thoughts? Ideas? Discuss!


Better panic location reporting for `unwrap()` and friends
#2

Also mentioned in the RFC:

4) Default arguments, see also RFC issue 323:

fn unwrap(self, 
          file: &'static str = file!(), 
          line: usize = line!(), 
          column: usize = column!()) -> T {

5) Support macros at method position (currently a syntax error so it is compatible), see also RFC issue 676

foo.unwrap!()

6) Make unwrap() adapt an attribute which changes the behavior of line!(), essentially the same as “1)” without exposing mir! to public

#[inline(always)] #[use_call_site_line_info]
fn unwrap(self) -> T {

7) Just introduce loc!() which expands to concat!(file!(), ":", line!(), ":", column!()) and tell people to use .expect(loc!())

8) Just promote error-chain instead.

“1)”, “2)”, “4)”, “6)” are compatible with the existing source code, while “0)”, “5)”, “7)”, “8)” requires user to modify their code to benefit from it.


#3

I think that’s a no-go (from the general feeling I got from the RFC discussion, and my personal opinion, I might be biased :wink: )

exposing use_call_site_line_info isn’t better or worse than exposing mir!, but the latter is a general solution that I’m guessing will come anyway at some point.

default arguments come with their own pros and cons, I don’t think the line info issue is enough motivation to speed up the default args discussion. Especially since this would be a very fancy addition to default args (not less hacky than the other “hacky” solutions).


#4

All solutions that requires foo.unwrap() to report caller’s line info would be hacky anyway :wink: We need to make unwrap aware who is calling it without being explicitly told. Which means one of:

  • we need to produce N copies of unwrap (“1)#[inline(always)], “2)” monomorphization), each specialized to know the line info of the caller (“1)” CallerSourceContext or “2)mir! + Inline)

  • we supply the line info via hidden arguments (i.e. “4)” default args, “10)” magic TLS, “11)” magic calling convention)

  • we somehow obtain the line info at runtime via backtrace (“12)” special obj section)

Most solutions are very specialized (except default args), i.e. they really can only solve the “pass line info to these functions” problem. I don’t like this direction very much.

So I’d like to have a consensus on the meta questions first:

  1. Do we want a very narrow solution (e.g. “2)” CallerSourceContext), which is only useful for the “line info for unwrap/expect” problem? Or a generic solution (“1)mir! or “4)” default args), which can also benefit other use cases?

  2. Do we want to keep foo.unwrap() unaffected, so user don’t need to update the source code? Or could we accept a slight change in syntax (“5)foo.unwrap!())? Because it seems a big complain of the original proposal is that it is ugly, not that it changes source code. Or wildly changing the syntax (“0)unwrap!(foo) or “9)foo?!) is actually not really a problem, just like how we introduced x? to replace try!(x)?

From the RFC discussion it seems most proposed alternatives are towards 1. a narrow use case which 2. doesn’t modify any API.

For me I’d prefer someone writing up RFC 676 and have foo.unwrap!() / foo.exepct!(), provided the Carrier trait for ? is implemented.


Some more ideas, and also gathered an idea from the RFC I’ve missed (calling convention):

  • Affects source code:

    9): introduce a ?! operator for forced unwrapping. foo?!.bar()?!.

  • Supply line info via hidden argument:

    10) Annotate unwrap as #[record_caller_line_info]. Whenever such a function is called, a magic TLS variable will be updated with the caller’s line info.

    11) Mark unwrap as an extern "Rust_with_caller_line_info" fn.

  • Supply line info at runtime via backtrace:

    12) Annotate unwrap as #[record_caller_line_info]. Creates a new .caller_line_info section, which stores a sorted key-value array of (Caller PC address → Caller line info). I don’t know if the linker is able to manage these sections though :confused:.

    13) Fix issue 24346 and call it a day. Make sure the user knows about RUST_BACKTRACE=1. Ignore release build.


#5

Compile-time line numbers are great - they don’t rely on unreliable symbol APIs.

If someone already using a macro like loc!, could they share? Refactoring my entire codebase in order to gain production line numbers (even if just the top frame) is a easy choice.


#6

I had another thought regarding this issue… What about adding an extra pass on a debug build where it’ll find all unwrap() calls and automatically rewrite them to a match which immediately panics with the correct line info?

It special-cases Option and Result, but I feel like that’s a better option than altering the entire language just to make a single use-case feasible (like the mir!() or type parameter hack). It’s also 100% backwards compatible and should be rather easy to implement.


#7

There is an RFC for this:

https://github.com/rust-lang/rfcs/pull/2091