E0611 - the what, why and how?

GeekyTwoShoes
5 min readJun 16, 2017

--

An introduction to the new Rust error code E0611.

Given a simple function foo

fn foo<'a>(x: &i32, y: &'a i32) -> &'a i32 {
if x > y { x } else { y }
}

threw up an error corresponding to Error Code E0312 .

error[E0312]: lifetime of reference outlives lifetime of borrowed content...

But with E0611, this is how the new error message looks.

error[E0611]: explicit lifetime required in the type of `x`

The What?

E0611 handles the case of lifetime errors where

  1. It is a Function Declaration or a Trait. Closures and Trait impls will be taken care of later
  2. One of the argument has a lifetime parameter.
  3. The other argument has a missing lifetime parameter.
  4. Error is of type RegionResolutionErrors and the specific type of ConcreteFailure.

The Why?

What’s E0312 for ?

As stated in the diagnostic.rs file

E0312: r##"A lifetime of reference outlives lifetime of borrowed content.Erroneous code example:
```compile_fail,E0312
fn make_child<'human, 'elve>(x: &mut &'human isize, y: &mut &'elve isize){
*x = *y; // error: lifetime of reference outlives lifetime of borrowed content}

The compiler cannot determine if the `human` lifetime will live long enough to keep up on the elve one. To solve this error, you have to give an explicit lifetime hierarchy:

To solve this error, you have to give an explicit lifetime hierarchy:fn make_child<'human, 'elve: 'human>(x: &mut &'human isize,                                     y: &mut &'elve isize) {    
*x = *y; // ok!
}
Or use the same lifetime for every variable:fn make_child<'elve>(x: &mut &'elve isize, y: &mut &'elve isize) {
*x = *y; // ok!
}

Why E0611?

Since the main idea is modifying error messages for existing lifetime errors based on the different cases instead of just having a single generalised error code for all of them piled up together, we decided to introduce a new error code E0611.

Here’s more on how to introduce a new error code .

error[E0611]: explicit lifetime required in the type of `y`
--> $DIR/ex1-return-one-existing-name-if-else.rs:12:27
|
11 | fn foo<'a>(x: &'a i32, y: &i32) -> &'a i32 {
| - consider changing the type of `y` to `&'a i32`
12 | if x > y { x } else { y }
| ^ lifetime `'a` required
error: aborting due to previous error(s)

The How?

1. Designing the message

Clearly, this is the more interesting and fun phase of all the three. Once we were clear on what kind of lifetime errors we will be tackling, the next thing to do was to design the error message.

struct_span_err!(self.tcx.sess, var.pat.span, E0611, “explicit lifetime required in the type of `{}`”, simple_name)

Design 1

error[E0611]: lifetime mismatch
--> $DIR/ex1-return-one-existing-name-if-else.rs:11:24
|
11 | fn foo<'a>(x: &'a i32, y: &i32) -> &'a i32 {
| ^ consider changing the type of `y` to `&'a i32`
12 | if x > y { x } else { y }
| - lifetime `'a` required

Design 2

error[E0611]: lifetime 'a required
--> $DIR/ex1-return-one-existing-name-if-else.rs:11:24
|
11 | fn foo<'a>(x: &'a i32, y: &i32) -> &'a i32 {
| ^ consider changing the type of `y` to `&'a i32`
12 | if x > y { x } else { y }
| - lifetime `'a` required

Final Design

error[E0611]: explicit lifetime required in the type of `y`
--> $DIR/ex1-return-one-existing-name-if-else.rs:11:24
|
11 | fn foo<'a>(x: &'a i32, y: &i32) -> &'a i32 {
| - consider changing the type of `y` to `&'a i32`
12 | if x > y { x } else { y }
| ^ lifetime `'a` required
error: aborting due to previous error(s)

2. Writing the code

Looking through debug logs, I understood the type of error being dealt with was of type of RegionResolutionError in particular, ConcreteFailure which is defined here. The words lifetimes and regions are used interchangeably.

