Lifetimes From Type Signatures?

Hello!

I am looking to do some analysis of how certain functions cause dependencies between variable lifetimes.

For example, with Vector<String>,

let x_0: Vec<String> = vec![ "A".to_string(), "B".to_string()];
let x_1: Option<&mut String> = x_0.get_mut(0);
let x_2: &mut String = x_1.unwrap();

We know that x_0 must outlive x_1 and x_2 because the reference is to a memory location owned by the vector x_0.

However, this is not clear by looking at the type signature of Vec::get_mut. There are no obvious lifetime annotations.

pub fn get_mut<I>(
    &mut self, 
    index: I
) -> Option<&mut <I as SliceIndex<[T]>>::Output>
where
    I: SliceIndex<[T]>, 

First of all, since rustc is already doing this, is there a way to extract that data for each function? If not, what is the easiest way to model this externally?

Please note that I ask for lifetime information on each function and not entire programs because the end goal for my project is to automatically assemble unit tests, thus I don't have the entire program to start out with. Lifetime information for each function in needed for semantically accurate assembly.

Thank you for your time.
~Yoshiki

The lifetime elision rules are very simple. You can read them here. I don't think that there are any good ways to extract it automatically from rustc.

1 Like

Hi @Alice.

Thank you for the quick response.

I have seen these rule before. Maybe I am being a little pedantic here, I was unsure if these rules also apply to references wrapped in other types.

i.e. &'a mut self 's lifetime gets projected into Option<&'a mut ...> even though &mut... is not a top-level "token"?

I assume that is the case because inference will not be possible without it.

Thanks.
~Yoshiki

Yes, in this case the third rule is in play, and as it says, the lifetime is applied to all output lifetimes.

1 Like

Have you thought about using the compiler as a library to gain access to its lifetime analysis code?

It shouldn't matter that your input is incomplete because the compiler is designed to keep analysing in the face of errors (e.g. a function at the bottom of your file can be type checked even if there's a type error somewhere higher up). You just need to make sure it doesn't bail out early like the CLI program normally would.

To help figure out how to access the information you care about, the rustc API docs are super useful. Here's the rough path I'd follow to start analysing lifetimes.

  • rustc_driver is the compiler's entrypoint. Pass a Callbacks to rustc_driver::run_compiler() to start the compilation process and inject your own logic
  • The after_analysis() callback will give you the compilation state after type-checking and lifetime resolution is done
  • We want to reach the type system context, which we can find by enter()ing the Queries::global_ctxt() passed to our callback
  • TyCtxt::fn_sig_by_hir_id() gives you a function's signature, and you can access the inputs via its decl
  • Once you can see a function's inputs and outputs, you can match on the Ty's TyKind and extract the variable's type's lifetime

You can unlock the rustc_* crates with #![feature(rustc_private)].

If I was working on an automated tool I'd want to use the compiler instead of implementing it myself. That way you avoid re-implementing a lot of rustc's infrastructure and have access to a wealth of existing code that already uses the rustc internals (rustc itself).

1 Like

Hi @Michael-F-Bryan.

That's an interesting idea. I forgot about rustic_driver, but that is an option.

Although our codebase is written in Java for historical reasons. We are planning to fully migrate to Rust some time this year, so we should be able to try it soon.

Thanks for the info.
~Yoshiki

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.