This is how the error variable looks for our fn foo. Let’s understand it’s components.

ConcreteFailure(Reborrow(p1.rs:3:27: 3:28), ReFree(CodeExtent(4/CallSiteScope { fn_id: NodeId(4), body_id: NodeId(36) }), BrNamed(CrateNum(0):DefIndex(2147483648), 'a(83))), ReFree(CodeExtent(4/CallSiteScope { fn_id: NodeId(4), body_id: NodeId(36) }), BrAnon(0)))

The first Refree(FreeRegion) corresponds to the x . The FreeRegion consists of DefId and a BoundRegion. The function arguments though bound in the function body are considered to be free variables in the declaration and hence, the name FreeRegion. These two fields of freeregion are of interest to us. The BrNamed region refers to the named region x. The second corresponds to the anonymous region y and that’s why BrAnon .

Now that we understand the components of the error, let’s move on to the next part which is to write code that prints

^ consider changing the type of `y` to `&'a i32`

Firstly, we need the parameter corresponding to the anonymous region`y` and secondly, what the new type of y `&’a i32`.

This function

find_arg_with_anonymous_region(&self, anon_region: Region<’tcx>, named_region: Region<’tcx>) -> Option<(&hir::Arg, ty::Ty<’tcx>)>

does both the above tasks for us. It returns the hir::Arg for y, and the type &'a i32, which is the type of y but with the anonymous region replaced with 'a. We are looking for the arguments in the Function Declaration and given the arguments we can get the one with the anonymous region. This is how an Argument is defined.

Arg { pat: pat(8: y), id: NodeId(7) }

Getting hold of the arguments itself wasn’t as easy as it seemed. Our initial thought process was to extract it from the FnDecl . We realised that that wouldn’t work as the regions we were looking for are a part of the hir::Body.

This is how the Fn Body is represented in code.

pub struct Body {    
pub arguments: HirVec<Arg>,
pub value: Expr
}

The code snippet below extracts the Body from the defId (freeregion.scope) of the free region and then iterates over the arguments of the Body.

ty::ReFree(ref free_region) => {

let id = free_region.scope;// is of type DefId,which identifies a particular definition let node_id = self.tcx.hir.as_local_node_id(id).unwrap();
let body_id = self.tcx.hir.maybe_body_owned_by(node_id).unwrap();

let body = self.tcx.hir.body(body_id);
body.arguments
.iter()
.filter_map(|arg| if let Some(tables) = self.in_progress_tables {
let ty = tables.borrow().node_id_to_type(arg.id);// Look up the type of the argument.

The variable tables stores the Ty<’tcx’> corresponding to hir Node id’s.

let new_arg_ty = self.tcx.fold_regions(&ty,&mut false,|r, _| 
if *r == *anon_region {
found_anon_region = true;
named_region
} else {
r
});

A TypeFolder, walks over a type and lets you change it as it goes. TypeFolder has a method fold_regions which we override to look for the anonymous region and return the Ty of the named region instead i.e.new_arg_ty.

This is how my mentor Niko Matsakis explained walking over a Type to me. Vec<Vec<&i32>> when walked gives

  • Vec<Vec<&i32>> — regions() is vec![]
  • Vec<&i32> — regions() is vec![]
  • &R i32 — regions() is vec![R]
  • i32 — regions() is vec![]

As we get the anonymous argument and the new type, we are ready to go. All we need to do is print the error message.

struct_span_err!(self.tcx.sess,span,E0611,"explicit lifetime required in parameter type")
.span_label(var.pat.span,
format!("consider changing type to `{}`", new_ty))
.span_label(span, format!("lifetime `{}` required", named))
.emit();

The post comes to an end but not the introduction to E0611. There’ll be more of the journey in follow-up posts. Till then, GoodBye!

--

--

GeekyTwoShoes

Computer Science Enthusiast. Rustacean. Dev @Microsoft.